From 7755a9bd9df9f1343a53431bee3d39f3d52cd9a9 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Tue, 7 Apr 2026 17:53:26 -0300 Subject: [PATCH 01/27] feat: init again --- .github/instructions/server.instructions.md | 123 +- package.json | 34 +- packages/cli/package.json | 4 +- packages/documents-core/CHANGELOG.md | 14 - packages/documents-core/package.json | 35 - packages/documents-core/src/database.ts | 114 - packages/documents-core/src/documents.ts | 317 - packages/documents-core/src/index.ts | 3 - packages/documents-core/src/types.ts | 40 - packages/documents-core/tsconfig.json | 3 - packages/documents-core/tsup.config.ts | 3 - packages/embeddings-core/CHANGELOG.md | 10 - packages/embeddings-core/package.json | 23 - packages/embeddings-core/src/embeddings.ts | 111 - packages/embeddings-core/src/index.ts | 2 - .../embeddings-core/src/providers/index.ts | 2 - .../embeddings-core/src/providers/ollama.ts | 43 - .../embeddings-core/src/providers/openai.ts | 71 - packages/embeddings-core/src/types.ts | 23 - packages/embeddings-core/tsconfig.json | 22 - packages/embeddings-core/tsup.config.ts | 5 - packages/files-core/CHANGELOG.md | 15 - packages/files-core/package.json | 26 - packages/files-core/src/database.ts | 106 - packages/files-core/src/db.ts | 8 - packages/files-core/src/files.ts | 92 - packages/files-core/src/index.ts | 4 - packages/files-core/src/storage/gcs.ts | 56 - packages/files-core/src/storage/index.ts | 3 - packages/files-core/src/storage/local.ts | 43 - packages/files-core/src/storage/s3.ts | 69 - packages/files-core/src/types.ts | 39 - packages/files-core/tsconfig.json | 3 - packages/files-core/tsup.config.ts | 3 - packages/postgresdb/jest.config.ts | 3 - packages/postgresdb/package.json | 26 +- packages/postgresdb/src/models/Document.ts | 6 + packages/postgresdb/tests/tsconfig.json | 10 - .../postgresdb/tests/unit/babel.config.cjs | 5 - packages/postgresdb/tests/unit/jest.config.ts | 8 - .../postgresdb/tests/unit/tests/File.test.ts | 134 - packages/server/package.json | 25 +- packages/server/src/Context.ts | 8 +- packages/server/src/app.ts | 7 +- packages/server/src/db.ts | 25 + packages/server/src/lib/files.ts | 72 + packages/server/src/mcp/index.ts | 1 - packages/server/src/mcp/prompts/index.ts | 1 - packages/server/src/mcp/resources/index.ts | 1 - packages/server/src/mcp/server.ts | 80 +- packages/server/src/mcp/tools.ts | 69 + packages/server/src/mcp/tools/documents.ts | 428 -- packages/server/src/mcp/tools/index.ts | 9 - .../server/src/rest/openapi/v1/documents.yaml | 473 -- .../server/src/rest/openapi/v1/files.yaml | 200 +- packages/server/src/rest/v1/documents.ts | 278 - packages/server/src/rest/v1/files.ts | 278 +- packages/server/src/rest/v1/index.ts | 4 +- packages/server/tests/tsconfig.json | 1 + packages/server/tests/unit/jest.config.ts | 7 +- packages/server/tests/unit/setupTests.ts | 26 - .../server/tests/unit/setupTestsAfterEnv.ts | 45 + packages/server/tests/unit/testClient.ts | 30 + .../server/tests/unit/tests/files.test.ts | 82 + packages/server/tests/unit/tests/mcp.test.ts | 21 + .../tests/unit/tests/rest/documents.test.ts | 463 -- .../tests/unit/tests/rest/files.test.ts | 248 - packages/server/tsconfig.json | 3 + packages/text-atomizer/CHANGELOG.md | 10 - packages/text-atomizer/jest.config.ts | 3 - packages/text-atomizer/package.json | 21 - packages/text-atomizer/src/index.ts | 151 - packages/text-atomizer/tests/tsconfig.json | 9 - .../text-atomizer/tests/unit/babel.config.cjs | 5 - .../text-atomizer/tests/unit/jest.config.ts | 3 - .../tests/unit/tests/atomizeText.test.ts | 370 -- packages/text-atomizer/tsconfig.json | 3 - packages/text-atomizer/tsup.config.ts | 3 - packages/website/docs/Introduction.md | 5 + packages/website/docs/getting-started.md | 89 - packages/website/docs/intro.md | 32 - packages/website/package.json | 2 +- packages/website/tsconfig.json | 3 +- pnpm-lock.yaml | 5232 ++++++++++------- 84 files changed, 3780 insertions(+), 6707 deletions(-) delete mode 100644 packages/documents-core/CHANGELOG.md delete mode 100644 packages/documents-core/package.json delete mode 100644 packages/documents-core/src/database.ts delete mode 100644 packages/documents-core/src/documents.ts delete mode 100644 packages/documents-core/src/index.ts delete mode 100644 packages/documents-core/src/types.ts delete mode 100644 packages/documents-core/tsconfig.json delete mode 100644 packages/documents-core/tsup.config.ts delete mode 100644 packages/embeddings-core/CHANGELOG.md delete mode 100644 packages/embeddings-core/package.json delete mode 100644 packages/embeddings-core/src/embeddings.ts delete mode 100644 packages/embeddings-core/src/index.ts delete mode 100644 packages/embeddings-core/src/providers/index.ts delete mode 100644 packages/embeddings-core/src/providers/ollama.ts delete mode 100644 packages/embeddings-core/src/providers/openai.ts delete mode 100644 packages/embeddings-core/src/types.ts delete mode 100644 packages/embeddings-core/tsconfig.json delete mode 100644 packages/embeddings-core/tsup.config.ts delete mode 100644 packages/files-core/CHANGELOG.md delete mode 100644 packages/files-core/package.json delete mode 100644 packages/files-core/src/database.ts delete mode 100644 packages/files-core/src/db.ts delete mode 100644 packages/files-core/src/files.ts delete mode 100644 packages/files-core/src/index.ts delete mode 100644 packages/files-core/src/storage/gcs.ts delete mode 100644 packages/files-core/src/storage/index.ts delete mode 100644 packages/files-core/src/storage/local.ts delete mode 100644 packages/files-core/src/storage/s3.ts delete mode 100644 packages/files-core/src/types.ts delete mode 100644 packages/files-core/tsconfig.json delete mode 100644 packages/files-core/tsup.config.ts delete mode 100644 packages/postgresdb/jest.config.ts delete mode 100644 packages/postgresdb/tests/tsconfig.json delete mode 100644 packages/postgresdb/tests/unit/babel.config.cjs delete mode 100644 packages/postgresdb/tests/unit/jest.config.ts delete mode 100644 packages/postgresdb/tests/unit/tests/File.test.ts create mode 100644 packages/server/src/db.ts create mode 100644 packages/server/src/lib/files.ts delete mode 100644 packages/server/src/mcp/index.ts delete mode 100644 packages/server/src/mcp/prompts/index.ts delete mode 100644 packages/server/src/mcp/resources/index.ts create mode 100644 packages/server/src/mcp/tools.ts delete mode 100644 packages/server/src/mcp/tools/documents.ts delete mode 100644 packages/server/src/mcp/tools/index.ts delete mode 100644 packages/server/src/rest/openapi/v1/documents.yaml delete mode 100644 packages/server/src/rest/v1/documents.ts delete mode 100644 packages/server/tests/unit/setupTests.ts create mode 100644 packages/server/tests/unit/setupTestsAfterEnv.ts create mode 100644 packages/server/tests/unit/testClient.ts create mode 100644 packages/server/tests/unit/tests/files.test.ts create mode 100644 packages/server/tests/unit/tests/mcp.test.ts delete mode 100644 packages/server/tests/unit/tests/rest/documents.test.ts delete mode 100644 packages/server/tests/unit/tests/rest/files.test.ts delete mode 100644 packages/text-atomizer/CHANGELOG.md delete mode 100644 packages/text-atomizer/jest.config.ts delete mode 100644 packages/text-atomizer/package.json delete mode 100644 packages/text-atomizer/src/index.ts delete mode 100644 packages/text-atomizer/tests/tsconfig.json delete mode 100644 packages/text-atomizer/tests/unit/babel.config.cjs delete mode 100644 packages/text-atomizer/tests/unit/jest.config.ts delete mode 100644 packages/text-atomizer/tests/unit/tests/atomizeText.test.ts delete mode 100644 packages/text-atomizer/tsconfig.json delete mode 100644 packages/text-atomizer/tsup.config.ts create mode 100644 packages/website/docs/Introduction.md delete mode 100644 packages/website/docs/intro.md diff --git a/.github/instructions/server.instructions.md b/.github/instructions/server.instructions.md index dc4f40a0..d4811b38 100644 --- a/.github/instructions/server.instructions.md +++ b/.github/instructions/server.instructions.md @@ -12,30 +12,78 @@ Follow the packages documentation: ## Architecture -The server src will have two folders: rest and mcp. +The server src will have three folders: rest, mcp, and lib. + +### Business Logic Layer + +All business logic (database queries, data transformations) must live in `src/lib/`, organized by resource: + +- `src/lib/files.ts` - All file-related business logic (listFiles, getFile, createFile, deleteFile) +- Additional resources follow the same pattern: `src/lib/.ts` + +Route handlers **must not** contain direct database calls. They are responsible only for HTTP concerns: parsing request bodies/params, calling lib functions, and setting response status/body. + +Lib functions **must always return plain mapped objects**, never raw model instances. This is required to avoid exposing sensitive or internal data (e.g., internal DB fields, hashed passwords, audit columns) through the API. Every function that queries the database must map the result to a plain object before returning it: + +```ts +export const getFile = async (args: { id: string }) => { + const file = await db.File.findByPk(args.id); + if (!file) return null; + return { + id: file.id, + filename: file.filename, + // ... all fields explicitly mapped + }; +}; +``` ### REST API Structure The REST API is organized by version and resource for better maintainability and versioning: -- `src/rest/v1/documents.ts` - Contains all document-related endpoints and handlers for API version 1 -- Future versions will follow the same pattern: `src/rest/v2/documents.ts`, etc. +- `src/rest/v1/files.ts` - Contains all file-related endpoints and handlers for API version 1 +- Future versions will follow the same pattern: `src/rest/v2/files.ts`, etc. -Each version folder contains resource-specific files. Currently, only the documents resource is implemented, but additional resources can be added as separate files (e.g., `users.ts`, `analytics.ts`) within each version folder. +Each version folder contains resource-specific files. Additional resources can be added as separate files (e.g., `users.ts`, `analytics.ts`) within each version folder. #### Router Organization API routes are not defined directly in `src/index.ts` to maintain clean separation of concerns: - `src/rest/router.ts` - Central REST API router that imports and mounts versioned routers -- Version-specific routers (e.g., `src/rest/v1/documents.ts`) define the actual route handlers +- Version-specific routers (e.g., `src/rest/v1/files.ts`) define the actual route handlers - `src/index.ts` remains focused on application setup, middleware, and mounting the main routers This structure ensures scalability and keeps the main entry point uncluttered as the API grows. +#### Swagger JSDoc + +Every route handler **must** have an `@openapi` JSDoc block immediately before it. The JSDoc must match the handler's actual behavior (paths, status codes, request/response shapes). + +```ts +/** + * @openapi + * /files: + * get: + * tags: + * - Files + * summary: List all files + * operationId: listFiles + * responses: + * '200': + * description: ... + */ +filesRouter.get('/files', async (ctx: Context) => { ... }); +``` + #### OpenAPI Documentation -When modifying or adding REST API endpoints, **always update the corresponding OpenAPI specification** in `src/rest/openapi/` and follow the guidelines outlined in `src/rest/openapi/README.md`. This includes: +When modifying or adding REST API endpoints, **always update both**: + +1. The `@openapi` JSDoc block on the route handler +2. The corresponding OpenAPI specification in `src/rest/openapi/v1/.yaml` + +Follow the guidelines in `src/rest/openapi/README.md`. This includes: - Updating paths, schemas, request/response bodies, and error responses - Adding descriptive examples and operation IDs @@ -57,23 +105,6 @@ The MCP (Model Context Protocol) folder is organized to separate concerns and ma This structure allows for easy addition of new tools and resources while keeping the code organized and maintainable. -### Core Functionality Guidelines - -**Important**: Core business logic and functionalities must be implemented in dedicated core packages (e.g., `@soat/documents-core`, `@soat/text-atomizer`) and never directly in the server folder. - -The server package should only contain: - -- HTTP routing and request handling -- Middleware configuration -- Integration with core packages -- API versioning and structure - -This separation ensures: - -- Reusability of core logic across different interfaces (CLI, web, etc.) -- Better testability of business logic -- Cleaner architecture with clear boundaries - ## Development To run the server in development mode with watch, navigate to the server package and run: @@ -144,49 +175,3 @@ Unit tests are located in the #file:../../packages/server/tests/unit/ folder. To ```bash pnpm --filter @soat/server test ``` - -### REST API Tests - -REST API tests are located in the #file:../../packages/server/tests/unit/tests/rest/ folder. These tests cover the various endpoints and functionalities of the REST API. - -- Each endpoint has corresponding test files that validate the expected behavior. -- Use mocks on #file:../../packages/server/tests/unit/setupTests.ts to mock external dependencies only and isolate the tests. -- Test the whole app, #file:../../packages/server/src/app.ts , to ensure all middleware and routes are properly integrated. Use supertest to simulate HTTP requests and validate responses. - In the example below, `saveFile` from `@soat/files-core` is mocked to test the file upload endpoint without actually saving a file. - - ```ts - import { saveFile } from '@soat/files-core'; - import { app } from 'src/app'; - import request from 'supertest'; - - test('should create a file via REST API', async () => { - const savedFile = { - id: 'test-id', - filename: 'test.txt', - content: 'Hello, World!', - metadata: {}, - }; - - jest.mocked(saveFile).mockResolvedValue(savedFile); - - const response = await request(app.callback()) - .post('/api/v1/files/upload') - .send({ - content: 'Hello, World!', - options: { metadata: { filename: 'test.txt' } }, - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(201); - expect(response.body).toEqual({ - id: 'test-id', - filename: 'test.txt', - success: true, - }); - expect(saveFile).toHaveBeenCalledWith({ - config: { local: { path: '/tmp/files' }, type: 'local' }, - content: 'Hello, World!', - options: { metadata: { filename: 'test.txt' } }, - }); - }); - ``` diff --git a/package.json b/package.json index ed8a3cb9..8a04bcde 100644 --- a/package.json +++ b/package.json @@ -15,30 +15,30 @@ }, "scripts": { "prepare": "husky", - "syncpack:fix": "syncpack fix-mismatches", - "syncpack:list": "syncpack list-mismatches" + "syncpack:fix": "syncpack fix", + "syncpack:lint": "syncpack lint" }, "devDependencies": { - "@commitlint/cli": "^20.1.0", - "@lerna-lite/changed": "^4.9.4", - "@lerna-lite/cli": "^4.9.4", - "@lerna-lite/list": "^4.9.4", - "@lerna-lite/version": "^4.9.4", - "@ttoss/config": "^1.35.12", - "@ttoss/eslint-config": "^1.26.6", - "@ttoss/monorepo": "^1.28.0", - "@types/node": "^25.0.3", + "@commitlint/cli": "^20.5.0", + "@lerna-lite/changed": "^5.0.0", + "@lerna-lite/cli": "^5.0.0", + "@lerna-lite/list": "^5.0.0", + "@lerna-lite/version": "^5.0.0", + "@ttoss/config": "^1.37.8", + "@ttoss/eslint-config": "^1.26.14", + "@ttoss/monorepo": "^1.29.8", + "@types/node": "^25.5.2", "eslint": "^9.39.1", "husky": "^9.1.7", - "lint-staged": "^16.2.7", - "prettier": "^3.7.4", - "syncpack": "13.0.4", - "turbo": "^2.6.2", - "typescript": "~5.9.3" + "lint-staged": "^16.4.0", + "prettier": "^3.8.1", + "syncpack": "^14.3.0", + "turbo": "^2.9.4", + "typescript": "~6.0.2" }, "engines": { "node": "^24.0.0", "pnpm": "^10.0.0" }, - "packageManager": "pnpm@10.26.2" + "packageManager": "pnpm@10.33.0" } diff --git a/packages/cli/package.json b/packages/cli/package.json index 5a27d94a..e0459f68 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -10,9 +10,9 @@ "commander": "^14.0.2" }, "devDependencies": { - "@ttoss/config": "^1.35.12", + "@ttoss/config": "^1.37.8", "@types/jest": "^30.0.0", - "jest": "^30.2.0", + "jest": "^30.3.0", "tsup": "^8.5.1", "tsx": "^4.21.0" }, diff --git a/packages/documents-core/CHANGELOG.md b/packages/documents-core/CHANGELOG.md deleted file mode 100644 index 9b8c4e76..00000000 --- a/packages/documents-core/CHANGELOG.md +++ /dev/null @@ -1,14 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# 0.0.0-alpha.2 (2026-01-06) - -### Bug Fixes - -* add version ([de8fab4](https://github.com/ttoss/soat/commit/de8fab4e0d51ba0e06e0b29f9b26ea8d147d92a6)) - -### Features - -* database working ([5a5d34d](https://github.com/ttoss/soat/commit/5a5d34d5820c0279b14f3a135b9a55f728cf8f65)) diff --git a/packages/documents-core/package.json b/packages/documents-core/package.json deleted file mode 100644 index ddb9345e..00000000 --- a/packages/documents-core/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@soat/documents-core", - "version": "0.0.0-alpha.2", - "description": "Core package for saving and managing documents with local, S3, and GCS storage", - "type": "module", - "exports": "./src/index.ts", - "types": "dist/index.d.ts", - "files": [ - "dist" - ], - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "test": "jest" - }, - "dependencies": { - "@soat/embeddings-core": "workspace:*", - "@soat/files-core": "workspace:*", - "@soat/postgresdb": "workspace:*", - "@ttoss/postgresdb": "^0.3.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@ttoss/config": "^1.35.12", - "@types/jest": "^30.0.0", - "@types/node": "^25.0.3", - "@types/uuid": "^10.0.0", - "jest": "^30.2.0", - "tsup": "^8.5.1", - "tsx": "^4.21.0" - } -} diff --git a/packages/documents-core/src/database.ts b/packages/documents-core/src/database.ts deleted file mode 100644 index 7e64ccd9..00000000 --- a/packages/documents-core/src/database.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { models } from '@soat/postgresdb'; - -import type { DocumentRecord } from './types'; - -const parseMetadata = ( - metadata?: string -): Record | undefined => { - if (!metadata) return undefined; - try { - return JSON.parse(metadata); - } catch { - return undefined; - } -}; - -const toDocumentRecord = ( - doc: InstanceType -): DocumentRecord => { - return { - id: doc.id, - title: doc.title, - fileId: doc.fileId, - embeddingModel: doc.embeddingModel, - embeddingProvider: doc.embeddingProvider, - embedding: doc.embedding, - metadata: parseMetadata(doc.metadata), - createdAt: doc.createdAt, - updatedAt: doc.updatedAt, - }; -}; - -export const saveDocumentRecord = async (args: { - id: string; - title?: string; - fileId: string; - embeddingModel?: string; - embeddingProvider?: string; - embedding?: number[]; - metadata?: Record; -}): Promise => { - const doc = await models.Document.create({ - ...args, - metadata: args.metadata ? JSON.stringify(args.metadata) : undefined, - } as Parameters[0]); - - return toDocumentRecord(doc); -}; - -export const getDocumentRecord = async ( - id: string -): Promise => { - const doc = await models.Document.findByPk(id); - if (!doc) return null; - return toDocumentRecord(doc); -}; - -export const updateDocumentRecord = async ( - id: string, - updates: Partial< - Pick< - DocumentRecord, - | 'title' - | 'embeddingModel' - | 'embeddingProvider' - | 'embedding' - | 'metadata' - > - > -): Promise => { - const doc = await models.Document.findByPk(id); - if (!doc) return null; - - const updateData: Partial<{ - title: string; - embeddingModel: string; - embeddingProvider: string; - embedding: number[]; - metadata: string | null; - }> = {}; - if (updates.title !== undefined) updateData.title = updates.title; - if (updates.embeddingModel !== undefined) - updateData.embeddingModel = updates.embeddingModel; - if (updates.embeddingProvider !== undefined) - updateData.embeddingProvider = updates.embeddingProvider; - if (updates.embedding !== undefined) updateData.embedding = updates.embedding; - if (updates.metadata !== undefined) { - updateData.metadata = updates.metadata - ? JSON.stringify(updates.metadata) - : null; - } - - await doc.update(updateData); - return getDocumentRecord(id); -}; - -export const deleteDocumentRecord = async (id: string): Promise => { - const doc = await models.Document.findByPk(id); - if (!doc) return false; - await doc.destroy(); - return true; -}; - -export const listDocumentRecords = async (): Promise => { - const docs = await models.Document.findAll(); - return docs.map(toDocumentRecord); -}; - -export const getDocumentRecordByFileId = async ( - fileId: string -): Promise => { - const doc = await models.Document.findOne({ where: { fileId } }); - if (!doc) return null; - return toDocumentRecord(doc); -}; diff --git a/packages/documents-core/src/documents.ts b/packages/documents-core/src/documents.ts deleted file mode 100644 index 19d9fa88..00000000 --- a/packages/documents-core/src/documents.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { generateEmbedding, type EmbeddingConfig } from '@soat/embeddings-core'; -import { - deleteFile, - retrieveFileById, - saveFile, - type StorageConfig, -} from '@soat/files-core'; -import { v4 as uuidv4 } from 'uuid'; - -import { - deleteDocumentRecord, - getDocumentRecord, - getDocumentRecordByFileId, - listDocumentRecords, - saveDocumentRecord, - updateDocumentRecord, -} from './database'; -import type { - CreateDocumentOptions, - Document, - DocumentRecord, - SearchDocumentsOptions, -} from './types'; - -export const createDocument = async (args: { - storageConfig: StorageConfig; - embeddingConfig?: EmbeddingConfig; - content: string; - options?: CreateDocumentOptions; -}): Promise => { - const { storageConfig, embeddingConfig, content, options } = args; - const id = uuidv4(); - - // Save content as markdown file - const file = await saveFile({ - config: storageConfig, - content, - options: { - contentType: 'text/markdown', - metadata: { - filename: `${id}.md`, - documentId: id, - ...options?.metadata, - }, - }, - }); - - let embeddingResult: - | { embedding: number[]; model: string; provider: string } - | undefined; - - // Generate embedding if config is provided and option is enabled (default: true) - if (embeddingConfig && options?.generateEmbedding !== false) { - const result = await generateEmbedding({ - config: embeddingConfig, - text: content, - }); - embeddingResult = { - embedding: result.embedding, - model: result.model, - provider: result.provider, - }; - } - - // Save document record - const record = await saveDocumentRecord({ - id, - title: options?.title, - fileId: file.id, - embeddingModel: embeddingResult?.model, - embeddingProvider: embeddingResult?.provider, - embedding: embeddingResult?.embedding, - metadata: options?.metadata, - }); - - return { - id: record.id, - title: record.title, - fileId: record.fileId, - content, - embeddingModel: record.embeddingModel, - embeddingProvider: record.embeddingProvider, - embedding: record.embedding, - metadata: record.metadata, - createdAt: record.createdAt, - updatedAt: record.updatedAt, - }; -}; - -export const getDocument = async (args: { - storageConfig: StorageConfig; - id: string; -}): Promise => { - const { storageConfig, id } = args; - - const record = await getDocumentRecord(id); - if (!record) return null; - - const file = await retrieveFileById({ - config: storageConfig, - id: record.fileId, - }); - - return { - id: record.id, - title: record.title, - fileId: record.fileId, - content: file?.content, - embeddingModel: record.embeddingModel, - embeddingProvider: record.embeddingProvider, - embedding: record.embedding, - metadata: record.metadata, - createdAt: record.createdAt, - updatedAt: record.updatedAt, - }; -}; - -export const updateDocument = async (args: { - storageConfig: StorageConfig; - embeddingConfig?: EmbeddingConfig; - id: string; - content?: string; - title?: string; - metadata?: Record; - regenerateEmbedding?: boolean; -}): Promise => { - const { - storageConfig, - embeddingConfig, - id, - content, - title, - metadata, - regenerateEmbedding, - } = args; - - const record = await getDocumentRecord(id); - if (!record) return null; - - let newFileId = record.fileId; - let embeddingResult: - | { embedding: number[]; model: string; provider: string } - | undefined; - - // Update content if provided - if (content !== undefined) { - // Delete old file - await deleteFile({ config: storageConfig, id: record.fileId }); - - // Save new file - const newFile = await saveFile({ - config: storageConfig, - content, - options: { - contentType: 'text/markdown', - metadata: { - filename: `${id}.md`, - documentId: id, - ...metadata, - }, - }, - }); - newFileId = newFile.id; - - // Regenerate embedding if config is provided - if (embeddingConfig && regenerateEmbedding !== false) { - const result = await generateEmbedding({ - config: embeddingConfig, - text: content, - }); - embeddingResult = { - embedding: result.embedding, - model: result.model, - provider: result.provider, - }; - } - } - - // Update document record - const updatedRecord = await updateDocumentRecord(id, { - title, - embeddingModel: embeddingResult?.model, - embeddingProvider: embeddingResult?.provider, - embedding: embeddingResult?.embedding, - metadata, - }); - - if (!updatedRecord) return null; - - // Get updated content - const file = await retrieveFileById({ - config: storageConfig, - id: newFileId, - }); - - return { - id: updatedRecord.id, - title: updatedRecord.title, - fileId: newFileId, - content: file?.content, - embeddingModel: updatedRecord.embeddingModel, - embeddingProvider: updatedRecord.embeddingProvider, - embedding: updatedRecord.embedding, - metadata: updatedRecord.metadata, - createdAt: updatedRecord.createdAt, - updatedAt: updatedRecord.updatedAt, - }; -}; - -export const deleteDocument = async (args: { - storageConfig: StorageConfig; - id: string; -}): Promise => { - const { storageConfig, id } = args; - - const record = await getDocumentRecord(id); - if (!record) return false; - - // Delete file - await deleteFile({ config: storageConfig, id: record.fileId }); - - // Delete document record - return deleteDocumentRecord(id); -}; - -export const listDocuments = async (): Promise => { - return listDocumentRecords(); -}; - -export const searchDocumentsBySimilarity = async (args: { - storageConfig: StorageConfig; - embeddingConfig: EmbeddingConfig; - query: string; - options?: SearchDocumentsOptions; -}): Promise => { - const { storageConfig, embeddingConfig, query, options } = args; - const limit = options?.limit ?? 10; - - // Generate embedding for query - const queryEmbedding = await generateEmbedding({ - config: embeddingConfig, - text: query, - }); - - // Get all documents with embeddings - const records = await listDocumentRecords(); - const documentsWithEmbeddings = records.filter( - (r) => r.embedding && r.embedding.length > 0 - ); - - // Calculate cosine similarity - const similarities = documentsWithEmbeddings.map((record) => { - const similarity = cosineSimilarity( - queryEmbedding.embedding, - record.embedding! - ); - return { record, similarity }; - }); - - // Sort by similarity and take top results - similarities.sort((a, b) => b.similarity - a.similarity); - const topResults = similarities.slice(0, limit); - - // Filter by threshold if provided - const filteredResults = options?.threshold - ? topResults.filter((r) => r.similarity >= options.threshold!) - : topResults; - - // Fetch content for each document - const documents: Document[] = []; - for (const { record } of filteredResults) { - const file = await retrieveFileById({ - config: storageConfig, - id: record.fileId, - }); - - documents.push({ - id: record.id, - title: record.title, - fileId: record.fileId, - content: file?.content, - embeddingModel: record.embeddingModel, - embeddingProvider: record.embeddingProvider, - embedding: record.embedding, - metadata: record.metadata, - createdAt: record.createdAt, - updatedAt: record.updatedAt, - }); - } - - return documents; -}; - -const cosineSimilarity = (a: number[], b: number[]): number => { - if (a.length !== b.length) { - throw new Error('Vectors must have the same length'); - } - - let dotProduct = 0; - let normA = 0; - let normB = 0; - - for (let i = 0; i < a.length; i++) { - dotProduct += a[i] * b[i]; - normA += a[i] * a[i]; - normB += b[i] * b[i]; - } - - normA = Math.sqrt(normA); - normB = Math.sqrt(normB); - - if (normA === 0 || normB === 0) { - return 0; - } - - return dotProduct / (normA * normB); -}; diff --git a/packages/documents-core/src/index.ts b/packages/documents-core/src/index.ts deleted file mode 100644 index a72ed738..00000000 --- a/packages/documents-core/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './database'; -export * from './documents'; -export * from './types'; diff --git a/packages/documents-core/src/types.ts b/packages/documents-core/src/types.ts deleted file mode 100644 index 26a3ef23..00000000 --- a/packages/documents-core/src/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { EmbeddingConfig } from '@soat/embeddings-core'; -import type { StorageConfig, UploadOptions } from '@soat/files-core'; - -export interface Document { - id: string; - title?: string; - fileId: string; - content?: string | Buffer; - embeddingModel?: string; - embeddingProvider?: string; - embedding?: number[]; - metadata?: Record; - createdAt?: Date; - updatedAt?: Date; -} - -export interface DocumentRecord { - id: string; - title?: string; - fileId: string; - embeddingModel?: string; - embeddingProvider?: string; - embedding?: number[]; - metadata?: Record; - createdAt: Date; - updatedAt: Date; -} - -export interface CreateDocumentOptions { - title?: string; - metadata?: Record; - generateEmbedding?: boolean; -} - -export interface SearchDocumentsOptions { - limit?: number; - threshold?: number; -} - -export type { EmbeddingConfig, StorageConfig, UploadOptions }; diff --git a/packages/documents-core/tsconfig.json b/packages/documents-core/tsconfig.json deleted file mode 100644 index fce4d054..00000000 --- a/packages/documents-core/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@ttoss/config/tsconfig.json" -} diff --git a/packages/documents-core/tsup.config.ts b/packages/documents-core/tsup.config.ts deleted file mode 100644 index 8db39855..00000000 --- a/packages/documents-core/tsup.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tsupConfig } from '@ttoss/config'; - -export default tsupConfig(); diff --git a/packages/embeddings-core/CHANGELOG.md b/packages/embeddings-core/CHANGELOG.md deleted file mode 100644 index 0c39e82d..00000000 --- a/packages/embeddings-core/CHANGELOG.md +++ /dev/null @@ -1,10 +0,0 @@ -# @soat/embeddings-core - -## 0.0.0-alpha.1 - -### Features - -- Initial implementation of embeddings-core package -- Support for Ollama embedding provider -- Support for OpenAI embedding provider -- Environment-based configuration with `EMBEDDINGS_OLLAMA_MODEL` and `EMBEDDINGS_OPENAI_KEY` diff --git a/packages/embeddings-core/package.json b/packages/embeddings-core/package.json deleted file mode 100644 index fe679a67..00000000 --- a/packages/embeddings-core/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@soat/embeddings-core", - "version": "0.0.0-alpha.1", - "description": "Core package for generating embeddings with Ollama and other providers", - "type": "module", - "exports": "./src/index.ts", - "types": "dist/index.d.ts", - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "test": "jest" - }, - "dependencies": { - "ollama": "^0.6.3" - }, - "devDependencies": { - "@ttoss/config": "^1.35.12", - "@types/node": "^25.0.3", - "jest": "^30.2.0", - "tsup": "^8.5.1", - "typescript": "~5.9.3" - } -} diff --git a/packages/embeddings-core/src/embeddings.ts b/packages/embeddings-core/src/embeddings.ts deleted file mode 100644 index becb89b2..00000000 --- a/packages/embeddings-core/src/embeddings.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as ollamaProvider from './providers/ollama.js'; -import * as openaiProvider from './providers/openai.js'; -import type { - EmbeddingConfig, - EmbeddingResult, - OllamaConfig, - OpenAIConfig, -} from './types.js'; - -const getProvider = (args: { config: EmbeddingConfig }) => { - const { config } = args; - switch (config.provider) { - case 'ollama': - return ollamaProvider; - case 'openai': - return openaiProvider; - default: - throw new Error(`Unsupported embedding provider: ${config.provider}`); - } -}; - -const getProviderConfig = (args: { config: EmbeddingConfig }) => { - const { config } = args; - switch (config.provider) { - case 'ollama': - if (!config.ollama) { - throw new Error('Ollama config is required for ollama provider'); - } - return config.ollama; - case 'openai': - if (!config.openai) { - throw new Error('OpenAI config is required for openai provider'); - } - return config.openai; - default: - throw new Error(`Unsupported embedding provider: ${config.provider}`); - } -}; - -export const generateEmbedding = async (args: { - config: EmbeddingConfig; - text: string; -}): Promise => { - const { config, text } = args; - const provider = getProvider({ config }); - const providerConfig = getProviderConfig({ config }); - - return provider.generateEmbedding({ - config: providerConfig as OllamaConfig & OpenAIConfig, - text, - }); -}; - -export const generateEmbeddings = async (args: { - config: EmbeddingConfig; - texts: string[]; -}): Promise => { - const { config, texts } = args; - const provider = getProvider({ config }); - const providerConfig = getProviderConfig({ config }); - - return provider.generateEmbeddings({ - config: providerConfig as OllamaConfig & OpenAIConfig, - texts, - }); -}; - -/** - * Creates an embedding config from environment variables. - * Supports: - * - EMBEDDINGS_OLLAMA_MODEL: Ollama model name (uses Ollama provider) - * - EMBEDDINGS_OLLAMA_HOST: Optional Ollama host URL - * - EMBEDDINGS_OPENAI_KEY: OpenAI API key (uses OpenAI provider) - * - EMBEDDINGS_OPENAI_MODEL: Optional OpenAI model name - * - * Priority: Ollama > OpenAI (if multiple are configured) - */ -export const getConfigFromEnv = (): EmbeddingConfig => { - // eslint-disable-next-line turbo/no-undeclared-env-vars - const ollamaModel = process.env.EMBEDDINGS_OLLAMA_MODEL; - // eslint-disable-next-line turbo/no-undeclared-env-vars - const ollamaHost = process.env.EMBEDDINGS_OLLAMA_HOST; - // eslint-disable-next-line turbo/no-undeclared-env-vars - const openaiKey = process.env.EMBEDDINGS_OPENAI_KEY; - // eslint-disable-next-line turbo/no-undeclared-env-vars - const openaiModel = process.env.EMBEDDINGS_OPENAI_MODEL; - - if (ollamaModel) { - return { - provider: 'ollama', - ollama: { - model: ollamaModel, - host: ollamaHost, - }, - }; - } - - if (openaiKey) { - return { - provider: 'openai', - openai: { - apiKey: openaiKey, - model: openaiModel, - }, - }; - } - - throw new Error( - 'No embedding provider configured. Set EMBEDDINGS_OLLAMA_MODEL or EMBEDDINGS_OPENAI_KEY' - ); -}; diff --git a/packages/embeddings-core/src/index.ts b/packages/embeddings-core/src/index.ts deleted file mode 100644 index 179a1287..00000000 --- a/packages/embeddings-core/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './embeddings'; -export * from './types'; diff --git a/packages/embeddings-core/src/providers/index.ts b/packages/embeddings-core/src/providers/index.ts deleted file mode 100644 index 514346ac..00000000 --- a/packages/embeddings-core/src/providers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as ollama from './ollama'; -export * as openai from './openai'; diff --git a/packages/embeddings-core/src/providers/ollama.ts b/packages/embeddings-core/src/providers/ollama.ts deleted file mode 100644 index e2c16701..00000000 --- a/packages/embeddings-core/src/providers/ollama.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Ollama } from 'ollama'; - -import type { EmbeddingResult, OllamaConfig } from '../types.js'; - -export const generateEmbedding = async (args: { - config: OllamaConfig; - text: string; -}): Promise => { - const { config, text } = args; - const ollama = new Ollama({ host: config.host }); - - const response = await ollama.embed({ - model: config.model, - input: text, - }); - - return { - embedding: response.embeddings[0], - model: config.model, - provider: 'ollama', - }; -}; - -export const generateEmbeddings = async (args: { - config: OllamaConfig; - texts: string[]; -}): Promise => { - const { config, texts } = args; - const ollama = new Ollama({ host: config.host }); - - const response = await ollama.embed({ - model: config.model, - input: texts, - }); - - return response.embeddings.map((embedding) => { - return { - embedding, - model: config.model, - provider: 'ollama' as const, - }; - }); -}; diff --git a/packages/embeddings-core/src/providers/openai.ts b/packages/embeddings-core/src/providers/openai.ts deleted file mode 100644 index 57ceef3f..00000000 --- a/packages/embeddings-core/src/providers/openai.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { EmbeddingResult, OpenAIConfig } from '../types.js'; - -const DEFAULT_MODEL = 'text-embedding-3-small'; - -export const generateEmbedding = async (args: { - config: OpenAIConfig; - text: string; -}): Promise => { - const { config, text } = args; - const model = config.model || DEFAULT_MODEL; - - const response = await fetch('https://api.openai.com/v1/embeddings', { - method: 'POST', - headers: { - Authorization: `Bearer ${config.apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model, - input: text, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`OpenAI API error: ${error}`); - } - - const data = await response.json(); - - return { - embedding: data.data[0].embedding, - model, - provider: 'openai', - }; -}; - -export const generateEmbeddings = async (args: { - config: OpenAIConfig; - texts: string[]; -}): Promise => { - const { config, texts } = args; - const model = config.model || DEFAULT_MODEL; - - const response = await fetch('https://api.openai.com/v1/embeddings', { - method: 'POST', - headers: { - Authorization: `Bearer ${config.apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model, - input: texts, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`OpenAI API error: ${error}`); - } - - const data = await response.json(); - - return data.data.map((item: { embedding: number[] }) => { - return { - embedding: item.embedding, - model, - provider: 'openai' as const, - }; - }); -}; diff --git a/packages/embeddings-core/src/types.ts b/packages/embeddings-core/src/types.ts deleted file mode 100644 index 5a9a8c18..00000000 --- a/packages/embeddings-core/src/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type EmbeddingProvider = 'ollama' | 'openai'; - -export interface OllamaConfig { - model: string; - host?: string; -} - -export interface OpenAIConfig { - apiKey: string; - model?: string; -} - -export interface EmbeddingConfig { - provider: EmbeddingProvider; - ollama?: OllamaConfig; - openai?: OpenAIConfig; -} - -export interface EmbeddingResult { - embedding: number[]; - model: string; - provider: EmbeddingProvider; -} diff --git a/packages/embeddings-core/tsconfig.json b/packages/embeddings-core/tsconfig.json deleted file mode 100644 index abc66b3b..00000000 --- a/packages/embeddings-core/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/embeddings-core/tsup.config.ts b/packages/embeddings-core/tsup.config.ts deleted file mode 100644 index b2b5fef7..00000000 --- a/packages/embeddings-core/tsup.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { tsupConfig } from '@ttoss/config'; - -export default tsupConfig({ - entryPoints: ['src/index.ts'], -}); diff --git a/packages/files-core/CHANGELOG.md b/packages/files-core/CHANGELOG.md deleted file mode 100644 index d1cd0504..00000000 --- a/packages/files-core/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# 0.0.0-alpha.2 (2026-01-06) - -### Bug Fixes - -* add version ([de8fab4](https://github.com/ttoss/soat/commit/de8fab4e0d51ba0e06e0b29f9b26ea8d147d92a6)) - -### Features - -* database working ([5a5d34d](https://github.com/ttoss/soat/commit/5a5d34d5820c0279b14f3a135b9a55f728cf8f65)) -* files rest api ([957c8b0](https://github.com/ttoss/soat/commit/957c8b0aa2b5a1b96dd3da789be2552e7bf34599)) diff --git a/packages/files-core/package.json b/packages/files-core/package.json deleted file mode 100644 index afd5f345..00000000 --- a/packages/files-core/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@soat/files-core", - "version": "0.0.0-alpha.2", - "description": "Core package for saving and managing files with local, S3, and GCS storage", - "exports": "./src/index.ts", - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "test": "jest" - }, - "dependencies": { - "@google-cloud/storage": "^7.0.0", - "@soat/postgresdb": "workspace:*", - "@ttoss/postgresdb": "^0.3.0", - "aws-sdk": "^2.0.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@ttoss/config": "^1.35.12", - "@types/node": "^25.0.3", - "@types/uuid": "^10.0.0", - "jest": "^30.2.0", - "tsup": "^8.5.1", - "typescript": "~5.9.3" - } -} diff --git a/packages/files-core/src/database.ts b/packages/files-core/src/database.ts deleted file mode 100644 index 00db128b..00000000 --- a/packages/files-core/src/database.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { models } from './db'; -import type { FileRecord, StorageConfig, UploadOptions } from './types'; - -export const saveFileRecord = async (args: { - id: string; - filename?: string; - contentType?: string; - size?: number; - storageType: StorageConfig['type']; - storagePath: string; - metadata?: Record; -}): Promise => { - const file = await models.File.create({ - ...args, - metadata: args.metadata ? JSON.stringify(args.metadata) : undefined, - } as any); - - return { - id: file.id, - filename: file.filename, - contentType: file.contentType, - size: file.size, - storageType: file.storageType, - storagePath: file.storagePath, - metadata: file.metadata ? JSON.parse(file.metadata) : undefined, - createdAt: file.createdAt, - updatedAt: file.updatedAt, - }; -}; - -export const getFileRecord = async (id: string): Promise => { - const file = await models.File.findByPk(id); - - if (!file) { - return null; - } - - return { - id: file.id, - filename: file.filename, - contentType: file.contentType, - size: file.size, - storageType: file.storageType, - storagePath: file.storagePath, - metadata: file.metadata ? JSON.parse(file.metadata) : undefined, - createdAt: file.createdAt, - updatedAt: file.updatedAt, - }; -}; - -export const updateFileRecord = async ( - id: string, - updates: Partial< - Pick - > -): Promise => { - const file = await models.File.findByPk(id); - - if (!file) { - return null; - } - - const updateData: any = {}; - if (updates.filename !== undefined) updateData.filename = updates.filename; - if (updates.contentType !== undefined) - updateData.contentType = updates.contentType; - if (updates.size !== undefined) updateData.size = updates.size; - if (updates.metadata !== undefined) { - updateData.metadata = updates.metadata - ? JSON.stringify(updates.metadata) - : null; - } - - await file.update(updateData); - - return getFileRecord(id); -}; - -export const deleteFileRecord = async (id: string): Promise => { - const file = await models.File.findByPk(id); - - if (!file) { - return false; - } - - await file.destroy(); - return true; -}; - -export const listFileRecords = async (): Promise => { - const files = await models.File.findAll(); - - return files.map((file) => { - return { - id: file.id, - filename: file.filename, - contentType: file.contentType, - size: file.size, - storageType: file.storageType, - storagePath: file.storagePath, - metadata: file.metadata ? JSON.parse(file.metadata) : undefined, - createdAt: file.createdAt, - updatedAt: file.updatedAt, - }; - }); -}; diff --git a/packages/files-core/src/db.ts b/packages/files-core/src/db.ts deleted file mode 100644 index 572ad4d2..00000000 --- a/packages/files-core/src/db.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { models } from '@soat/postgresdb'; -import { initialize } from '@ttoss/postgresdb'; - -export const initializeDatabase = async () => { - return initialize({ models }); -}; - -export { models }; diff --git a/packages/files-core/src/files.ts b/packages/files-core/src/files.ts deleted file mode 100644 index fc021899..00000000 --- a/packages/files-core/src/files.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { promises as fs } from 'node:fs'; - -import { v4 as uuidv4 } from 'uuid'; - -import { deleteFileRecord, getFileRecord, saveFileRecord } from './database'; -import * as gcs from './storage/gcs'; -import * as local from './storage/local'; -import * as s3 from './storage/s3'; -import type { FileData, StorageConfig, UploadOptions } from './types'; - -const getStorage = (args: { config: StorageConfig }) => { - const { config } = args; - switch (config.type) { - case 'local': - return local; - case 's3': - return s3; - case 'gcs': - return gcs; - default: - throw new Error(`Unsupported storage type: ${config.type}`); - } -}; - -export const saveFile = async (args: { - config: StorageConfig; - content: string | Buffer; - options?: UploadOptions; -}): Promise => { - const { config, content, options } = args; - const id = uuidv4(); - const storage = getStorage({ config }); - await storage.save({ id, content, config }); - - // Save file record to database - await saveFileRecord({ - id, - contentType: options?.contentType, - size: Buffer.isBuffer(content) - ? content.length - : Buffer.byteLength(content), - storageType: config.type, - storagePath: id, - metadata: options?.metadata, - }); - - return { id, content }; -}; - -export const uploadFile = async (args: { - config: StorageConfig; - filePath: string; - options?: UploadOptions; -}): Promise => { - const { config, filePath, options } = args; - const content = await fs.readFile(filePath); - const filename = - (options?.metadata?.filename as string) || filePath.split('/').pop(); - return saveFile({ - config, - content, - options: { ...options, metadata: { ...options?.metadata, filename } }, - }); -}; - -export const retrieveFileById = async (args: { - config: StorageConfig; - id: string; -}): Promise => { - const { config, id } = args; - try { - const storage = getStorage({ config }); - const content = await storage.retrieve({ id, config }); - return { id, content }; - } catch { - return null; - } -}; - -export const deleteFile = async (args: { - config: StorageConfig; - id: string; -}): Promise => { - const { config, id } = args; - - // Delete from storage - const storage = getStorage({ config }); - await storage.deleteFile({ id, config }); - - // Delete from database - return deleteFileRecord(id); -}; diff --git a/packages/files-core/src/index.ts b/packages/files-core/src/index.ts deleted file mode 100644 index 906c8d20..00000000 --- a/packages/files-core/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './database'; -export * from './db'; -export * from './files'; -export * from './types'; diff --git a/packages/files-core/src/storage/gcs.ts b/packages/files-core/src/storage/gcs.ts deleted file mode 100644 index db6cc7d0..00000000 --- a/packages/files-core/src/storage/gcs.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Storage } from '@google-cloud/storage'; - -import type { StorageConfig } from '../types'; - -export const save = async (args: { - id: string; - content: string | Buffer; - config: StorageConfig; -}): Promise => { - const { id, content, config } = args; - if (!config.gcs) { - throw new Error('GCS config not provided'); - } - const storage = new Storage({ - keyFilename: config.gcs.keyFilename, - projectId: config.gcs.projectId, - }); - const bucket = storage.bucket(config.gcs.bucket); - const file = bucket.file(id); - await file.save(content); -}; - -export const retrieve = async (args: { - id: string; - config: StorageConfig; -}): Promise => { - const { id, config } = args; - if (!config.gcs) { - throw new Error('GCS config not provided'); - } - const storage = new Storage({ - keyFilename: config.gcs.keyFilename, - projectId: config.gcs.projectId, - }); - const bucket = storage.bucket(config.gcs.bucket); - const file = bucket.file(id); - const [buffer] = await file.download(); - return buffer; -}; - -export const deleteFile = async (args: { - id: string; - config: StorageConfig; -}): Promise => { - const { id, config } = args; - if (!config.gcs) { - throw new Error('GCS config not provided'); - } - const storage = new Storage({ - keyFilename: config.gcs.keyFilename, - projectId: config.gcs.projectId, - }); - const bucket = storage.bucket(config.gcs.bucket); - const file = bucket.file(id); - await file.delete(); -}; diff --git a/packages/files-core/src/storage/index.ts b/packages/files-core/src/storage/index.ts deleted file mode 100644 index 90a80466..00000000 --- a/packages/files-core/src/storage/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './local'; -export * from './s3'; -export * from './gcs'; diff --git a/packages/files-core/src/storage/local.ts b/packages/files-core/src/storage/local.ts deleted file mode 100644 index 0c0ddcc6..00000000 --- a/packages/files-core/src/storage/local.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; - -import type { StorageConfig } from '../types'; - -export const save = async (args: { - id: string; - content: string | Buffer; - config: StorageConfig; -}): Promise => { - const { id, content, config } = args; - if (!config.local) { - throw new Error('Local config not provided'); - } - const filePath = path.join(config.local.path, id); - // Ensure directory exists - await fs.mkdir(config.local.path, { recursive: true }); - await fs.writeFile(filePath, content); -}; - -export const retrieve = async (args: { - id: string; - config: StorageConfig; -}): Promise => { - const { id, config } = args; - if (!config.local) { - throw new Error('Local config not provided'); - } - const filePath = path.join(config.local.path, id); - return fs.readFile(filePath); -}; - -export const deleteFile = async (args: { - id: string; - config: StorageConfig; -}): Promise => { - const { id, config } = args; - if (!config.local) { - throw new Error('Local config not provided'); - } - const filePath = path.join(config.local.path, id); - await fs.unlink(filePath); -}; diff --git a/packages/files-core/src/storage/s3.ts b/packages/files-core/src/storage/s3.ts deleted file mode 100644 index d5aab773..00000000 --- a/packages/files-core/src/storage/s3.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { S3 } from 'aws-sdk'; - -import type { StorageConfig } from '../types'; - -export const save = async (args: { - id: string; - content: string | Buffer; - config: StorageConfig; -}): Promise => { - const { id, content, config } = args; - if (!config.s3) { - throw new Error('S3 config not provided'); - } - const s3 = new S3({ - region: config.s3.region, - accessKeyId: config.s3.accessKeyId, - secretAccessKey: config.s3.secretAccessKey, - }); - await s3 - .putObject({ - Bucket: config.s3.bucket, - Key: id, - Body: content, - }) - .promise(); -}; - -export const retrieve = async (args: { - id: string; - config: StorageConfig; -}): Promise => { - const { id, config } = args; - if (!config.s3) { - throw new Error('S3 config not provided'); - } - const s3 = new S3({ - region: config.s3.region, - accessKeyId: config.s3.accessKeyId, - secretAccessKey: config.s3.secretAccessKey, - }); - const data = await s3 - .getObject({ - Bucket: config.s3.bucket, - Key: id, - }) - .promise(); - return data.Body as Buffer; -}; - -export const deleteFile = async (args: { - id: string; - config: StorageConfig; -}): Promise => { - const { id, config } = args; - if (!config.s3) { - throw new Error('S3 config not provided'); - } - const s3 = new S3({ - region: config.s3.region, - accessKeyId: config.s3.accessKeyId, - secretAccessKey: config.s3.secretAccessKey, - }); - await s3 - .deleteObject({ - Bucket: config.s3.bucket, - Key: id, - }) - .promise(); -}; diff --git a/packages/files-core/src/types.ts b/packages/files-core/src/types.ts deleted file mode 100644 index dfbc8652..00000000 --- a/packages/files-core/src/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -export interface StorageConfig { - type: 'local' | 's3' | 'gcs'; - local?: { - path: string; - }; - s3?: { - bucket: string; - region: string; - accessKeyId: string; - secretAccessKey: string; - }; - gcs?: { - bucket: string; - keyFilename?: string; - projectId?: string; - }; -} - -export interface UploadOptions { - contentType?: string; - metadata?: Record; -} - -export interface FileData { - id: string; - content: string | Buffer; -} - -export interface FileRecord { - id: string; - filename?: string; - contentType?: string; - size?: number; - storageType: 'local' | 's3' | 'gcs'; - storagePath: string; - metadata?: Record; - createdAt: Date; - updatedAt: Date; -} diff --git a/packages/files-core/tsconfig.json b/packages/files-core/tsconfig.json deleted file mode 100644 index fce4d054..00000000 --- a/packages/files-core/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@ttoss/config/tsconfig.json" -} diff --git a/packages/files-core/tsup.config.ts b/packages/files-core/tsup.config.ts deleted file mode 100644 index 8db39855..00000000 --- a/packages/files-core/tsup.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tsupConfig } from '@ttoss/config'; - -export default tsupConfig(); diff --git a/packages/postgresdb/jest.config.ts b/packages/postgresdb/jest.config.ts deleted file mode 100644 index c94e5234..00000000 --- a/packages/postgresdb/jest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { jestRootConfig } from '@ttoss/config'; - -export default jestRootConfig(); diff --git a/packages/postgresdb/package.json b/packages/postgresdb/package.json index dd48d37c..6bf7142f 100644 --- a/packages/postgresdb/package.json +++ b/packages/postgresdb/package.json @@ -4,12 +4,14 @@ "version": "0.0.0-alpha.2", "description": "Database models and operations for SOAT packages", "type": "module", - "exports": "./src/index.ts", - "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/esm/index.js" + } + }, "scripts": { "build": "tsup", - "pretest": "pnpm run build", - "test": "jest --projects tests/unit", "sync": "ttoss-postgresdb sync", "db-dev:start": "docker compose -f docker-compose.dev.yml up -d", "db-dev:stop": "docker compose -f docker-compose.dev.yml down", @@ -17,17 +19,15 @@ "db-dev:logs": "docker compose -f docker-compose.dev.yml logs -f" }, "dependencies": { - "@ttoss/postgresdb": "^0.3.0" + "@ttoss/postgresdb": "^0.8.0" }, "devDependencies": { - "@testcontainers/postgresql": "^11.11.0", - "@ttoss/config": "^1.35.12", - "@ttoss/postgresdb-cli": "^0.1.24", - "@ttoss/test-utils": "^4.0.2", - "@types/jest": "^30.0.0", - "@types/node": "^25.0.3", - "jest": "^30.2.0", + "@testcontainers/postgresql": "^11.13.0", + "@ttoss/config": "^1.37.8", + "@ttoss/postgresdb-cli": "^0.2.8", + "@ttoss/test-utils": "^4.2.8", + "@types/node": "^25.5.2", "tsup": "^8.5.1", - "typescript": "~5.9.3" + "typescript": "~6.0.2" } } diff --git a/packages/postgresdb/src/models/Document.ts b/packages/postgresdb/src/models/Document.ts index d17631da..0434e697 100644 --- a/packages/postgresdb/src/models/Document.ts +++ b/packages/postgresdb/src/models/Document.ts @@ -24,6 +24,12 @@ export class Document extends Model { @Column({ type: DataType.VECTOR(1536) }) declare embedding?: number[]; + @Column({ type: DataType.UUID }) + declare conversationId?: string; + + @Column({ type: DataType.INTEGER }) + declare position?: number; + @Column({ type: DataType.TEXT }) declare metadata?: string; // JSON string diff --git a/packages/postgresdb/tests/tsconfig.json b/packages/postgresdb/tests/tsconfig.json deleted file mode 100644 index bcb13129..00000000 --- a/packages/postgresdb/tests/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@ttoss/config/tsconfig.test.json", - "compilerOptions": { - "paths": { - "dist/*": ["../dist/*"], - "src/*": ["../src/*"], - "tests/*": ["./*"] - } - } -} diff --git a/packages/postgresdb/tests/unit/babel.config.cjs b/packages/postgresdb/tests/unit/babel.config.cjs deleted file mode 100644 index 9c95923b..00000000 --- a/packages/postgresdb/tests/unit/babel.config.cjs +++ /dev/null @@ -1,5 +0,0 @@ -const { babelConfig } = require('@ttoss/config'); - -const config = babelConfig({}); - -module.exports = config; diff --git a/packages/postgresdb/tests/unit/jest.config.ts b/packages/postgresdb/tests/unit/jest.config.ts deleted file mode 100644 index b44e2f2b..00000000 --- a/packages/postgresdb/tests/unit/jest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { jestUnitConfig } from '@ttoss/config'; -import { getTransformIgnorePatterns } from '@ttoss/test-utils'; - -export default jestUnitConfig({ - transformIgnorePatterns: getTransformIgnorePatterns({ - esmModules: ['@ttoss/postgresdb'], - }), -}); diff --git a/packages/postgresdb/tests/unit/tests/File.test.ts b/packages/postgresdb/tests/unit/tests/File.test.ts deleted file mode 100644 index 87e9f477..00000000 --- a/packages/postgresdb/tests/unit/tests/File.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql'; -import { PostgreSqlContainer } from '@testcontainers/postgresql'; -import type { Sequelize } from '@ttoss/postgresdb'; -import { initialize } from '@ttoss/postgresdb'; -import { models } from 'dist/index'; - -let sequelize: Sequelize; -let postgresContainer: StartedPostgreSqlContainer; - -jest.setTimeout(60000); - -beforeAll(async () => { - // Start PostgreSQL container - postgresContainer = await new PostgreSqlContainer( - 'pgvector/pgvector:0.8.1-pg18-trixie' - ).start(); - - // Initialize database with container credentials - const db = await initialize({ - createVectorExtension: true, - models, - logging: false, - username: postgresContainer.getUsername(), - password: postgresContainer.getPassword(), - database: postgresContainer.getDatabase(), - host: postgresContainer.getHost(), - port: postgresContainer.getPort(), - }); - - sequelize = db.sequelize; - - // Sync database schema - await sequelize.sync(); -}); - -afterAll(async () => { - await sequelize.close(); - await postgresContainer.stop(); -}); - -describe('File model', () => { - test('should create and retrieve file', async () => { - const metadata = { uploadedBy: 'test-user' }; - const fileData = { - filename: 'test.txt', - contentType: 'text/plain', - size: 1024, - storageType: 'local' as const, - storagePath: '/tmp/test-file-id', - metadata: JSON.stringify(metadata), - }; - - const file = await models.File.create(fileData); - - expect(file.filename).toBe(fileData.filename); - expect(file.contentType).toBe(fileData.contentType); - expect(file.size).toBe(fileData.size); - expect(file.storageType).toBe(fileData.storageType); - expect(file.storagePath).toBe(fileData.storagePath); - expect(JSON.parse(file.metadata!)).toEqual(metadata); - - const foundFile = await models.File.findByPk(file.id); - - expect(foundFile).toMatchObject({ - id: file.id, - filename: fileData.filename, - contentType: fileData.contentType, - size: fileData.size, - storageType: fileData.storageType, - storagePath: fileData.storagePath, - }); - expect(JSON.parse(foundFile!.metadata!)).toEqual(metadata); - }); - - test('should update file metadata', async () => { - const fileData = { - filename: 'update-test.txt', - contentType: 'text/plain', - size: 512, - storageType: 's3' as const, - storagePath: 's3://bucket/update-test-file-id', - }; - - const file = await models.File.create(fileData); - - // Update metadata - const newMetadata = { updatedBy: 'admin', version: 2 }; - await file.update({ - metadata: JSON.stringify(newMetadata), - size: 1024, - }); - - const updatedFile = await models.File.findByPk(file.id); - - expect(updatedFile!.size).toBe(1024); - expect(JSON.parse(updatedFile!.metadata!)).toEqual(newMetadata); - }); - - test('should delete file', async () => { - const fileData = { - filename: 'delete-test.txt', - contentType: 'application/json', - size: 256, - storageType: 'gcs' as const, - storagePath: 'gs://bucket/delete-test-file-id', - }; - - const file = await models.File.create(fileData); - - const createdFile = await models.File.findByPk(file.id); - expect(createdFile).toBeTruthy(); - - await createdFile!.destroy(); - - const deletedFile = await models.File.findByPk(file.id); - expect(deletedFile).toBeNull(); - }); - - test('should handle files without metadata', async () => { - const fileData = { - contentType: 'image/png', - size: 2048, - storageType: 'local' as const, - storagePath: '/tmp/no-metadata-file-id', - }; - - const file = await models.File.create(fileData); - - expect(file.metadata).toBeNull(); - - const foundFile = await models.File.findByPk(file.id); - expect(foundFile!.metadata).toBeNull(); - }); -}); diff --git a/packages/server/package.json b/packages/server/package.json index 2930437b..418fc604 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -6,28 +6,27 @@ "build": "tsup", "dev": "tsx watch src/server.ts", "lint-openapi": "spectral lint packages/server/src/rest/openapi/**/*.yaml", + "pretest": "pnpm run --filter @soat/postgresdb build", "test": "jest --projects tests/unit" }, "dependencies": { - "@soat/documents-core": "workspace:*", - "@soat/embeddings-core": "workspace:*", - "@soat/files-core": "workspace:*", "@soat/postgresdb": "workspace:*", - "@ttoss/http-server": "^0.3.2", - "@ttoss/http-server-mcp": "^0.3.2", - "@ttoss/postgresdb": "^0.3.0", - "dotenv": "^17.2.3", + "@ttoss/http-server": "^0.5.8", + "@ttoss/http-server-mcp": "^0.11.0", + "@ttoss/postgresdb": "^0.8.0", + "dotenv": "^17.4.1", "ollama": "^0.6.3", - "pg": "^8.16.3" + "pg": "^8.20.0" }, "devDependencies": { + "@testcontainers/postgresql": "^11.13.0", "@stoplight/spectral-cli": "^6.15.0", - "@ttoss/config": "^1.35.12", - "@ttoss/test-utils": "^4.0.2", + "@ttoss/config": "^1.37.8", + "@ttoss/test-utils": "^4.2.8", "@types/jest": "^30.0.0", - "@types/pg": "^8.16.0", - "@types/supertest": "^6.0.3", - "jest": "^30.2.0", + "@types/pg": "^8.20.0", + "@types/supertest": "^7.2.0", + "jest": "^30.3.0", "supertest": "^7.2.2", "tsup": "^8.5.1", "tsx": "^4.21.0" diff --git a/packages/server/src/Context.ts b/packages/server/src/Context.ts index f9f078e2..644d0b9b 100644 --- a/packages/server/src/Context.ts +++ b/packages/server/src/Context.ts @@ -1,2 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export type Context = any; +import type { DB } from './db'; + +export type Context = { + db: DB; + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} & Record; diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 9c3b2ffb..e4aae3f1 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -1,6 +1,6 @@ import { App, bodyParser, cors } from '@ttoss/http-server'; -// import { mcpRouter } from './mcp'; +import { mcpRouter } from './mcp/server'; import { restRouter } from './rest/router'; const app = new App(); @@ -8,9 +8,10 @@ const app = new App(); app.use(cors()); app.use(bodyParser()); -// app.use(mcpRouter.routes()); - app.use(restRouter.routes()); app.use(restRouter.allowedMethods()); +app.use(mcpRouter.routes()); +app.use(mcpRouter.allowedMethods()); + export { app }; diff --git a/packages/server/src/db.ts b/packages/server/src/db.ts new file mode 100644 index 00000000..7d7e05da --- /dev/null +++ b/packages/server/src/db.ts @@ -0,0 +1,25 @@ +import { models } from '@soat/postgresdb'; +import type { App } from '@ttoss/http-server'; +import { initialize } from '@ttoss/postgresdb'; + +export { models }; + +export type DB = Awaited>>; + +export let db: DB; + +export const initializeDatabase = async (app: App) => { + db = await initialize({ + models, + createVectorExtension: true, + host: process.env.DATABASE_HOST, + port: Number(process.env.DATABASE_PORT), + database: process.env.DATABASE_NAME, + username: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + }); + + app.context.db = db; + + return db; +}; diff --git a/packages/server/src/lib/files.ts b/packages/server/src/lib/files.ts new file mode 100644 index 00000000..7b54f108 --- /dev/null +++ b/packages/server/src/lib/files.ts @@ -0,0 +1,72 @@ +import { db } from '../db'; + +export const listFiles = async () => { + const allFiles = await db.File.findAll(); + return allFiles.map((file) => { + return { + id: file.id, + filename: file.filename, + contentType: file.contentType, + size: file.size, + storageType: file.storageType, + storagePath: file.storagePath, + metadata: file.metadata, + createdAt: file.createdAt, + updatedAt: file.updatedAt, + }; + }); +}; + +export const getFile = async (args: { id: string }) => { + const file = await db.File.findByPk(args.id); + + if (!file) { + return null; + } + + return { + id: file.id, + filename: file.filename, + contentType: file.contentType, + size: file.size, + storageType: file.storageType, + storagePath: file.storagePath, + metadata: file.metadata, + createdAt: file.createdAt, + updatedAt: file.updatedAt, + }; +}; + +export const createFile = async (args: { + filename?: string; + contentType?: string; + size?: number; + storageType: 'local' | 's3' | 'gcs'; + storagePath: string; + metadata?: string; +}) => { + const file = await db.File.create(args); + + return { + id: file.id, + filename: file.filename, + contentType: file.contentType, + size: file.size, + storageType: file.storageType, + storagePath: file.storagePath, + metadata: file.metadata, + createdAt: file.createdAt, + updatedAt: file.updatedAt, + }; +}; + +export const deleteFile = async (args: { id: string }) => { + const file = await db.File.findByPk(args.id); + + if (!file) { + return null; + } + + await file.destroy(); + return true; +}; diff --git a/packages/server/src/mcp/index.ts b/packages/server/src/mcp/index.ts deleted file mode 100644 index 7b91cca8..00000000 --- a/packages/server/src/mcp/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { mcpRouter } from './server'; diff --git a/packages/server/src/mcp/prompts/index.ts b/packages/server/src/mcp/prompts/index.ts deleted file mode 100644 index 8e19d816..00000000 --- a/packages/server/src/mcp/prompts/index.ts +++ /dev/null @@ -1 +0,0 @@ -// MCP prompts will be defined here diff --git a/packages/server/src/mcp/resources/index.ts b/packages/server/src/mcp/resources/index.ts deleted file mode 100644 index 29a85229..00000000 --- a/packages/server/src/mcp/resources/index.ts +++ /dev/null @@ -1 +0,0 @@ -// MCP resources will be defined here diff --git a/packages/server/src/mcp/server.ts b/packages/server/src/mcp/server.ts index 8bfdc788..6183bcf0 100644 --- a/packages/server/src/mcp/server.ts +++ b/packages/server/src/mcp/server.ts @@ -1,74 +1,22 @@ -import { createMcpRouter, Server as McpServer } from '@ttoss/http-server-mcp'; +import { createMcpRouter, McpServer } from '@ttoss/http-server-mcp'; -import pkg from '../../package.json' with { type: 'json' }; -import { - createDocumentTool, - deleteDocumentTool, - getDocumentTool, - listDocumentsTool, - searchDocumentsTool, - updateDocumentTool, -} from './tools'; +import { registerTools } from './tools'; const mcpServer = new McpServer({ - name: 'soat-server', - version: pkg.version, + name: 'soat', + version: '1.0.0', }); -mcpServer.registerTool( - listDocumentsTool.name, - { - description: listDocumentsTool.description, - inputSchema: listDocumentsTool.inputSchema, - }, - listDocumentsTool.handler -); - -mcpServer.registerTool( - createDocumentTool.name, - { - description: createDocumentTool.description, - inputSchema: createDocumentTool.inputSchema, - }, - createDocumentTool.handler -); +registerTools(mcpServer); -mcpServer.registerTool( - getDocumentTool.name, - { - description: getDocumentTool.description, - inputSchema: getDocumentTool.inputSchema, +const mcpRouter = createMcpRouter(mcpServer, { + // eslint-disable-next-line turbo/no-undeclared-env-vars + apiBaseUrl: `http://localhost:${process.env.PORT || 5047}/api/v1`, + getApiHeaders: (ctx) => { + return { + authorization: (ctx.headers.authorization as string) ?? '', + }; }, - getDocumentTool.handler -); - -mcpServer.registerTool( - updateDocumentTool.name, - { - description: updateDocumentTool.description, - inputSchema: updateDocumentTool.inputSchema, - }, - updateDocumentTool.handler -); - -mcpServer.registerTool( - deleteDocumentTool.name, - { - description: deleteDocumentTool.description, - inputSchema: deleteDocumentTool.inputSchema, - }, - deleteDocumentTool.handler -); - -mcpServer.registerTool( - searchDocumentsTool.name, - { - description: searchDocumentsTool.description, - inputSchema: searchDocumentsTool.inputSchema, - }, - searchDocumentsTool.handler -); - -export const mcpRouter = createMcpRouter(mcpServer, { - path: '/mcp', }); + +export { mcpRouter }; diff --git a/packages/server/src/mcp/tools.ts b/packages/server/src/mcp/tools.ts new file mode 100644 index 00000000..0552ebbb --- /dev/null +++ b/packages/server/src/mcp/tools.ts @@ -0,0 +1,69 @@ +import type { McpServer } from '@ttoss/http-server-mcp'; +import { apiCall, z } from '@ttoss/http-server-mcp'; + +const registerTools = (server: McpServer) => { + server.registerTool( + 'list-files', + { + description: 'List all files', + inputSchema: {}, + }, + async () => { + const data = await apiCall('GET', '/files'); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'get-file', + { + description: 'Get a file by ID', + inputSchema: { + id: z.string().describe('File ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/files/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'create-file', + { + description: 'Create a new file record', + inputSchema: { + storageType: z + .enum(['local', 's3', 'gcs']) + .describe('Storage backend type'), + storagePath: z.string().describe('Path in the storage backend'), + filename: z.string().optional().describe('Original filename'), + contentType: z.string().optional().describe('MIME content type'), + size: z.number().optional().describe('File size in bytes'), + metadata: z.string().optional().describe('Additional metadata'), + }, + }, + async (body) => { + const data = await apiCall('POST', '/files', { body }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'delete-file', + { + description: 'Delete a file by ID', + inputSchema: { + id: z.string().describe('File ID'), + }, + }, + async ({ id }) => { + await apiCall('DELETE', `/files/${id}`); + return { + content: [{ type: 'text', text: `File ${id} deleted successfully` }], + }; + } + ); +}; + +export { registerTools }; diff --git a/packages/server/src/mcp/tools/documents.ts b/packages/server/src/mcp/tools/documents.ts deleted file mode 100644 index 8ab21699..00000000 --- a/packages/server/src/mcp/tools/documents.ts +++ /dev/null @@ -1,428 +0,0 @@ -import { - createDocument, - deleteDocument, - type EmbeddingConfig, - getDocument, - listDocuments, - searchDocumentsBySimilarity, - type StorageConfig, - updateDocument, -} from '@soat/documents-core'; -import { getConfigFromEnv } from '@soat/embeddings-core'; -import { z } from '@ttoss/http-server-mcp'; - -const defaultStorageConfig: StorageConfig = { - type: 'local', - local: { - path: '/tmp/documents', - }, -}; - -const getEmbeddingConfig = (): EmbeddingConfig | undefined => { - try { - return getConfigFromEnv(); - } catch { - return undefined; - } -}; - -export const listDocumentsTool = { - name: 'list-documents', - description: 'List all documents', - inputSchema: z.object({}), - handler: async () => { - try { - const documents = await listDocuments(); - return { - content: [ - { - type: 'text', - text: JSON.stringify( - documents.map((doc) => { - return { - id: doc.id, - title: doc.title, - fileId: doc.fileId, - embeddingModel: doc.embeddingModel, - embeddingProvider: doc.embeddingProvider, - metadata: doc.metadata, - createdAt: doc.createdAt, - updatedAt: doc.updatedAt, - }; - }), - null, - 2 - ), - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error listing documents: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - }, - ], - }; - } - }, -}; - -export const createDocumentTool = { - name: 'create-document', - description: 'Create a new document', - inputSchema: z.object({ - content: z.string().describe('The content of the document'), - title: z - .string() - .optional() - .describe('The title of the document (optional)'), - metadata: z - .record(z.any()) - .optional() - .describe('Additional metadata for the document (optional)'), - generateEmbedding: z - .boolean() - .optional() - .describe('Whether to generate embeddings for the document (optional)'), - }), - handler: async (args: { - content: string; - title?: string; - metadata?: Record; - generateEmbedding?: boolean; - }) => { - try { - const embeddingConfig = getEmbeddingConfig(); - - const document = await createDocument({ - storageConfig: defaultStorageConfig, - embeddingConfig, - content: args.content, - options: { - title: args.title, - metadata: args.metadata, - generateEmbedding: args.generateEmbedding, - }, - }); - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - id: document.id, - title: document.title, - fileId: document.fileId, - embeddingModel: document.embeddingModel, - embeddingProvider: document.embeddingProvider, - hasEmbedding: !!document.embedding, - metadata: document.metadata, - createdAt: document.createdAt, - updatedAt: document.updatedAt, - }, - null, - 2 - ), - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error creating document: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - }, - ], - }; - } - }, -}; - -export const getDocumentTool = { - name: 'get-document', - description: 'Get a document by ID', - inputSchema: z.object({ - id: z.string().describe('The ID of the document to retrieve'), - }), - handler: async (args: { id: string }) => { - try { - const document = await getDocument({ - storageConfig: defaultStorageConfig, - id: args.id, - }); - - if (!document) { - return { - content: [ - { - type: 'text', - text: 'Document not found', - }, - ], - }; - } - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - id: document.id, - title: document.title, - fileId: document.fileId, - content: document.content?.toString(), - embeddingModel: document.embeddingModel, - embeddingProvider: document.embeddingProvider, - hasEmbedding: !!document.embedding, - metadata: document.metadata, - createdAt: document.createdAt, - updatedAt: document.updatedAt, - }, - null, - 2 - ), - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error getting document: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - }, - ], - }; - } - }, -}; - -export const updateDocumentTool = { - name: 'update-document', - description: 'Update an existing document', - inputSchema: z.object({ - id: z.string().describe('The ID of the document to update'), - content: z - .string() - .optional() - .describe('The new content of the document (optional)'), - title: z - .string() - .optional() - .describe('The new title of the document (optional)'), - metadata: z - .record(z.any()) - .optional() - .describe('The new metadata for the document (optional)'), - regenerateEmbedding: z - .boolean() - .optional() - .describe('Whether to regenerate embeddings for the document (optional)'), - }), - handler: async (args: { - id: string; - content?: string; - title?: string; - metadata?: Record; - regenerateEmbedding?: boolean; - }) => { - try { - const embeddingConfig = getEmbeddingConfig(); - - const document = await updateDocument({ - storageConfig: defaultStorageConfig, - embeddingConfig, - id: args.id, - content: args.content, - title: args.title, - metadata: args.metadata, - regenerateEmbedding: args.regenerateEmbedding, - }); - - if (!document) { - return { - content: [ - { - type: 'text', - text: 'Document not found', - }, - ], - }; - } - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - id: document.id, - title: document.title, - fileId: document.fileId, - content: document.content?.toString(), - embeddingModel: document.embeddingModel, - embeddingProvider: document.embeddingProvider, - hasEmbedding: !!document.embedding, - metadata: document.metadata, - createdAt: document.createdAt, - updatedAt: document.updatedAt, - }, - null, - 2 - ), - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error updating document: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - }, - ], - }; - } - }, -}; - -export const deleteDocumentTool = { - name: 'delete-document', - description: 'Delete a document by ID', - inputSchema: z.object({ - id: z.string().describe('The ID of the document to delete'), - }), - handler: async (args: { id: string }) => { - try { - const deleted = await deleteDocument({ - storageConfig: defaultStorageConfig, - id: args.id, - }); - - if (!deleted) { - return { - content: [ - { - type: 'text', - text: 'Document not found', - }, - ], - }; - } - - return { - content: [ - { - type: 'text', - text: 'Document deleted successfully', - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error deleting document: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - }, - ], - }; - } - }, -}; - -export const searchDocumentsTool = { - name: 'search-documents', - description: 'Search documents by similarity using embeddings', - inputSchema: z.object({ - query: z.string().describe('The search query'), - limit: z - .number() - .optional() - .describe('Maximum number of documents to return (optional)'), - threshold: z - .number() - .optional() - .describe('Similarity threshold (optional)'), - }), - handler: async (args: { - query: string; - limit?: number; - threshold?: number; - }) => { - try { - const embeddingConfig = getEmbeddingConfig(); - if (!embeddingConfig) { - return { - content: [ - { - type: 'text', - text: 'Embedding configuration is required for search. Set EMBEDDINGS_OLLAMA_MODEL or EMBEDDINGS_OPENAI_KEY', - }, - ], - }; - } - - const documents = await searchDocumentsBySimilarity({ - storageConfig: defaultStorageConfig, - embeddingConfig, - query: args.query, - options: { - limit: args.limit, - threshold: args.threshold, - }, - }); - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - documents.map((doc) => { - return { - id: doc.id, - title: doc.title, - fileId: doc.fileId, - content: doc.content?.toString(), - embeddingModel: doc.embeddingModel, - embeddingProvider: doc.embeddingProvider, - metadata: doc.metadata, - createdAt: doc.createdAt, - updatedAt: doc.updatedAt, - }; - }), - null, - 2 - ), - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error searching documents: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - }, - ], - }; - } - }, -}; diff --git a/packages/server/src/mcp/tools/index.ts b/packages/server/src/mcp/tools/index.ts deleted file mode 100644 index ccf1cab7..00000000 --- a/packages/server/src/mcp/tools/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Export all MCP tools here -export { - createDocumentTool, - deleteDocumentTool, - getDocumentTool, - listDocumentsTool, - searchDocumentsTool, - updateDocumentTool, -} from './documents'; diff --git a/packages/server/src/rest/openapi/v1/documents.yaml b/packages/server/src/rest/openapi/v1/documents.yaml deleted file mode 100644 index c11b857b..00000000 --- a/packages/server/src/rest/openapi/v1/documents.yaml +++ /dev/null @@ -1,473 +0,0 @@ -openapi: 3.0.3 -info: - title: SOAT Documents API - version: 1.0.0 - description: API for managing documents with embeddings support - contact: - name: SOAT Team - url: https://github.com/ttoss/soat -servers: - - url: http://0.0.0.0:5047/api/v1 - description: Development server -paths: - /documents: - get: - tags: - - Documents - summary: List all documents - description: Returns a list of all documents stored in the system - operationId: listDocuments - responses: - '200': - description: Documents list returned successfully - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - documents: - type: array - items: - $ref: '#/components/schemas/DocumentRecord' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - Documents - summary: Create a new document - description: Creates a new document with optional embedding generation. The document content is saved as a markdown file. - operationId: createDocument - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - content - properties: - content: - type: string - description: Document content in markdown format - example: '# My Document\n\nThis is the content of my document.' - title: - type: string - description: Document title - example: 'My Document' - metadata: - type: object - additionalProperties: true - description: Additional metadata for the document - example: - author: 'John Doe' - tags: ['example', 'documentation'] - generateEmbedding: - type: boolean - description: Whether to generate embedding for the document (default true if embedding config is available) - default: true - responses: - '201': - description: Document created successfully - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - document: - $ref: '#/components/schemas/DocumentResponse' - '400': - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /documents/search: - get: - tags: - - Documents - summary: Search documents by similarity - description: Searches for documents similar to the query using vector similarity. Requires embedding configuration (EMBEDDINGS_OLLAMA_MODEL or EMBEDDINGS_OPENAI_KEY). - operationId: searchDocuments - parameters: - - name: query - in: query - required: true - description: Search query text - schema: - type: string - example: 'machine learning concepts' - - name: limit - in: query - required: false - description: Maximum number of results to return - schema: - type: integer - minimum: 1 - maximum: 100 - default: 10 - example: 5 - - name: threshold - in: query - required: false - description: Minimum similarity threshold (0-1) - schema: - type: number - minimum: 0 - maximum: 1 - example: 0.7 - responses: - '200': - description: Search results returned successfully - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - documents: - type: array - items: - $ref: '#/components/schemas/DocumentWithContent' - '400': - description: Bad request (query missing or embedding not configured) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /documents/{id}: - get: - tags: - - Documents - summary: Get a document by ID - description: Returns a document with its content - operationId: getDocument - parameters: - - name: id - in: path - required: true - description: Document ID - schema: - type: string - format: uuid - example: '550e8400-e29b-41d4-a716-446655440000' - responses: - '200': - description: Document returned successfully - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - document: - $ref: '#/components/schemas/DocumentWithContent' - '400': - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Document not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - put: - tags: - - Documents - summary: Update a document - description: Updates an existing document. If content is changed, a new file is created and the old one is deleted. Embedding can be regenerated. - operationId: updateDocument - parameters: - - name: id - in: path - required: true - description: Document ID - schema: - type: string - format: uuid - example: '550e8400-e29b-41d4-a716-446655440000' - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - content: - type: string - description: New document content in markdown format - example: '# Updated Document\n\nThis is the updated content.' - title: - type: string - description: New document title - example: 'Updated Title' - metadata: - type: object - additionalProperties: true - description: New metadata for the document - example: - author: 'Jane Doe' - version: 2 - regenerateEmbedding: - type: boolean - description: Whether to regenerate embedding when content changes (default true) - default: true - responses: - '200': - description: Document updated successfully - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - document: - $ref: '#/components/schemas/DocumentWithContent' - '400': - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Document not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Documents - summary: Delete a document - description: Deletes a document and its associated file - operationId: deleteDocument - parameters: - - name: id - in: path - required: true - description: Document ID - schema: - type: string - format: uuid - example: '550e8400-e29b-41d4-a716-446655440000' - responses: - '200': - description: Document deleted successfully - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - '400': - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Document not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' -components: - schemas: - DocumentRecord: - type: object - properties: - id: - type: string - format: uuid - description: Document ID - example: '550e8400-e29b-41d4-a716-446655440000' - title: - type: string - description: Document title - example: 'My Document' - fileId: - type: string - format: uuid - description: Associated file ID - example: '660e8400-e29b-41d4-a716-446655440001' - embeddingModel: - type: string - description: Model used to generate embedding - example: 'nomic-embed-text' - embeddingProvider: - type: string - description: Provider used for embedding - enum: ['ollama', 'openai'] - example: 'ollama' - hasEmbedding: - type: boolean - description: Whether the document has an embedding - example: true - metadata: - type: object - additionalProperties: true - description: Document metadata - example: - author: 'John Doe' - createdAt: - type: string - format: date-time - description: Creation timestamp - example: '2026-01-07T10:00:00Z' - updatedAt: - type: string - format: date-time - description: Last update timestamp - example: '2026-01-07T10:00:00Z' - DocumentResponse: - type: object - properties: - id: - type: string - format: uuid - description: Document ID - example: '550e8400-e29b-41d4-a716-446655440000' - title: - type: string - description: Document title - example: 'My Document' - fileId: - type: string - format: uuid - description: Associated file ID - example: '660e8400-e29b-41d4-a716-446655440001' - embeddingModel: - type: string - description: Model used to generate embedding - example: 'nomic-embed-text' - embeddingProvider: - type: string - description: Provider used for embedding - enum: ['ollama', 'openai'] - example: 'ollama' - hasEmbedding: - type: boolean - description: Whether the document has an embedding - example: true - metadata: - type: object - additionalProperties: true - description: Document metadata - example: - author: 'John Doe' - createdAt: - type: string - format: date-time - description: Creation timestamp - example: '2026-01-07T10:00:00Z' - updatedAt: - type: string - format: date-time - description: Last update timestamp - example: '2026-01-07T10:00:00Z' - DocumentWithContent: - type: object - properties: - id: - type: string - format: uuid - description: Document ID - example: '550e8400-e29b-41d4-a716-446655440000' - title: - type: string - description: Document title - example: 'My Document' - fileId: - type: string - format: uuid - description: Associated file ID - example: '660e8400-e29b-41d4-a716-446655440001' - content: - type: string - description: Document content - example: '# My Document\n\nThis is the content of my document.' - embeddingModel: - type: string - description: Model used to generate embedding - example: 'nomic-embed-text' - embeddingProvider: - type: string - description: Provider used for embedding - enum: ['ollama', 'openai'] - example: 'ollama' - metadata: - type: object - additionalProperties: true - description: Document metadata - example: - author: 'John Doe' - createdAt: - type: string - format: date-time - description: Creation timestamp - example: '2026-01-07T10:00:00Z' - updatedAt: - type: string - format: date-time - description: Last update timestamp - example: '2026-01-07T10:00:00Z' - ErrorResponse: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - description: Error message - example: 'An error occurred' diff --git a/packages/server/src/rest/openapi/v1/files.yaml b/packages/server/src/rest/openapi/v1/files.yaml index e0ef67e1..67054a2a 100644 --- a/packages/server/src/rest/openapi/v1/files.yaml +++ b/packages/server/src/rest/openapi/v1/files.yaml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: SOAT Files API version: 1.0.0 - description: API para gerenciar arquivos (Files resource) + description: API for managing files (Files resource) contact: name: SOAT Team url: https://github.com/ttoss/soat @@ -14,37 +14,30 @@ paths: get: tags: - Files - summary: Lista todos os arquivos - description: Retorna uma lista com todos os arquivos armazenados + summary: List all files + description: Returns a list of all stored files operationId: listFiles responses: '200': - description: Lista de arquivos retornada com sucesso + description: List of files returned successfully content: application/json: schema: - type: object - properties: - success: - type: boolean - example: true - files: - type: array - items: - $ref: '#/components/schemas/FileRecord' + type: array + items: + $ref: '#/components/schemas/FileRecord' '500': - description: Erro interno do servidor + description: Internal server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - /files/upload: post: tags: - Files - summary: Faz upload de um arquivo - description: Cria um novo arquivo no sistema - operationId: uploadFile + summary: Create a file + description: Creates a new file record in the system + operationId: createFile requestBody: required: true content: @@ -52,48 +45,43 @@ paths: schema: type: object required: - - content + - storageType + - storagePath properties: - content: + filename: type: string - description: Conteúdo do arquivo - example: 'Hello World!' - options: - type: object - properties: - contentType: - type: string - example: text/plain - metadata: - type: object - additionalProperties: true - example: - filename: test.txt + description: Name of the file + example: 'document.pdf' + contentType: + type: string + description: MIME type of the file + example: 'application/pdf' + size: + type: integer + description: File size in bytes + example: 1024 + storageType: + type: string + enum: [local, s3, gcs] + description: Storage backend type + example: 'local' + storagePath: + type: string + description: Path where the file is stored + example: '/uploads/document.pdf' + metadata: + type: string + description: JSON string with additional metadata + example: '{"author":"John"}' responses: - '200': - description: Arquivo criado com sucesso - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - file: - type: object - properties: - id: - type: string - example: 'abc123' - '400': - description: Requisição inválida + '201': + description: File created successfully content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' + $ref: '#/components/schemas/FileRecord' '500': - description: Erro interno do servidor + description: Internal server error content: application/json: schema: @@ -102,47 +90,32 @@ paths: get: tags: - Files - summary: Obtém um arquivo por ID - description: Retorna os dados e metadados de um arquivo específico - operationId: getFileById + summary: Get a file by ID + description: Returns the data and metadata of a specific file + operationId: getFile parameters: - name: id in: path required: true - description: ID do arquivo + description: File ID schema: type: string example: 'abc123' responses: '200': - description: Arquivo encontrado - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - file: - type: object - description: Conteúdo do arquivo - record: - $ref: '#/components/schemas/FileRecord' - '400': - description: Requisição inválida + description: File found content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' + $ref: '#/components/schemas/FileRecord' '404': - description: Arquivo não encontrado + description: File not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': - description: Erro interno do servidor + description: Internal server error content: application/json: schema: @@ -150,42 +123,28 @@ paths: delete: tags: - Files - summary: Deleta um arquivo - description: Remove um arquivo do sistema pelo ID + summary: Delete a file + description: Removes a file from the system by ID operationId: deleteFile parameters: - name: id in: path required: true - description: ID do arquivo a ser deletado + description: ID of the file to delete schema: type: string example: 'abc123' responses: - '200': - description: Arquivo deletado com sucesso - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - '400': - description: Requisição inválida - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' + '204': + description: File deleted successfully '404': - description: Arquivo não encontrado + description: File not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': - description: Erro interno do servidor + description: Internal server error content: application/json: schema: @@ -194,34 +153,49 @@ components: schemas: FileRecord: type: object - description: Metadados de um arquivo armazenado + description: Stored file metadata properties: id: type: string - description: Identificador único do arquivo - name: + description: Unique file identifier + example: 'abc123' + filename: + type: string + description: Name of the file + example: 'document.pdf' + contentType: type: string - description: Nome do arquivo + description: MIME type of the file + example: 'application/pdf' size: type: integer - description: Tamanho em bytes - contentType: + description: File size in bytes + example: 1024 + storageType: + type: string + enum: [local, s3, gcs] + description: Storage backend type + example: 'local' + storagePath: + type: string + description: Path where the file is stored + example: '/uploads/document.pdf' + metadata: type: string - description: Tipo MIME do arquivo + description: JSON string with additional metadata + example: '{"author":"John"}' createdAt: type: string format: date-time - description: Data de criação - metadata: - type: object - additionalProperties: true - description: Metadados adicionais + description: Creation timestamp + updatedAt: + type: string + format: date-time + description: Last update timestamp ErrorResponse: type: object properties: - success: - type: boolean - example: false error: type: string - description: Mensagem de erro + description: Error message + example: 'File not found' diff --git a/packages/server/src/rest/v1/documents.ts b/packages/server/src/rest/v1/documents.ts deleted file mode 100644 index c6e44c06..00000000 --- a/packages/server/src/rest/v1/documents.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { - createDocument, - deleteDocument, - type EmbeddingConfig, - getDocument, - listDocuments, - searchDocumentsBySimilarity, - type StorageConfig, - updateDocument, -} from '@soat/documents-core'; -import { getConfigFromEnv } from '@soat/embeddings-core'; -import { Router } from '@ttoss/http-server'; - -import type { Context } from '../../Context'; - -const defaultStorageConfig: StorageConfig = { - type: 'local', - local: { - path: '/tmp/documents', - }, -}; - -const getEmbeddingConfig = (): EmbeddingConfig | undefined => { - try { - return getConfigFromEnv(); - } catch { - return undefined; - } -}; - -const documentsRouter = new Router(); - -documentsRouter.get('/', async (ctx: Context) => { - try { - const documents = await listDocuments(); - ctx.status = 200; - ctx.body = { success: true, documents }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}); - -documentsRouter.post('/', async (ctx: Context) => { - try { - const { content, title, metadata, generateEmbedding } = ctx.request.body; - if (!content) { - ctx.status = 400; - ctx.body = { success: false, error: 'Content is required' }; - return; - } - - const embeddingConfig = getEmbeddingConfig(); - - const document = await createDocument({ - storageConfig: defaultStorageConfig, - embeddingConfig, - content, - options: { - title, - metadata, - generateEmbedding, - }, - }); - - ctx.status = 201; - ctx.body = { - success: true, - document: { - id: document.id, - title: document.title, - fileId: document.fileId, - embeddingModel: document.embeddingModel, - embeddingProvider: document.embeddingProvider, - hasEmbedding: !!document.embedding, - metadata: document.metadata, - createdAt: document.createdAt, - updatedAt: document.updatedAt, - }, - }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}); - -documentsRouter.get('/search', async (ctx: Context) => { - try { - const { query, limit, threshold } = ctx.query; - if (!query || typeof query !== 'string') { - ctx.status = 400; - ctx.body = { success: false, error: 'Query is required' }; - return; - } - - const embeddingConfig = getEmbeddingConfig(); - if (!embeddingConfig) { - ctx.status = 400; - ctx.body = { - success: false, - error: - 'Embedding configuration is required for search. Set EMBEDDINGS_OLLAMA_MODEL or EMBEDDINGS_OPENAI_KEY', - }; - return; - } - - const documents = await searchDocumentsBySimilarity({ - storageConfig: defaultStorageConfig, - embeddingConfig, - query, - options: { - limit: limit ? parseInt(limit as string, 10) : undefined, - threshold: threshold ? parseFloat(threshold as string) : undefined, - }, - }); - - ctx.status = 200; - ctx.body = { - success: true, - documents: documents.map((doc) => { - return { - id: doc.id, - title: doc.title, - fileId: doc.fileId, - content: doc.content?.toString(), - embeddingModel: doc.embeddingModel, - embeddingProvider: doc.embeddingProvider, - metadata: doc.metadata, - createdAt: doc.createdAt, - updatedAt: doc.updatedAt, - }; - }), - }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}); - -documentsRouter.get('/:id', async (ctx: Context) => { - try { - const { id } = ctx.params; - if (!id) { - ctx.status = 400; - ctx.body = { success: false, error: 'ID is required' }; - return; - } - - const document = await getDocument({ - storageConfig: defaultStorageConfig, - id, - }); - - if (!document) { - ctx.status = 404; - ctx.body = { success: false, error: 'Document not found' }; - return; - } - - ctx.status = 200; - ctx.body = { - success: true, - document: { - id: document.id, - title: document.title, - fileId: document.fileId, - content: document.content?.toString(), - embeddingModel: document.embeddingModel, - embeddingProvider: document.embeddingProvider, - hasEmbedding: !!document.embedding, - metadata: document.metadata, - createdAt: document.createdAt, - updatedAt: document.updatedAt, - }, - }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}); - -documentsRouter.put('/:id', async (ctx: Context) => { - try { - const { id } = ctx.params; - if (!id) { - ctx.status = 400; - ctx.body = { success: false, error: 'ID is required' }; - return; - } - - const { content, title, metadata, regenerateEmbedding } = ctx.request.body; - const embeddingConfig = getEmbeddingConfig(); - - const document = await updateDocument({ - storageConfig: defaultStorageConfig, - embeddingConfig, - id, - content, - title, - metadata, - regenerateEmbedding, - }); - - if (!document) { - ctx.status = 404; - ctx.body = { success: false, error: 'Document not found' }; - return; - } - - ctx.status = 200; - ctx.body = { - success: true, - document: { - id: document.id, - title: document.title, - fileId: document.fileId, - content: document.content?.toString(), - embeddingModel: document.embeddingModel, - embeddingProvider: document.embeddingProvider, - hasEmbedding: !!document.embedding, - metadata: document.metadata, - createdAt: document.createdAt, - updatedAt: document.updatedAt, - }, - }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}); - -documentsRouter.delete('/:id', async (ctx: Context) => { - try { - const { id } = ctx.params; - if (!id) { - ctx.status = 400; - ctx.body = { success: false, error: 'ID is required' }; - return; - } - - const deleted = await deleteDocument({ - storageConfig: defaultStorageConfig, - id, - }); - - if (!deleted) { - ctx.status = 404; - ctx.body = { success: false, error: 'Document not found' }; - return; - } - - ctx.status = 200; - ctx.body = { success: true }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}); - -export { documentsRouter }; diff --git a/packages/server/src/rest/v1/files.ts b/packages/server/src/rest/v1/files.ts index 4c09fd8c..0a242092 100644 --- a/packages/server/src/rest/v1/files.ts +++ b/packages/server/src/rest/v1/files.ts @@ -1,111 +1,197 @@ -import { - deleteFile, - getFileRecord, - listFileRecords, - retrieveFileById, - saveFile, - type StorageConfig, -} from '@soat/files-core'; import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { createFile, deleteFile, getFile, listFiles } from 'src/lib/files'; -import type { Context } from '../../Context'; +const filesRouter = new Router(); -const defaultConfig: StorageConfig = { - type: 'local', - local: { - path: '/tmp/files', - }, -}; +/** + * @openapi + * /files: + * get: + * tags: + * - Files + * summary: List all files + * description: Returns a list of all stored files + * operationId: listFiles + * responses: + * '200': + * description: List of files returned successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/FileRecord' + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.get('/files', async (ctx: Context) => { + ctx.body = await listFiles(); +}); -const filesRouter = new Router(); +/** + * @openapi + * /files/{id}: + * get: + * tags: + * - Files + * summary: Get a file by ID + * description: Returns the data and metadata of a specific file + * operationId: getFile + * parameters: + * - name: id + * in: path + * required: true + * description: File ID + * schema: + * type: string + * example: 'abc123' + * responses: + * '200': + * description: File found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FileRecord' + * '404': + * description: File not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.get('/files/:id', async (ctx: Context) => { + const file = await getFile({ id: ctx.params.id }); -filesRouter.get('/', async (ctx: Context) => { - try { - const files = await listFileRecords(); - ctx.status = 200; - ctx.body = { success: true, files }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; + if (!file) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; } -}); -filesRouter.post('/upload', async (ctx: Context) => { - try { - const { content, options } = ctx.request.body; - if (!content) { - ctx.status = 400; - ctx.body = { success: false, error: 'Content is required' }; - return; - } - const file = await saveFile({ config: defaultConfig, content, options }); - ctx.status = 201; - ctx.body = { - success: true, - id: file.id, - filename: options?.metadata?.filename, - }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } + ctx.body = file; }); -filesRouter.get('/:id', async (ctx: Context) => { - try { - const { id } = ctx.params; - if (!id) { - ctx.status = 400; - ctx.body = { success: false, error: 'ID is required' }; - return; - } - const file = await retrieveFileById({ config: defaultConfig, id }); - if (!file) { - ctx.status = 404; - ctx.body = { success: false, error: 'File not found' }; - return; - } - const record = await getFileRecord(id); - ctx.status = 200; - ctx.body = { success: true, file, record }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } +/** + * @openapi + * /files: + * post: + * tags: + * - Files + * summary: Create a file + * description: Creates a new file record in the system + * operationId: createFile + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - storageType + * - storagePath + * properties: + * filename: + * type: string + * example: 'document.pdf' + * contentType: + * type: string + * example: 'application/pdf' + * size: + * type: integer + * example: 1024 + * storageType: + * type: string + * enum: [local, s3, gcs] + * example: 'local' + * storagePath: + * type: string + * example: '/uploads/document.pdf' + * metadata: + * type: string + * example: '{"author":"John"}' + * responses: + * '201': + * description: File created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FileRecord' + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.post('/files', async (ctx: Context) => { + const body = ctx.request.body as { + filename?: string; + contentType?: string; + size?: number; + storageType: 'local' | 's3' | 'gcs'; + storagePath: string; + metadata?: string; + }; + + const file = await createFile(body); + ctx.status = 201; + ctx.body = file; }); -filesRouter.delete('/:id', async (ctx: Context) => { - try { - const { id } = ctx.params; - if (!id) { - ctx.status = 400; - ctx.body = { success: false, error: 'ID is required' }; - return; - } - const deleted = await deleteFile({ config: defaultConfig, id }); - if (!deleted) { - ctx.status = 404; - ctx.body = { success: false, error: 'File not found' }; - return; - } - ctx.status = 200; - ctx.body = { success: true }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; +/** + * @openapi + * /files/{id}: + * delete: + * tags: + * - Files + * summary: Delete a file + * description: Removes a file from the system by ID + * operationId: deleteFile + * parameters: + * - name: id + * in: path + * required: true + * description: ID of the file to delete + * schema: + * type: string + * example: 'abc123' + * responses: + * '204': + * description: File deleted successfully + * '404': + * description: File not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.delete('/files/:id', async (ctx: Context) => { + const result = await deleteFile({ id: ctx.params.id }); + + if (result === null) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; } + + ctx.status = 204; }); export { filesRouter }; diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index 3cae26c3..a53af145 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -1,11 +1,9 @@ import { Router } from '@ttoss/http-server'; -import { documentsRouter } from './documents'; import { filesRouter } from './files'; const v1Router = new Router(); -v1Router.use('/documents', documentsRouter.routes()); -v1Router.use('/files', filesRouter.routes()); +v1Router.use(filesRouter.routes()); export { v1Router }; diff --git a/packages/server/tests/tsconfig.json b/packages/server/tests/tsconfig.json index 397c60cd..e48943c2 100644 --- a/packages/server/tests/tsconfig.json +++ b/packages/server/tests/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@ttoss/config/tsconfig.test.json", "compilerOptions": { "paths": { + "dist": ["../dist/"], "src/*": ["../src/*"], "tests/*": ["./*"] } diff --git a/packages/server/tests/unit/jest.config.ts b/packages/server/tests/unit/jest.config.ts index 6ba531c0..9468854f 100644 --- a/packages/server/tests/unit/jest.config.ts +++ b/packages/server/tests/unit/jest.config.ts @@ -1,8 +1,13 @@ import { jestUnitConfig } from '@ttoss/config'; +import { getTransformIgnorePatterns } from '@ttoss/test-utils'; export default jestUnitConfig({ coverageThreshold: { global: {}, }, - setupFiles: ['/setupTests.ts'], + // setupFiles: ['/setupTests.ts'], + setupFilesAfterEnv: ['/setupTestsAfterEnv.ts'], + transformIgnorePatterns: getTransformIgnorePatterns({ + esmModules: ['@ttoss/postgresdb', '@ttoss/http-server-mcp'], + }), }); diff --git a/packages/server/tests/unit/setupTests.ts b/packages/server/tests/unit/setupTests.ts deleted file mode 100644 index 3db7c7d9..00000000 --- a/packages/server/tests/unit/setupTests.ts +++ /dev/null @@ -1,26 +0,0 @@ -jest.mock('@soat/postgresdb'); - -jest.mock('@soat/documents-core', () => { - return { - createDocument: jest.fn(), - getDocument: jest.fn(), - updateDocument: jest.fn(), - deleteDocument: jest.fn(), - listDocuments: jest.fn(), - searchDocumentsBySimilarity: jest.fn(), - }; -}); -jest.mock('@soat/embeddings-core', () => { - return { - getConfigFromEnv: jest.fn(), - }; -}); -jest.mock('@soat/files-core', () => { - return { - saveFile: jest.fn(), - deleteFile: jest.fn(), - retrieveFileById: jest.fn(), - listFileRecords: jest.fn(), - getFileRecord: jest.fn(), - }; -}); diff --git a/packages/server/tests/unit/setupTestsAfterEnv.ts b/packages/server/tests/unit/setupTestsAfterEnv.ts new file mode 100644 index 00000000..9449f273 --- /dev/null +++ b/packages/server/tests/unit/setupTestsAfterEnv.ts @@ -0,0 +1,45 @@ +import { models } from '@soat/postgresdb'; +import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql'; +import { PostgreSqlContainer } from '@testcontainers/postgresql'; +import type { Sequelize } from '@ttoss/postgresdb'; +import { initialize } from '@ttoss/postgresdb'; +import { app } from 'src/app'; +import { initializeDatabase } from 'src/db'; + +let sequelize: Sequelize; +let postgresContainer: StartedPostgreSqlContainer; + +jest.setTimeout(120000); + +beforeAll(async () => { + postgresContainer = await new PostgreSqlContainer( + 'pgvector/pgvector:0.8.1-pg18-trixie' + ).start(); + + try { + const db = await initialize({ + models, + logging: false, + username: postgresContainer.getUsername(), + password: postgresContainer.getPassword(), + database: postgresContainer.getDatabase(), + host: postgresContainer.getHost(), + port: postgresContainer.getPort(), + }); + + await initializeDatabase(app); + + sequelize = db.sequelize; + + await sequelize.sync(); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error during database initialization:', error); + throw error; + } +}); + +afterAll(async () => { + await sequelize?.close(); + await postgresContainer?.stop(); +}); diff --git a/packages/server/tests/unit/testClient.ts b/packages/server/tests/unit/testClient.ts new file mode 100644 index 00000000..e22365d2 --- /dev/null +++ b/packages/server/tests/unit/testClient.ts @@ -0,0 +1,30 @@ +import { app } from 'src/app'; +import request from 'supertest'; + +export const testClient = request(app.callback()); + +/** + * Helper to create authenticated requests with proper Origin header + * Use this for requests that require JWT authentication + */ +export const authenticatedTestClient = (token: string) => { + const Authorization = `Bearer ${token}`; + + return { + get: (url: string) => { + return testClient.get(url).set('Authorization', Authorization); + }, + post: (url: string) => { + return testClient.post(url).set('Authorization', Authorization); + }, + put: (url: string) => { + return testClient.put(url).set('Authorization', Authorization); + }, + patch: (url: string) => { + return testClient.patch(url).set('Authorization', Authorization); + }, + delete: (url: string) => { + return testClient.delete(url).set('Authorization', Authorization); + }, + }; +}; diff --git a/packages/server/tests/unit/tests/files.test.ts b/packages/server/tests/unit/tests/files.test.ts new file mode 100644 index 00000000..75bc5e46 --- /dev/null +++ b/packages/server/tests/unit/tests/files.test.ts @@ -0,0 +1,82 @@ +import { testClient } from '../testClient'; + +describe('GET /api/v1/files', () => { + test('should return empty list initially', async () => { + const response = await testClient.get('/api/v1/files'); + + expect(response.status).toBe(200); + expect(response.body).toEqual([]); + }); +}); + +describe('POST /api/v1/files', () => { + test('should create a file', async () => { + const payload = { + filename: 'test.txt', + contentType: 'text/plain', + size: 1024, + storageType: 'local', + storagePath: '/tmp/test.txt', + }; + + const response = await testClient.post('/api/v1/files').send(payload); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.filename).toBe(payload.filename); + expect(response.body.contentType).toBe(payload.contentType); + expect(response.body.size).toBe(payload.size); + expect(response.body.storageType).toBe(payload.storageType); + expect(response.body.storagePath).toBe(payload.storagePath); + }); +}); + +describe('GET /api/v1/files/:id', () => { + test('should return a created file by id', async () => { + const payload = { + filename: 'image.png', + contentType: 'image/png', + size: 2048, + storageType: 's3', + storagePath: 's3://bucket/image.png', + }; + + const createResponse = await testClient.post('/api/v1/files').send(payload); + const { id } = createResponse.body; + + const response = await testClient.get(`/api/v1/files/${id}`); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(id); + expect(response.body.filename).toBe(payload.filename); + }); + + test('should return 404 for unknown id', async () => { + const response = await testClient.get( + '/api/v1/files/00000000-0000-0000-0000-000000000000' + ); + + expect(response.status).toBe(404); + }); +}); + +describe('DELETE /api/v1/files/:id', () => { + test('should delete a file', async () => { + const payload = { + filename: 'delete-me.txt', + contentType: 'text/plain', + size: 512, + storageType: 'local', + storagePath: '/tmp/delete-me.txt', + }; + + const createResponse = await testClient.post('/api/v1/files').send(payload); + const { id } = createResponse.body; + + const deleteResponse = await testClient.delete(`/api/v1/files/${id}`); + expect(deleteResponse.status).toBe(204); + + const getResponse = await testClient.get(`/api/v1/files/${id}`); + expect(getResponse.status).toBe(404); + }); +}); diff --git a/packages/server/tests/unit/tests/mcp.test.ts b/packages/server/tests/unit/tests/mcp.test.ts new file mode 100644 index 00000000..a5570772 --- /dev/null +++ b/packages/server/tests/unit/tests/mcp.test.ts @@ -0,0 +1,21 @@ +import { testClient } from '../testClient'; + +describe('MCP tools/list', () => { + test('registers the expected tools', async () => { + const res = await testClient + .post('/mcp') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json, text/event-stream') + .send({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }); + + expect(res.status).toBe(200); + const tools: { name: string }[] = res.body.result.tools; + const names = tools.map((t) => { + return t.name; + }); + expect(names).toContain('list-files'); + expect(names).toContain('get-file'); + expect(names).toContain('create-file'); + expect(names).toContain('delete-file'); + }); +}); diff --git a/packages/server/tests/unit/tests/rest/documents.test.ts b/packages/server/tests/unit/tests/rest/documents.test.ts deleted file mode 100644 index 1cc8ef6a..00000000 --- a/packages/server/tests/unit/tests/rest/documents.test.ts +++ /dev/null @@ -1,463 +0,0 @@ -import { - createDocument, - deleteDocument, - type DocumentRecord, - getDocument, - listDocuments, - searchDocumentsBySimilarity, - updateDocument, -} from '@soat/documents-core'; -import { getConfigFromEnv } from '@soat/embeddings-core'; -import { app } from 'src/app'; -import request from 'supertest'; - -describe('Documents API', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.mocked(getConfigFromEnv).mockReset(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - jest.mocked(getConfigFromEnv).mockReturnValue(undefined as any); - }); - - describe('GET /api/v1/documents/', () => { - test('should list documents successfully', async () => { - const mockDocuments = [ - { - id: 'doc-1', - title: 'Test Document 1', - fileId: 'file-1', - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - metadata: {}, - createdAt: new Date('2026-01-09T16:27:02.008Z'), - updatedAt: new Date('2026-01-09T16:27:02.008Z'), - }, - ]; - - jest - .mocked(listDocuments) - .mockResolvedValue(mockDocuments as DocumentRecord[]); - - const response = await request(app.callback()) - .get('/api/v1/documents/') - .set('Accept', 'application/json'); - - expect(response.status).toBe(200); - expect(response.body).toEqual({ - success: true, - documents: [ - { - id: 'doc-1', - title: 'Test Document 1', - fileId: 'file-1', - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - metadata: {}, - createdAt: '2026-01-09T16:27:02.008Z', - updatedAt: '2026-01-09T16:27:02.008Z', - }, - ], - }); - expect(listDocuments).toHaveBeenCalled(); - }); - - test('should handle error when listing documents', async () => { - jest.mocked(listDocuments).mockRejectedValue(new Error('Database error')); - - const response = await request(app.callback()) - .get('/api/v1/documents/') - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Database error', - }); - }); - }); - - describe('POST /api/v1/documents/', () => { - test('should create document successfully', async () => { - const mockDocument = { - id: 'doc-1', - title: 'Test Document', - fileId: 'file-1', - content: Buffer.from('Test content'), - embedding: [0.1, 0.2, 0.3], - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - metadata: { key: 'value' }, - createdAt: new Date(), - updatedAt: new Date(), - }; - - jest.mocked(createDocument).mockResolvedValue(mockDocument); - - const response = await request(app.callback()) - .post('/api/v1/documents/') - .send({ - content: 'Test content', - title: 'Test Document', - metadata: { key: 'value' }, - generateEmbedding: true, - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(201); - expect(response.body).toEqual({ - success: true, - document: { - id: 'doc-1', - title: 'Test Document', - fileId: 'file-1', - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - hasEmbedding: true, - metadata: { key: 'value' }, - createdAt: mockDocument.createdAt.toISOString(), - updatedAt: mockDocument.updatedAt.toISOString(), - }, - }); - expect(createDocument).toHaveBeenCalledWith({ - storageConfig: { type: 'local', local: { path: '/tmp/documents' } }, - embeddingConfig: undefined, // Assuming no env config in tests - content: 'Test content', - options: { - title: 'Test Document', - metadata: { key: 'value' }, - generateEmbedding: true, - }, - }); - }); - - test('should return 400 if content is missing', async () => { - const response = await request(app.callback()) - .post('/api/v1/documents/') - .send({ - title: 'Test Document', - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(400); - expect(response.body).toEqual({ - success: false, - error: 'Content is required', - }); - }); - - test('should handle error when creating document', async () => { - jest - .mocked(createDocument) - .mockRejectedValue(new Error('Creation error')); - - const response = await request(app.callback()) - .post('/api/v1/documents/') - .send({ - content: 'Test content', - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Creation error', - }); - }); - }); - - describe('GET /api/v1/documents/search', () => { - test('should search documents successfully', async () => { - const mockDocuments = [ - { - id: 'doc-1', - title: 'Test Document', - fileId: 'file-1', - content: Buffer.from('Test content'), - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - metadata: {}, - createdAt: new Date(), - updatedAt: new Date(), - }, - ]; - - jest.mocked(getConfigFromEnv).mockReturnValue({ - provider: 'ollama', - ollama: { model: 'test', host: 'localhost' }, - }); - jest.mocked(searchDocumentsBySimilarity).mockResolvedValue(mockDocuments); - - const response = await request(app.callback()) - .get('/api/v1/documents/search?query=test') - .set('Accept', 'application/json'); - - expect(response.status).toBe(200); - expect(response.body).toEqual({ - success: true, - documents: [ - { - id: 'doc-1', - title: 'Test Document', - fileId: 'file-1', - content: 'Test content', - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - metadata: {}, - createdAt: mockDocuments[0].createdAt.toISOString(), - updatedAt: mockDocuments[0].updatedAt.toISOString(), - }, - ], - }); - expect(searchDocumentsBySimilarity).toHaveBeenCalledWith({ - storageConfig: { type: 'local', local: { path: '/tmp/documents' } }, - embeddingConfig: { - provider: 'ollama', - ollama: { model: 'test', host: 'localhost' }, - }, - query: 'test', - options: {}, - }); - }); - - test('should return 400 if query is missing', async () => { - const response = await request(app.callback()) - .get('/api/v1/documents/search') - .set('Accept', 'application/json'); - - expect(response.status).toBe(400); - expect(response.body).toEqual({ - success: false, - error: 'Query is required', - }); - }); - - test('should handle error when searching documents', async () => { - jest.mocked(getConfigFromEnv).mockReturnValue({ - provider: 'ollama', - ollama: { model: 'test', host: 'localhost' }, - }); - jest - .mocked(searchDocumentsBySimilarity) - .mockRejectedValue(new Error('Search error')); - - const response = await request(app.callback()) - .get('/api/v1/documents/search?query=test') - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Search error', - }); - }); - }); - - describe('GET /api/v1/documents/:id', () => { - test('should get document by id successfully', async () => { - const mockDocument = { - id: 'doc-1', - title: 'Test Document', - fileId: 'file-1', - content: Buffer.from('Test content'), - embedding: [0.1, 0.2, 0.3], - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - metadata: {}, - createdAt: new Date(), - updatedAt: new Date(), - }; - - jest.mocked(getDocument).mockResolvedValue(mockDocument); - - const response = await request(app.callback()) - .get('/api/v1/documents/doc-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(200); - expect(response.body).toEqual({ - success: true, - document: { - id: 'doc-1', - title: 'Test Document', - fileId: 'file-1', - content: 'Test content', - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - hasEmbedding: true, - metadata: {}, - createdAt: mockDocument.createdAt.toISOString(), - updatedAt: mockDocument.updatedAt.toISOString(), - }, - }); - expect(getDocument).toHaveBeenCalledWith({ - storageConfig: { type: 'local', local: { path: '/tmp/documents' } }, - id: 'doc-1', - }); - }); - - test('should return 404 if document not found', async () => { - jest.mocked(getDocument).mockResolvedValue(null); - - const response = await request(app.callback()) - .get('/api/v1/documents/doc-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(404); - expect(response.body).toEqual({ - success: false, - error: 'Document not found', - }); - }); - - test('should handle error when getting document', async () => { - jest.mocked(getDocument).mockRejectedValue(new Error('Get error')); - - const response = await request(app.callback()) - .get('/api/v1/documents/doc-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Get error', - }); - }); - }); - - describe('PUT /api/v1/documents/:id', () => { - test('should update document successfully', async () => { - const mockDocument = { - id: 'doc-1', - title: 'Updated Document', - fileId: 'file-1', - content: Buffer.from('Updated content'), - embedding: [0.1, 0.2, 0.3], - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - metadata: { updated: true }, - createdAt: new Date(), - updatedAt: new Date(), - }; - - jest.mocked(updateDocument).mockResolvedValue(mockDocument); - - const response = await request(app.callback()) - .put('/api/v1/documents/doc-1') - .send({ - content: 'Updated content', - title: 'Updated Document', - metadata: { updated: true }, - regenerateEmbedding: true, - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(200); - expect(response.body).toEqual({ - success: true, - document: { - id: 'doc-1', - title: 'Updated Document', - fileId: 'file-1', - content: 'Updated content', - embeddingModel: 'test-model', - embeddingProvider: 'test-provider', - hasEmbedding: true, - metadata: { updated: true }, - createdAt: mockDocument.createdAt.toISOString(), - updatedAt: mockDocument.updatedAt.toISOString(), - }, - }); - expect(updateDocument).toHaveBeenCalledWith( - expect.objectContaining({ - storageConfig: { type: 'local', local: { path: '/tmp/documents' } }, - id: 'doc-1', - content: 'Updated content', - title: 'Updated Document', - metadata: { updated: true }, - regenerateEmbedding: true, - }) - ); - }); - - test('should return 404 if document not found', async () => { - jest.mocked(updateDocument).mockResolvedValue(null); - - const response = await request(app.callback()) - .put('/api/v1/documents/doc-1') - .send({ - content: 'Updated content', - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(404); - expect(response.body).toEqual({ - success: false, - error: 'Document not found', - }); - }); - - test('should handle error when updating document', async () => { - jest.mocked(updateDocument).mockRejectedValue(new Error('Update error')); - - const response = await request(app.callback()) - .put('/api/v1/documents/doc-1') - .send({ - content: 'Updated content', - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Update error', - }); - }); - }); - - describe('DELETE /api/v1/documents/:id', () => { - test('should delete document successfully', async () => { - jest.mocked(deleteDocument).mockResolvedValue(true); - - const response = await request(app.callback()) - .delete('/api/v1/documents/doc-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(200); - expect(response.body).toEqual({ - success: true, - }); - expect(deleteDocument).toHaveBeenCalledWith({ - storageConfig: { type: 'local', local: { path: '/tmp/documents' } }, - id: 'doc-1', - }); - }); - - test('should return 404 if document not found', async () => { - jest.mocked(deleteDocument).mockResolvedValue(false); - - const response = await request(app.callback()) - .delete('/api/v1/documents/doc-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(404); - expect(response.body).toEqual({ - success: false, - error: 'Document not found', - }); - }); - - test('should handle error when deleting document', async () => { - jest.mocked(deleteDocument).mockRejectedValue(new Error('Delete error')); - - const response = await request(app.callback()) - .delete('/api/v1/documents/doc-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Delete error', - }); - }); - }); -}); diff --git a/packages/server/tests/unit/tests/rest/files.test.ts b/packages/server/tests/unit/tests/rest/files.test.ts deleted file mode 100644 index 96687b0b..00000000 --- a/packages/server/tests/unit/tests/rest/files.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { - deleteFile, - type FileData, - type FileRecord, - getFileRecord, - listFileRecords, - retrieveFileById, - saveFile, -} from '@soat/files-core'; -import { app } from 'src/app'; -import request from 'supertest'; - -describe('Files API', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('GET /api/v1/files/', () => { - test('should list files successfully', async () => { - const mockFiles = [ - { - id: 'file-1', - filename: 'test1.txt', - contentType: 'text/plain', - size: 100, - storageType: 'local', - storagePath: '/tmp/files/test1.txt', - metadata: {}, - createdAt: '2026-01-09T16:27:01.999Z', - updatedAt: '2026-01-09T16:27:01.999Z', - }, - ]; - - jest - .mocked(listFileRecords) - .mockResolvedValue(mockFiles as unknown as FileRecord[]); - - const response = await request(app.callback()) - .get('/api/v1/files/') - .set('Accept', 'application/json'); - - expect(response.status).toBe(200); - expect(response.body).toEqual({ - success: true, - files: mockFiles, - }); - expect(listFileRecords).toHaveBeenCalled(); - }); - - test('should handle error when listing files', async () => { - jest - .mocked(listFileRecords) - .mockRejectedValue(new Error('Database error')); - - const response = await request(app.callback()) - .get('/api/v1/files/') - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Database error', - }); - }); - }); - - describe('POST /api/v1/files/upload', () => { - test('should create a file via REST API', async () => { - const savedFile = { - id: 'test-id', - filename: 'test.txt', - content: 'Hello, World!', - metadata: {}, - }; - - jest.mocked(saveFile).mockResolvedValue(savedFile); - - const response = await request(app.callback()) - .post('/api/v1/files/upload') - .send({ - content: 'Hello, World!', - options: { metadata: { filename: 'test.txt' } }, - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(201); - expect(response.body).toEqual({ - id: 'test-id', - filename: 'test.txt', - success: true, - }); - expect(saveFile).toHaveBeenCalledWith({ - config: { local: { path: '/tmp/files' }, type: 'local' }, - content: 'Hello, World!', - options: { metadata: { filename: 'test.txt' } }, - }); - }); - - test('should return 400 if content is missing', async () => { - const response = await request(app.callback()) - .post('/api/v1/files/upload') - .send({ - options: { metadata: { filename: 'test.txt' } }, - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(400); - expect(response.body).toEqual({ - success: false, - error: 'Content is required', - }); - }); - - test('should handle error when uploading file', async () => { - jest.mocked(saveFile).mockRejectedValue(new Error('Upload error')); - - const response = await request(app.callback()) - .post('/api/v1/files/upload') - .send({ - content: 'Hello, World!', - options: { metadata: { filename: 'test.txt' } }, - }) - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Upload error', - }); - }); - }); - - describe('GET /api/v1/files/:id', () => { - test('should get file by id successfully', async () => { - const mockFile = { - id: 'file-1', - content: 'Hello, World!', - }; - const mockRecord = { - id: 'file-1', - filename: 'test.txt', - contentType: 'text/plain', - size: 13, - storageType: 'local', - storagePath: '/tmp/files/test.txt', - metadata: { filename: 'test.txt' }, - createdAt: '2026-01-09T16:27:02.079Z', - updatedAt: '2026-01-09T16:27:02.079Z', - }; - - jest.mocked(retrieveFileById).mockResolvedValue(mockFile as FileData); - jest - .mocked(getFileRecord) - .mockResolvedValue(mockRecord as unknown as FileRecord); - - const response = await request(app.callback()) - .get('/api/v1/files/file-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(200); - expect(response.body).toEqual({ - success: true, - file: mockFile, - record: mockRecord, - }); - expect(retrieveFileById).toHaveBeenCalledWith({ - config: { local: { path: '/tmp/files' }, type: 'local' }, - id: 'file-1', - }); - expect(getFileRecord).toHaveBeenCalledWith('file-1'); - }); - - test('should return 404 if file not found', async () => { - jest.mocked(retrieveFileById).mockResolvedValue(null); - - const response = await request(app.callback()) - .get('/api/v1/files/file-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(404); - expect(response.body).toEqual({ - success: false, - error: 'File not found', - }); - }); - - test('should handle error when getting file', async () => { - jest.mocked(retrieveFileById).mockRejectedValue(new Error('Get error')); - - const response = await request(app.callback()) - .get('/api/v1/files/file-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Get error', - }); - }); - }); - - describe('DELETE /api/v1/files/:id', () => { - test('should delete file successfully', async () => { - jest.mocked(deleteFile).mockResolvedValue(true); - - const response = await request(app.callback()) - .delete('/api/v1/files/file-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(200); - expect(response.body).toEqual({ - success: true, - }); - expect(deleteFile).toHaveBeenCalledWith({ - config: { local: { path: '/tmp/files' }, type: 'local' }, - id: 'file-1', - }); - }); - - test('should return 404 if file not found', async () => { - jest.mocked(deleteFile).mockResolvedValue(false); - - const response = await request(app.callback()) - .delete('/api/v1/files/file-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(404); - expect(response.body).toEqual({ - success: false, - error: 'File not found', - }); - }); - - test('should handle error when deleting file', async () => { - jest.mocked(deleteFile).mockRejectedValue(new Error('Delete error')); - - const response = await request(app.callback()) - .delete('/api/v1/files/file-1') - .set('Accept', 'application/json'); - - expect(response.status).toBe(500); - expect(response.body).toEqual({ - success: false, - error: 'Delete error', - }); - }); - }); -}); diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index ddeb0e87..3e864d46 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "@ttoss/config/tsconfig.json", "compilerOptions": { + "paths": { + "src/*": ["./src/*"] + }, "experimentalDecorators": true, "emitDecoratorMetadata": true }, diff --git a/packages/text-atomizer/CHANGELOG.md b/packages/text-atomizer/CHANGELOG.md deleted file mode 100644 index c87dbaa8..00000000 --- a/packages/text-atomizer/CHANGELOG.md +++ /dev/null @@ -1,10 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# 0.0.0-alpha.2 (2026-01-06) - -### Bug Fixes - -* add version ([08f4a9f](https://github.com/ttoss/soat/commit/08f4a9f2510947348849b88e5d33b7b44b981986)) diff --git a/packages/text-atomizer/jest.config.ts b/packages/text-atomizer/jest.config.ts deleted file mode 100644 index c94e5234..00000000 --- a/packages/text-atomizer/jest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { jestRootConfig } from '@ttoss/config'; - -export default jestRootConfig(); diff --git a/packages/text-atomizer/package.json b/packages/text-atomizer/package.json deleted file mode 100644 index d19f0288..00000000 --- a/packages/text-atomizer/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@soat/text-atomizer", - "version": "0.0.0-alpha.2", - "scripts": { - "build": "tsup", - "test": "jest --projects tests/unit" - }, - "dependencies": { - "ollama": "^0.6.3" - }, - "devDependencies": { - "@ttoss/config": "^1.35.12", - "@types/jest": "^30.0.0", - "jest": "^30.2.0", - "tsup": "^8.5.1", - "tsx": "^4.21.0" - }, - "files": [ - "dist" - ] -} diff --git a/packages/text-atomizer/src/index.ts b/packages/text-atomizer/src/index.ts deleted file mode 100644 index 4e32cf3d..00000000 --- a/packages/text-atomizer/src/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -import ollama from 'ollama'; - -const SEED = 42; - -const SYSTEM_PROMPT = ` -Act as a linguist specialized in syntactic analysis. Your task is to perform a structural decomposition of the sentences provided. - -You must produce a two-level analysis: - -Level 1: Identify and separate the two essential terms of the sentence: the Subject and the Predicate. -Level 2: Decompose the Predicate into its core verb and its dependents. - -Output Format: - -Return a JSON object with the following structure: -{ - "subject": "[Identified Subject]", - "predicate": { - "text": "[Identified Predicate]", - "verb": "[Main verb nucleus of the predicate]", - "complements": ["[Verb complements / required arguments]"] , - "adjuncts": ["[Adjuncts / optional modifiers]"] - } -} - -Rules: - -- Maintain the analysis in the same language as the original sentence. -- If the sentence is impersonal (subjectless), keep the subject field null. -- Be precise in defining the boundary where the subject ends and the predicate begins. -- predicate.text MUST be the full predicate span as it appears in the sentence (everything after the subject), including the verb and all complements/adjuncts (minus final punctuation). -- The predicate.verb must be the main verb nucleus (a verb form as it appears in the sentence). -- Complements are dependents required by the verb (objects, predicatives, required prepositional complements). If none, return an empty array. -- Adjuncts are optional modifiers (adverbials, optional prepositional phrases, temporal/locative/manner phrases, etc). If none, return an empty array. -- Do not invent words. Prefer extracting spans from the original sentence. - -Normalization rules for complements/adjuncts strings: - -- Each item in complements/adjuncts should be the smallest meaningful span (usually a noun phrase). -- If a dependent is introduced by a preposition (e.g., "over the lazy dog"), DO NOT include the preposition in the complements/adjuncts item; include only the object of the preposition (e.g., "the lazy dog"). -- Even when you omit the preposition in the array item, predicate.text must still include the full prepositional phrase as it appears in the sentence. - -Example (follow exactly): - -Input: "The quick brown fox jumps over the lazy dog." -Output: -{ - "subject": "The quick brown fox", - "predicate": { - "text": "jumps over the lazy dog", - "verb": "jumps", - "complements": ["the lazy dog"], - "adjuncts": [] - } -} -`; - -type PredicateDecomposition = { - text: string; - verb: string; - complements: string[]; - adjuncts: string[]; -}; - -type DecompositionResult = { - subject: string | null; - predicate: PredicateDecomposition; -}; - -export const atomizeText = async (args: { text: string }) => { - const response = await ollama.chat({ - model: 'gemma3:1b', - format: 'json', - messages: [ - { role: 'system', content: SYSTEM_PROMPT }, - { - role: 'user', - content: `Analyze and decompose the following sentence:\n\n"${args.text}"`, - }, - ], - options: { - seed: SEED, - temperature: 0, - }, - }); - - const content = response.message.content; - - let result: DecompositionResult; - try { - result = JSON.parse(content) as DecompositionResult; - } catch (error) { - throw new Error( - `Failed to parse model JSON response: ${error instanceof Error ? error.message : String(error)}` - ); - } - - if (typeof result.subject !== 'string' && result.subject !== null) { - throw new Error('Invalid subject in response'); - } - - if ( - typeof result.predicate !== 'object' || - result.predicate === null || - Array.isArray(result.predicate) - ) { - throw new Error('Invalid predicate in response'); - } - - if (typeof result.predicate.text !== 'string') { - throw new Error('Invalid predicate.text in response'); - } - - if (typeof result.predicate.verb !== 'string') { - throw new Error('Invalid predicate.verb in response'); - } - - if (!Array.isArray(result.predicate.complements)) { - throw new Error('Invalid predicate.complements in response'); - } - - if ( - !result.predicate.complements.every((value) => { - return typeof value === 'string'; - }) - ) { - throw new Error('Invalid predicate.complements item in response'); - } - - if (!Array.isArray(result.predicate.adjuncts)) { - throw new Error('Invalid predicate.adjuncts in response'); - } - - if ( - !result.predicate.adjuncts.every((value) => { - return typeof value === 'string'; - }) - ) { - throw new Error('Invalid predicate.adjuncts item in response'); - } - - return result; -}; - -// atomizeText({ text: 'The quick brown fox jumps over the lazy dog.' }).then( -// (result) => { -// if (result) { -// console.log('Decomposed:', result); -// } -// } -// ); diff --git a/packages/text-atomizer/tests/tsconfig.json b/packages/text-atomizer/tests/tsconfig.json deleted file mode 100644 index 397c60cd..00000000 --- a/packages/text-atomizer/tests/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@ttoss/config/tsconfig.test.json", - "compilerOptions": { - "paths": { - "src/*": ["../src/*"], - "tests/*": ["./*"] - } - } -} diff --git a/packages/text-atomizer/tests/unit/babel.config.cjs b/packages/text-atomizer/tests/unit/babel.config.cjs deleted file mode 100644 index 9c95923b..00000000 --- a/packages/text-atomizer/tests/unit/babel.config.cjs +++ /dev/null @@ -1,5 +0,0 @@ -const { babelConfig } = require('@ttoss/config'); - -const config = babelConfig({}); - -module.exports = config; diff --git a/packages/text-atomizer/tests/unit/jest.config.ts b/packages/text-atomizer/tests/unit/jest.config.ts deleted file mode 100644 index 065c9a60..00000000 --- a/packages/text-atomizer/tests/unit/jest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { jestUnitConfig } from '@ttoss/config'; - -export default jestUnitConfig(); diff --git a/packages/text-atomizer/tests/unit/tests/atomizeText.test.ts b/packages/text-atomizer/tests/unit/tests/atomizeText.test.ts deleted file mode 100644 index 5208ee1b..00000000 --- a/packages/text-atomizer/tests/unit/tests/atomizeText.test.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { atomizeText } from 'src/index'; - -jest.setTimeout(30000); - -test.each([ - { - // Simple transitive with direct object (phrase) - text: 'The teacher gave the students a difficult test.', - expected: { - discourse: { - speechAct: 'statement', - topic: 'The teacher', - focus: 'a difficult test', - }, - subject: { - text: 'The teacher', - type: 'phrase', - head: 'teacher', - clauseType: null, - appositive: null, - }, - predicate: { - text: 'gave the students a difficult test', - verbalGroup: { - text: 'gave', - mainVerb: 'gave', - auxiliaries: [], - modals: [], - voice: 'active', - tense: 'past', - aspect: 'simple', - }, - directObject: { - text: 'a difficult test', - type: 'phrase', - head: 'test', - clauseType: null, - }, - indirectObject: { - text: 'the students', - type: 'phrase', - head: 'students', - }, - prepositionalObject: null, - subjectComplement: null, - objectComplement: null, - agent: null, - adverbials: [], - }, - }, - }, - { - // Noun clause as direct object - text: 'I believe that she is honest.', - expected: { - discourse: { - speechAct: 'statement', - topic: 'I', - focus: 'that she is honest', - }, - subject: { - text: 'I', - type: 'phrase', - head: 'I', - clauseType: null, - appositive: null, - }, - predicate: { - text: 'believe that she is honest', - verbalGroup: { - text: 'believe', - mainVerb: 'believe', - auxiliaries: [], - modals: [], - voice: 'active', - tense: 'present', - aspect: 'simple', - }, - directObject: { - text: 'that she is honest', - type: 'noun_clause', - head: null, - clauseType: 'that', - }, - indirectObject: null, - prepositionalObject: null, - subjectComplement: null, - objectComplement: null, - agent: null, - adverbials: [], - }, - }, - }, - { - // Infinitive clause as direct object - text: 'I want to leave.', - expected: { - discourse: { speechAct: 'statement', topic: 'I', focus: 'to leave' }, - subject: { - text: 'I', - type: 'phrase', - head: 'I', - clauseType: null, - appositive: null, - }, - predicate: { - text: 'want to leave', - verbalGroup: { - text: 'want', - mainVerb: 'want', - auxiliaries: [], - modals: [], - voice: 'active', - tense: 'present', - aspect: 'simple', - }, - directObject: { - text: 'to leave', - type: 'infinitive_clause', - head: null, - clauseType: 'to', - }, - indirectObject: null, - prepositionalObject: null, - subjectComplement: null, - objectComplement: null, - agent: null, - adverbials: [], - }, - }, - }, - { - // Prepositional object (verb-required PP) - text: 'He depends on you.', - expected: { - discourse: { speechAct: 'statement', topic: 'He', focus: 'on you' }, - subject: { - text: 'He', - type: 'phrase', - head: 'He', - clauseType: null, - appositive: null, - }, - predicate: { - text: 'depends on you', - verbalGroup: { - text: 'depends', - mainVerb: 'depend', - auxiliaries: [], - modals: [], - voice: 'active', - tense: 'present', - aspect: 'simple', - }, - directObject: null, - indirectObject: null, - prepositionalObject: { - text: 'on you', - preposition: 'on', - object: { text: 'you', head: 'you' }, - }, - subjectComplement: null, - objectComplement: null, - agent: null, - adverbials: [], - }, - }, - }, - { - // Passive voice with agent - text: 'The cake was eaten by John.', - expected: { - discourse: { - speechAct: 'statement', - topic: 'The cake', - focus: 'by John', - }, - subject: { - text: 'The cake', - type: 'phrase', - head: 'cake', - clauseType: null, - appositive: null, - }, - predicate: { - text: 'was eaten by John', - verbalGroup: { - text: 'was eaten', - mainVerb: 'eat', - auxiliaries: ['was'], - modals: [], - voice: 'passive', - tense: 'past', - aspect: 'simple', - }, - directObject: null, - indirectObject: null, - prepositionalObject: null, - subjectComplement: null, - objectComplement: null, - agent: { text: 'by John', head: 'John' }, - adverbials: [], - }, - }, - }, - { - // Gerund as subject - text: 'Running is healthy.', - expected: { - discourse: { speechAct: 'statement', topic: 'Running', focus: 'healthy' }, - subject: { - text: 'Running', - type: 'gerund_clause', - head: null, - clauseType: null, - appositive: null, - }, - predicate: { - text: 'is healthy', - verbalGroup: { - text: 'is', - mainVerb: 'be', - auxiliaries: [], - modals: [], - voice: 'active', - tense: 'present', - aspect: 'simple', - }, - directObject: null, - indirectObject: null, - prepositionalObject: null, - subjectComplement: { - text: 'healthy', - type: 'phrase', - head: 'healthy', - clauseType: null, - }, - objectComplement: null, - agent: null, - adverbials: [], - }, - }, - }, - { - // Appositive - text: 'My friend, a doctor, is here.', - expected: { - discourse: { speechAct: 'statement', topic: 'My friend', focus: 'here' }, - subject: { - text: 'My friend', - type: 'phrase', - head: 'friend', - clauseType: null, - appositive: { text: 'a doctor', head: 'doctor' }, - }, - predicate: { - text: 'is here', - verbalGroup: { - text: 'is', - mainVerb: 'be', - auxiliaries: [], - modals: [], - voice: 'active', - tense: 'present', - aspect: 'simple', - }, - directObject: null, - indirectObject: null, - prepositionalObject: null, - subjectComplement: null, - objectComplement: null, - agent: null, - adverbials: [ - { - text: 'here', - type: 'phrase', - head: 'here', - clauseType: null, - semanticRole: 'place', - }, - ], - }, - }, - }, - { - // Present perfect progressive (complex verbal group) - text: 'She has been working hard.', - expected: { - discourse: { speechAct: 'statement', topic: 'She', focus: 'hard' }, - subject: { - text: 'She', - type: 'phrase', - head: 'She', - clauseType: null, - appositive: null, - }, - predicate: { - text: 'has been working hard', - verbalGroup: { - text: 'has been working', - mainVerb: 'work', - auxiliaries: ['has', 'been'], - modals: [], - voice: 'active', - tense: 'present', - aspect: 'perfect_progressive', - }, - directObject: null, - indirectObject: null, - prepositionalObject: null, - subjectComplement: null, - objectComplement: null, - agent: null, - adverbials: [ - { - text: 'hard', - type: 'phrase', - head: 'hard', - clauseType: null, - semanticRole: 'manner', - }, - ], - }, - }, - }, - { - // Participle clause as adverbial - text: 'Having finished the work, she left.', - expected: { - discourse: { speechAct: 'statement', topic: 'she', focus: 'left' }, - subject: { - text: 'she', - type: 'phrase', - head: 'she', - clauseType: null, - appositive: null, - }, - predicate: { - text: 'left', - verbalGroup: { - text: 'left', - mainVerb: 'leave', - auxiliaries: [], - modals: [], - voice: 'active', - tense: 'past', - aspect: 'simple', - }, - directObject: null, - indirectObject: null, - prepositionalObject: null, - subjectComplement: null, - objectComplement: null, - agent: null, - adverbials: [ - { - text: 'Having finished the work', - type: 'participle_clause', - head: null, - clauseType: null, - semanticRole: 'time', - }, - ], - }, - }, - }, -])('atomizeText correctly decomposes: "$text"', async ({ text, expected }) => { - const result = await atomizeText({ text }); - expect(result).toEqual(expected); -}); diff --git a/packages/text-atomizer/tsconfig.json b/packages/text-atomizer/tsconfig.json deleted file mode 100644 index fce4d054..00000000 --- a/packages/text-atomizer/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@ttoss/config/tsconfig.json" -} diff --git a/packages/text-atomizer/tsup.config.ts b/packages/text-atomizer/tsup.config.ts deleted file mode 100644 index 76e36f0f..00000000 --- a/packages/text-atomizer/tsup.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tsupConfig } from '@ttoss/config'; - -export const tsup = tsupConfig(); diff --git a/packages/website/docs/Introduction.md b/packages/website/docs/Introduction.md new file mode 100644 index 00000000..4a9105c5 --- /dev/null +++ b/packages/website/docs/Introduction.md @@ -0,0 +1,5 @@ +--- +sidebar_position: 1 +--- + +# Introduction diff --git a/packages/website/docs/getting-started.md b/packages/website/docs/getting-started.md index c410c618..30161d89 100644 --- a/packages/website/docs/getting-started.md +++ b/packages/website/docs/getting-started.md @@ -3,92 +3,3 @@ sidebar_position: 2 --- # Getting Started - -The fastest way to run SOAT is using Docker Compose. This ensures you have the SOAT API Server and the vector-enabled PostgreSQL database running together correctly. - -## Prerequisites - -- [Docker Desktop](https://www.docker.com/products/docker-desktop/) or Docker Engine installed. -- `git` (optional, for cloning the repo). - -## Quick Start - -1. **Clone the Repository** - - ```bash - git clone https://github.com/ttoss/soat.git - cd soat - ``` - -2. **Start the Services** - - We provide a standard `docker-compose.yml` configuration (create this file in your root folder if it doesn't exist, based on the example below). - - Create a `docker-compose.yml` in the root of your project: - - ```yaml - services: - database: - image: pgvector/pgvector:0.8.1-pg18-trixie - container_name: soat-database - environment: - POSTGRES_DB: soat_db - POSTGRES_USER: soat_user - POSTGRES_PASSWORD: soat_password - ports: - - '5432:5432' - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U soat_user -d soat_db'] - interval: 10s - timeout: 5s - retries: 5 - - server: - image: ghcr.io/ttoss/soat-server:latest # Assuming a published image, or build locally - # For local development, you might build from ./packages/server - # build: ./packages/server - container_name: soat-server - ports: - - '3000:3000' - environment: - DATABASE_URL: postgres://soat_user:soat_password@database:5432/soat_db - PORT: 3000 - depends_on: - database: - condition: service_healthy - - volumes: - postgres_data: - ``` - - > **Note:** If you are running from source, you may need to build the server package locally. - -3. **Run Docker Compose** - - ```bash - docker-compose up -d - ``` - - Your SOAT Server should now be running at `http://localhost:3000`. - -4. **Verify Installation** - - You can test if the server is running by checking the health endpoint or documentation (if enabled): - - ```bash - curl http://localhost:3000/health - ``` - -## Environment Variables - -The server behaves differently based on configuration. Important variables include: - -- `DATABASE_URL`: Connection string for PostgreSQL. -- `OPENAI_API_KEY`: Required if you are using OpenAI for embeddings (default). -- `OLLAMA_HOST`: (Optional) URL for local Ollama instance if using local embeddings. - -## Next Steps - -Now that your server is running, let's **[Connect an Agent](./tutorials/connect-mcp.md)** to it! diff --git a/packages/website/docs/intro.md b/packages/website/docs/intro.md deleted file mode 100644 index ff2d3990..00000000 --- a/packages/website/docs/intro.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Introduction - -**SOAT (Source of Agentic Truth)** is an open-source framework designed to give AI agents **Persistent Memory**. - -In the evolving landscape of AI, agents are becoming more autonomous and capable. However, they often suffer from "amnesia" between sessions or have limited context windows. SOAT solves this by providing a dedicated memory server that allows agents to store and retrieve information semantically. - -## Why SOAT? - -- **🧠 Persistent Memory**: Agents can store text, files, and structured data that survives across sessions. -- **🔎 Semantic Search**: Built on `pgvector`, SOAT enables agents to find information not just by keywords, but by _meaning_. -- **🔌 MCP Native**: Full support for the **Model Context Protocol (MCP)**, allowing seamless integration with Claude Desktop, Cursor, and other MCP-compliant tools. -- **⚡ Simple API**: A standardized REST API for building custom integrations. - -## Core Concepts - -### Memory & Embeddings - -When you send text to SOAT, it doesn't just save the string. It generates a **vector embedding**—a mathematical representation of the text's meaning. This allows the system to calculate similarity between different pieces of information. - -### The MCP Server - -The Model Context Protocol (MCP) is a standard for connecting AI models to external data. SOAT acts as an MCP Server, exposing tools like `add_memory` and `search_memory` that agents can discover and use automatically. - -## Next Steps - -- **[Getting Started](./getting-started.md)**: Spin up your own SOAT server in minutes using Docker. -- **[Connect with MCP](./tutorials/connect-mcp.md)**: Learn how to connect Claude Desktop to your new memory bank. -- **[Storing Memory](./tutorials/storing-memory.md)**: A guide to the memory tools available to your agents. diff --git a/packages/website/package.json b/packages/website/package.json index 3cd721ef..64038b4f 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -31,7 +31,7 @@ "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", - "typescript": "~5.9.3" + "typescript": "~6.0.2" }, "browserslist": { "production": [ diff --git a/packages/website/tsconfig.json b/packages/website/tsconfig.json index 920d7a65..ae701a6d 100644 --- a/packages/website/tsconfig.json +++ b/packages/website/tsconfig.json @@ -2,7 +2,8 @@ // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", "compilerOptions": { - "baseUrl": "." + "baseUrl": ".", + "ignoreDeprecations": "6.0" }, "exclude": [".docusaurus", "build"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e97ae543..15f8952a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,32 +9,32 @@ importers: .: devDependencies: '@commitlint/cli': - specifier: ^20.1.0 - version: 20.2.0(@types/node@25.0.3)(typescript@5.9.3) + specifier: ^20.5.0 + version: 20.5.0(@types/node@25.5.2)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.2) '@lerna-lite/changed': - specifier: ^4.9.4 - version: 4.10.2(@lerna-lite/version@4.10.2)(@types/node@25.0.3) + specifier: ^5.0.0 + version: 5.0.0(@lerna-lite/version@5.0.0)(@types/node@25.5.2) '@lerna-lite/cli': - specifier: ^4.9.4 - version: 4.10.2(@lerna-lite/list@4.10.2)(@lerna-lite/version@4.10.2)(@types/node@25.0.3) + specifier: ^5.0.0 + version: 5.0.0(@lerna-lite/list@5.0.0)(@lerna-lite/version@5.0.0)(@types/node@25.5.2) '@lerna-lite/list': - specifier: ^4.9.4 - version: 4.10.2(@lerna-lite/version@4.10.2)(@types/node@25.0.3) + specifier: ^5.0.0 + version: 5.0.0(@lerna-lite/version@5.0.0)(@types/node@25.5.2) '@lerna-lite/version': - specifier: ^4.9.4 - version: 4.10.2(@lerna-lite/list@4.10.2)(@types/node@25.0.3)(conventional-commits-filter@5.0.0) + specifier: ^5.0.0 + version: 5.0.0(@lerna-lite/list@5.0.0)(@types/node@25.5.2)(conventional-commits-filter@5.0.0) '@ttoss/config': - specifier: ^1.35.12 - version: 1.35.12 + specifier: ^1.37.8 + version: 1.37.8 '@ttoss/eslint-config': - specifier: ^1.26.6 - version: 1.26.6(@testing-library/dom@10.4.1)(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)))(prettier@3.7.4)(turbo@2.6.3)(typescript@5.9.3) + specifier: ^1.26.14 + version: 1.26.14(@testing-library/dom@10.4.1)(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(prettier@3.8.1)(turbo@2.9.4)(typescript@6.0.2) '@ttoss/monorepo': - specifier: ^1.28.0 - version: 1.28.0 + specifier: ^1.29.8 + version: 1.29.8 '@types/node': - specifier: ^25.0.3 - version: 25.0.3 + specifier: ^25.5.2 + version: 25.5.2 eslint: specifier: ^9.39.1 version: 9.39.2(jiti@2.6.1) @@ -42,20 +42,20 @@ importers: specifier: ^9.1.7 version: 9.1.7 lint-staged: - specifier: ^16.2.7 - version: 16.2.7 + specifier: ^16.4.0 + version: 16.4.0 prettier: - specifier: ^3.7.4 - version: 3.7.4 + specifier: ^3.8.1 + version: 3.8.1 syncpack: - specifier: 13.0.4 - version: 13.0.4(typescript@5.9.3) + specifier: ^14.3.0 + version: 14.3.0 turbo: - specifier: ^2.6.2 - version: 2.6.3 + specifier: ^2.9.4 + version: 2.9.4 typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ~6.0.2 + version: 6.0.2 packages/cli: dependencies: @@ -67,236 +67,103 @@ importers: version: 14.0.2 devDependencies: '@ttoss/config': - specifier: ^1.35.12 - version: 1.35.12 + specifier: ^1.37.8 + version: 1.37.8 '@types/jest': specifier: ^30.0.0 version: 30.0.0 jest: - specifier: ^30.2.0 - version: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) + specifier: ^30.3.0 + version: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) tsx: specifier: ^4.21.0 version: 4.21.0 - packages/documents-core: - dependencies: - '@soat/embeddings-core': - specifier: workspace:* - version: link:../embeddings-core - '@soat/files-core': - specifier: workspace:* - version: link:../files-core - '@soat/postgresdb': - specifier: workspace:* - version: link:../postgresdb - '@ttoss/postgresdb': - specifier: ^0.3.0 - version: 0.3.0(@types/node@25.0.3)(@types/validator@13.15.10)(reflect-metadata@0.2.2) - uuid: - specifier: ^10.0.0 - version: 10.0.0 - devDependencies: - '@ttoss/config': - specifier: ^1.35.12 - version: 1.35.12 - '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 - '@types/node': - specifier: ^25.0.3 - version: 25.0.3 - '@types/uuid': - specifier: ^10.0.0 - version: 10.0.0 - jest: - specifier: ^30.2.0 - version: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) - tsup: - specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) - tsx: - specifier: ^4.21.0 - version: 4.21.0 - - packages/embeddings-core: - dependencies: - ollama: - specifier: ^0.6.3 - version: 0.6.3 - devDependencies: - '@ttoss/config': - specifier: ^1.35.12 - version: 1.35.12 - '@types/node': - specifier: ^25.0.3 - version: 25.0.3 - jest: - specifier: ^30.2.0 - version: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) - tsup: - specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) - typescript: - specifier: ~5.9.3 - version: 5.9.3 - - packages/files-core: - dependencies: - '@google-cloud/storage': - specifier: ^7.0.0 - version: 7.18.0(encoding@0.1.13) - '@soat/postgresdb': - specifier: workspace:* - version: link:../postgresdb - '@ttoss/postgresdb': - specifier: ^0.3.0 - version: 0.3.0(@types/node@25.0.3)(@types/validator@13.15.10)(reflect-metadata@0.2.2) - aws-sdk: - specifier: ^2.0.0 - version: 2.1693.0 - uuid: - specifier: ^10.0.0 - version: 10.0.0 - devDependencies: - '@ttoss/config': - specifier: ^1.35.12 - version: 1.35.12 - '@types/node': - specifier: ^25.0.3 - version: 25.0.3 - '@types/uuid': - specifier: ^10.0.0 - version: 10.0.0 - jest: - specifier: ^30.2.0 - version: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) - tsup: - specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) - typescript: - specifier: ~5.9.3 - version: 5.9.3 - packages/postgresdb: dependencies: '@ttoss/postgresdb': - specifier: ^0.3.0 - version: 0.3.0(@types/node@25.0.3)(@types/validator@13.15.10)(reflect-metadata@0.2.2) + specifier: ^0.8.0 + version: 0.8.0(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2) devDependencies: '@testcontainers/postgresql': - specifier: ^11.11.0 - version: 11.11.0 + specifier: ^11.13.0 + version: 11.13.0 '@ttoss/config': - specifier: ^1.35.12 - version: 1.35.12 + specifier: ^1.37.8 + version: 1.37.8 '@ttoss/postgresdb-cli': - specifier: ^0.1.24 - version: 0.1.24 + specifier: ^0.2.8 + version: 0.2.8 '@ttoss/test-utils': - specifier: ^4.0.2 - version: 4.0.2(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 + specifier: ^4.2.8 + version: 4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/node': - specifier: ^25.0.3 - version: 25.0.3 - jest: - specifier: ^30.2.0 - version: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) + specifier: ^25.5.2 + version: 25.5.2 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ~6.0.2 + version: 6.0.2 packages/server: dependencies: - '@soat/documents-core': - specifier: workspace:* - version: link:../documents-core - '@soat/embeddings-core': - specifier: workspace:* - version: link:../embeddings-core - '@soat/files-core': - specifier: workspace:* - version: link:../files-core '@soat/postgresdb': specifier: workspace:* version: link:../postgresdb '@ttoss/http-server': - specifier: ^0.3.2 - version: 0.3.2 + specifier: ^0.5.8 + version: 0.5.8 '@ttoss/http-server-mcp': - specifier: ^0.3.2 - version: 0.3.2 + specifier: ^0.11.0 + version: 0.11.0 '@ttoss/postgresdb': - specifier: ^0.3.0 - version: 0.3.0(@types/node@25.0.3)(@types/validator@13.15.10)(reflect-metadata@0.2.2) + specifier: ^0.8.0 + version: 0.8.0(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2) dotenv: - specifier: ^17.2.3 - version: 17.2.3 + specifier: ^17.4.1 + version: 17.4.1 ollama: specifier: ^0.6.3 version: 0.6.3 pg: - specifier: ^8.16.3 - version: 8.16.3 + specifier: ^8.20.0 + version: 8.20.0 devDependencies: '@stoplight/spectral-cli': specifier: ^6.15.0 version: 6.15.0(encoding@0.1.13) + '@testcontainers/postgresql': + specifier: ^11.13.0 + version: 11.13.0 '@ttoss/config': - specifier: ^1.35.12 - version: 1.35.12 + specifier: ^1.37.8 + version: 1.37.8 '@ttoss/test-utils': - specifier: ^4.0.2 - version: 4.0.2(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^4.2.8 + version: 4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/jest': specifier: ^30.0.0 version: 30.0.0 '@types/pg': - specifier: ^8.16.0 - version: 8.16.0 + specifier: ^8.20.0 + version: 8.20.0 '@types/supertest': - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.2.0 + version: 7.2.0 jest: - specifier: ^30.2.0 - version: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) + specifier: ^30.3.0 + version: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) supertest: specifier: ^7.2.2 version: 7.2.2 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) - tsx: - specifier: ^4.21.0 - version: 4.21.0 - - packages/text-atomizer: - dependencies: - ollama: - specifier: ^0.6.3 - version: 0.6.3 - devDependencies: - '@ttoss/config': - specifier: ^1.35.12 - version: 1.35.12 - '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 - jest: - specifier: ^30.2.0 - version: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) - tsup: - specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -305,10 +172,10 @@ importers: dependencies: '@docusaurus/core': specifier: 3.9.2 - version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/preset-classic': specifier: 3.9.2 - version: 3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@5.9.3) + version: 3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@6.0.2) '@mdx-js/react': specifier: ^3.0.0 version: 3.1.1(@types/react@19.2.7)(react@19.2.3) @@ -317,10 +184,10 @@ importers: version: 2.1.1 docusaurus-plugin-openapi-docs: specifier: ^4.0.0 - version: 4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3) + version: 4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3) docusaurus-theme-openapi-docs: specifier: ^4.0.0 - version: 4.5.1(@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(sass@1.97.2)(webpack@5.104.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1) + version: 4.5.1(@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(sass@1.97.2)(webpack@5.104.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1) prism-react-renderer: specifier: ^2.3.0 version: 2.4.1(react@19.2.3) @@ -341,8 +208,8 @@ importers: specifier: 3.9.2 version: 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) typescript: - specifier: ~5.9.3 - version: 5.9.3 + specifier: ~6.0.2 + version: 6.0.2 packages: @@ -462,18 +329,34 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.5': resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.28.5': resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.28.5': resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -482,12 +365,22 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.28.5': resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-create-regexp-features-plugin@7.28.5': resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} engines: {node: '>=6.9.0'} @@ -499,6 +392,11 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} @@ -511,12 +409,22 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.3': resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.27.1': resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} @@ -525,6 +433,10 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.27.1': resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} engines: {node: '>=6.9.0'} @@ -537,6 +449,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} engines: {node: '>=6.9.0'} @@ -561,11 +479,20 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.28.5': resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} engines: {node: '>=6.9.0'} @@ -596,8 +523,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-proposal-decorators@7.28.0': - resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': + resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -629,8 +562,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-decorators@7.27.1': - resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -646,12 +579,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.28.6': + resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-attributes@7.27.1': resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-meta@7.10.4': resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -734,12 +679,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-async-to-generator@7.27.1': resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-async-to-generator@7.28.6': + resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-block-scoped-functions@7.27.1': resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} engines: {node: '>=6.9.0'} @@ -752,30 +709,60 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-block-scoping@7.28.6': + resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-class-properties@7.27.1': resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-class-properties@7.28.6': + resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-class-static-block@7.28.3': resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 + '@babel/plugin-transform-class-static-block@7.28.6': + resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + '@babel/plugin-transform-classes@7.28.4': resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-classes@7.28.6': + resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-computed-properties@7.27.1': resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-computed-properties@7.28.6': + resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-destructuring@7.28.5': resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} engines: {node: '>=6.9.0'} @@ -788,6 +775,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-dotall-regex@7.28.6': + resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-duplicate-keys@7.27.1': resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} engines: {node: '>=6.9.0'} @@ -800,6 +793,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/plugin-transform-dynamic-import@7.27.1': resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} engines: {node: '>=6.9.0'} @@ -812,12 +811,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-explicit-resource-management@7.28.6': + resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-exponentiation-operator@7.28.5': resolution: {integrity: sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-exponentiation-operator@7.28.6': + resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-export-namespace-from@7.27.1': resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} engines: {node: '>=6.9.0'} @@ -842,6 +853,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-json-strings@7.28.6': + resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-literals@7.27.1': resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} engines: {node: '>=6.9.0'} @@ -854,6 +871,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-logical-assignment-operators@7.28.6': + resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-member-expression-literals@7.27.1': resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} engines: {node: '>=6.9.0'} @@ -872,12 +895,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-systemjs@7.28.5': resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-systemjs@7.29.0': + resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-umd@7.27.1': resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} engines: {node: '>=6.9.0'} @@ -890,6 +925,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/plugin-transform-new-target@7.27.1': resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} engines: {node: '>=6.9.0'} @@ -902,18 +943,36 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': + resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-numeric-separator@7.27.1': resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-numeric-separator@7.28.6': + resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-object-rest-spread@7.28.4': resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-object-rest-spread@7.28.6': + resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-object-super@7.27.1': resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} engines: {node: '>=6.9.0'} @@ -926,12 +985,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-optional-catch-binding@7.28.6': + resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-optional-chaining@7.28.5': resolution: {integrity: sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-optional-chaining@7.28.6': + resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-parameters@7.27.7': resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} engines: {node: '>=6.9.0'} @@ -944,12 +1015,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-private-methods@7.28.6': + resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-private-property-in-object@7.27.1': resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-private-property-in-object@7.28.6': + resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-property-literals@7.27.1': resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} engines: {node: '>=6.9.0'} @@ -992,12 +1075,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-regenerator@7.29.0': + resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-regexp-modifiers@7.27.1': resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-transform-regexp-modifiers@7.28.6': + resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/plugin-transform-reserved-words@7.27.1': resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} engines: {node: '>=6.9.0'} @@ -1022,6 +1117,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-spread@7.28.6': + resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-sticky-regex@7.27.1': resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} engines: {node: '>=6.9.0'} @@ -1058,6 +1159,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-unicode-property-regex@7.28.6': + resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-unicode-regex@7.27.1': resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} engines: {node: '>=6.9.0'} @@ -1070,12 +1177,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-transform-unicode-sets-regex@7.28.6': + resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/preset-env@7.28.5': resolution: {integrity: sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/preset-env@7.29.2': + resolution: {integrity: sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/preset-modules@0.1.6-no-external-plugins': resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: @@ -1105,14 +1224,26 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.5': resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} @@ -1123,85 +1254,81 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@commitlint/cli@20.2.0': - resolution: {integrity: sha512-l37HkrPZ2DZy26rKiTUvdq/LZtlMcxz+PeLv9dzK9NzoFGuJdOQyYU7IEkEQj0pO++uYue89wzOpZ0hcTtoqUA==} + '@commitlint/cli@20.5.0': + resolution: {integrity: sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==} engines: {node: '>=v18'} hasBin: true - '@commitlint/config-conventional@19.8.1': - resolution: {integrity: sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==} + '@commitlint/config-conventional@20.5.0': + resolution: {integrity: sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==} engines: {node: '>=v18'} - '@commitlint/config-validator@20.2.0': - resolution: {integrity: sha512-SQCBGsL9MFk8utWNSthdxd9iOD1pIVZSHxGBwYIGfd67RTjxqzFOSAYeQVXOu3IxRC3YrTOH37ThnTLjUlyF2w==} + '@commitlint/config-validator@20.5.0': + resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==} engines: {node: '>=v18'} - '@commitlint/ensure@20.2.0': - resolution: {integrity: sha512-+8TgIGv89rOWyt3eC6lcR1H7hqChAKkpawytlq9P1i/HYugFRVqgoKJ8dhd89fMnlrQTLjA5E97/4sF09QwdoA==} + '@commitlint/ensure@20.5.0': + resolution: {integrity: sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==} engines: {node: '>=v18'} '@commitlint/execute-rule@20.0.0': resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} engines: {node: '>=v18'} - '@commitlint/format@20.2.0': - resolution: {integrity: sha512-PhNoLNhxpfIBlW/i90uZ3yG3hwSSYx7n4d9Yc+2FAorAHS0D9btYRK4ZZXX+Gm3W5tDtu911ow/eWRfcRVgNWg==} + '@commitlint/format@20.5.0': + resolution: {integrity: sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==} engines: {node: '>=v18'} - '@commitlint/is-ignored@20.2.0': - resolution: {integrity: sha512-Lz0OGeZCo/QHUDLx5LmZc0EocwanneYJUM8z0bfWexArk62HKMLfLIodwXuKTO5y0s6ddXaTexrYHs7v96EOmw==} + '@commitlint/is-ignored@20.5.0': + resolution: {integrity: sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==} engines: {node: '>=v18'} - '@commitlint/lint@20.2.0': - resolution: {integrity: sha512-cQEEB+jlmyQbyiji/kmh8pUJSDeUmPiWq23kFV0EtW3eM+uAaMLMuoTMajbrtWYWQpPzOMDjYltQ8jxHeHgITg==} + '@commitlint/lint@20.5.0': + resolution: {integrity: sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==} engines: {node: '>=v18'} - '@commitlint/load@20.2.0': - resolution: {integrity: sha512-iAK2GaBM8sPFTSwtagI67HrLKHIUxQc2BgpgNc/UMNme6LfmtHpIxQoN1TbP+X1iz58jq32HL1GbrFTCzcMi6g==} + '@commitlint/load@20.5.0': + resolution: {integrity: sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==} engines: {node: '>=v18'} - '@commitlint/message@20.0.0': - resolution: {integrity: sha512-gLX4YmKnZqSwkmSB9OckQUrI5VyXEYiv3J5JKZRxIp8jOQsWjZgHSG/OgEfMQBK9ibdclEdAyIPYggwXoFGXjQ==} + '@commitlint/message@20.4.3': + resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} engines: {node: '>=v18'} - '@commitlint/parse@20.2.0': - resolution: {integrity: sha512-LXStagGU1ivh07X7sM+hnEr4BvzFYn1iBJ6DRg2QsIN8lBfSzyvkUcVCDwok9Ia4PWiEgei5HQjju6xfJ1YaSQ==} + '@commitlint/parse@20.5.0': + resolution: {integrity: sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==} engines: {node: '>=v18'} - '@commitlint/read@20.2.0': - resolution: {integrity: sha512-+SjF9mxm5JCbe+8grOpXCXMMRzAnE0WWijhhtasdrpJoAFJYd5UgRTj/oCq5W3HJTwbvTOsijEJ0SUGImECD7Q==} + '@commitlint/read@20.5.0': + resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==} engines: {node: '>=v18'} - '@commitlint/resolve-extends@20.2.0': - resolution: {integrity: sha512-KVoLDi9BEuqeq+G0wRABn4azLRiCC22/YHR2aCquwx6bzCHAIN8hMt3Nuf1VFxq/c8ai6s8qBxE8+ZD4HeFTlQ==} + '@commitlint/resolve-extends@20.5.0': + resolution: {integrity: sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==} engines: {node: '>=v18'} - '@commitlint/rules@20.2.0': - resolution: {integrity: sha512-27rHGpeAjnYl/A+qUUiYDa7Yn1WIjof/dFJjYW4gA1Ug+LUGa1P0AexzGZ5NBxTbAlmDgaxSZkLLxtLVqtg8PQ==} + '@commitlint/rules@20.5.0': + resolution: {integrity: sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==} engines: {node: '>=v18'} '@commitlint/to-lines@20.0.0': resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} engines: {node: '>=v18'} - '@commitlint/top-level@20.0.0': - resolution: {integrity: sha512-drXaPSP2EcopukrUXvUXmsQMu3Ey/FuJDc/5oiW4heoCfoE5BdLQyuc7veGeE3aoQaTVqZnh4D5WTWe2vefYKg==} - engines: {node: '>=v18'} - - '@commitlint/types@19.8.1': - resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} + '@commitlint/top-level@20.4.3': + resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} engines: {node: '>=v18'} - '@commitlint/types@20.2.0': - resolution: {integrity: sha512-KTy0OqRDLR5y/zZMnizyx09z/rPlPC/zKhYgH8o/q6PuAjoQAKlRfY4zzv0M64yybQ//6//4H1n14pxaLZfUnA==} + '@commitlint/types@20.5.0': + resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} engines: {node: '>=v18'} - '@conventional-changelog/git-client@2.5.1': - resolution: {integrity: sha512-lAw7iA5oTPWOLjiweb7DlGEMDEvzqzLLa6aWOly2FSZ64IwLE8T458rC+o+WvI31Doz6joM7X2DoNog7mX8r4A==} + '@conventional-changelog/git-client@2.6.0': + resolution: {integrity: sha512-T+uPDciKf0/ioNNDpMGc8FDsehJClZP0yR3Q5MN6wE/Y/1QZ7F+80OgznnTCOlMEG4AV0LvH2UJi3C/nBnaBUg==} engines: {node: '>=18'} peerDependencies: conventional-commits-filter: ^5.0.0 - conventional-commits-parser: ^6.1.0 + conventional-commits-parser: ^6.3.0 peerDependenciesMeta: conventional-commits-filter: optional: true @@ -1752,171 +1879,333 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.1': resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.1': resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.1': resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.1': resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.1': resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.1': resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.1': resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.1': resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.1': resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.1': resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.1': resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.1': resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.1': resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.1': resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.1': resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.1': resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.1': resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.1': resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.1': resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.1': resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.1': resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.1': resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.1': resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.1': resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.1': resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@2.0.0': - resolution: {integrity: sha512-T9AfE1G1uv4wwq94ozgTGio5EUQBqAVe1X9qsQtSNVEYW6j3hvtZVm8Smr4qL1qDPFg+lOB2cL5RxTRMzq4CTA==} + '@eslint/compat@2.0.4': + resolution: {integrity: sha512-o598tCGstJv9Kk4XapwP+oDij9HD9Qr3V37ABzTfdzVvbFciV+sfg9zSW6olj6G/IXj7p89SwSzPnZ+JUEPIPg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: - eslint: ^8.40 || 9 + eslint: ^8.40 || 9 || 10 peerDependenciesMeta: eslint: optional: true @@ -1933,14 +2222,18 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.0.0': - resolution: {integrity: sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==} + '@eslint/core@1.2.0': + resolution: {integrity: sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/eslintrc@3.3.3': resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.2': resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1956,53 +2249,41 @@ packages: '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} - '@faker-js/faker@10.2.0': - resolution: {integrity: sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==} + '@faker-js/faker@10.4.0': + resolution: {integrity: sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} '@faker-js/faker@5.5.3': resolution: {integrity: sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==} deprecated: Please update to a newer version. - '@formatjs/ecma402-abstract@2.3.6': - resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + '@formatjs/bigdecimal@0.2.0': + resolution: {integrity: sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==} - '@formatjs/fast-memoize@2.2.7': - resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + '@formatjs/ecma402-abstract@3.2.0': + resolution: {integrity: sha512-dHnqHgBo6GXYGRsepaE1wmsC2etaivOWd5VaJstZd+HI2zR3DCUjbDVZRtoPGkkXZmyHvBwrdEUuqfvzhF/DtQ==} - '@formatjs/icu-messageformat-parser@2.11.4': - resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + '@formatjs/fast-memoize@3.1.1': + resolution: {integrity: sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==} - '@formatjs/icu-skeleton-parser@1.8.16': - resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + '@formatjs/icu-messageformat-parser@3.5.3': + resolution: {integrity: sha512-HJWZ9S6JWey6iY5+YXE3Kd0ofWU1sC2KTTp56e1168g/xxWvVvr8k9G4fexIgwYV9wbtjY7kGYK5FjoWB3B2OQ==} - '@formatjs/intl-localematcher@0.6.2': - resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + '@formatjs/icu-skeleton-parser@2.1.3': + resolution: {integrity: sha512-9mFp8TJ166ZM2pcjKwsBWXrDnOJGT7vMEScVgLygUODPOsE8S6f/FHoacvrlHK1B4dYZk8vSCNruyPU64AfgJQ==} - '@formatjs/ts-transformer@3.14.2': - resolution: {integrity: sha512-c47ij+2Xi4jMDO3Hz01BDF3yB4575Gkoq24sFzVw1K1kpHvITsFfdlXQbhxScBwJi2gBhMpuZ++XsTUZ9O0Law==} + '@formatjs/intl-localematcher@0.8.2': + resolution: {integrity: sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==} + + '@formatjs/ts-transformer@4.4.3': + resolution: {integrity: sha512-K7hAYg8RRrgdvygvgwBYPmXCZwiXW0Q9XNorxg9z9ahiPKMXpumvmAV4PLu8/9qIqCRL7wANLGhYl9PyB/CqPA==} + engines: {node: '>= 20.12.0'} peerDependencies: ts-jest: ^29 peerDependenciesMeta: ts-jest: optional: true - '@google-cloud/paginator@5.0.2': - resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} - engines: {node: '>=14.0.0'} - - '@google-cloud/projectify@4.0.0': - resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} - engines: {node: '>=14.0.0'} - - '@google-cloud/promisify@4.0.0': - resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} - engines: {node: '>=14'} - - '@google-cloud/storage@7.18.0': - resolution: {integrity: sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ==} - engines: {node: '>=14'} - '@grpc/grpc-js@1.14.3': resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} engines: {node: '>=12.10.0'} @@ -2026,6 +2307,12 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@hono/node-server@1.19.13': + resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@hookform/error-message@2.0.1': resolution: {integrity: sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==} peerDependencies: @@ -2049,53 +2336,53 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@inquirer/ansi@1.0.2': - resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} - engines: {node: '>=18'} + '@inquirer/ansi@2.0.5': + resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - '@inquirer/core@10.3.2': - resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} - engines: {node: '>=18'} + '@inquirer/core@11.1.8': + resolution: {integrity: sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/expand@4.0.23': - resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} - engines: {node: '>=18'} + '@inquirer/expand@5.0.11': + resolution: {integrity: sha512-yxSO89MQ7t4LTCwtsXQ/ppcfw2otLsum6nF+TM9pKesy3k2AhVDUIkaiJIwG6lzm/csc5n38MaFKLY0TrSHzEA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/figures@1.0.15': - resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} - engines: {node: '>=18'} + '@inquirer/figures@2.0.5': + resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - '@inquirer/input@4.3.1': - resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} - engines: {node: '>=18'} + '@inquirer/input@5.0.11': + resolution: {integrity: sha512-twUWidn4ocPO8qi6fRM7tNWt7W1FOnOZqQ+/+PsfLUacMR5rFLDPK9ql0nBPwxi0oELbo8T5NhRs8B2+qQEqFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/select@4.4.2': - resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} - engines: {node: '>=18'} + '@inquirer/select@5.1.3': + resolution: {integrity: sha512-zYyqWgGQi3NhBcNq4Isc5rB3oEdQEh1Q/EcAnOW0FK4MpnXWkvSBYgA4cYrTM4A9UB573omouZbnL9JJ74Mq3A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/type@3.0.10': - resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} - engines: {node: '>=18'} + '@inquirer/type@4.0.5': + resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: @@ -2126,12 +2413,12 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - '@jest/console@30.2.0': - resolution: {integrity: sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==} + '@jest/console@30.3.0': + resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/core@30.2.0': - resolution: {integrity: sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==} + '@jest/core@30.3.0': + resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -2143,6 +2430,10 @@ packages: resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/diff-sequences@30.3.0': + resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment-jsdom-abstract@30.2.0': resolution: {integrity: sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2157,32 +2448,44 @@ packages: resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@30.3.0': + resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect-utils@30.2.0': resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect@30.2.0': - resolution: {integrity: sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==} + '@jest/expect-utils@30.3.0': + resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.3.0': + resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/fake-timers@30.2.0': resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/fake-timers@30.3.0': + resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/get-type@30.1.0': resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/globals@30.2.0': - resolution: {integrity: sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==} + '@jest/globals@30.3.0': + resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/pattern@30.0.1': resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/reporters@30.2.0': - resolution: {integrity: sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==} + '@jest/reporters@30.3.0': + resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -2198,24 +2501,24 @@ packages: resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/snapshot-utils@30.2.0': - resolution: {integrity: sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==} + '@jest/snapshot-utils@30.3.0': + resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/source-map@30.0.1': resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-result@30.2.0': - resolution: {integrity: sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==} + '@jest/test-result@30.3.0': + resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-sequencer@30.2.0': - resolution: {integrity: sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==} + '@jest/test-sequencer@30.3.0': + resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/transform@30.2.0': - resolution: {integrity: sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==} + '@jest/transform@30.3.0': + resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/types@29.6.3': @@ -2226,6 +2529,10 @@ packages: resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@30.3.0': + resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2331,16 +2638,19 @@ packages: peerDependencies: koa: ^2.0.0 || ^3.0.0 + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} - '@lerna-lite/changed@4.10.2': - resolution: {integrity: sha512-9gua2+7owjyvHPWDOJYumNNGhHxMCbCkYM/rv/cemafvj7ncHcCvVydPTVBuSNwayC/ufSk+o0zFCtF91JixSQ==} - engines: {node: ^20.17.0 || >=22.9.0} + '@lerna-lite/changed@5.0.0': + resolution: {integrity: sha512-BnhK576DhYQo4B9r0Jy5PAEG3tWCQgeSDRaRqa44tfmSsDaGE4cYTHjSf3gN+ponl5xSL41bIH059uR+2PjQdQ==} + engines: {node: ^22.17.0 || >=24.0.0} - '@lerna-lite/cli@4.10.2': - resolution: {integrity: sha512-yT5/z/FvVKWO9NDF7VTN5bYkuF96yH5VPl1I6n37BOOj/KFtPSG2SEMclbkJ+CCYw/kmV7sxHeodIyWnpJ8Ggw==} - engines: {node: ^20.17.0 || >=22.9.0} + '@lerna-lite/cli@5.0.0': + resolution: {integrity: sha512-kb9MXI+t6EMdPTreUEAGXQPAtnqGhiXDll1lSWFNFwZ+XWI1mj/tB2UDDbE7/g41r4p17yI1J8RYUyFIzFMaFA==} + engines: {node: ^22.17.0 || >=24.0.0} hasBin: true peerDependencies: '@lerna-lite/exec': '*' @@ -2363,29 +2673,29 @@ packages: '@lerna-lite/watch': optional: true - '@lerna-lite/core@4.10.2': - resolution: {integrity: sha512-LTGO6tWIBHi5clHECz5tqhtWCUedlyX4n93SbIP1n4ehPS+qSuHh0fJ3QvQQX18jvktKFwaR7AKzrL21lxtGYQ==} - engines: {node: ^20.17.0 || >=22.9.0} + '@lerna-lite/core@5.0.0': + resolution: {integrity: sha512-A30bmrqhKsUVRyfQeCeLo3ht9s+l2Rdi++MxBKlhK/Zwnkq3sv9l2iIiU91ItKJ3FP2FO7EEzOWxrp0Wcwgwdg==} + engines: {node: ^22.17.0 || >=24.0.0} - '@lerna-lite/init@4.10.2': - resolution: {integrity: sha512-TNnSQ7ewaY/jSvJGQSTRQT+vHt/wa3LKEEM7jkH4VSG4wCbzIV2u3xCOZ1n+oVaBaH2FO54qUKaqU8qruxVy4g==} - engines: {node: ^20.17.0 || >=22.9.0} + '@lerna-lite/init@5.0.0': + resolution: {integrity: sha512-nCZ2yG0ExTMG27dMZ2IeDzwbR9Wsq4P6ZbqGlGSLicCMRtU9VsvtveDI4Jtkhjn9WNaO6FFu8ktbwPiQ0i88Xw==} + engines: {node: ^22.17.0 || >=24.0.0} - '@lerna-lite/list@4.10.2': - resolution: {integrity: sha512-a2RuHz2rgFhTpIicv0zdt39ePXSAHbq+1AKLyJiANTz2vs9N8Y4WV0iBRG0cOudHnTceundBHc+9PaYVcOkbAA==} - engines: {node: ^20.17.0 || >=22.9.0} + '@lerna-lite/list@5.0.0': + resolution: {integrity: sha512-OQelKYKSqUvOdzLF7jPMj8SlE+2LASqkoxJhzjTPN05zRtz0lBD6VPERUpq9GYhlyNhg6vXBgyEUD+p5REjy/A==} + engines: {node: ^22.17.0 || >=24.0.0} - '@lerna-lite/listable@4.10.2': - resolution: {integrity: sha512-bnXm5Cd+QZ0brZkqt8pYWn/fxdGjz0g50tljZqN8lOpVF8VLrtqiaIOdob47TGxt3L68DL4FiBzIiSZNT93XqA==} - engines: {node: ^20.17.0 || >=22.9.0} + '@lerna-lite/listable@5.0.0': + resolution: {integrity: sha512-zpZUwX2NOM9pVziVM1uutVCFibxF2mWFcqT6anCITud2QN4ov6gSbKYiBLg8VcE7gZW8YlYw6Ib5eC2P1oZJfQ==} + engines: {node: ^22.17.0 || >=24.0.0} - '@lerna-lite/npmlog@4.10.0': - resolution: {integrity: sha512-vwI9qbhbbEjZJW/xXcOypqbIp3QXjsFD0kxGeHpGWXheeMtQSkRicJHH6v2dwVFid10EQmET47ItlCRAMhp12g==} - engines: {node: ^20.17.0 || >=22.9.0} + '@lerna-lite/npmlog@5.0.0': + resolution: {integrity: sha512-ZFgl88Las1pCBUoDsDFi9G1vCKXiauMCqrT2qkv3g7lxInvKRc0jGHG68W/NV7iMOD5JU5fhWRwBQmJ1DPsrbA==} + engines: {node: ^22.17.0 || >=24.0.0} - '@lerna-lite/version@4.10.2': - resolution: {integrity: sha512-Vp989fhidy2nULnOVhakaO2yqevUggcZmOWg6XoI8TV7lRDNPuxujMmWG71KNI4OSKeOq0aHrMR8XkyIYejwpw==} - engines: {node: ^20.17.0 || >=22.9.0} + '@lerna-lite/version@5.0.0': + resolution: {integrity: sha512-WfsCoRsrfQm1gYi6XKB+Yh5tvAGkL0PYBEEjo8hzfBO+YynxAyI1VWbZwI/fLNNZcDR9E8u7ceUE9B6gFbkGTA==} + engines: {node: ^22.17.0 || >=24.0.0} '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -2396,8 +2706,8 @@ packages: '@types/react': '>=16' react: '>=16' - '@modelcontextprotocol/sdk@1.24.3': - resolution: {integrity: sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -2449,8 +2759,8 @@ packages: resolution: {integrity: sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==} engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/run-script@10.0.3': - resolution: {integrity: sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==} + '@npmcli/run-script@10.0.4': + resolution: {integrity: sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==} engines: {node: ^20.17.0 || >=22.9.0} '@octokit/auth-token@6.0.0': @@ -2544,36 +2854,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -2717,56 +3033,67 @@ packages: resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.53.3': resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.3': resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.53.3': resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.53.3': resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.53.3': resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.53.3': resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.53.3': resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.53.3': resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.3': resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.53.3': resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.53.3': resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} @@ -2812,8 +3139,12 @@ packages: resolution: {integrity: sha512-3nWd8irxvDI6v856wpPCHZ+08iQR0oHTZfzAZmnbsLzf+Sf1odraP6uKOHDZToXq3RPRV/LbqGVlSCogm9cJjg==} engines: {node: '>=18'} - '@simple-libs/stream-utils@1.1.0': - resolution: {integrity: sha512-6rsHTjodIn/t90lv5snQjRPVtOosM7Vp0AKdrObymq45ojlgVwnpAqdc+0OBBrpEiy31zZ6/TKeIVqV1HwvnuQ==} + '@simple-libs/hosted-git-info@1.0.2': + resolution: {integrity: sha512-aAmGQdMH+ZinytKuA2832u0ATeOFNYNk4meBEXtB5xaPotUgggYNhq5tYU/v17wEbmTW5P9iHNqNrFyrhnqBAg==} + engines: {node: '>=18'} + + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} engines: {node: '>=18'} '@sinclair/typebox@0.27.8': @@ -2830,10 +3161,6 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -2844,6 +3171,9 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@sinonjs/fake-timers@15.3.0': + resolution: {integrity: sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==} + '@slorber/react-helmet-async@1.3.0': resolution: {integrity: sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==} peerDependencies: @@ -3035,8 +3365,8 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@testcontainers/postgresql@11.11.0': - resolution: {integrity: sha512-Og64I/h5LKLVvUTkAcLeTXfFcMhh3dCHCypN3Uzd+tQMd70SpCfQ0LCP9v/U+MS7JBRzU9EmqhUFkTOm4hyZWw==} + '@testcontainers/postgresql@11.13.0': + resolution: {integrity: sha512-Y+HLf+IEu9+h3MgxRhnFunw9ldk3jxN2PO6zyejN5AiDd1aSeBQ7hfpc/4MlMm65St2DdLGf39oE/vdW1+hc/Q==} '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} @@ -3046,8 +3376,8 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.1': - resolution: {integrity: sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -3067,10 +3397,6 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tootallnate/once@2.0.0': - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -3087,41 +3413,71 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@ttoss/config@1.35.12': - resolution: {integrity: sha512-anFVs+giW3Lcbmf7Ah+Ni0RbngDTAM0i20FRXJeGE+1DbMxPhxUSr5GKvNcGv/0keS3tZc38fQfM1C1uPgsLCw==} + '@ttoss/config@1.37.8': + resolution: {integrity: sha512-Ikk44m4mM+xiuh4JaubdFCVtTKDRpM4lfZ2I/bU7uNJ9uGk+Vv7d+PxpYXwvb3aaNhTEHYfs4OMRnC4KSRyHmQ==} - '@ttoss/eslint-config@1.26.6': - resolution: {integrity: sha512-l6R7F0ToH8HpzcwoHqiX/Z7f5m1mbUHPFWDdFTI5jzPP534yumZIVbCjDADDS3l+cp5G8Nxjtiiu/c8RlywVhA==} + '@ttoss/eslint-config@1.26.14': + resolution: {integrity: sha512-r4XF6u0+WaqVApGxS5WFgViuOML8k+qU43s9UjGB8RIropCIRwjTAh9NM7XMl0hw/iGdIdqmnqs+NPt0seVMBQ==} peerDependencies: eslint: '>=9.20.0' - '@ttoss/http-server-mcp@0.3.2': - resolution: {integrity: sha512-RDnqoKm2UQpUOgt5BFvhepsevvNBLr0DKEzpUYQodHzVJFqAyWoGeCNpAIkujz9Bv5BHDdi1cbfSOTmbmu1sww==} + '@ttoss/http-server-mcp@0.11.0': + resolution: {integrity: sha512-VWTWZDB4waHxhx5ZM5P4Yk8jtdR/LRDRTCGnHmkufIybaAUDvzQ8pBnCnpTw21Li18mZw7CLBZsN5Pja0uWJ/Q==} - '@ttoss/http-server@0.3.2': - resolution: {integrity: sha512-ghM10mkZc3P8hYG+Z9eKR3YGURf2/dZIvYIu0GLAvLL+s/AJlRZBEpEaJpcjimwzZo1hq6wtNkfHQBnOGLIOhA==} + '@ttoss/http-server@0.5.8': + resolution: {integrity: sha512-Vwur4xGiFaDSGQJ6gPzryZWUE8srraND2fYLQPjJHvviTHxldRuJoTF5afJGiqpvsMLCv9JuUOeOiGDFuFl8yA==} '@ttoss/logger@0.7.1': resolution: {integrity: sha512-PxuFk1TvIRLxVYr5PXKLDsmmfTw+lV6Dl8IP6NA2JPgmT8J7EDjv5mAOTdJteIiUZwCPL2a1Z/rrYHU8pf2LPQ==} - '@ttoss/monorepo@1.28.0': - resolution: {integrity: sha512-kP6Kmi/YYbz/CvJzsNTSLHWakz+/SNyRUWiptvj9lfIOUrUKHiN/lKdKjeAfPyee9zpEYEwb48YDUpkV1NHrBg==} + '@ttoss/monorepo@1.29.8': + resolution: {integrity: sha512-Z24zgzFbtqJoqz7gr6XTQnBTMR1Mhl1GWZz6TKp68ZdLgporbqMqhKeAt9JhsK9AiuXYN1Zt2xJQPmGe6A3VkA==} hasBin: true - '@ttoss/postgresdb-cli@0.1.24': - resolution: {integrity: sha512-7PT1jvgu7ko2KOIBBQoEvsnnzzBN+ivfw3Z1DpucmQKPZAZ2sfpJis1nQxLKK1ktBlbX4XD9Y3UCmczYn2NOIg==} + '@ttoss/postgresdb-cli@0.2.8': + resolution: {integrity: sha512-X7bV26o4dipUerWqDjfOOU2akOibJcMP1TfqYlHTNXIc1DJPEHFNj+dZGE5bypenH3tCMrGs7nG+k0X6HUAb3Q==} hasBin: true - '@ttoss/postgresdb@0.3.0': - resolution: {integrity: sha512-YSNcS2ulsdbWJJkRC4S9ARy4dIR4U+wqT+kAVCb+HtMuqZRR38dZ5DmhYJ2DV0Q/eIgUplvFz9C2GMMDdTq7Gg==} + '@ttoss/postgresdb@0.8.0': + resolution: {integrity: sha512-icGG5SBGjWomyEXUJ/n4hUMVowyRSoGEGI4zpmJ/+pTGilel/vQYig+R8Sco1Lwq+rVwnfpC2yGCMzir3VZkVw==} - '@ttoss/test-utils@4.0.2': - resolution: {integrity: sha512-lmXG49ufbcM//SQtcaopzDw83HgjfAEzLgrarlRKDaKLZL1Yx/CEPCQvoG0T1z4kp3CpYhvsFEWgwBZ6ta4ttA==} + '@ttoss/test-utils@4.2.8': + resolution: {integrity: sha512-3Tw3miD59SZOQK3GQJwsmh5yd957zVauMB0wnHDH6L/nSELuhS9+T2uuLWXiO+pIt/GA025zjQDDGxvOgVmhrw==} peerDependencies: jest: ^30.0.0 react: '>=16.8.0' react-dom: '>=16.8.0' + '@turbo/darwin-64@2.9.4': + resolution: {integrity: sha512-ZSlPqJ5Vqg/wgVw8P3AOVCIosnbBilOxLq7TMz3MN/9U46DUYfdG2jtfevNDufyxyrg98pcPs/GBgDRaaids6g==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.9.4': + resolution: {integrity: sha512-9cjTWe4OiNlFMSRggPNh+TJlRs7MS5FWrHc96MOzft5vESWjjpvaadYPv5ykDW7b45mVHOF2U/W+48LoX9USWw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.9.4': + resolution: {integrity: sha512-Cl1GjxqBXQ+r9KKowmXG+lhD1gclLp48/SE7NxL//66iaMytRw0uiphWGOkccD92iPiRjHLRUaA9lOTtgr5OCA==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.9.4': + resolution: {integrity: sha512-j2hPAKVmGNN2EsKigEWD+43y9m7zaPhNAs6ptsyfq0u7evHHBAXAwOfv86OEMg/gvC+pwGip0i1CIm1bR1vYug==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.9.4': + resolution: {integrity: sha512-1jWPjCe9ZRmsDTXE7uzqfySNQspnUx0g6caqvwps+k/sc+fm9hC/4zRQKlXZLbVmP3Xxp601Ju71boegHdnYGw==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.9.4': + resolution: {integrity: sha512-dlko15TQVu/BFYmIY018Y3covWMRQlUgAkD+OOk+Rokcfj6VY02Vv4mCfT/Zns6B4q8jGbOd6IZhnCFYsE8Viw==} + cpu: [arm64] + os: [win32] + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -3149,9 +3505,6 @@ packages: '@types/bonjour@3.5.13': resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} - '@types/caseless@0.12.5': - resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} - '@types/co-body@6.1.3': resolution: {integrity: sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==} @@ -3161,9 +3514,6 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/conventional-commits-parser@5.0.2': - resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} - '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} @@ -3173,8 +3523,8 @@ packages: '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} - '@types/dockerode@3.3.47': - resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} '@types/es-aggregate-error@1.0.6': resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==} @@ -3283,8 +3633,8 @@ packages: '@types/node@22.19.3': resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} - '@types/node@25.0.3': - resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -3302,11 +3652,11 @@ packages: '@types/parse5@6.0.3': resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} - '@types/pg@8.16.0': - resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - '@types/picomatch@3.0.2': - resolution: {integrity: sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==} + '@types/picomatch@4.0.3': + resolution: {integrity: sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==} '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -3344,9 +3694,6 @@ packages: '@types/relay-test-utils@19.0.0': resolution: {integrity: sha512-yC/wVgDetV+88HrHLKlesIfju3RGsp32vLs0IiwBPKD0npQSTrFddcP3FNPe5Kkk4/Y/kgY3oR/DA8g8Bmmppw==} - '@types/request@2.48.13': - resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} - '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} @@ -3386,8 +3733,8 @@ packages: '@types/superagent@8.1.9': resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} - '@types/supertest@6.0.3': - resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/supertest@7.2.0': + resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==} '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -3401,9 +3748,6 @@ packages: '@types/urijs@1.19.26': resolution: {integrity: sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==} - '@types/uuid@10.0.0': - resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} - '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -3416,20 +3760,20 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.49.0': - resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.49.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser': ^8.58.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.49.0': - resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/project-service@8.49.0': resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} @@ -3437,33 +3781,59 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@8.49.0': resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.49.0': resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.49.0': - resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/types@8.49.0': resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.49.0': resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.49.0': resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3471,13 +3841,27 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.49.0': resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@unicode/unicode-17.0.0@1.6.16': + resolution: {integrity: sha512-advq5p36zZ+PDRUpDkWcHHR++R19kx0LYB5iG3bj0KB8mYVKg0ywS996e2bXeXxDb8XdOF7KTivcx7VkYie1pg==} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -3517,41 +3901,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -3628,10 +4020,6 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - JSONStream@1.3.5: - resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} - hasBin: true - abbrev@4.0.0: resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3672,10 +4060,6 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -3732,6 +4116,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.11.0: resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} @@ -3753,10 +4140,6 @@ packages: ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -3800,9 +4183,6 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - aproba@2.1.0: - resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} - archiver-utils@5.0.2: resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} engines: {node: '>= 14'} @@ -3872,10 +4252,6 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - arrify@2.0.1: - resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} - engines: {node: '>=8'} - as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} @@ -3903,9 +4279,6 @@ packages: async-lock@1.4.1: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} - async-retry@1.3.3: - resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} - async@3.2.2: resolution: {integrity: sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==} @@ -3937,10 +4310,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - aws-sdk@2.1693.0: - resolution: {integrity: sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==} - engines: {node: '>= 10.0.0'} - axe-core@4.11.0: resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} @@ -3957,8 +4326,8 @@ packages: react-native-b4a: optional: true - babel-jest@30.2.0: - resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} + babel-jest@30.3.0: + resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@babel/core': ^7.11.0 || ^8.0.0-0 @@ -3973,15 +4342,15 @@ packages: babel-plugin-dynamic-import-node@2.3.3: resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} - babel-plugin-formatjs@10.5.41: - resolution: {integrity: sha512-ZpozYGek+Bdyl52LgzW1MhPYBRKbROdbHBuBz7KAO88Ht7GcyCBiwJxpAjVDb0YBA9LGKUemGQOLdEDkRCe2hg==} + babel-plugin-formatjs@11.3.2: + resolution: {integrity: sha512-/l7gaxOGyuHLk7sTNxsM4LXKy0rqa2S2hs5VemLEvGcyaCeUXVxdGxVgMxnh/dkoiOlx75nJKqgC914DEQ+FmQ==} babel-plugin-istanbul@7.0.1: resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} engines: {node: '>=12'} - babel-plugin-jest-hoist@30.2.0: - resolution: {integrity: sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==} + babel-plugin-jest-hoist@30.3.0: + resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} babel-plugin-polyfill-corejs2@0.4.14: @@ -3989,16 +4358,31 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs3@0.13.0: resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-regenerator@0.6.5: resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-transform-import-meta@2.3.3: resolution: {integrity: sha512-bbh30qz1m6ZU1ybJoNOhA2zaDvmeXMnGNBMVMDOJ1Fni4+wMBoy/j7MTRVmqAUCIcy54/rEnr9VEBsfcgbpm3Q==} peerDependencies: @@ -4009,8 +4393,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 - babel-preset-jest@30.2.0: - resolution: {integrity: sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==} + babel-preset-jest@30.3.0: + resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@babel/core': ^7.11.0 || ^8.0.0-beta.1 @@ -4021,6 +4405,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + bare-events@2.8.2: resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} peerDependencies: @@ -4078,9 +4466,6 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -4116,6 +4501,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -4132,15 +4521,9 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@4.9.2: - resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -4243,10 +4626,6 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chalk-template@1.1.2: - resolution: {integrity: sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==} - engines: {node: '>=14.16'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -4312,6 +4691,10 @@ packages: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + cjs-module-lexer@2.1.1: resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==} @@ -4335,10 +4718,6 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} @@ -4427,14 +4806,14 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} - commander@14.0.2: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -4525,29 +4904,25 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - conventional-changelog-angular@7.0.0: - resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} - engines: {node: '>=16'} - - conventional-changelog-angular@8.1.0: - resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==} + conventional-changelog-angular@8.3.1: + resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==} engines: {node: '>=18'} - conventional-changelog-conventionalcommits@7.0.2: - resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} - engines: {node: '>=16'} + conventional-changelog-conventionalcommits@9.3.1: + resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} + engines: {node: '>=18'} conventional-changelog-preset-loader@5.0.0: resolution: {integrity: sha512-SetDSntXLk8Jh1NOAl1Gu5uLiCNSYenB5tm0YVeZKePRIgDW9lQImromTwLa3c/Gae298tsgOM+/CYT9XAl0NA==} engines: {node: '>=18'} - conventional-changelog-writer@8.2.0: - resolution: {integrity: sha512-Y2aW4596l9AEvFJRwFGJGiQjt2sBYTjPD18DdvxX9Vpz0Z7HQ+g1Z+6iYDAm1vR3QOJrDBkRHixHK/+FhkR6Pw==} + conventional-changelog-writer@8.4.0: + resolution: {integrity: sha512-HHBFkk1EECxxmCi4CTu091iuDpQv5/OavuCUAuZmrkWpmYfyD816nom1CvtfXJ/uYfAAjavgHvXHX291tSLK8g==} engines: {node: '>=18'} hasBin: true - conventional-changelog@7.1.1: - resolution: {integrity: sha512-rlqa8Lgh8YzT3Akruk05DR79j5gN9NCglHtJZwpi6vxVeaoagz+84UAtKQj/sT+RsfGaZkt3cdFCjcN6yjr5sw==} + conventional-changelog@7.2.0: + resolution: {integrity: sha512-BEdgG+vPl53EVlTTk9sZ96aagFp0AQ5pw/ggiQMy2SClLbTo1r0l+8dSg79gkLOO5DS1Lswuhp5fWn6RwE+ivg==} engines: {node: '>=18'} hasBin: true @@ -4555,13 +4930,8 @@ packages: resolution: {integrity: sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==} engines: {node: '>=18'} - conventional-commits-parser@5.0.0: - resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} - engines: {node: '>=16'} - hasBin: true - - conventional-commits-parser@6.2.1: - resolution: {integrity: sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA==} + conventional-commits-parser@6.4.0: + resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} engines: {node: '>=18'} hasBin: true @@ -4604,6 +4974,9 @@ packages: core-js-compat@3.47.0: resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + core-js-pure@3.47.0: resolution: {integrity: sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==} @@ -4638,8 +5011,8 @@ packages: typescript: optional: true - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' @@ -4805,10 +5178,6 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - dargs@8.1.0: - resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} - engines: {node: '>=12'} - data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} @@ -4874,6 +5243,14 @@ packages: babel-plugin-macros: optional: true + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-equal@1.0.1: resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} @@ -4884,10 +5261,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge-ts@7.1.5: - resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} - engines: {node: '>=16.0.0'} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -4997,8 +5370,8 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} - docker-compose@1.3.0: - resolution: {integrity: sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==} + docker-compose@1.4.2: + resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==} engines: {node: '>= 6.0.0'} docker-modem@5.0.6: @@ -5089,8 +5462,13 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + dottie@2.0.6: resolution: {integrity: sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -5099,21 +5477,12 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - duplexify@4.1.3: - resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@3.19.12: - resolution: {integrity: sha512-7F9RGTrCTC3D7nh9Zw+3VlJWwZgo5k33KA+476BAaD0rKIXKZsY/jQ+ipyhR/Avo239Fi6GqAVFs1mqM1IJ7yg==} - electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -5121,10 +5490,6 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - emoji-regex-xs@2.0.1: - resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} - engines: {node: '>=10.0.0'} - emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -5158,10 +5523,6 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} - enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} - entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} @@ -5240,6 +5601,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -5273,8 +5639,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-config-turbo@2.6.3: - resolution: {integrity: sha512-HS6aanr+Cg4X1Ss8AObgdsa9LvSi1rHAU7sHWqD4MWKe+/4uPt0Zqt6VqX1QMIJI6bles+QxSpMPnahMN9hPLg==} + eslint-config-turbo@2.9.4: + resolution: {integrity: sha512-Mbkr1xA22hbp2FUFKt1hEUhTN858wndXbk/h1NdKcvfqn4gxJQoUHyEBDONN0KxgXEhWHl3Le3W4EM6TraN4qw==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' @@ -5325,10 +5691,10 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-formatjs@5.4.2: - resolution: {integrity: sha512-IdJt/il0FASmk/aJDzl96Zh0tovm+KVhCbA5d+YC14gOpeFe1n6766JMi/RP9YOY9dhe6BbWEJnk9dPJwMMngw==} + eslint-plugin-formatjs@6.4.4: + resolution: {integrity: sha512-ggmS9LhU2D7+RJEE+c6yQmxjE1yqU+/CfSxu2aVQQ6dVqW+dC5QpCOogCLVk3EkhWKF99eEUeG4b8HhImDSqnA==} peerDependencies: - eslint: ^9.23.0 + eslint: 9 || 10 eslint-plugin-import@2.32.0: resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} @@ -5350,18 +5716,21 @@ packages: '@testing-library/dom': optional: true - eslint-plugin-jest@29.5.0: - resolution: {integrity: sha512-DAi9H8xN/TUuNOt+xDP1RqpCJLsSxBb5u1zXSpCyp0VAWGL8MBAg5t7/Dk+76iX7d1LhWu4DDH77IQNUolLDyg==} + eslint-plugin-jest@29.15.1: + resolution: {integrity: sha512-6BjyErCQauz3zfJvzLw/kAez2lf4LEpbHLvWBfEcG4EI0ZiRSwjoH2uZulMouU8kRkBH+S0rhqn11IhTvxKgKw==} engines: {node: ^20.12.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@typescript-eslint/eslint-plugin': ^8.0.0 - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 jest: '*' + typescript: '>=4.8.4 <7.0.0' peerDependenciesMeta: '@typescript-eslint/eslint-plugin': optional: true jest: optional: true + typescript: + optional: true eslint-plugin-jsx-a11y@6.10.2: resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} @@ -5375,8 +5744,8 @@ packages: peerDependencies: eslint: '>=9.17.0' - eslint-plugin-prettier@5.5.4: - resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -5400,11 +5769,10 @@ packages: peerDependencies: eslint: '>=0.8.0' - eslint-plugin-react-refresh@0.4.25: - resolution: {integrity: sha512-dRUD2LOdEqI4zXHqbQ442blQAzdSuShAaiSq5Vtyy6LT08YUf0oOjBDo4VPx0dCPgiPWh1WB4dtbLOd0kOlDPQ==} - deprecated: This version introduced false positive. This was reverted in 0.4.26 + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} peerDependencies: - eslint: '>=8.40' + eslint: ^9 || ^10 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -5420,14 +5788,14 @@ packages: peerDependencies: eslint: '>=5.0.0' - eslint-plugin-turbo@2.6.3: - resolution: {integrity: sha512-91WZ+suhT/pk+qNS0/rqT43xLUlUblsa3a8jKmAStGhkJCmR2uX0oWo/e0Edb+It8MdnteXuYpCkvsK4Vw8FtA==} + eslint-plugin-turbo@2.9.4: + resolution: {integrity: sha512-8gIBw+QC7jLOjYLthoLYcDGf0pEefAukkMAl60exv3HkS3NO7AuXzzFjY3iDGDc4s5mbkC6f/692DCdb3XHwMg==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' - eslint-plugin-unicorn@62.0.0: - resolution: {integrity: sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==} + eslint-plugin-unicorn@64.0.0: + resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} engines: {node: ^20.10.0 || >=21.0.0} peerDependencies: eslint: '>=9.38.0' @@ -5448,6 +5816,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.2: resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5542,10 +5914,6 @@ packages: events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} - events@1.1.1: - resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} - engines: {node: '>=0.4.x'} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -5577,11 +5945,15 @@ packages: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + expect@30.3.0: + resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + express-rate-limit@8.3.2: + resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -5601,10 +5973,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fast-check@3.23.2: - resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} - engines: {node: '>=8.0.0'} - fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -5646,9 +6014,8 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-parser@4.5.3: - resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} - hasBin: true + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -5742,10 +6109,6 @@ packages: resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - find-up@7.0.0: - resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} - engines: {node: '>=18'} - fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -5784,10 +6147,6 @@ packages: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} - form-data@2.5.5: - resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} - engines: {node: '>= 0.12'} - form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -5826,6 +6185,10 @@ packages: resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -5852,14 +6215,6 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - gaxios@6.7.1: - resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} - engines: {node: '>=14'} - - gcp-metadata@6.1.1: - resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} - engines: {node: '>=14'} - generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -5913,9 +6268,9 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - git-raw-commits@4.0.0: - resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} - engines: {node: '>=16'} + git-raw-commits@5.0.1: + resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} + engines: {node: '>=18'} hasBin: true git-up@8.1.1: @@ -5946,6 +6301,7 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.0: @@ -5954,7 +6310,7 @@ packages: glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} @@ -5972,8 +6328,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} engines: {node: '>=18'} globalthis@1.0.4: @@ -5988,18 +6344,6 @@ packages: resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - globby@14.1.0: - resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} - engines: {node: '>=18'} - - google-auth-library@9.15.1: - resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} - engines: {node: '>=14'} - - google-logging-utils@0.0.2: - resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} - engines: {node: '>=14'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -6031,10 +6375,6 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} - gtoken@7.1.0: - resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} - engines: {node: '>=14.0.0'} - gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -6073,9 +6413,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - has-yarn@3.0.0: resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6142,9 +6479,9 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hosted-git-info@7.0.2: - resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} - engines: {node: ^16.14.0 || >=18.0.0} + hono@4.12.12: + resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} + engines: {node: '>=16.9.0'} hosted-git-info@8.1.0: resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==} @@ -6165,9 +6502,6 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} - html-entities@2.6.0: - resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -6234,10 +6568,6 @@ packages: http-parser-js@0.5.10: resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} - http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -6265,10 +6595,6 @@ packages: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -6312,9 +6638,6 @@ packages: resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==} engines: {node: '>=4'} - ieee754@1.1.13: - resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -6365,10 +6688,6 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} - index-to-position@1.2.0: - resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} - engines: {node: '>=18'} - infima@0.2.0-alpha.45: resolution: {integrity: sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==} engines: {node: '>=12'} @@ -6441,10 +6760,6 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-arguments@1.2.0: - resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -6556,10 +6871,6 @@ packages: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} - is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -6652,10 +6963,6 @@ packages: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} - is-text-path@2.0.0: - resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} - engines: {node: '>=8'} - is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} @@ -6663,10 +6970,6 @@ packages: is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} @@ -6742,16 +7045,16 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jest-changed-files@30.2.0: - resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==} + jest-changed-files@30.3.0: + resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-circus@30.2.0: - resolution: {integrity: sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==} + jest-circus@30.3.0: + resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-cli@30.2.0: - resolution: {integrity: sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==} + jest-cli@30.3.0: + resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: @@ -6760,8 +7063,8 @@ packages: node-notifier: optional: true - jest-config@30.2.0: - resolution: {integrity: sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==} + jest-config@30.3.0: + resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@types/node': '*' @@ -6779,12 +7082,16 @@ packages: resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-diff@30.3.0: + resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@30.2.0: resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-each@30.2.0: - resolution: {integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==} + jest-each@30.3.0: + resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-environment-jsdom@30.2.0: @@ -6796,30 +7103,42 @@ packages: canvas: optional: true - jest-environment-node@30.2.0: - resolution: {integrity: sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==} + jest-environment-node@30.3.0: + resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-haste-map@30.2.0: - resolution: {integrity: sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==} + jest-haste-map@30.3.0: + resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-leak-detector@30.2.0: - resolution: {integrity: sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==} + jest-leak-detector@30.3.0: + resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-matcher-utils@30.2.0: resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-matcher-utils@30.3.0: + resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@30.2.0: resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@30.3.0: + resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@30.2.0: resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@30.3.0: + resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-pnp-resolver@1.2.3: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -6833,24 +7152,24 @@ packages: resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve-dependencies@30.2.0: - resolution: {integrity: sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==} + jest-resolve-dependencies@30.3.0: + resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve@30.2.0: - resolution: {integrity: sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==} + jest-resolve@30.3.0: + resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runner@30.2.0: - resolution: {integrity: sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==} + jest-runner@30.3.0: + resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runtime@30.2.0: - resolution: {integrity: sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==} + jest-runtime@30.3.0: + resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-snapshot@30.2.0: - resolution: {integrity: sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==} + jest-snapshot@30.3.0: + resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-util@29.7.0: @@ -6861,12 +7180,16 @@ packages: resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-validate@30.2.0: - resolution: {integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==} + jest-util@30.3.0: + resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.3.0: + resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-watcher@30.2.0: - resolution: {integrity: sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==} + jest-watcher@30.3.0: + resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-worker@27.5.1: @@ -6877,12 +7200,12 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-worker@30.2.0: - resolution: {integrity: sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==} + jest-worker@30.3.0: + resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest@30.2.0: - resolution: {integrity: sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==} + jest@30.3.0: + resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: @@ -6899,10 +7222,6 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jmespath@0.16.0: - resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} - engines: {node: '>= 0.6.0'} - joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} @@ -6950,9 +7269,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -6983,6 +7299,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==} @@ -7005,19 +7324,12 @@ packages: jsonc-parser@2.2.1: resolution: {integrity: sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==} - jsonc-parser@3.3.1: - resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} jsonify@0.0.1: resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} - jsonparse@1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} - jsonpath-plus@10.3.0: resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} engines: {node: '>=18.0.0'} @@ -7031,16 +7343,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -7060,6 +7365,14 @@ packages: koa-compose@4.1.0: resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + koa-send@5.0.1: + resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==} + engines: {node: '>= 8'} + + koa-static@5.0.0: + resolution: {integrity: sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==} + engines: {node: '>= 7.6.0'} + koa@3.1.1: resolution: {integrity: sha512-KDDuvpfqSK0ZKEO2gCPedNjl5wYpfj+HNiuVRlbhd1A88S3M0ySkdf2V/EJ4NWt5dwh5PXCdcenrKK2IQJAxsg==} engines: {node: '>= 18'} @@ -7097,8 +7410,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@16.2.7: - resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} engines: {node: '>=20.17'} hasBin: true @@ -7144,9 +7457,6 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -7177,10 +7487,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} - log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -7368,10 +7674,6 @@ packages: memfs@4.51.1: resolution: {integrity: sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==} - meow@12.1.1: - resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} - engines: {node: '>=16.10'} - meow@13.2.0: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} @@ -7630,11 +7932,6 @@ packages: engines: {node: '>=4.0.0'} hasBin: true - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -7668,9 +7965,16 @@ packages: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -7721,8 +8025,8 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} hasBin: true @@ -7761,9 +8065,9 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -7771,10 +8075,6 @@ packages: nan@2.24.0: resolution: {integrity: sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==} - nano-spawn@2.0.0: - resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} - engines: {node: '>=20.17'} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -7865,10 +8165,6 @@ packages: engines: {node: ^20.17.0 || >=22.9.0} hasBin: true - normalize-package-data@6.0.2: - resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} - engines: {node: ^16.14.0 || >=18.0.0} - normalize-package-data@7.0.1: resolution: {integrity: sha512-linxNAT6M0ebEYZOx2tO6vBEFsVgnPpv+AVjk0wJHfaUIbq31Jm3T6vvZaarnOeWDh8ShnwXuaAyM7WT3RzErA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -7889,10 +8185,6 @@ packages: resolution: {integrity: sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==} engines: {node: ^20.17.0 || >=22.9.0} - npm-package-arg@12.0.2: - resolution: {integrity: sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==} - engines: {node: ^18.17.0 || >=20.5.0} - npm-package-arg@13.0.2: resolution: {integrity: sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -8026,10 +8318,6 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@8.2.0: - resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} - engines: {node: '>=18'} - own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -8054,8 +8342,8 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@7.2.0: - resolution: {integrity: sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==} + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} engines: {node: '>=20'} p-locate@4.1.0: @@ -8078,22 +8366,14 @@ packages: resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} - p-pipe@4.0.0: - resolution: {integrity: sha512-HkPfFklpZQPUKBFXzKFB6ihLriIHxnmuQdK9WmLDwe4hf2PdhhfWT/FJa+pc3bA1ywvKXtedxIRmd4Y7BTXE4w==} - engines: {node: '>=12'} - p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} - p-queue@9.0.1: - resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==} + p-queue@9.1.2: + resolution: {integrity: sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw==} engines: {node: '>=20'} - p-reduce@3.0.0: - resolution: {integrity: sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==} - engines: {node: '>=12'} - p-retry@6.2.1: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} @@ -8138,10 +8418,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse-json@8.3.0: - resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} - engines: {node: '>=18'} - parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -8225,40 +8501,36 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - path-type@6.0.0: - resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} - engines: {node: '>=18'} - path@0.12.7: resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pg-cloudflare@1.2.7: - resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} - pg-connection-string@2.9.1: - resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-pool@3.10.1: - resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} peerDependencies: pg: '>=8.0' - pg-protocol@1.10.3: - resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.16.3: - resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -8284,15 +8556,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pidtree@0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - - pify@6.1.0: - resolution: {integrity: sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==} - engines: {node: '>=14.16'} - pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -8765,16 +9028,16 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} prettier-package-json@2.8.0: resolution: {integrity: sha512-WxtodH/wWavfw3MR7yK/GrS4pASEQ+iSTkdtSxPJWvqzG55ir5nvbLt9rw5AOiEcqqPCRM92WCtR1rk3TG3JSQ==} hasBin: true - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -8789,6 +9052,10 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -8809,10 +9076,6 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} - proc-log@5.0.0: - resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} - engines: {node: ^18.17.0 || >=20.5.0} - proc-log@6.1.0: resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -8841,9 +9104,9 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - properties-reader@2.3.0: - resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} - engines: {node: '>=14'} + properties-reader@3.0.1: + resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} + engines: {node: '>=18'} property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} @@ -8868,9 +9131,6 @@ packages: pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - punycode@1.3.2: - resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} - punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -8882,9 +9142,6 @@ packages: resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} engines: {node: '>=12.20'} - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} @@ -8896,11 +9153,6 @@ packages: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} - querystring@0.2.0: - resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} - engines: {node: '>=0.4.x'} - deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9024,14 +9276,6 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} - read-pkg@9.0.1: - resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} - engines: {node: '>=18'} - - read-yaml-file@2.1.0: - resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} - engines: {node: '>=10.13'} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -9231,6 +9475,10 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-path@1.4.0: + resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} + engines: {node: '>= 0.8'} + resolve-pathname@3.0.0: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} @@ -9257,10 +9505,6 @@ packages: retry-as-promised@7.1.1: resolution: {integrity: sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==} - retry-request@7.0.2: - resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} - engines: {node: '>=14'} - retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -9359,9 +9603,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - sax@1.2.1: - resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} - sax@1.4.3: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} @@ -9415,6 +9656,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.2: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} @@ -9491,9 +9737,6 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} - set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -9608,10 +9851,6 @@ packages: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} engines: {node: '>=12'} - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} @@ -9642,10 +9881,6 @@ packages: resolution: {integrity: sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==} engines: {node: '>= 6.3.0'} - sort-keys@5.1.0: - resolution: {integrity: sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==} - engines: {node: '>=12'} - sort-keys@6.0.0: resolution: {integrity: sha512-ueSlHJMwpIw42CJ4B11Uxzh/S0p0AlOyiNktlv2KOu5e1JpUE6DlC4AAUjXqesHdBRv/g0wC9Q4vwq0NP2pA9w==} engines: {node: '>=20'} @@ -9751,20 +9986,10 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} - stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stream-events@1.0.5: - resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} - - stream-shift@1.0.3: - resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -9876,12 +10101,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strnum@1.1.2: - resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} - - stubs@3.0.0: - resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} - style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -9949,9 +10168,53 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} - syncpack@13.0.4: - resolution: {integrity: sha512-kJ9VlRxNCsBD5pJAE29oXeBYbPLhEySQmK4HdpsLv81I6fcDDW17xeJqMwiU3H7/woAVsbgq25DJNS8BeiN5+w==} - engines: {node: '>=18.18.0'} + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + syncpack-darwin-arm64@14.3.0: + resolution: {integrity: sha512-gpbkBzO7yqa3BONc4EU3jY07yiPSZdoAxcpnz8REV9Bc6FkmKfOejCpYIh8RaogGPS4gOLJ/RUJEECqAaHTcjA==} + cpu: [arm64] + os: [darwin] + + syncpack-darwin-x64@14.3.0: + resolution: {integrity: sha512-wTpl6Qj5qGIHrYhpCrlNnosmhQqvUoidqqmxtdM3f+j+b+OkTtpkUl2tdE28h3aeEEUPf9ClQnHuwRJMYNlrJw==} + cpu: [x64] + os: [darwin] + + syncpack-linux-arm64-musl@14.3.0: + resolution: {integrity: sha512-AezJ5dv0s+l/p1l4/wBatYhM6SZEKLcyNKggSOX5uISzqbSKwj/Aak13pBXWarzS+N6LnOl4PMcwRMJPOUfN/g==} + cpu: [arm64] + os: [linux] + + syncpack-linux-arm64@14.3.0: + resolution: {integrity: sha512-Vcf9zWkJGRqb5mGPKi9E+s/mB/Tw08LmKGRaiyKJjK8bhd1Ds65O8A2lOidy3jg0NOOojqREmDsli74Xd0z6KA==} + cpu: [arm64] + os: [linux] + + syncpack-linux-x64-musl@14.3.0: + resolution: {integrity: sha512-tIRF0lvBJcoIwcO05/Q6j30CAg0jzn+A5eQL+06Ncq8CE5i8fBWrVwN1U/QQ4fzT+tondNWH/2BR5zlaB1VUpQ==} + cpu: [x64] + os: [linux] + + syncpack-linux-x64@14.3.0: + resolution: {integrity: sha512-n/4iBJnoOCe5An+WYlaqfSxOKQ7Id3TZTpxOpI60Cucq3yqwq0JHQUielj6JBtVaxvo2rAsTwbCLyp2aB0SD2g==} + cpu: [x64] + os: [linux] + + syncpack-windows-arm64@14.3.0: + resolution: {integrity: sha512-ZTX0rSUTJZjIde3qPKLEqU7IEt4KxFsmq6gKLfESx5rB1rxk/B1Ljv6nTYph5QVItEzHQHUXDWnqa9yCb15myw==} + cpu: [arm64] + os: [win32] + + syncpack-windows-x64@14.3.0: + resolution: {integrity: sha512-zvbphZw40wFZYeEW2oJYqA9Uw6IOwNIpgFmVNdWdGmShPF/Zu1cavHEcGwtQmvVzwuTEyiu67uxQ3hDC2q6vtg==} + cpu: [x64] + os: [win32] + + syncpack@14.3.0: + resolution: {integrity: sha512-8/WtPPxxGFqE21JPFz6Bpw0m9BT3lzMYDcewJFj++EBPmCaDWowTnx0R4V7ofH/1z3HAIaAps6g2GHL76l1Y2g==} + engines: {node: '>=14.17.0'} hasBin: true tapable@2.3.0: @@ -9961,8 +10224,8 @@ packages: tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - tar-fs@3.1.1: - resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + tar-fs@3.1.2: + resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} @@ -9974,10 +10237,7 @@ packages: tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} - - teeny-request@9.0.0: - resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} - engines: {node: '>=14'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} @@ -10004,16 +10264,12 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - testcontainers@11.11.0: - resolution: {integrity: sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==} + testcontainers@11.13.0: + resolution: {integrity: sha512-fzTvgOtd6U/esOzgmDatJh79OSK0tU6vjDOJ3B6ICrrJf0dqCWtFdpOr6f/g/KixMxKDTDbszmZYjSORJXsVCQ==} text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} - text-extensions@2.4.0: - resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} - engines: {node: '>=8'} - text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -10034,16 +10290,9 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} - tightrope@0.2.0: - resolution: {integrity: sha512-Kw36UHxJEELq2VUqdaSGR2/8cAsPgMtvX8uGVU6Jk26O66PhXec0A5ZnRYs47btbtwPDpXXF66+Fo3vimCM9aQ==} - engines: {node: '>=16'} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -10057,6 +10306,10 @@ packages: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -10065,10 +10318,6 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} - engines: {node: '>=14.0.0'} - tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} @@ -10131,6 +10380,12 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -10148,9 +10403,6 @@ packages: '@swc/wasm': optional: true - ts-toolbelt@9.6.0: - resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} - tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -10188,38 +10440,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo-darwin-64@2.6.3: - resolution: {integrity: sha512-BlJJDc1CQ7SK5Y5qnl7AzpkvKSnpkfPmnA+HeU/sgny3oHZckPV2776ebO2M33CYDSor7+8HQwaodY++IINhYg==} - cpu: [x64] - os: [darwin] - - turbo-darwin-arm64@2.6.3: - resolution: {integrity: sha512-MwVt7rBKiOK7zdYerenfCRTypefw4kZCue35IJga9CH1+S50+KTiCkT6LBqo0hHeoH2iKuI0ldTF2a0aB72z3w==} - cpu: [arm64] - os: [darwin] - - turbo-linux-64@2.6.3: - resolution: {integrity: sha512-cqpcw+dXxbnPtNnzeeSyWprjmuFVpHJqKcs7Jym5oXlu/ZcovEASUIUZVN3OGEM6Y/OTyyw0z09tOHNt5yBAVg==} - cpu: [x64] - os: [linux] - - turbo-linux-arm64@2.6.3: - resolution: {integrity: sha512-MterpZQmjXyr4uM7zOgFSFL3oRdNKeflY7nsjxJb2TklsYqiu3Z9pQ4zRVFFH8n0mLGna7MbQMZuKoWqqHb45w==} - cpu: [arm64] - os: [linux] - - turbo-windows-64@2.6.3: - resolution: {integrity: sha512-biDU70v9dLwnBdLf+daoDlNJVvqOOP8YEjqNipBHzgclbQlXbsi6Gqqelp5er81Qo3BiRgmTNx79oaZQTPb07Q==} - cpu: [x64] - os: [win32] - - turbo-windows-arm64@2.6.3: - resolution: {integrity: sha512-dDHVKpSeukah3VsI/xMEKeTnV9V9cjlpFSUs4bmsUiLu3Yv2ENlgVEZv65wxbeE0bh0jjpmElDT+P1KaCxArQQ==} - cpu: [arm64] - os: [win32] - - turbo@2.6.3: - resolution: {integrity: sha512-bf6YKUv11l5Xfcmg76PyWoy/e2vbkkxFNBGJSnfdSXQC33ZiUfutYh6IXidc5MhsnrFkWfdNNLyaRk+kHMLlwA==} + turbo@2.9.4: + resolution: {integrity: sha512-wZ/kMcZCuK5oEp7sXSSo/5fzKjP9I2EhoiarZjyCm2Ixk0WxFrC/h0gF3686eHHINoFQOOSWgB/pGfvkR8rkgQ==} hasBin: true tweetnacl@0.14.5: @@ -10245,10 +10467,6 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -10279,15 +10497,15 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.49.0: - resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} + typescript-eslint@8.58.0: + resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -10313,11 +10531,11 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici@7.16.0: - resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} + undici@7.24.7: + resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} engines: {node: '>=20.18.1'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -10328,9 +10546,6 @@ packages: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} engines: {node: '>=4'} - unicode-emoji-utils@1.3.1: - resolution: {integrity: sha512-6PiQxmnlsOsqzZCZz0sykSyMy/r1HiJiOWWXV98+BDva583DU4CtBeyDNsi4wMYUIbjUtMs4RgAuyft0EKLoVw==} - unicode-match-property-ecmascript@2.0.0: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} @@ -10343,10 +10558,6 @@ packages: resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -10445,9 +10656,6 @@ packages: file-loader: optional: true - url@0.10.3: - resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} - url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} @@ -10468,9 +10676,6 @@ packages: util@0.10.4: resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} - util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} @@ -10486,22 +10691,10 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} - hasBin: true - - uuid@8.0.0: - resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} - hasBin: true - uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'} @@ -10520,10 +10713,6 @@ packages: validate-npm-package-name@3.0.0: resolution: {integrity: sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==} - validate-npm-package-name@6.0.2: - resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} - engines: {node: ^18.17.0 || >=20.5.0} - validate-npm-package-name@7.0.0: resolution: {integrity: sha512-bwVk/OK+Qu108aJcMAEiU4yavHUI7aN20TgZNBj9MR2iU1zPUl1Z1Otr7771ExfYTPTvfN8ZJ1pbr5Iklgt4xg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -10714,9 +10903,6 @@ packages: engines: {node: ^20.17.0 || >=22.9.0} hasBin: true - wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - widest-line@4.0.1: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} @@ -10734,10 +10920,6 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -10764,22 +10946,14 @@ packages: resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} engines: {node: ^18.17.0 || >=20.5.0} - write-file-atomic@7.0.0: - resolution: {integrity: sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==} + write-file-atomic@7.0.1: + resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - write-json-file@6.0.0: - resolution: {integrity: sha512-MNHcU3f9WxnNyR6MxsYSj64Jz0+dwIpisWKWq9gqLj/GwmA9INg3BZ3vt70/HB3GEwrnDQWr4RPrywnhNzmUFA==} - engines: {node: '>=18'} - write-json-file@7.0.0: resolution: {integrity: sha512-rj8As6LkachKauGxvZkFzCEd6hIRTi9FKtCNKOa4SaH5vPOiACbGcmPUEJXgkhTHwzNsYmcSbD3C9a6whBfyOg==} engines: {node: '>=20'} - write-package@7.2.0: - resolution: {integrity: sha512-uMQTubF/vcu+Wd0b5BGtDmiXePd/+44hUWQz2nZPbs92/BnxRo74tqs+hqDo12RLiEd+CXFKUwxvvIZvtt34Jw==} - engines: {node: '>=18'} - ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -10828,14 +11002,6 @@ packages: resolution: {integrity: sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==} engines: {node: '>= 10'} - xml2js@0.6.2: - resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} - engines: {node: '>=4.0.0'} - - xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -10869,6 +11035,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -10897,10 +11068,6 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} - engines: {node: '>=18'} - yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -10912,10 +11079,10 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod-to-json-schema@3.25.0: - resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: - zod: ^3.25 || ^4 + zod: ^3.25.28 || ^4 zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} @@ -11096,8 +11263,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.5': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.28.5': dependencies: '@babel/code-frame': 7.27.1 @@ -11118,6 +11293,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.28.5 @@ -11126,6 +11321,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.28.5 @@ -11138,6 +11341,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11151,13 +11362,46 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.28.5)': + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 - regexpu-core: 6.4.0 - semver: 6.3.1 - + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.5 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11169,6 +11413,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + '@babel/helper-globals@7.28.0': {} '@babel/helper-member-expression-to-functions@7.28.5': @@ -11185,6 +11440,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11194,12 +11456,32 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.28.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.27.1': dependencies: '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11209,6 +11491,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.3 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11218,6 +11509,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-replace-supers@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.5 @@ -11244,10 +11553,19 @@ snapshots: '@babel/template': 7.27.2 '@babel/types': 7.28.5 + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.28.5': dependencies: '@babel/types': 7.28.5 + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11256,16 +11574,34 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11275,6 +11611,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11283,12 +11628,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.5)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color @@ -11296,6 +11649,10 @@ snapshots: dependencies: '@babel/core': 7.28.5 + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11316,10 +11673,10 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.5)': dependencies: @@ -11331,11 +11688,21 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11351,6 +11718,11 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11396,17 +11768,33 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11416,6 +11804,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11425,16 +11822,35 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11443,6 +11859,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11451,6 +11875,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11463,12 +11895,30 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/template': 7.27.2 + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template': 7.28.6 + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11477,28 +11927,58 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11507,16 +11987,34 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-exponentiation-operator@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11525,6 +12023,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11534,26 +12040,55 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-logical-assignment-operators@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11562,6 +12097,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11570,6 +12113,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11580,6 +12139,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11588,27 +12157,56 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11620,6 +12218,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11628,11 +12237,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-optional-chaining@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11641,11 +12263,32 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-optional-chaining@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11654,6 +12297,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11663,11 +12314,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11678,6 +12343,11 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11685,6 +12355,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11696,28 +12373,61 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-runtime@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11735,6 +12445,11 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11743,21 +12458,44 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11769,29 +12507,63 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/preset-env@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/compat-data': 7.28.5 @@ -11868,6 +12640,82 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-env@7.29.2(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) + '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11875,6 +12723,13 @@ snapshots: '@babel/types': 7.28.5 esutils: 2.0.3 + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.28.5 + esutils: 2.0.3 + '@babel/preset-react@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11887,6 +12742,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-react@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/preset-typescript@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11898,6 +12765,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/runtime-corejs3@7.28.4': dependencies: core-js-pure: 3.47.0 @@ -11910,6 +12788,12 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 @@ -11922,11 +12806,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@balena/dockerignore@1.0.2': {} '@bcoe/v8-coverage@0.2.3': {} @@ -11934,32 +12835,34 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@commitlint/cli@20.2.0(@types/node@25.0.3)(typescript@5.9.3)': + '@commitlint/cli@20.5.0(@types/node@25.5.2)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.2)': dependencies: - '@commitlint/format': 20.2.0 - '@commitlint/lint': 20.2.0 - '@commitlint/load': 20.2.0(@types/node@25.0.3)(typescript@5.9.3) - '@commitlint/read': 20.2.0 - '@commitlint/types': 20.2.0 + '@commitlint/format': 20.5.0 + '@commitlint/lint': 20.5.0 + '@commitlint/load': 20.5.0(@types/node@25.5.2)(typescript@6.0.2) + '@commitlint/read': 20.5.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) + '@commitlint/types': 20.5.0 tinyexec: 1.0.2 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' + - conventional-commits-filter + - conventional-commits-parser - typescript - '@commitlint/config-conventional@19.8.1': + '@commitlint/config-conventional@20.5.0': dependencies: - '@commitlint/types': 19.8.1 - conventional-changelog-conventionalcommits: 7.0.2 + '@commitlint/types': 20.5.0 + conventional-changelog-conventionalcommits: 9.3.1 - '@commitlint/config-validator@20.2.0': + '@commitlint/config-validator@20.5.0': dependencies: - '@commitlint/types': 20.2.0 + '@commitlint/types': 20.5.0 ajv: 8.17.1 - '@commitlint/ensure@20.2.0': + '@commitlint/ensure@20.5.0': dependencies: - '@commitlint/types': 20.2.0 + '@commitlint/types': 20.5.0 lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 @@ -11968,95 +12871,92 @@ snapshots: '@commitlint/execute-rule@20.0.0': {} - '@commitlint/format@20.2.0': + '@commitlint/format@20.5.0': dependencies: - '@commitlint/types': 20.2.0 - chalk: 5.6.2 + '@commitlint/types': 20.5.0 + picocolors: 1.1.1 - '@commitlint/is-ignored@20.2.0': + '@commitlint/is-ignored@20.5.0': dependencies: - '@commitlint/types': 20.2.0 + '@commitlint/types': 20.5.0 semver: 7.7.3 - '@commitlint/lint@20.2.0': + '@commitlint/lint@20.5.0': dependencies: - '@commitlint/is-ignored': 20.2.0 - '@commitlint/parse': 20.2.0 - '@commitlint/rules': 20.2.0 - '@commitlint/types': 20.2.0 + '@commitlint/is-ignored': 20.5.0 + '@commitlint/parse': 20.5.0 + '@commitlint/rules': 20.5.0 + '@commitlint/types': 20.5.0 - '@commitlint/load@20.2.0(@types/node@25.0.3)(typescript@5.9.3)': + '@commitlint/load@20.5.0(@types/node@25.5.2)(typescript@6.0.2)': dependencies: - '@commitlint/config-validator': 20.2.0 + '@commitlint/config-validator': 20.5.0 '@commitlint/execute-rule': 20.0.0 - '@commitlint/resolve-extends': 20.2.0 - '@commitlint/types': 20.2.0 - chalk: 5.6.2 - cosmiconfig: 9.0.0(typescript@5.9.3) - cosmiconfig-typescript-loader: 6.2.0(@types/node@25.0.3)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - lodash.uniq: 4.5.0 + '@commitlint/resolve-extends': 20.5.0 + '@commitlint/types': 20.5.0 + cosmiconfig: 9.0.1(typescript@6.0.2) + cosmiconfig-typescript-loader: 6.2.0(@types/node@25.5.2)(cosmiconfig@9.0.1(typescript@6.0.2))(typescript@6.0.2) + is-plain-obj: 4.1.0 + lodash.mergewith: 4.6.2 + picocolors: 1.1.1 transitivePeerDependencies: - '@types/node' - typescript - '@commitlint/message@20.0.0': {} + '@commitlint/message@20.4.3': {} - '@commitlint/parse@20.2.0': + '@commitlint/parse@20.5.0': dependencies: - '@commitlint/types': 20.2.0 - conventional-changelog-angular: 7.0.0 - conventional-commits-parser: 5.0.0 + '@commitlint/types': 20.5.0 + conventional-changelog-angular: 8.3.1 + conventional-commits-parser: 6.4.0 - '@commitlint/read@20.2.0': + '@commitlint/read@20.5.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)': dependencies: - '@commitlint/top-level': 20.0.0 - '@commitlint/types': 20.2.0 - git-raw-commits: 4.0.0 + '@commitlint/top-level': 20.4.3 + '@commitlint/types': 20.5.0 + git-raw-commits: 5.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) minimist: 1.2.8 tinyexec: 1.0.2 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser - '@commitlint/resolve-extends@20.2.0': + '@commitlint/resolve-extends@20.5.0': dependencies: - '@commitlint/config-validator': 20.2.0 - '@commitlint/types': 20.2.0 + '@commitlint/config-validator': 20.5.0 + '@commitlint/types': 20.5.0 global-directory: 4.0.1 import-meta-resolve: 4.2.0 lodash.mergewith: 4.6.2 resolve-from: 5.0.0 - '@commitlint/rules@20.2.0': + '@commitlint/rules@20.5.0': dependencies: - '@commitlint/ensure': 20.2.0 - '@commitlint/message': 20.0.0 + '@commitlint/ensure': 20.5.0 + '@commitlint/message': 20.4.3 '@commitlint/to-lines': 20.0.0 - '@commitlint/types': 20.2.0 + '@commitlint/types': 20.5.0 '@commitlint/to-lines@20.0.0': {} - '@commitlint/top-level@20.0.0': + '@commitlint/top-level@20.4.3': dependencies: - find-up: 7.0.0 - - '@commitlint/types@19.8.1': - dependencies: - '@types/conventional-commits-parser': 5.0.2 - chalk: 5.6.2 + escalade: 3.2.0 - '@commitlint/types@20.2.0': + '@commitlint/types@20.5.0': dependencies: - '@types/conventional-commits-parser': 5.0.2 - chalk: 5.6.2 + conventional-commits-parser: 6.4.0 + picocolors: 1.1.1 - '@conventional-changelog/git-client@2.5.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1)': + '@conventional-changelog/git-client@2.6.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)': dependencies: '@simple-libs/child-process-utils': 1.0.1 - '@simple-libs/stream-utils': 1.1.0 - semver: 7.7.3 + '@simple-libs/stream-utils': 1.2.0 + semver: 7.7.4 optionalDependencies: conventional-commits-filter: 5.0.0 - conventional-commits-parser: 6.2.1 + conventional-commits-parser: 6.4.0 '@cspotcode/source-map-support@0.8.1': dependencies: @@ -12426,7 +13326,7 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/bundler@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: '@babel/core': 7.28.5 '@docusaurus/babel': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -12445,7 +13345,7 @@ snapshots: mini-css-extract-plugin: 2.9.4(webpack@5.104.1) null-loader: 4.0.1(webpack@5.104.1) postcss: 8.5.6 - postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.104.1) + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@6.0.2)(webpack@5.104.1) postcss-preset-env: 10.6.0(postcss@8.5.6) terser-webpack-plugin: 5.3.16(webpack@5.104.1) tslib: 2.8.1 @@ -12467,10 +13367,10 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: '@docusaurus/babel': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/bundler': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/bundler': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -12596,13 +13496,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -12637,13 +13537,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -12677,9 +13577,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -12707,9 +13607,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -12734,9 +13634,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) fs-extra: 11.3.2 @@ -12762,9 +13662,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -12788,9 +13688,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/gtag.js': 0.0.12 @@ -12815,9 +13715,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -12841,9 +13741,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/logger': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -12872,14 +13772,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@svgr/core': 8.1.0(typescript@5.9.3) - '@svgr/webpack': 8.1.0(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@6.0.2) + '@svgr/webpack': 8.1.0(typescript@6.0.2) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 @@ -12902,22 +13802,22 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@5.9.3)': - dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@5.9.3) + '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@6.0.2)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@6.0.2) '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -12947,16 +13847,16 @@ snapshots: '@types/react': 19.2.7 react: 19.2.3 - '@docusaurus/theme-classic@3.9.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@docusaurus/theme-classic@3.9.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -12994,11 +13894,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/history': 4.7.11 @@ -13018,13 +13918,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@6.0.2)': dependencies: '@docsearch/react': 4.4.0(@algolia/client-search@5.46.2)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3) - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/logger': 3.9.2 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -13187,91 +14087,174 @@ snapshots: '@esbuild/aix-ppc64@0.27.1': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.27.1': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.27.1': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.27.1': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.27.1': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.27.1': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.27.1': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.27.1': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.27.1': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + '@esbuild/linux-arm@0.27.1': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + '@esbuild/linux-ia32@0.27.1': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.27.1': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.27.1': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.27.1': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.27.1': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.27.1': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.27.1': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + '@esbuild/netbsd-arm64@0.27.1': optional: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.27.1': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + '@esbuild/openbsd-arm64@0.27.1': optional: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.27.1': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + '@esbuild/openharmony-arm64@0.27.1': optional: true + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.27.1': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.27.1': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.27.1': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.27.1': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))': dependencies: eslint: 9.39.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.0.0(eslint@9.39.2(jiti@2.6.1))': + '@eslint/compat@2.0.4(eslint@9.39.2(jiti@2.6.1))': dependencies: - '@eslint/core': 1.0.0 + '@eslint/core': 1.2.0 optionalDependencies: eslint: 9.39.2(jiti@2.6.1) @@ -13291,7 +14274,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@1.0.0': + '@eslint/core@1.2.0': dependencies: '@types/json-schema': 7.0.15 @@ -13309,6 +14292,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + '@eslint/js@9.39.2': {} '@eslint/object-schema@2.1.7': {} @@ -13320,74 +14317,39 @@ snapshots: '@exodus/schemasafe@1.3.0': {} - '@faker-js/faker@10.2.0': {} + '@faker-js/faker@10.4.0': {} '@faker-js/faker@5.5.3': {} - '@formatjs/ecma402-abstract@2.3.6': - dependencies: - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/intl-localematcher': 0.6.2 - decimal.js: 10.6.0 - tslib: 2.8.1 + '@formatjs/bigdecimal@0.2.0': {} - '@formatjs/fast-memoize@2.2.7': + '@formatjs/ecma402-abstract@3.2.0': dependencies: - tslib: 2.8.1 + '@formatjs/bigdecimal': 0.2.0 + '@formatjs/fast-memoize': 3.1.1 + '@formatjs/intl-localematcher': 0.8.2 - '@formatjs/icu-messageformat-parser@2.11.4': + '@formatjs/fast-memoize@3.1.1': {} + + '@formatjs/icu-messageformat-parser@3.5.3': dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/icu-skeleton-parser': 1.8.16 - tslib: 2.8.1 + '@formatjs/ecma402-abstract': 3.2.0 + '@formatjs/icu-skeleton-parser': 2.1.3 - '@formatjs/icu-skeleton-parser@1.8.16': + '@formatjs/icu-skeleton-parser@2.1.3': dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - tslib: 2.8.1 + '@formatjs/ecma402-abstract': 3.2.0 - '@formatjs/intl-localematcher@0.6.2': + '@formatjs/intl-localematcher@0.8.2': dependencies: - tslib: 2.8.1 + '@formatjs/fast-memoize': 3.1.1 - '@formatjs/ts-transformer@3.14.2': + '@formatjs/ts-transformer@4.4.3': dependencies: - '@formatjs/icu-messageformat-parser': 2.11.4 + '@formatjs/icu-messageformat-parser': 3.5.3 '@types/node': 22.19.3 - chalk: 4.1.2 json-stable-stringify: 1.3.0 - tslib: 2.8.1 - typescript: 5.9.3 - - '@google-cloud/paginator@5.0.2': - dependencies: - arrify: 2.0.1 - extend: 3.0.2 - - '@google-cloud/projectify@4.0.0': {} - - '@google-cloud/promisify@4.0.0': {} - - '@google-cloud/storage@7.18.0(encoding@0.1.13)': - dependencies: - '@google-cloud/paginator': 5.0.2 - '@google-cloud/projectify': 4.0.0 - '@google-cloud/promisify': 4.0.0 - abort-controller: 3.0.0 - async-retry: 1.3.3 - duplexify: 4.1.3 - fast-xml-parser: 4.5.3 - gaxios: 6.7.1(encoding@0.1.13) - google-auth-library: 9.15.1(encoding@0.1.13) - html-entities: 2.6.0 - mime: 3.0.0 - p-limit: 3.1.0 - retry-request: 7.0.2(encoding@0.1.13) - teeny-request: 9.0.0(encoding@0.1.13) - uuid: 8.3.2 - transitivePeerDependencies: - - encoding - - supports-color + typescript: 6.0.2 '@grpc/grpc-js@1.14.3': dependencies: @@ -13416,6 +14378,10 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@hono/node-server@1.19.13(hono@4.12.12)': + dependencies: + hono: 4.12.12 + '@hookform/error-message@2.0.1(react-dom@19.2.3(react@19.2.3))(react-hook-form@7.70.0(react@19.2.3))(react@19.2.3)': dependencies: react: 19.2.3 @@ -13433,51 +14399,48 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.5': {} - '@inquirer/core@10.3.2(@types/node@25.0.3)': + '@inquirer/core@11.1.8(@types/node@25.5.2)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.3) + '@inquirer/ansi': 2.0.5 + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.5.2) cli-width: 4.1.0 - mute-stream: 2.0.0 + fast-wrap-ansi: 0.2.0 + mute-stream: 3.0.0 signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 - '@inquirer/expand@4.0.23(@types/node@25.0.3)': + '@inquirer/expand@5.0.11(@types/node@25.5.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.3) - '@inquirer/type': 3.0.10(@types/node@25.0.3) - yoctocolors-cjs: 2.1.3 + '@inquirer/core': 11.1.8(@types/node@25.5.2) + '@inquirer/type': 4.0.5(@types/node@25.5.2) optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 - '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.5': {} - '@inquirer/input@4.3.1(@types/node@25.0.3)': + '@inquirer/input@5.0.11(@types/node@25.5.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.3) - '@inquirer/type': 3.0.10(@types/node@25.0.3) + '@inquirer/core': 11.1.8(@types/node@25.5.2) + '@inquirer/type': 4.0.5(@types/node@25.5.2) optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 - '@inquirer/select@4.4.2(@types/node@25.0.3)': + '@inquirer/select@5.1.3(@types/node@25.5.2)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.0.3) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.3) - yoctocolors-cjs: 2.1.3 + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.8(@types/node@25.5.2) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.5.2) optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 - '@inquirer/type@3.0.10(@types/node@25.0.3)': + '@inquirer/type@4.0.5(@types/node@25.5.2)': optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@isaacs/balanced-match@4.0.1': {} @@ -13508,44 +14471,43 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@jest/console@30.2.0': + '@jest/console@30.3.0': dependencies: - '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 chalk: 4.1.2 - jest-message-util: 30.2.0 - jest-util: 30.2.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 slash: 3.0.0 - '@jest/core@30.2.0(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3))': + '@jest/core@30.3.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2))': dependencies: - '@jest/console': 30.2.0 + '@jest/console': 30.3.0 '@jest/pattern': 30.0.1 - '@jest/reporters': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@jest/reporters': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 4.3.1 exit-x: 0.2.2 graceful-fs: 4.2.11 - jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) - jest-haste-map: 30.2.0 - jest-message-util: 30.2.0 + jest-changed-files: 30.3.0 + jest-config: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-resolve-dependencies: 30.2.0 - jest-runner: 30.2.0 - jest-runtime: 30.2.0 - jest-snapshot: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - jest-watcher: 30.2.0 - micromatch: 4.0.8 - pretty-format: 30.2.0 + jest-resolve: 30.3.0 + jest-resolve-dependencies: 30.3.0 + jest-runner: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + jest-watcher: 30.3.0 + pretty-format: 30.3.0 slash: 3.0.0 transitivePeerDependencies: - babel-plugin-macros @@ -13555,13 +14517,15 @@ snapshots: '@jest/diff-sequences@30.0.1': {} + '@jest/diff-sequences@30.3.0': {} + '@jest/environment-jsdom-abstract@30.2.0(jsdom@26.1.0)': dependencies: '@jest/environment': 30.2.0 '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 '@types/jsdom': 21.1.7 - '@types/node': 25.0.3 + '@types/node': 25.5.2 jest-mock: 30.2.0 jest-util: 30.2.0 jsdom: 26.1.0 @@ -13570,17 +14534,28 @@ snapshots: dependencies: '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@types/node': 25.5.2 jest-mock: 30.2.0 + '@jest/environment@30.3.0': + dependencies: + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 + jest-mock: 30.3.0 + '@jest/expect-utils@30.2.0': dependencies: '@jest/get-type': 30.1.0 - '@jest/expect@30.2.0': + '@jest/expect-utils@30.3.0': dependencies: - expect: 30.2.0 - jest-snapshot: 30.2.0 + '@jest/get-type': 30.1.0 + + '@jest/expect@30.3.0': + dependencies: + expect: 30.3.0 + jest-snapshot: 30.3.0 transitivePeerDependencies: - supports-color @@ -13588,36 +14563,45 @@ snapshots: dependencies: '@jest/types': 30.2.0 '@sinonjs/fake-timers': 13.0.5 - '@types/node': 25.0.3 + '@types/node': 25.5.2 jest-message-util: 30.2.0 jest-mock: 30.2.0 jest-util: 30.2.0 + '@jest/fake-timers@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@sinonjs/fake-timers': 15.3.0 + '@types/node': 25.5.2 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + '@jest/get-type@30.1.0': {} - '@jest/globals@30.2.0': + '@jest/globals@30.3.0': dependencies: - '@jest/environment': 30.2.0 - '@jest/expect': 30.2.0 - '@jest/types': 30.2.0 - jest-mock: 30.2.0 + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/types': 30.3.0 + jest-mock: 30.3.0 transitivePeerDependencies: - supports-color '@jest/pattern@30.0.1': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 jest-regex-util: 30.0.1 - '@jest/reporters@30.2.0': + '@jest/reporters@30.3.0': dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 + '@jest/console': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 25.0.3 + '@types/node': 25.5.2 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 @@ -13628,9 +14612,9 @@ snapshots: istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - jest-message-util: 30.2.0 - jest-util: 30.2.0 - jest-worker: 30.2.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + jest-worker: 30.3.0 slash: 3.0.0 string-length: 4.0.2 v8-to-istanbul: 9.3.0 @@ -13645,9 +14629,9 @@ snapshots: dependencies: '@sinclair/typebox': 0.34.45 - '@jest/snapshot-utils@30.2.0': + '@jest/snapshot-utils@30.3.0': dependencies: - '@jest/types': 30.2.0 + '@jest/types': 30.3.0 chalk: 4.1.2 graceful-fs: 4.2.11 natural-compare: 1.4.0 @@ -13658,34 +14642,33 @@ snapshots: callsites: 3.1.0 graceful-fs: 4.2.11 - '@jest/test-result@30.2.0': + '@jest/test-result@30.3.0': dependencies: - '@jest/console': 30.2.0 - '@jest/types': 30.2.0 + '@jest/console': 30.3.0 + '@jest/types': 30.3.0 '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.3 - '@jest/test-sequencer@30.2.0': + '@jest/test-sequencer@30.3.0': dependencies: - '@jest/test-result': 30.2.0 + '@jest/test-result': 30.3.0 graceful-fs: 4.2.11 - jest-haste-map: 30.2.0 + jest-haste-map: 30.3.0 slash: 3.0.0 - '@jest/transform@30.2.0': + '@jest/transform@30.3.0': dependencies: '@babel/core': 7.28.5 - '@jest/types': 30.2.0 + '@jest/types': 30.3.0 '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 7.0.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-haste-map: 30.2.0 + jest-haste-map: 30.3.0 jest-regex-util: 30.0.1 - jest-util: 30.2.0 - micromatch: 4.0.8 + jest-util: 30.3.0 pirates: 4.0.7 slash: 3.0.0 write-file-atomic: 5.0.1 @@ -13697,7 +14680,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -13707,7 +14690,17 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.0.3 + '@types/node': 25.5.2 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jest/types@30.3.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 25.5.2 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -13820,14 +14813,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@leichtgewicht/ip-codec@2.0.5': {} - '@lerna-lite/changed@4.10.2(@lerna-lite/version@4.10.2)(@types/node@25.0.3)': + '@lerna-lite/changed@5.0.0(@lerna-lite/version@5.0.0)(@types/node@25.5.2)': dependencies: - '@lerna-lite/cli': 4.10.2(@lerna-lite/list@4.10.2)(@lerna-lite/version@4.10.2)(@types/node@25.0.3) - '@lerna-lite/core': 4.10.2(@types/node@25.0.3) - '@lerna-lite/list': 4.10.2(@lerna-lite/version@4.10.2)(@types/node@25.0.3) - '@lerna-lite/listable': 4.10.2(@types/node@25.0.3) + '@lerna-lite/cli': 5.0.0(@lerna-lite/list@5.0.0)(@lerna-lite/version@5.0.0)(@types/node@25.5.2) + '@lerna-lite/core': 5.0.0(@types/node@25.5.2) + '@lerna-lite/list': 5.0.0(@lerna-lite/version@5.0.0)(@types/node@25.5.2) + '@lerna-lite/listable': 5.0.0(@types/node@25.5.2) transitivePeerDependencies: - '@lerna-lite/exec' - '@lerna-lite/publish' @@ -13838,61 +14837,56 @@ snapshots: - babel-plugin-macros - supports-color - '@lerna-lite/cli@4.10.2(@lerna-lite/list@4.10.2)(@lerna-lite/version@4.10.2)(@types/node@25.0.3)': + '@lerna-lite/cli@5.0.0(@lerna-lite/list@5.0.0)(@lerna-lite/version@5.0.0)(@types/node@25.5.2)': dependencies: - '@lerna-lite/core': 4.10.2(@types/node@25.0.3) - '@lerna-lite/init': 4.10.2(@types/node@25.0.3) - '@lerna-lite/npmlog': 4.10.0 - dedent: 1.7.0 - dotenv: 17.2.3 + '@lerna-lite/core': 5.0.0(@types/node@25.5.2) + '@lerna-lite/init': 5.0.0(@types/node@25.5.2) + '@lerna-lite/npmlog': 5.0.0 + dedent: 1.7.2 + dotenv: 17.4.1 import-local: 3.2.0 load-json-file: 7.0.1 yargs: 18.0.0 optionalDependencies: - '@lerna-lite/list': 4.10.2(@lerna-lite/version@4.10.2)(@types/node@25.0.3) - '@lerna-lite/version': 4.10.2(@lerna-lite/list@4.10.2)(@types/node@25.0.3)(conventional-commits-filter@5.0.0) + '@lerna-lite/list': 5.0.0(@lerna-lite/version@5.0.0)(@types/node@25.5.2) + '@lerna-lite/version': 5.0.0(@lerna-lite/list@5.0.0)(@types/node@25.5.2)(conventional-commits-filter@5.0.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - '@lerna-lite/core@4.10.2(@types/node@25.0.3)': + '@lerna-lite/core@5.0.0(@types/node@25.5.2)': dependencies: - '@inquirer/expand': 4.0.23(@types/node@25.0.3) - '@inquirer/input': 4.3.1(@types/node@25.0.3) - '@inquirer/select': 4.4.2(@types/node@25.0.3) - '@lerna-lite/npmlog': 4.10.0 - '@npmcli/run-script': 10.0.3 - ci-info: 4.3.1 - config-chain: 1.1.13 - dedent: 1.7.0 + '@inquirer/expand': 5.0.11(@types/node@25.5.2) + '@inquirer/input': 5.0.11(@types/node@25.5.2) + '@inquirer/select': 5.1.3(@types/node@25.5.2) + '@lerna-lite/npmlog': 5.0.0 + '@npmcli/run-script': 10.0.4 + ci-info: 4.4.0 + dedent: 1.7.2 execa: 9.6.1 - fs-extra: 11.3.2 - glob-parent: 6.0.2 + fs-extra: 11.3.4 json5: 2.2.3 lilconfig: 3.1.3 load-json-file: 7.0.1 npm-package-arg: 13.0.2 p-map: 7.0.4 - p-queue: 9.0.1 - semver: 7.7.3 - slash: 5.1.0 + p-queue: 9.1.2 + semver: 7.7.4 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - write-file-atomic: 7.0.0 + write-file-atomic: 7.0.1 write-json-file: 7.0.0 - write-package: 7.2.0 - yaml: 2.8.2 + yaml: 2.8.3 zeptomatch: 2.1.0 transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - '@lerna-lite/init@4.10.2(@types/node@25.0.3)': + '@lerna-lite/init@5.0.0(@types/node@25.5.2)': dependencies: - '@lerna-lite/core': 4.10.2(@types/node@25.0.3) - fs-extra: 11.3.2 + '@lerna-lite/core': 5.0.0(@types/node@25.5.2) + fs-extra: 11.3.4 p-map: 7.0.4 write-json-file: 7.0.0 transitivePeerDependencies: @@ -13900,11 +14894,11 @@ snapshots: - babel-plugin-macros - supports-color - '@lerna-lite/list@4.10.2(@lerna-lite/version@4.10.2)(@types/node@25.0.3)': + '@lerna-lite/list@5.0.0(@lerna-lite/version@5.0.0)(@types/node@25.5.2)': dependencies: - '@lerna-lite/cli': 4.10.2(@lerna-lite/list@4.10.2)(@lerna-lite/version@4.10.2)(@types/node@25.0.3) - '@lerna-lite/core': 4.10.2(@types/node@25.0.3) - '@lerna-lite/listable': 4.10.2(@types/node@25.0.3) + '@lerna-lite/cli': 5.0.0(@lerna-lite/list@5.0.0)(@lerna-lite/version@5.0.0)(@types/node@25.5.2) + '@lerna-lite/core': 5.0.0(@types/node@25.5.2) + '@lerna-lite/listable': 5.0.0(@types/node@25.5.2) transitivePeerDependencies: - '@lerna-lite/exec' - '@lerna-lite/publish' @@ -13915,55 +14909,42 @@ snapshots: - babel-plugin-macros - supports-color - '@lerna-lite/listable@4.10.2(@types/node@25.0.3)': + '@lerna-lite/listable@5.0.0(@types/node@25.5.2)': dependencies: - '@lerna-lite/core': 4.10.2(@types/node@25.0.3) + '@lerna-lite/core': 5.0.0(@types/node@25.5.2) columnify: 1.6.0 - tinyrainbow: 3.0.3 transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - '@lerna-lite/npmlog@4.10.0': + '@lerna-lite/npmlog@5.0.0': dependencies: - aproba: 2.1.0 fast-string-width: 3.0.2 - has-unicode: 2.0.1 - set-blocking: 2.0.0 signal-exit: 4.1.0 - tinyrainbow: 3.0.3 - wide-align: 1.1.5 - '@lerna-lite/version@4.10.2(@lerna-lite/list@4.10.2)(@types/node@25.0.3)(conventional-commits-filter@5.0.0)': + '@lerna-lite/version@5.0.0(@lerna-lite/list@5.0.0)(@types/node@25.5.2)(conventional-commits-filter@5.0.0)': dependencies: - '@conventional-changelog/git-client': 2.5.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1) - '@lerna-lite/cli': 4.10.2(@lerna-lite/list@4.10.2)(@lerna-lite/version@4.10.2)(@types/node@25.0.3) - '@lerna-lite/core': 4.10.2(@types/node@25.0.3) - '@lerna-lite/npmlog': 4.10.0 + '@conventional-changelog/git-client': 2.6.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) + '@lerna-lite/cli': 5.0.0(@lerna-lite/list@5.0.0)(@lerna-lite/version@5.0.0)(@types/node@25.5.2) + '@lerna-lite/core': 5.0.0(@types/node@25.5.2) + '@lerna-lite/npmlog': 5.0.0 '@octokit/plugin-enterprise-rest': 6.0.1 '@octokit/rest': 22.0.1 - conventional-changelog: 7.1.1(conventional-commits-filter@5.0.0) - conventional-changelog-angular: 8.1.0 - conventional-changelog-writer: 8.2.0 - conventional-commits-parser: 6.2.1 + conventional-changelog: 7.2.0(conventional-commits-filter@5.0.0) + conventional-changelog-angular: 8.3.1 + conventional-changelog-writer: 8.4.0 + conventional-commits-parser: 6.4.0 conventional-recommended-bump: 11.2.0 - dedent: 1.7.0 - fs-extra: 11.3.2 + dedent: 1.7.2 + fs-extra: 11.3.4 git-url-parse: 16.1.0 - is-stream: 4.0.1 load-json-file: 7.0.1 new-github-release-url: 2.0.0 npm-package-arg: 13.0.2 - p-limit: 7.2.0 + p-limit: 7.3.0 p-map: 7.0.4 - p-pipe: 4.0.0 - p-reduce: 3.0.0 - pify: 6.1.0 - semver: 7.7.3 - slash: 5.1.0 - tinyrainbow: 3.0.3 - uuid: 13.0.0 + semver: 7.7.4 write-json-file: 7.0.0 zeptomatch: 2.1.0 transitivePeerDependencies: @@ -14013,8 +14994,9 @@ snapshots: '@types/react': 19.2.7 react: 19.2.3 - '@modelcontextprotocol/sdk@1.24.3(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: + '@hono/node-server': 1.19.13(hono@4.12.12) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -14023,12 +15005,14 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.0.6 express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) + express-rate-limit: 8.3.2(express@5.2.1) + hono: 4.12.12 jose: 6.1.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.0(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -14065,7 +15049,7 @@ snapshots: '@npmcli/fs@5.0.0': dependencies: - semver: 7.7.3 + semver: 7.7.4 '@npmcli/git@7.0.1': dependencies: @@ -14075,7 +15059,7 @@ snapshots: npm-pick-manifest: 11.0.3 proc-log: 6.1.0 promise-retry: 2.0.1 - semver: 7.7.3 + semver: 7.7.4 which: 6.0.0 '@npmcli/node-gyp@5.0.0': {} @@ -14087,21 +15071,20 @@ snapshots: hosted-git-info: 9.0.2 json-parse-even-better-errors: 5.0.0 proc-log: 6.1.0 - semver: 7.7.3 + semver: 7.7.4 validate-npm-package-license: 3.0.4 '@npmcli/promise-spawn@9.0.1': dependencies: which: 6.0.0 - '@npmcli/run-script@10.0.3': + '@npmcli/run-script@10.0.4': dependencies: '@npmcli/node-gyp': 5.0.0 '@npmcli/package-json': 7.0.4 '@npmcli/promise-spawn': 9.0.1 node-gyp: 12.1.0 proc-log: 6.1.0 - which: 6.0.0 transitivePeerDependencies: - supports-color @@ -14409,12 +15392,12 @@ snapshots: '@simple-libs/child-process-utils@1.0.1': dependencies: - '@simple-libs/stream-utils': 1.1.0 + '@simple-libs/stream-utils': 1.2.0 '@types/node': 22.19.3 - '@simple-libs/stream-utils@1.1.0': - dependencies: - '@types/node': 22.19.3 + '@simple-libs/hosted-git-info@1.0.2': {} + + '@simple-libs/stream-utils@1.2.0': {} '@sinclair/typebox@0.27.8': {} @@ -14424,8 +15407,6 @@ snapshots: '@sindresorhus/is@5.6.0': {} - '@sindresorhus/merge-streams@2.3.0': {} - '@sindresorhus/merge-streams@4.0.0': {} '@sinonjs/commons@3.0.1': @@ -14436,6 +15417,10 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@15.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 @@ -14616,7 +15601,7 @@ snapshots: '@stoplight/spectral-rulesets': 1.22.0(encoding@0.1.13) '@stoplight/spectral-runtime': 1.1.4(encoding@0.1.13) '@stoplight/types': 13.20.0 - '@types/node': 25.0.3 + '@types/node': 25.5.2 pony-cause: 1.1.1 rollup: 2.79.2 tslib: 2.8.1 @@ -14633,7 +15618,7 @@ snapshots: '@stoplight/spectral-runtime': 1.1.4(encoding@0.1.13) '@stoplight/types': 13.20.0 '@stoplight/yaml': 4.2.3 - '@types/node': 25.0.3 + '@types/node': 25.5.2 ajv: 8.17.1 ast-types: 0.14.2 astring: 1.9.0 @@ -14752,12 +15737,12 @@ snapshots: '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.5) '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.5) - '@svgr/core@8.1.0(typescript@5.9.3)': + '@svgr/core@8.1.0(typescript@6.0.2)': dependencies: '@babel/core': 7.28.5 '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@6.0.2) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -14768,35 +15753,35 @@ snapshots: '@babel/types': 7.28.5 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@6.0.2))': dependencies: '@babel/core': 7.28.5 '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) - '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@6.0.2) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@6.0.2))(typescript@6.0.2)': dependencies: - '@svgr/core': 8.1.0(typescript@5.9.3) - cosmiconfig: 8.3.6(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@6.0.2) + cosmiconfig: 8.3.6(typescript@6.0.2) deepmerge: 4.3.1 svgo: 3.3.2 transitivePeerDependencies: - typescript - '@svgr/webpack@8.1.0(typescript@5.9.3)': + '@svgr/webpack@8.1.0(typescript@6.0.2)': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.28.5) '@babel/preset-env': 7.28.5(@babel/core@7.28.5) '@babel/preset-react': 7.28.5(@babel/core@7.28.5) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) - '@svgr/core': 8.1.0(typescript@5.9.3) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@6.0.2) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@6.0.2)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@6.0.2))(typescript@6.0.2) transitivePeerDependencies: - supports-color - typescript @@ -14805,9 +15790,9 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@testcontainers/postgresql@11.11.0': + '@testcontainers/postgresql@11.13.0': dependencies: - testcontainers: 11.11.0 + testcontainers: 11.13.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -14834,7 +15819,7 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.1(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 '@testing-library/dom': 10.4.1 @@ -14847,8 +15832,6 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tootallnate/once@2.0.0': {} - '@trysound/sax@0.2.0': {} '@tsconfig/node10@1.0.12': @@ -14863,17 +15846,17 @@ snapshots: '@tsconfig/node16@1.0.4': optional: true - '@ttoss/config@1.35.12': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.5) - '@babel/preset-env': 7.28.5(@babel/core@7.28.5) - '@babel/preset-react': 7.28.5(@babel/core@7.28.5) - '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) - '@commitlint/config-conventional': 19.8.1 - '@formatjs/ts-transformer': 3.14.2 - babel-plugin-formatjs: 10.5.41 - babel-plugin-transform-import-meta: 2.3.3(@babel/core@7.28.5) + '@ttoss/config@1.37.8': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + '@babel/preset-react': 7.28.5(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@commitlint/config-conventional': 20.5.0 + '@formatjs/ts-transformer': 4.4.3 + babel-plugin-formatjs: 11.3.2 + babel-plugin-transform-import-meta: 2.3.3(@babel/core@7.29.0) deepmerge: 4.3.1 identity-obj-proxy: 3.0.0 prettier-package-json: 2.8.0 @@ -14881,32 +15864,32 @@ snapshots: - supports-color - ts-jest - '@ttoss/eslint-config@1.26.6(@testing-library/dom@10.4.1)(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)))(prettier@3.7.4)(turbo@2.6.3)(typescript@5.9.3)': + '@ttoss/eslint-config@1.26.14(@testing-library/dom@10.4.1)(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(prettier@3.8.1)(turbo@2.9.4)(typescript@6.0.2)': dependencies: - '@eslint/compat': 2.0.0(eslint@9.39.2(jiti@2.6.1)) - '@eslint/eslintrc': 3.3.3 + '@eslint/compat': 2.0.4(eslint@9.39.2(jiti@2.6.1)) + '@eslint/eslintrc': 3.3.5 '@eslint/js': 9.39.2 - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) - eslint-config-turbo: 2.6.3(eslint@9.39.2(jiti@2.6.1))(turbo@2.6.3) + eslint-config-turbo: 2.9.4(eslint@9.39.2(jiti@2.6.1))(turbo@2.9.4) eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-formatjs: 5.4.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-jest: 29.5.0(@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)))(typescript@5.9.3) + eslint-plugin-formatjs: 6.4.4(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-jest: 29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2) eslint-plugin-jest-dom: 5.5.0(@testing-library/dom@10.4.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-prefer-arrow-functions: 3.9.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) + eslint-plugin-prefer-arrow-functions: 3.9.1(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-namespace-import: 1.0.5(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react-refresh: 0.4.25(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-refresh: 0.5.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-relay: 2.0.0 eslint-plugin-simple-import-sort: 12.1.1(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-unicorn: 62.0.0(eslint@9.39.2(jiti@2.6.1)) - globals: 16.5.0 - typescript-eslint: 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-unicorn: 64.0.0(eslint@9.39.2(jiti@2.6.1)) + globals: 17.4.0 + typescript-eslint: 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) transitivePeerDependencies: - '@testing-library/dom' - '@types/eslint' @@ -14920,45 +15903,46 @@ snapshots: - turbo - typescript - '@ttoss/http-server-mcp@0.3.2': + '@ttoss/http-server-mcp@0.11.0': dependencies: - '@modelcontextprotocol/sdk': 1.24.3(zod@3.25.76) - '@ttoss/http-server': 0.3.2 + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) + '@ttoss/http-server': 0.5.8 zod: 3.25.76 transitivePeerDependencies: - '@cfworker/json-schema' - supports-color - '@ttoss/http-server@0.3.2': + '@ttoss/http-server@0.5.8': dependencies: '@koa/bodyparser': 6.0.0(koa@3.1.1) '@koa/cors': 5.0.0 '@koa/multer': 4.0.0(koa@3.1.1)(multer@2.0.2) '@koa/router': 15.1.1(koa@3.1.1) koa: 3.1.1 + koa-static: 5.0.0 multer: 2.0.2 transitivePeerDependencies: - supports-color '@ttoss/logger@0.7.1': {} - '@ttoss/monorepo@1.28.0': + '@ttoss/monorepo@1.29.8': dependencies: commander: 14.0.2 - '@ttoss/postgresdb-cli@0.1.24': + '@ttoss/postgresdb-cli@0.2.8': dependencies: commander: 14.0.2 dotenv: 17.2.3 - esbuild: 0.27.1 + esbuild: 0.27.7 sequelize-erd: 1.3.1 - '@ttoss/postgresdb@0.3.0(@types/node@25.0.3)(@types/validator@13.15.10)(reflect-metadata@0.2.2)': + '@ttoss/postgresdb@0.8.0(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2)': dependencies: - pg: 8.16.3 + pg: 8.20.0 pgvector: 0.2.1 - sequelize: 6.37.7(pg@8.16.3) - sequelize-typescript: 2.1.6(@types/node@25.0.3)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.7(pg@8.16.3)) + sequelize: 6.37.7(pg@8.20.0) + sequelize-typescript: 2.1.6(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.7(pg@8.20.0)) transitivePeerDependencies: - '@types/node' - '@types/validator' @@ -14974,16 +15958,16 @@ snapshots: - supports-color - tedious - '@ttoss/test-utils@4.0.2(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@ttoss/test-utils@4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@emotion/jest': 11.14.2(@types/jest@30.0.0) - '@faker-js/faker': 10.2.0 + '@faker-js/faker': 10.4.0 '@testing-library/dom': 10.4.1 '@testing-library/jest-dom': 6.9.1 - '@testing-library/react': 16.3.1(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@testing-library/react': 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@types/relay-test-utils': 19.0.0 - jest: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) + jest: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) jest-environment-jsdom: 30.2.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -15000,6 +15984,24 @@ snapshots: - supports-color - utf-8-validate + '@turbo/darwin-64@2.9.4': + optional: true + + '@turbo/darwin-arm64@2.9.4': + optional: true + + '@turbo/linux-64@2.9.4': + optional: true + + '@turbo/linux-arm64@2.9.4': + optional: true + + '@turbo/windows-64@2.9.4': + optional: true + + '@turbo/windows-arm64@2.9.4': + optional: true + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -15010,14 +16012,14 @@ snapshots: '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/babel__helper-plugin-utils@7.10.3': dependencies: @@ -15026,40 +16028,34 @@ snapshots: '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/bonjour@3.5.13': dependencies: - '@types/node': 25.0.3 - - '@types/caseless@0.12.5': {} + '@types/node': 25.5.2 '@types/co-body@6.1.3': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/qs': 6.14.0 '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 4.19.7 - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/connect@3.4.38': dependencies: - '@types/node': 25.0.3 - - '@types/conventional-commits-parser@5.0.2': - dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/cookiejar@2.1.5': {} @@ -15069,18 +16065,18 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/ssh2': 1.15.5 - '@types/dockerode@3.3.47': + '@types/dockerode@4.0.1': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/ssh2': 1.15.5 '@types/es-aggregate-error@1.0.6': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/eslint-scope@3.7.7': dependencies: @@ -15102,7 +16098,7 @@ snapshots: '@types/express-serve-static-core@4.19.7': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -15139,7 +16135,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/istanbul-lib-coverage@2.0.6': {} @@ -15158,7 +16154,7 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -15186,7 +16182,7 @@ snapshots: '@types/node-forge@1.3.14': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/node@17.0.45': {} @@ -15198,9 +16194,9 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@25.0.3': + '@types/node@25.5.2': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 '@types/normalize-package-data@2.4.4': {} @@ -15214,13 +16210,13 @@ snapshots: '@types/parse5@6.0.3': {} - '@types/pg@8.16.0': + '@types/pg@8.20.0': dependencies: - '@types/node': 25.0.3 - pg-protocol: 1.10.3 + '@types/node': 25.5.2 + pg-protocol: 1.13.0 pg-types: 2.2.0 - '@types/picomatch@3.0.2': {} + '@types/picomatch@4.0.3': {} '@types/prismjs@1.26.5': {} @@ -15271,29 +16267,22 @@ snapshots: '@types/react-relay': 18.2.1 '@types/relay-runtime': 20.1.0 - '@types/request@2.48.13': - dependencies: - '@types/caseless': 0.12.5 - '@types/node': 25.0.3 - '@types/tough-cookie': 4.0.5 - form-data: 2.5.5 - '@types/retry@0.12.2': {} '@types/sarif@2.1.7': {} '@types/sax@1.2.7': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/send@1.2.1': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/serve-index@1.9.4': dependencies: @@ -15302,20 +16291,20 @@ snapshots: '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/send': 0.17.6 '@types/sockjs@0.3.36': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/ssh2-streams@0.1.13': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/ssh2@0.5.52': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/ssh2-streams': 0.1.13 '@types/ssh2@1.15.5': @@ -15328,10 +16317,10 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 25.0.3 + '@types/node': 25.5.2 form-data: 4.0.5 - '@types/supertest@6.0.3': + '@types/supertest@7.2.0': dependencies: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 @@ -15344,13 +16333,11 @@ snapshots: '@types/urijs@1.19.26': {} - '@types/uuid@10.0.0': {} - '@types/validator@13.15.10': {} '@types/ws@8.18.1': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/yargs-parser@21.0.3': {} @@ -15358,40 +16345,49 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.0 eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.49.0(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@6.0.2) '@typescript-eslint/types': 8.49.0 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.58.0(typescript@6.0.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + debug: 4.4.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -15400,47 +16396,84 @@ snapshots: '@typescript-eslint/types': 8.49.0 '@typescript-eslint/visitor-keys': 8.49.0 - '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': + '@typescript-eslint/scope-manager@8.58.0': dependencies: - typescript: 5.9.3 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.49.0(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + typescript: 6.0.2 + + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + + '@typescript-eslint/type-utils@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.49.0': {} - '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': + '@typescript-eslint/types@8.58.0': {} + + '@typescript-eslint/typescript-estree@8.49.0(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) + '@typescript-eslint/project-service': 8.49.0(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@6.0.2) '@typescript-eslint/types': 8.49.0 '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.1.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.58.0(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.49.0(typescript@6.0.2) + eslint: 9.39.2(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -15449,8 +16482,15 @@ snapshots: '@typescript-eslint/types': 8.49.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} + '@unicode/unicode-17.0.0@1.6.16': {} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -15592,11 +16632,6 @@ snapshots: '@xtuc/long@4.2.2': {} - JSONStream@1.3.5: - dependencies: - jsonparse: 1.3.1 - through: 2.3.8 - abbrev@4.0.0: {} abort-controller@3.0.0: @@ -15629,12 +16664,6 @@ snapshots: address@1.2.2: {} - agent-base@6.0.2: - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - agent-base@7.1.4: {} aggregate-error@3.1.0: @@ -15690,6 +16719,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.11.0: dependencies: fast-deep-equal: 3.1.3 @@ -15734,8 +16770,6 @@ snapshots: dependencies: string-width: 4.2.3 - ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -15767,8 +16801,6 @@ snapshots: append-field@1.0.0: {} - aproba@2.1.0: {} - archiver-utils@5.0.2: dependencies: glob: 10.5.0 @@ -15882,8 +16914,6 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - arrify@2.0.1: {} - as-table@1.0.55: dependencies: printable-characters: 1.0.42 @@ -15906,10 +16936,6 @@ snapshots: async-lock@1.4.1: {} - async-retry@1.3.3: - dependencies: - retry: 0.13.1 - async@3.2.2: {} async@3.2.4: {} @@ -15935,32 +16961,19 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - aws-sdk@2.1693.0: - dependencies: - buffer: 4.9.2 - events: 1.1.1 - ieee754: 1.1.13 - jmespath: 0.16.0 - querystring: 0.2.0 - sax: 1.2.1 - url: 0.10.3 - util: 0.12.5 - uuid: 8.0.0 - xml2js: 0.6.2 - axe-core@4.11.0: {} axobject-query@4.1.0: {} b4a@1.7.3: {} - babel-jest@30.2.0(@babel/core@7.28.5): + babel-jest@30.3.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 - '@jest/transform': 30.2.0 + '@jest/transform': 30.3.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.2.0(@babel/core@7.28.5) + babel-preset-jest: 30.3.0(@babel/core@7.28.5) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -15978,19 +16991,18 @@ snapshots: dependencies: object.assign: 4.1.7 - babel-plugin-formatjs@10.5.41: + babel-plugin-formatjs@11.3.2: dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - '@formatjs/icu-messageformat-parser': 2.11.4 - '@formatjs/ts-transformer': 3.14.2 + '@babel/types': 7.29.0 + '@formatjs/icu-messageformat-parser': 3.5.3 + '@formatjs/ts-transformer': 4.4.3 '@types/babel__core': 7.20.5 '@types/babel__helper-plugin-utils': 7.10.3 '@types/babel__traverse': 7.28.0 - tslib: 2.8.1 transitivePeerDependencies: - supports-color - ts-jest @@ -16005,7 +17017,7 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-jest-hoist@30.2.0: + babel-plugin-jest-hoist@30.3.0: dependencies: '@types/babel__core': 7.20.5 @@ -16018,6 +17030,15 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -16026,6 +17047,14 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -16033,9 +17062,16 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-transform-import-meta@2.3.3(@babel/core@7.28.5): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-import-meta@2.3.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 '@babel/template': 7.27.2 tslib: 2.8.1 @@ -16058,16 +17094,18 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) - babel-preset-jest@30.2.0(@babel/core@7.28.5): + babel-preset-jest@30.3.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 - babel-plugin-jest-hoist: 30.2.0 + babel-plugin-jest-hoist: 30.3.0 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) bail@2.0.2: {} balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-events@2.8.2: {} bare-fs@4.5.2: @@ -16119,8 +17157,6 @@ snapshots: big.js@5.2.2: {} - bignumber.js@9.3.1: {} - binary-extensions@2.3.0: {} bl@4.1.0: @@ -16198,6 +17234,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -16216,16 +17256,8 @@ snapshots: buffer-crc32@1.0.0: {} - buffer-equal-constant-time@1.0.1: {} - buffer-from@1.1.2: {} - buffer@4.9.2: - dependencies: - base64-js: 1.5.1 - ieee754: 1.1.13 - isarray: 1.0.0 - buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -16333,10 +17365,6 @@ snapshots: ccount@2.0.1: {} - chalk-template@1.1.2: - dependencies: - chalk: 5.6.2 - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -16403,6 +17431,8 @@ snapshots: ci-info@4.3.1: {} + ci-info@4.4.0: {} + cjs-module-lexer@2.1.1: {} clean-css@5.3.3: @@ -16421,8 +17451,6 @@ snapshots: dependencies: restore-cursor: 5.1.0 - cli-spinners@2.9.2: {} - cli-table3@0.6.5: dependencies: string-width: 4.2.3 @@ -16507,10 +17535,10 @@ snapshots: commander@10.0.1: {} - commander@13.1.0: {} - commander@14.0.2: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -16607,34 +17635,32 @@ snapshots: content-type@1.0.5: {} - conventional-changelog-angular@7.0.0: - dependencies: - compare-func: 2.0.0 - - conventional-changelog-angular@8.1.0: + conventional-changelog-angular@8.3.1: dependencies: compare-func: 2.0.0 - conventional-changelog-conventionalcommits@7.0.2: + conventional-changelog-conventionalcommits@9.3.1: dependencies: compare-func: 2.0.0 conventional-changelog-preset-loader@5.0.0: {} - conventional-changelog-writer@8.2.0: + conventional-changelog-writer@8.4.0: dependencies: + '@simple-libs/stream-utils': 1.2.0 conventional-commits-filter: 5.0.0 handlebars: 4.7.8 meow: 13.2.0 - semver: 7.7.3 + semver: 7.7.4 - conventional-changelog@7.1.1(conventional-commits-filter@5.0.0): + conventional-changelog@7.2.0(conventional-commits-filter@5.0.0): dependencies: - '@conventional-changelog/git-client': 2.5.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1) + '@conventional-changelog/git-client': 2.6.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) + '@simple-libs/hosted-git-info': 1.0.2 '@types/normalize-package-data': 2.4.4 conventional-changelog-preset-loader: 5.0.0 - conventional-changelog-writer: 8.2.0 - conventional-commits-parser: 6.2.1 + conventional-changelog-writer: 8.4.0 + conventional-commits-parser: 6.4.0 fd-package-json: 2.0.0 meow: 13.2.0 normalize-package-data: 7.0.1 @@ -16643,23 +17669,17 @@ snapshots: conventional-commits-filter@5.0.0: {} - conventional-commits-parser@5.0.0: - dependencies: - JSONStream: 1.3.5 - is-text-path: 2.0.0 - meow: 12.1.1 - split2: 4.2.0 - - conventional-commits-parser@6.2.1: + conventional-commits-parser@6.4.0: dependencies: + '@simple-libs/stream-utils': 1.2.0 meow: 13.2.0 conventional-recommended-bump@11.2.0: dependencies: - '@conventional-changelog/git-client': 2.5.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1) + '@conventional-changelog/git-client': 2.6.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) conventional-changelog-preset-loader: 5.0.0 conventional-commits-filter: 5.0.0 - conventional-commits-parser: 6.2.1 + conventional-commits-parser: 6.4.0 meow: 13.2.0 convert-source-map@2.0.0: {} @@ -16693,6 +17713,10 @@ snapshots: dependencies: browserslist: 4.28.1 + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.1 + core-js-pure@3.47.0: {} core-js@3.47.0: {} @@ -16704,12 +17728,12 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.3)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): + cosmiconfig-typescript-loader@6.2.0(@types/node@25.5.2)(cosmiconfig@9.0.1(typescript@6.0.2))(typescript@6.0.2): dependencies: - '@types/node': 25.0.3 - cosmiconfig: 9.0.0(typescript@5.9.3) + '@types/node': 25.5.2 + cosmiconfig: 9.0.1(typescript@6.0.2) jiti: 2.6.1 - typescript: 5.9.3 + typescript: 6.0.2 cosmiconfig@7.1.0: dependencies: @@ -16719,23 +17743,23 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@8.3.6(typescript@5.9.3): + cosmiconfig@8.3.6(typescript@6.0.2): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 - cosmiconfig@9.0.0(typescript@5.9.3): + cosmiconfig@9.0.1(typescript@6.0.2): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 cpu-features@0.0.10: dependencies: @@ -16918,8 +17942,6 @@ snapshots: damerau-levenshtein@1.0.8: {} - dargs@8.1.0: {} - data-uri-to-buffer@2.0.2: {} data-urls@5.0.0: @@ -16971,14 +17993,14 @@ snapshots: dedent@1.7.0: {} + dedent@1.7.2: {} + deep-equal@1.0.1: {} deep-extend@0.6.0: {} deep-is@0.1.4: {} - deepmerge-ts@7.1.5: {} - deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -17066,7 +18088,7 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 - docker-compose@1.3.0: + docker-compose@1.4.2: dependencies: yaml: 2.8.2 @@ -17095,10 +18117,10 @@ snapshots: dependencies: esutils: 2.0.3 - docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3): + docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3): dependencies: '@apidevtools/json-schema-ref-parser': 11.9.3 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@redocly/openapi-core': 1.34.6 @@ -17120,9 +18142,9 @@ snapshots: - encoding - supports-color - docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(sass@1.97.2)(webpack@5.104.1): + docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(sass@1.97.2)(webpack@5.104.1): dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) sass: 1.97.2 sass-loader: 16.0.6(sass@1.97.2)(webpack@5.104.1) transitivePeerDependencies: @@ -17131,9 +18153,9 @@ snapshots: - sass-embedded - webpack - docusaurus-theme-openapi-docs@4.5.1(@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(sass@1.97.2)(webpack@5.104.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1): + docusaurus-theme-openapi-docs@4.5.1(@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(sass@1.97.2)(webpack@5.104.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1): dependencies: - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@hookform/error-message': 2.0.1(react-dom@19.2.3(react@19.2.3))(react-hook-form@7.70.0(react@19.2.3))(react@19.2.3) '@reduxjs/toolkit': 1.9.7(react-redux@7.2.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) allof-merge: 0.6.7 @@ -17141,8 +18163,8 @@ snapshots: clsx: 1.2.1 copy-text-to-clipboard: 3.2.2 crypto-js: 4.2.0 - docusaurus-plugin-openapi-docs: 4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3) - docusaurus-plugin-sass: 0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(sass@1.97.2)(webpack@5.104.1) + docusaurus-plugin-openapi-docs: 4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3) + docusaurus-plugin-sass: 0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(sass@1.97.2)(webpack@5.104.1) file-saver: 2.0.5 lodash: 4.17.21 pako: 2.1.0 @@ -17233,6 +18255,8 @@ snapshots: dotenv@17.2.3: {} + dotenv@17.4.1: {} + dottie@2.0.6: {} dunder-proto@1.0.1: @@ -17243,32 +18267,14 @@ snapshots: duplexer@0.1.2: {} - duplexify@4.1.3: - dependencies: - end-of-stream: 1.4.5 - inherits: 2.0.4 - readable-stream: 3.6.2 - stream-shift: 1.0.3 - eastasianwidth@0.2.0: {} - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - ee-first@1.1.1: {} - effect@3.19.12: - dependencies: - '@standard-schema/spec': 1.0.0 - fast-check: 3.23.2 - electron-to-chromium@1.5.267: {} emittery@0.13.1: {} - emoji-regex-xs@2.0.1: {} - emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -17297,11 +18303,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 - enquirer@2.4.1: - dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 - entities@2.2.0: {} entities@4.5.0: {} @@ -17477,6 +18478,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.1 '@esbuild/win32-x64': 0.27.1 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-goat@4.0.0: {} @@ -17495,11 +18525,11 @@ snapshots: dependencies: eslint: 9.39.2(jiti@2.6.1) - eslint-config-turbo@2.6.3(eslint@9.39.2(jiti@2.6.1))(turbo@2.6.3): + eslint-config-turbo@2.9.4(eslint@9.39.2(jiti@2.6.1))(turbo@2.9.4): dependencies: eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-turbo: 2.6.3(eslint@9.39.2(jiti@2.6.1))(turbo@2.6.3) - turbo: 2.6.3 + eslint-plugin-turbo: 2.9.4(eslint@9.39.2(jiti@2.6.1))(turbo@2.9.4) + turbo: 2.9.4 eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: @@ -17527,39 +18557,34 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-formatjs@5.4.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-formatjs@6.4.4(eslint@9.39.2(jiti@2.6.1)): dependencies: - '@formatjs/icu-messageformat-parser': 2.11.4 - '@formatjs/ts-transformer': 3.14.2 - '@types/eslint': 9.6.1 - '@types/picomatch': 3.0.2 - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@formatjs/icu-messageformat-parser': 3.5.3 + '@formatjs/ts-transformer': 4.4.3 + '@types/picomatch': 4.0.3 + '@unicode/unicode-17.0.0': 1.6.16 eslint: 9.39.2(jiti@2.6.1) magic-string: 0.30.21 picomatch: 4.0.3 - tslib: 2.8.1 - unicode-emoji-utils: 1.3.1 transitivePeerDependencies: - - supports-color - ts-jest - - typescript - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -17570,7 +18595,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -17582,7 +18607,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -17596,16 +18621,16 @@ snapshots: optionalDependencies: '@testing-library/dom': 10.4.1 - eslint-plugin-jest@29.5.0(@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)))(typescript@5.9.3): + eslint-plugin-jest@29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - jest: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) + jest: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - - typescript eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): dependencies: @@ -17626,21 +18651,21 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prefer-arrow-functions@3.9.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-prefer-arrow-functions@3.9.1(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2): dependencies: '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4): + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1): dependencies: eslint: 9.39.2(jiti@2.6.1) - prettier: 3.7.4 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 + prettier: 3.8.1 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 optionalDependencies: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) @@ -17660,7 +18685,7 @@ snapshots: dependencies: eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-react-refresh@0.4.25(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react-refresh@0.5.2(eslint@9.39.2(jiti@2.6.1)): dependencies: eslint: 9.39.2(jiti@2.6.1) @@ -17694,32 +18719,30 @@ snapshots: dependencies: eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-turbo@2.6.3(eslint@9.39.2(jiti@2.6.1))(turbo@2.6.3): + eslint-plugin-turbo@2.9.4(eslint@9.39.2(jiti@2.6.1))(turbo@2.9.4): dependencies: dotenv: 16.0.3 eslint: 9.39.2(jiti@2.6.1) - turbo: 2.6.3 + turbo: 2.9.4 - eslint-plugin-unicorn@62.0.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-unicorn@64.0.0(eslint@9.39.2(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) - '@eslint/plugin-kit': 0.4.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) change-case: 5.4.4 - ci-info: 4.3.1 + ci-info: 4.4.0 clean-regexp: 1.0.0 - core-js-compat: 3.47.0 + core-js-compat: 3.49.0 eslint: 9.39.2(jiti@2.6.1) - esquery: 1.6.0 find-up-simple: 1.0.1 - globals: 16.5.0 + globals: 17.4.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 jsesc: 3.1.0 pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.13.0 - semver: 7.7.3 + semver: 7.7.4 strip-indent: 4.1.1 eslint-scope@5.1.1: @@ -17736,6 +18759,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.39.2(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) @@ -17846,7 +18871,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 require-like: 0.1.2 event-target-shim@5.0.1: {} @@ -17861,8 +18886,6 @@ snapshots: transitivePeerDependencies: - bare-abort-controller - events@1.1.1: {} - events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -17911,11 +18934,21 @@ snapshots: jest-mock: 30.2.0 jest-util: 30.2.0 + expect@30.3.0: + dependencies: + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + exponential-backoff@3.1.3: {} - express-rate-limit@7.5.1(express@5.2.1): + express-rate-limit@8.3.2(express@5.2.1): dependencies: express: 5.2.1 + ip-address: 10.1.0 express@4.22.1: dependencies: @@ -17992,10 +19025,6 @@ snapshots: extend@3.0.2: {} - fast-check@3.23.2: - dependencies: - pure-rand: 6.1.0 - fast-content-type-parse@3.0.0: {} fast-deep-equal@3.1.3: {} @@ -18036,9 +19065,9 @@ snapshots: fast-uri@3.1.0: {} - fast-xml-parser@4.5.3: + fast-wrap-ansi@0.2.0: dependencies: - strnum: 1.1.2 + fast-string-width: 3.0.2 fastq@1.19.1: dependencies: @@ -18153,12 +19182,6 @@ snapshots: locate-path: 7.2.0 path-exists: 5.0.0 - find-up@7.0.0: - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - unicorn-magic: 0.1.0 - fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 @@ -18189,15 +19212,6 @@ snapshots: form-data-encoder@2.1.4: {} - form-data@2.5.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - safe-buffer: 5.2.1 - form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -18236,6 +19250,12 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 @@ -18265,26 +19285,6 @@ snapshots: functions-have-names@1.2.3: {} - gaxios@6.7.1(encoding@0.1.13): - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - is-stream: 2.0.1 - node-fetch: 2.7.0(encoding@0.1.13) - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - gcp-metadata@6.1.1(encoding@0.1.13): - dependencies: - gaxios: 6.7.1(encoding@0.1.13) - google-logging-utils: 0.0.2 - json-bigint: 1.0.0 - transitivePeerDependencies: - - encoding - - supports-color - generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -18339,11 +19339,13 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - git-raw-commits@4.0.0: + git-raw-commits@5.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0): dependencies: - dargs: 8.1.0 - meow: 12.1.1 - split2: 4.2.0 + '@conventional-changelog/git-client': 2.6.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) + meow: 13.2.0 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser git-up@8.1.1: dependencies: @@ -18390,7 +19392,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 @@ -18413,7 +19415,7 @@ snapshots: globals@14.0.0: {} - globals@16.5.0: {} + globals@17.4.0: {} globalthis@1.0.4: dependencies: @@ -18437,29 +19439,6 @@ snapshots: merge2: 1.4.1 slash: 4.0.0 - globby@14.1.0: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.3 - ignore: 7.0.5 - path-type: 6.0.0 - slash: 5.1.0 - unicorn-magic: 0.3.0 - - google-auth-library@9.15.1(encoding@0.1.13): - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 6.7.1(encoding@0.1.13) - gcp-metadata: 6.1.1(encoding@0.1.13) - gtoken: 7.1.0(encoding@0.1.13) - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - google-logging-utils@0.0.2: {} - gopd@1.2.0: {} got@12.6.1: @@ -18497,14 +19476,6 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 - gtoken@7.1.0(encoding@0.1.13): - dependencies: - gaxios: 6.7.1(encoding@0.1.13) - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -18540,8 +19511,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - has-unicode@2.0.1: {} - has-yarn@3.0.0: {} hasown@2.0.2: @@ -18710,9 +19679,7 @@ snapshots: dependencies: react-is: 16.13.1 - hosted-git-info@7.0.2: - dependencies: - lru-cache: 10.4.3 + hono@4.12.12: {} hosted-git-info@8.1.0: dependencies: @@ -18735,8 +19702,6 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 - html-entities@2.6.0: {} - html-escaper@2.0.2: {} html-minifier-terser@6.1.0: @@ -18823,14 +19788,6 @@ snapshots: http-parser-js@0.5.10: {} - http-proxy-agent@5.0.0: - dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -18867,13 +19824,6 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -18909,8 +19859,6 @@ snapshots: dependencies: harmony-reflect: 1.6.2 - ieee754@1.1.13: {} - ieee754@1.2.1: {} ignore@5.3.2: {} @@ -18943,8 +19891,6 @@ snapshots: indent-string@5.0.0: {} - index-to-position@1.2.0: {} - infima@0.2.0-alpha.45: {} inflation@2.1.0: {} @@ -18997,11 +19943,6 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - is-arguments@1.2.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -19107,8 +20048,6 @@ snapshots: global-dirs: 3.0.1 is-path-inside: 3.0.3 - is-interactive@2.0.0: {} - is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -19180,18 +20119,12 @@ snapshots: has-symbols: 1.1.0 safe-regex-test: 1.1.0 - is-text-path@2.0.0: - dependencies: - text-extensions: 2.4.0 - is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.19 is-typedarray@1.0.0: {} - is-unicode-supported@1.3.0: {} - is-unicode-supported@2.1.0: {} is-weakmap@2.0.2: {} @@ -19273,31 +20206,31 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jest-changed-files@30.2.0: + jest-changed-files@30.3.0: dependencies: execa: 5.1.1 - jest-util: 30.2.0 + jest-util: 30.3.0 p-limit: 3.1.0 - jest-circus@30.2.0: + jest-circus@30.3.0: dependencies: - '@jest/environment': 30.2.0 - '@jest/expect': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.0 is-generator-fn: 2.1.0 - jest-each: 30.2.0 - jest-matcher-utils: 30.2.0 - jest-message-util: 30.2.0 - jest-runtime: 30.2.0 - jest-snapshot: 30.2.0 - jest-util: 30.2.0 + jest-each: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 p-limit: 3.1.0 - pretty-format: 30.2.0 + pretty-format: 30.3.0 pure-rand: 7.0.1 slash: 3.0.0 stack-utils: 2.0.6 @@ -19305,17 +20238,17 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)): + jest-cli@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)): dependencies: - '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) - '@jest/test-result': 30.2.0 - '@jest/types': 30.2.0 + '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) - jest-util: 30.2.0 - jest-validate: 30.2.0 + jest-config: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + jest-util: 30.3.0 + jest-validate: 30.3.0 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' @@ -19324,35 +20257,34 @@ snapshots: - supports-color - ts-node - jest-config@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)): + jest-config@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) + '@jest/test-sequencer': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.28.5) chalk: 4.1.2 ci-info: 4.3.1 deepmerge: 4.3.1 glob: 10.5.0 graceful-fs: 4.2.11 - jest-circus: 30.2.0 + jest-circus: 30.3.0 jest-docblock: 30.2.0 - jest-environment-node: 30.2.0 + jest-environment-node: 30.3.0 jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-runner: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - micromatch: 4.0.8 + jest-resolve: 30.3.0 + jest-runner: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 parse-json: 5.2.0 - pretty-format: 30.2.0 + pretty-format: 30.3.0 slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 25.0.3 - ts-node: 10.9.2(@types/node@25.0.3)(typescript@5.9.3) + '@types/node': 25.5.2 + ts-node: 10.9.2(@types/node@25.5.2)(typescript@6.0.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -19364,59 +20296,66 @@ snapshots: chalk: 4.1.2 pretty-format: 30.2.0 + jest-diff@30.3.0: + dependencies: + '@jest/diff-sequences': 30.3.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.3.0 + jest-docblock@30.2.0: dependencies: detect-newline: 3.1.0 - jest-each@30.2.0: + jest-each@30.3.0: dependencies: '@jest/get-type': 30.1.0 - '@jest/types': 30.2.0 - chalk: 4.1.2 - jest-util: 30.2.0 - pretty-format: 30.2.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + jest-util: 30.3.0 + pretty-format: 30.3.0 jest-environment-jsdom@30.2.0: dependencies: '@jest/environment': 30.2.0 '@jest/environment-jsdom-abstract': 30.2.0(jsdom@26.1.0) '@types/jsdom': 21.1.7 - '@types/node': 25.0.3 + '@types/node': 25.5.2 jsdom: 26.1.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - jest-environment-node@30.2.0: + jest-environment-node@30.3.0: dependencies: - '@jest/environment': 30.2.0 - '@jest/fake-timers': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 25.0.3 - jest-mock: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 + jest-mock: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 - jest-haste-map@30.2.0: + jest-haste-map@30.3.0: dependencies: - '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 jest-regex-util: 30.0.1 - jest-util: 30.2.0 - jest-worker: 30.2.0 - micromatch: 4.0.8 + jest-util: 30.3.0 + jest-worker: 30.3.0 + picomatch: 4.0.3 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 - jest-leak-detector@30.2.0: + jest-leak-detector@30.3.0: dependencies: '@jest/get-type': 30.1.0 - pretty-format: 30.2.0 + pretty-format: 30.3.0 jest-matcher-utils@30.2.0: dependencies: @@ -19425,6 +20364,13 @@ snapshots: jest-diff: 30.2.0 pretty-format: 30.2.0 + jest-matcher-utils@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.3.0 + pretty-format: 30.3.0 + jest-message-util@30.2.0: dependencies: '@babel/code-frame': 7.27.1 @@ -19437,111 +20383,129 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 + jest-message-util@30.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 30.3.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + pretty-format: 30.3.0 + slash: 3.0.0 + stack-utils: 2.0.6 + jest-mock@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@types/node': 25.5.2 jest-util: 30.2.0 - jest-pnp-resolver@1.2.3(jest-resolve@30.2.0): + jest-mock@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 25.5.2 + jest-util: 30.3.0 + + jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): optionalDependencies: - jest-resolve: 30.2.0 + jest-resolve: 30.3.0 jest-regex-util@30.0.1: {} - jest-resolve-dependencies@30.2.0: + jest-resolve-dependencies@30.3.0: dependencies: jest-regex-util: 30.0.1 - jest-snapshot: 30.2.0 + jest-snapshot: 30.3.0 transitivePeerDependencies: - supports-color - jest-resolve@30.2.0: + jest-resolve@30.3.0: dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 - jest-haste-map: 30.2.0 - jest-pnp-resolver: 1.2.3(jest-resolve@30.2.0) - jest-util: 30.2.0 - jest-validate: 30.2.0 + jest-haste-map: 30.3.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0) + jest-util: 30.3.0 + jest-validate: 30.3.0 slash: 3.0.0 unrs-resolver: 1.11.1 - jest-runner@30.2.0: + jest-runner@30.3.0: dependencies: - '@jest/console': 30.2.0 - '@jest/environment': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@jest/console': 30.3.0 + '@jest/environment': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 graceful-fs: 4.2.11 jest-docblock: 30.2.0 - jest-environment-node: 30.2.0 - jest-haste-map: 30.2.0 - jest-leak-detector: 30.2.0 - jest-message-util: 30.2.0 - jest-resolve: 30.2.0 - jest-runtime: 30.2.0 - jest-util: 30.2.0 - jest-watcher: 30.2.0 - jest-worker: 30.2.0 + jest-environment-node: 30.3.0 + jest-haste-map: 30.3.0 + jest-leak-detector: 30.3.0 + jest-message-util: 30.3.0 + jest-resolve: 30.3.0 + jest-runtime: 30.3.0 + jest-util: 30.3.0 + jest-watcher: 30.3.0 + jest-worker: 30.3.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color - jest-runtime@30.2.0: + jest-runtime@30.3.0: dependencies: - '@jest/environment': 30.2.0 - '@jest/fake-timers': 30.2.0 - '@jest/globals': 30.2.0 + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/globals': 30.3.0 '@jest/source-map': 30.0.1 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 chalk: 4.1.2 cjs-module-lexer: 2.1.1 collect-v8-coverage: 1.0.3 glob: 10.5.0 graceful-fs: 4.2.11 - jest-haste-map: 30.2.0 - jest-message-util: 30.2.0 - jest-mock: 30.2.0 + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-snapshot: 30.2.0 - jest-util: 30.2.0 + jest-resolve: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@30.2.0: + jest-snapshot@30.3.0: dependencies: '@babel/core': 7.28.5 '@babel/generator': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) '@babel/types': 7.28.5 - '@jest/expect-utils': 30.2.0 + '@jest/expect-utils': 30.3.0 '@jest/get-type': 30.1.0 - '@jest/snapshot-utils': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 + '@jest/snapshot-utils': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) chalk: 4.1.2 - expect: 30.2.0 + expect: 30.3.0 graceful-fs: 4.2.11 - jest-diff: 30.2.0 - jest-matcher-utils: 30.2.0 - jest-message-util: 30.2.0 - jest-util: 30.2.0 - pretty-format: 30.2.0 + jest-diff: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + pretty-format: 30.3.0 semver: 7.7.3 synckit: 0.11.11 transitivePeerDependencies: @@ -19550,7 +20514,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 25.0.3 + '@types/node': 25.5.2 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -19559,59 +20523,68 @@ snapshots: jest-util@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@types/node': 25.5.2 + chalk: 4.1.2 + ci-info: 4.3.1 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + + jest-util@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 25.5.2 chalk: 4.1.2 ci-info: 4.3.1 graceful-fs: 4.2.11 picomatch: 4.0.3 - jest-validate@30.2.0: + jest-validate@30.3.0: dependencies: '@jest/get-type': 30.1.0 - '@jest/types': 30.2.0 + '@jest/types': 30.3.0 camelcase: 6.3.0 chalk: 4.1.2 leven: 3.1.0 - pretty-format: 30.2.0 + pretty-format: 30.3.0 - jest-watcher@30.2.0: + jest-watcher@30.3.0: dependencies: - '@jest/test-result': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 25.0.3 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.5.2 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 30.2.0 + jest-util: 30.3.0 string-length: 4.0.2 jest-worker@27.5.1: dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest-worker@30.2.0: + jest-worker@30.3.0: dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@ungap/structured-clone': 1.3.0 - jest-util: 30.2.0 + jest-util: 30.3.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)): + jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)): dependencies: - '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) - '@jest/types': 30.2.0 + '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + '@jest/types': 30.3.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@25.0.3)(ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3)) + jest-cli: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -19623,8 +20596,6 @@ snapshots: jiti@2.6.1: {} - jmespath@0.16.0: {} - joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -19685,10 +20656,6 @@ snapshots: jsesc@3.1.0: {} - json-bigint@1.0.0: - dependencies: - bignumber.js: 9.3.1 - json-buffer@3.0.1: {} json-crawl@0.5.3: {} @@ -19715,6 +20682,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: {} @@ -19735,8 +20704,6 @@ snapshots: jsonc-parser@2.2.1: {} - jsonc-parser@3.3.1: {} - jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -19745,8 +20712,6 @@ snapshots: jsonify@0.0.1: {} - jsonparse@1.3.1: {} - jsonpath-plus@10.3.0: dependencies: '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) @@ -19762,17 +20727,6 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 - keygrip@1.1.0: dependencies: tsscmp: 1.0.6 @@ -19789,6 +20743,21 @@ snapshots: koa-compose@4.1.0: {} + koa-send@5.0.1: + dependencies: + debug: 4.4.3 + http-errors: 1.8.1 + resolve-path: 1.4.0 + transitivePeerDependencies: + - supports-color + + koa-static@5.0.0: + dependencies: + debug: 3.2.7 + koa-send: 5.0.1 + transitivePeerDependencies: + - supports-color + koa@3.1.1: dependencies: accepts: 1.3.8 @@ -19840,14 +20809,13 @@ snapshots: lines-and-columns@1.2.4: {} - lint-staged@16.2.7: + lint-staged@16.4.0: dependencies: - commander: 14.0.2 + commander: 14.0.3 listr2: 9.0.5 - micromatch: 4.0.8 - nano-spawn: 2.0.0 - pidtree: 0.6.0 + picomatch: 4.0.3 string-argv: 0.3.2 + tinyexec: 1.1.1 yaml: 2.8.2 liquid-json@0.3.1: {} @@ -19889,8 +20857,6 @@ snapshots: lodash.debounce@4.0.8: {} - lodash.isplainobject@4.0.6: {} - lodash.kebabcase@4.1.1: {} lodash.memoize@4.1.2: {} @@ -19911,11 +20877,6 @@ snapshots: lodash@4.17.21: {} - log-symbols@6.0.0: - dependencies: - chalk: 5.6.2 - is-unicode-supported: 1.3.0 - log-update@6.1.0: dependencies: ansi-escapes: 7.2.0 @@ -20307,8 +21268,6 @@ snapshots: tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 - meow@12.1.1: {} - meow@13.2.0: {} merge-descriptors@1.0.3: {} @@ -20824,8 +21783,6 @@ snapshots: mime@2.6.0: {} - mime@3.0.0: {} - mimic-fn@2.1.0: {} mimic-function@5.0.1: {} @@ -20848,10 +21805,18 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.0 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.2 @@ -20902,7 +21867,7 @@ snapshots: dependencies: minimist: 1.2.8 - mkdirp@1.0.4: {} + mkdirp@3.0.1: {} mlly@1.8.0: dependencies: @@ -20942,7 +21907,7 @@ snapshots: mustache@4.2.0: {} - mute-stream@2.0.0: {} + mute-stream@3.0.0: {} mz@2.7.0: dependencies: @@ -20953,8 +21918,6 @@ snapshots: nan@2.24.0: optional: true - nano-spawn@2.0.0: {} - nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -21020,7 +21983,7 @@ snapshots: make-fetch-happen: 15.0.3 nopt: 9.0.0 proc-log: 6.1.0 - semver: 7.7.3 + semver: 7.7.4 tar: 7.5.2 tinyglobby: 0.2.15 which: 6.0.0 @@ -21044,16 +22007,10 @@ snapshots: dependencies: abbrev: 4.0.0 - normalize-package-data@6.0.2: - dependencies: - hosted-git-info: 7.0.2 - semver: 7.7.3 - validate-npm-package-license: 3.0.4 - normalize-package-data@7.0.1: dependencies: hosted-git-info: 8.1.0 - semver: 7.7.3 + semver: 7.7.4 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -21062,22 +22019,15 @@ snapshots: npm-install-checks@8.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 npm-normalize-package-bin@5.0.0: {} - npm-package-arg@12.0.2: - dependencies: - hosted-git-info: 8.1.0 - proc-log: 5.0.0 - semver: 7.7.3 - validate-npm-package-name: 6.0.2 - npm-package-arg@13.0.2: dependencies: hosted-git-info: 9.0.2 proc-log: 6.1.0 - semver: 7.7.3 + semver: 7.7.4 validate-npm-package-name: 7.0.0 npm-pick-manifest@11.0.3: @@ -21085,7 +22035,7 @@ snapshots: npm-install-checks: 8.0.0 npm-normalize-package-bin: 5.0.0 npm-package-arg: 13.0.2 - semver: 7.7.3 + semver: 7.7.4 npm-run-path@4.0.1: dependencies: @@ -21264,18 +22214,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@8.2.0: - dependencies: - chalk: 5.6.2 - cli-cursor: 5.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 2.1.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.2.0 - strip-ansi: 7.1.2 - own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -21298,7 +22236,7 @@ snapshots: dependencies: yocto-queue: 1.2.2 - p-limit@7.2.0: + p-limit@7.3.0: dependencies: yocto-queue: 1.2.2 @@ -21320,20 +22258,16 @@ snapshots: p-map@7.0.4: {} - p-pipe@4.0.0: {} - p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 p-timeout: 3.2.0 - p-queue@9.0.1: + p-queue@9.1.2: dependencies: eventemitter3: 5.0.1 p-timeout: 7.0.1 - p-reduce@3.0.0: {} - p-retry@6.2.1: dependencies: '@types/retry': 0.12.2 @@ -21389,12 +22323,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse-json@8.3.0: - dependencies: - '@babel/code-frame': 7.27.1 - index-to-position: 1.2.0 - type-fest: 4.41.0 - parse-ms@4.0.0: {} parse-numeric-range@1.3.0: {} @@ -21464,8 +22392,6 @@ snapshots: path-type@4.0.0: {} - path-type@6.0.0: {} - path@0.12.7: dependencies: process: 0.11.10 @@ -21473,18 +22399,18 @@ snapshots: pathe@2.0.3: {} - pg-cloudflare@1.2.7: + pg-cloudflare@1.3.0: optional: true - pg-connection-string@2.9.1: {} + pg-connection-string@2.12.0: {} pg-int8@1.0.1: {} - pg-pool@3.10.1(pg@8.16.3): + pg-pool@3.13.0(pg@8.20.0): dependencies: - pg: 8.16.3 + pg: 8.20.0 - pg-protocol@1.10.3: {} + pg-protocol@1.13.0: {} pg-types@2.2.0: dependencies: @@ -21494,15 +22420,15 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.16.3: + pg@8.20.0: dependencies: - pg-connection-string: 2.9.1 - pg-pool: 3.10.1(pg@8.16.3) - pg-protocol: 1.10.3 + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.2.7 + pg-cloudflare: 1.3.0 pgpass@1.0.5: dependencies: @@ -21516,10 +22442,6 @@ snapshots: picomatch@4.0.3: {} - pidtree@0.6.0: {} - - pify@6.1.0: {} - pirates@4.0.7: {} pkce-challenge@5.0.1: {} @@ -21686,18 +22608,18 @@ snapshots: '@csstools/utilities': 2.0.0(postcss@8.5.6) postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 - postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.104.1): + postcss-loader@7.3.4(postcss@8.5.6)(typescript@6.0.2)(webpack@5.104.1): dependencies: - cosmiconfig: 8.3.6(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@6.0.2) jiti: 1.21.7 postcss: 8.5.6 semver: 7.7.3 @@ -22033,7 +22955,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 @@ -22049,7 +22971,7 @@ snapshots: sort-object-keys: 1.1.3 sort-order: 1.1.2 - prettier@3.7.4: {} + prettier@3.8.1: {} pretty-error@4.0.0: dependencies: @@ -22068,6 +22990,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -22084,8 +23012,6 @@ snapshots: prismjs@1.30.0: {} - proc-log@5.0.0: {} - proc-log@6.1.0: {} process-nextick-args@2.0.1: {} @@ -22118,9 +23044,12 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 - properties-reader@2.3.0: + properties-reader@3.0.1: dependencies: - mkdirp: 1.0.4 + '@kwsites/file-exists': 1.1.1 + mkdirp: 3.0.1 + transitivePeerDependencies: + - supports-color property-information@6.5.0: {} @@ -22140,7 +23069,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 25.0.3 + '@types/node': 25.5.2 long: 5.3.2 protocols@2.0.2: {} @@ -22155,8 +23084,6 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 - punycode@1.3.2: {} - punycode@1.4.1: {} punycode@2.3.1: {} @@ -22165,8 +23092,6 @@ snapshots: dependencies: escape-goat: 4.0.0 - pure-rand@6.1.0: {} - pure-rand@7.0.1: {} qs@6.14.0: @@ -22177,8 +23102,6 @@ snapshots: dependencies: side-channel: 1.1.0 - querystring@0.2.0: {} - queue-microtask@1.2.3: {} quick-lru@5.1.1: {} @@ -22326,19 +23249,6 @@ snapshots: react@19.2.3: {} - read-pkg@9.0.1: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 6.0.2 - parse-json: 8.3.0 - type-fest: 4.41.0 - unicorn-magic: 0.1.0 - - read-yaml-file@2.1.0: - dependencies: - js-yaml: 4.1.1 - strip-bom: 4.0.0 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -22352,7 +23262,7 @@ snapshots: readable-stream@3.6.2: dependencies: inherits: 2.0.4 - string_decoder: 1.1.1 + string_decoder: 1.3.0 util-deprecate: 1.0.2 readable-stream@4.7.0: @@ -22640,6 +23550,11 @@ snapshots: resolve-from@5.0.0: {} + resolve-path@1.4.0: + dependencies: + http-errors: 1.6.3 + path-is-absolute: 1.0.1 + resolve-pathname@3.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -22667,15 +23582,6 @@ snapshots: retry-as-promised@7.1.1: {} - retry-request@7.0.2(encoding@0.1.13): - dependencies: - '@types/request': 2.48.13 - extend: 3.0.2 - teeny-request: 9.0.0(encoding@0.1.13) - transitivePeerDependencies: - - encoding - - supports-color - retry@0.12.0: {} retry@0.13.1: {} @@ -22787,8 +23693,6 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.1 - sax@1.2.1: {} - sax@1.4.3: {} saxes@6.0.0: @@ -22836,6 +23740,8 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + send@0.19.2: dependencies: debug: 2.6.9 @@ -22877,15 +23783,15 @@ snapshots: sequelize-pool@7.1.0: {} - sequelize-typescript@2.1.6(@types/node@25.0.3)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.7(pg@8.16.3)): + sequelize-typescript@2.1.6(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.7(pg@8.20.0)): dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 '@types/validator': 13.15.10 glob: 7.2.0 reflect-metadata: 0.2.2 - sequelize: 6.37.7(pg@8.16.3) + sequelize: 6.37.7(pg@8.20.0) - sequelize@6.37.7(pg@8.16.3): + sequelize@6.37.7(pg@8.20.0): dependencies: '@types/debug': 4.1.12 '@types/validator': 13.15.10 @@ -22895,16 +23801,16 @@ snapshots: lodash: 4.17.21 moment: 2.30.1 moment-timezone: 0.5.48 - pg-connection-string: 2.9.1 + pg-connection-string: 2.12.0 retry-as-promised: 7.1.1 - semver: 7.7.3 + semver: 7.7.4 sequelize-pool: 7.1.0 toposort-class: 1.0.1 uuid: 8.3.2 validator: 13.15.26 wkx: 0.5.0 optionalDependencies: - pg: 8.16.3 + pg: 8.20.0 transitivePeerDependencies: - supports-color @@ -22952,8 +23858,6 @@ snapshots: transitivePeerDependencies: - supports-color - set-blocking@2.0.0: {} - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -23087,8 +23991,6 @@ snapshots: slash@4.0.0: {} - slash@5.1.0: {} - slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -23124,10 +24026,6 @@ snapshots: sort-css-media-queries@2.2.0: {} - sort-keys@5.1.0: - dependencies: - is-plain-obj: 4.1.0 - sort-keys@6.0.0: dependencies: is-plain-obj: 4.1.0 @@ -23235,19 +24133,11 @@ snapshots: std-env@3.10.0: {} - stdin-discarder@0.2.2: {} - stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 - stream-events@1.0.5: - dependencies: - stubs: 3.0.0 - - stream-shift@1.0.3: {} - streamsearch@1.1.0: {} streamx@2.23.0: @@ -23386,10 +24276,6 @@ snapshots: strip-json-comments@3.1.1: {} - strnum@1.1.2: {} - - stubs@3.0.0: {} - style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -23492,27 +24378,44 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - syncpack@13.0.4(typescript@5.9.3): + synckit@0.11.12: dependencies: - chalk: 5.6.2 - chalk-template: 1.1.2 - commander: 13.1.0 - cosmiconfig: 9.0.0(typescript@5.9.3) - effect: 3.19.12 - enquirer: 2.4.1 - fast-check: 3.23.2 - globby: 14.1.0 - jsonc-parser: 3.3.1 - minimatch: 9.0.5 - npm-package-arg: 12.0.2 - ora: 8.2.0 - prompts: 2.4.2 - read-yaml-file: 2.1.0 - semver: 7.7.3 - tightrope: 0.2.0 - ts-toolbelt: 9.6.0 - transitivePeerDependencies: - - typescript + '@pkgr/core': 0.2.9 + + syncpack-darwin-arm64@14.3.0: + optional: true + + syncpack-darwin-x64@14.3.0: + optional: true + + syncpack-linux-arm64-musl@14.3.0: + optional: true + + syncpack-linux-arm64@14.3.0: + optional: true + + syncpack-linux-x64-musl@14.3.0: + optional: true + + syncpack-linux-x64@14.3.0: + optional: true + + syncpack-windows-arm64@14.3.0: + optional: true + + syncpack-windows-x64@14.3.0: + optional: true + + syncpack@14.3.0: + optionalDependencies: + syncpack-darwin-arm64: 14.3.0 + syncpack-darwin-x64: 14.3.0 + syncpack-linux-arm64: 14.3.0 + syncpack-linux-arm64-musl: 14.3.0 + syncpack-linux-x64: 14.3.0 + syncpack-linux-x64-musl: 14.3.0 + syncpack-windows-arm64: 14.3.0 + syncpack-windows-x64: 14.3.0 tapable@2.3.0: {} @@ -23523,7 +24426,7 @@ snapshots: pump: 3.0.3 tar-stream: 2.2.0 - tar-fs@3.1.1: + tar-fs@3.1.2: dependencies: pump: 3.0.3 tar-stream: 3.1.7 @@ -23560,17 +24463,6 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - teeny-request@9.0.0(encoding@0.1.13): - dependencies: - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - node-fetch: 2.7.0(encoding@0.1.13) - stream-events: 1.0.5 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - terser-webpack-plugin@5.3.16(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -23593,23 +24485,23 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - testcontainers@11.11.0: + testcontainers@11.13.0: dependencies: '@balena/dockerignore': 1.0.2 - '@types/dockerode': 3.3.47 + '@types/dockerode': 4.0.1 archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 debug: 4.4.3 - docker-compose: 1.3.0 + docker-compose: 1.4.2 dockerode: 4.0.9 get-port: 7.1.0 proper-lockfile: 4.1.2 - properties-reader: 2.3.0 + properties-reader: 3.0.1 ssh-remote-port-forward: 1.0.4 - tar-fs: 3.1.1 + tar-fs: 3.1.2 tmp: 0.2.5 - undici: 7.16.0 + undici: 7.24.7 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -23622,8 +24514,6 @@ snapshots: transitivePeerDependencies: - react-native-b4a - text-extensions@2.4.0: {} - text-table@0.2.0: {} thenify-all@1.6.0: @@ -23640,12 +24530,8 @@ snapshots: throttleit@2.1.0: {} - through@2.3.8: {} - thunky@1.1.0: {} - tightrope@0.2.0: {} - tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -23654,6 +24540,8 @@ snapshots: tinyexec@1.0.2: {} + tinyexec@1.1.1: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -23661,8 +24549,6 @@ snapshots: tinypool@1.1.1: {} - tinyrainbow@3.0.3: {} - tldts-core@6.1.86: {} tldts@6.1.86: @@ -23703,33 +24589,35 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.1.0(typescript@5.9.3): + ts-api-utils@2.1.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 + + ts-api-utils@2.5.0(typescript@6.0.2): + dependencies: + typescript: 6.0.2 ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@25.0.3)(typescript@5.9.3): + ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 25.0.3 + '@types/node': 25.5.2 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.9.3 + typescript: 6.0.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true - ts-toolbelt@9.6.0: {} - tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -23743,7 +24631,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.1) cac: 6.7.14 @@ -23754,7 +24642,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.53.3 source-map: 0.7.6 @@ -23764,7 +24652,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.6 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - jiti - supports-color @@ -23778,32 +24666,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo-darwin-64@2.6.3: - optional: true - - turbo-darwin-arm64@2.6.3: - optional: true - - turbo-linux-64@2.6.3: - optional: true - - turbo-linux-arm64@2.6.3: - optional: true - - turbo-windows-64@2.6.3: - optional: true - - turbo-windows-arm64@2.6.3: - optional: true - - turbo@2.6.3: + turbo@2.9.4: optionalDependencies: - turbo-darwin-64: 2.6.3 - turbo-darwin-arm64: 2.6.3 - turbo-linux-64: 2.6.3 - turbo-linux-arm64: 2.6.3 - turbo-windows-64: 2.6.3 - turbo-windows-arm64: 2.6.3 + '@turbo/darwin-64': 2.9.4 + '@turbo/darwin-arm64': 2.9.4 + '@turbo/linux-64': 2.9.4 + '@turbo/linux-arm64': 2.9.4 + '@turbo/windows-64': 2.9.4 + '@turbo/windows-arm64': 2.9.4 tweetnacl@0.14.5: {} @@ -23819,8 +24689,6 @@ snapshots: type-fest@2.19.0: {} - type-fest@4.41.0: {} - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -23871,18 +24739,18 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} + typescript@6.0.2: {} ua-parser-js@1.0.41: {} @@ -23902,18 +24770,14 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.16.0: {} + undici-types@7.18.2: {} - undici@7.16.0: {} + undici@7.24.7: {} unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-emoji-modifier-base@1.0.0: {} - unicode-emoji-utils@1.3.1: - dependencies: - emoji-regex-xs: 2.0.1 - unicode-match-property-ecmascript@2.0.0: dependencies: unicode-canonical-property-names-ecmascript: 2.0.1 @@ -23923,8 +24787,6 @@ snapshots: unicode-property-aliases-ecmascript@2.2.0: {} - unicorn-magic@0.1.0: {} - unicorn-magic@0.3.0: {} unified@10.1.2: @@ -24079,11 +24941,6 @@ snapshots: optionalDependencies: file-loader: 6.2.0(webpack@5.104.1) - url@0.10.3: - dependencies: - punycode: 1.3.2 - querystring: 0.2.0 - url@0.11.4: dependencies: punycode: 1.4.1 @@ -24103,14 +24960,6 @@ snapshots: dependencies: inherits: 2.0.3 - util@0.12.5: - dependencies: - inherits: 2.0.4 - is-arguments: 1.2.0 - is-generator-function: 1.1.2 - is-typed-array: 1.1.15 - which-typed-array: 1.1.19 - utila@0.4.0: {} utility-types@3.11.0: {} @@ -24119,14 +24968,8 @@ snapshots: uuid@10.0.0: {} - uuid@13.0.0: {} - - uuid@8.0.0: {} - uuid@8.3.2: {} - uuid@9.0.1: {} - uvu@0.5.6: dependencies: dequal: 2.0.3 @@ -24152,8 +24995,6 @@ snapshots: dependencies: builtins: 1.0.3 - validate-npm-package-name@6.0.2: {} - validate-npm-package-name@7.0.0: {} validate.io-array@1.0.6: {} @@ -24442,10 +25283,6 @@ snapshots: dependencies: isexe: 3.1.1 - wide-align@1.1.5: - dependencies: - string-width: 4.2.3 - widest-line@4.0.1: dependencies: string-width: 5.1.2 @@ -24454,18 +25291,12 @@ snapshots: wkx@0.5.0: dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.2 word-wrap@1.2.5: {} wordwrap@1.0.0: {} - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -24503,18 +25334,10 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - write-file-atomic@7.0.0: + write-file-atomic@7.0.1: dependencies: - imurmurhash: 0.1.4 signal-exit: 4.1.0 - write-json-file@6.0.0: - dependencies: - detect-indent: 7.0.2 - is-plain-obj: 4.1.0 - sort-keys: 5.1.0 - write-file-atomic: 5.0.1 - write-json-file@7.0.0: dependencies: detect-indent: 7.0.2 @@ -24522,14 +25345,6 @@ snapshots: sort-keys: 6.0.0 write-file-atomic: 6.0.0 - write-package@7.2.0: - dependencies: - deepmerge-ts: 7.1.5 - read-pkg: 9.0.1 - sort-keys: 5.1.0 - type-fest: 4.41.0 - write-json-file: 6.0.0 - ws@7.5.10: {} ws@8.18.3: {} @@ -24552,13 +25367,6 @@ snapshots: xml-parser-xo@3.2.0: {} - xml2js@0.6.2: - dependencies: - sax: 1.2.1 - xmlbuilder: 11.0.1 - - xmlbuilder@11.0.1: {} - xmlchars@2.2.0: {} xtend@4.0.2: {} @@ -24577,6 +25385,8 @@ snapshots: yaml@2.8.2: {} + yaml@2.8.3: {} + yargs-parser@21.1.1: {} yargs-parser@22.0.0: {} @@ -24607,8 +25417,6 @@ snapshots: yocto-queue@1.2.2: {} - yoctocolors-cjs@2.1.3: {} - yoctocolors@2.1.2: {} zeptomatch@2.1.0: @@ -24622,7 +25430,7 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-to-json-schema@3.25.0(zod@3.25.76): + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 From 62dd89ece3a906472b9a93677e14d5e00d488f80 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Wed, 8 Apr 2026 06:48:56 -0300 Subject: [PATCH 02/27] feat: auth middleware first implementation --- .../instructions/postgresdb.instructions.md | 16 + .github/instructions/server.instructions.md | 19 +- packages/postgresdb/package.json | 3 +- packages/postgresdb/src/index.ts | 1 + packages/postgresdb/src/models/ApiKey.ts | 97 +++++ packages/postgresdb/src/models/Document.ts | 41 -- packages/postgresdb/src/models/File.ts | 21 +- packages/postgresdb/src/models/Project.ts | 34 ++ .../postgresdb/src/models/ProjectPolicy.ts | 67 ++++ packages/postgresdb/src/models/User.ts | 48 +++ packages/postgresdb/src/models/UserProject.ts | 57 +++ packages/postgresdb/src/models/index.ts | 6 +- packages/postgresdb/src/utils/publicId.ts | 52 +++ packages/server/package.json | 10 +- packages/server/src/Context.ts | 9 + packages/server/src/app.ts | 2 + packages/server/src/lib/files.ts | 57 +-- packages/server/src/lib/permissions.ts | 71 ++++ packages/server/src/lib/projects.ts | 65 +++ packages/server/src/lib/users.ts | 104 +++++ packages/server/src/mcp/server.ts | 2 +- .../src/mcp/{tools.ts => tools/files.ts} | 0 packages/server/src/mcp/tools/index.ts | 9 + packages/server/src/middleware/auth.ts | 110 +++++ .../server/src/rest/openapi/v1/users.yaml | 181 +++++++++ packages/server/src/rest/v1/index.ts | 4 + packages/server/src/rest/v1/projects.ts | 69 ++++ packages/server/src/rest/v1/users.ts | 375 ++++++++++++++++++ packages/server/tests/unit/jest.config.ts | 2 +- packages/server/tests/unit/testClient.ts | 14 + .../server/tests/unit/tests/files.test.ts | 82 ---- .../server/tests/unit/tests/projects.test.ts | 25 ++ .../server/tests/unit/tests/users.test.ts | 248 ++++++++++++ packages/website/docs/modules/iam.md | 127 ++++++ pnpm-lock.yaml | 183 +++++++-- turbo.json | 2 +- 36 files changed, 2007 insertions(+), 206 deletions(-) create mode 100644 packages/postgresdb/src/models/ApiKey.ts delete mode 100644 packages/postgresdb/src/models/Document.ts create mode 100644 packages/postgresdb/src/models/Project.ts create mode 100644 packages/postgresdb/src/models/ProjectPolicy.ts create mode 100644 packages/postgresdb/src/models/User.ts create mode 100644 packages/postgresdb/src/models/UserProject.ts create mode 100644 packages/postgresdb/src/utils/publicId.ts create mode 100644 packages/server/src/lib/permissions.ts create mode 100644 packages/server/src/lib/projects.ts create mode 100644 packages/server/src/lib/users.ts rename packages/server/src/mcp/{tools.ts => tools/files.ts} (100%) create mode 100644 packages/server/src/mcp/tools/index.ts create mode 100644 packages/server/src/middleware/auth.ts create mode 100644 packages/server/src/rest/openapi/v1/users.yaml create mode 100644 packages/server/src/rest/v1/projects.ts create mode 100644 packages/server/src/rest/v1/users.ts delete mode 100644 packages/server/tests/unit/tests/files.test.ts create mode 100644 packages/server/tests/unit/tests/projects.test.ts create mode 100644 packages/server/tests/unit/tests/users.test.ts create mode 100644 packages/website/docs/modules/iam.md diff --git a/.github/instructions/postgresdb.instructions.md b/.github/instructions/postgresdb.instructions.md index 5b29b7ab..c4f04a1c 100644 --- a/.github/instructions/postgresdb.instructions.md +++ b/.github/instructions/postgresdb.instructions.md @@ -8,3 +8,19 @@ description: Instructions for the PostgresDB package usage and integration. Check `#fetch https://ttoss.dev/docs/modules/packages/postgresdb/` for the official documentation of the `@ttoss/postgresdb` package used in this module. If you modify the database schema, ensure to make the tests pass by running `pnpm test` in the `packages/postgresdb`. + +## Public ID + +All models must have a `publicId` column (see `src/utils/publicId.ts`). The `publicId` is the only identifier exposed to external consumers. The internal `id` (UUID primary key) is for database-level joins only and must never be returned through any API or tool. + +When adding a new model, register a corresponding prefix in `src/utils/publicId.ts` (e.g., `user: 'usr_'`) and use it in the model's `beforeValidate` hook via `generatePublicId`. + +## Rebuilding After Model Changes + +After adding or modifying a model, rebuild the package so dependents (e.g., `@soat/server`) pick up the updated types: + +```bash +pnpm --filter @soat/postgresdb build +``` + +Without this step, TypeScript in the server package will report errors like `Property 'User' does not exist on type`. diff --git a/.github/instructions/server.instructions.md b/.github/instructions/server.instructions.md index d4811b38..d34b50dd 100644 --- a/.github/instructions/server.instructions.md +++ b/.github/instructions/server.instructions.md @@ -25,18 +25,25 @@ Route handlers **must not** contain direct database calls. They are responsible Lib functions **must always return plain mapped objects**, never raw model instances. This is required to avoid exposing sensitive or internal data (e.g., internal DB fields, hashed passwords, audit columns) through the API. Every function that queries the database must map the result to a plain object before returning it: +### Public ID as `id` + +The internal database `id` (primary key) **must never be returned to the user**. Always expose `publicId` as `id` in API responses: + ```ts export const getFile = async (args: { id: string }) => { - const file = await db.File.findByPk(args.id); + const file = await db.File.findOne({ where: { publicId: args.id } }); if (!file) return null; return { - id: file.id, + id: file.publicId, // publicId is exposed as `id` filename: file.filename, - // ... all fields explicitly mapped + // ... all fields explicitly mapped, internal `id` is never included }; }; ``` +- Route parameters and query inputs that reference a resource by ID will always be `publicId` values. +- The database `id` column is for internal joins only and must not appear in any API response, OpenAPI schema, or MCP tool output. + ### REST API Structure The REST API is organized by version and resource for better maintainability and versioning: @@ -175,3 +182,9 @@ Unit tests are located in the #file:../../packages/server/tests/unit/ folder. To ```bash pnpm --filter @soat/server test ``` + +To run tests for a specific file, use the `--testPathPatterns` flag: + +```bash +pnpm --filter @soat/server test --testPathPatterns=users.test.ts +``` diff --git a/packages/postgresdb/package.json b/packages/postgresdb/package.json index 6bf7142f..11e11e99 100644 --- a/packages/postgresdb/package.json +++ b/packages/postgresdb/package.json @@ -19,7 +19,8 @@ "db-dev:logs": "docker compose -f docker-compose.dev.yml logs -f" }, "dependencies": { - "@ttoss/postgresdb": "^0.8.0" + "@ttoss/postgresdb": "^0.8.0", + "nanoid": "^5.1.7" }, "devDependencies": { "@testcontainers/postgresql": "^11.13.0", diff --git a/packages/postgresdb/src/index.ts b/packages/postgresdb/src/index.ts index 27ddcb30..b8fea7f7 100644 --- a/packages/postgresdb/src/index.ts +++ b/packages/postgresdb/src/index.ts @@ -1 +1,2 @@ export * as models from './models'; +export * from './utils/publicId'; diff --git a/packages/postgresdb/src/models/ApiKey.ts b/packages/postgresdb/src/models/ApiKey.ts new file mode 100644 index 00000000..53f29bdd --- /dev/null +++ b/packages/postgresdb/src/models/ApiKey.ts @@ -0,0 +1,97 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; +import { Project } from './Project'; +import { ProjectPolicy } from './ProjectPolicy'; +import { User } from './User'; + +@Table({ + tableName: 'api_keys', + hooks: { + beforeValidate: (instance: ApiKey) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.apiKey); + } + }, + }, +}) +export class ApiKey extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @ForeignKey(() => { + return User; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare userId: number; + + @BelongsTo(() => { + return User; + }) + declare user: User; + + @ForeignKey(() => { + return Project; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare projectId: number; + + @BelongsTo(() => { + return Project; + }) + declare project: Project; + + @ForeignKey(() => { + return ProjectPolicy; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare policyId: number; + + @BelongsTo(() => { + return ProjectPolicy; + }) + declare policy: ProjectPolicy; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare name: string; + + /** + * First 8 characters of the raw API key. Stored in plaintext to allow + * fast DB lookup before running bcrypt.compare against keyHash. + */ + @Column({ + type: DataType.STRING(8), + allowNull: false, + }) + declare keyPrefix: string; + + /** + * Bcrypt hash of the raw API key value (soat_sk_). + * The raw key is shown once at creation and never stored in plaintext. + */ + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare keyHash: string; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/Document.ts b/packages/postgresdb/src/models/Document.ts deleted file mode 100644 index 0434e697..00000000 --- a/packages/postgresdb/src/models/Document.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Column, DataType, Model, Table } from '@ttoss/postgresdb'; - -@Table({ tableName: 'documents' }) -export class Document extends Model { - @Column({ - type: DataType.UUID, - primaryKey: true, - defaultValue: DataType.UUIDV4, - }) - declare id: string; - - @Column({ type: DataType.STRING }) - declare title?: string; - - @Column({ type: DataType.UUID }) - declare fileId: string; - - @Column({ type: DataType.STRING }) - declare embeddingModel?: string; - - @Column({ type: DataType.STRING }) - declare embeddingProvider?: string; - - @Column({ type: DataType.VECTOR(1536) }) - declare embedding?: number[]; - - @Column({ type: DataType.UUID }) - declare conversationId?: string; - - @Column({ type: DataType.INTEGER }) - declare position?: number; - - @Column({ type: DataType.TEXT }) - declare metadata?: string; // JSON string - - @Column({ type: DataType.DATE }) - declare createdAt: Date; - - @Column({ type: DataType.DATE }) - declare updatedAt: Date; -} diff --git a/packages/postgresdb/src/models/File.ts b/packages/postgresdb/src/models/File.ts index 94c40bb4..fd7fa1fe 100644 --- a/packages/postgresdb/src/models/File.ts +++ b/packages/postgresdb/src/models/File.ts @@ -1,13 +1,24 @@ import { Column, DataType, Model, Table } from '@ttoss/postgresdb'; -@Table({ tableName: 'files' }) +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; + +@Table({ + tableName: 'files', + hooks: { + beforeValidate: (instance: File) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.file); + } + }, + }, +}) export class File extends Model { @Column({ - type: DataType.UUID, - primaryKey: true, - defaultValue: DataType.UUIDV4, + type: DataType.STRING(32), + unique: true, + allowNull: false, }) - declare id: string; + declare publicId: string; @Column({ type: DataType.STRING }) declare filename?: string; diff --git a/packages/postgresdb/src/models/Project.ts b/packages/postgresdb/src/models/Project.ts new file mode 100644 index 00000000..5e6228e0 --- /dev/null +++ b/packages/postgresdb/src/models/Project.ts @@ -0,0 +1,34 @@ +import { Column, DataType, HasMany, Model, Table } from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; + +@Table({ + tableName: 'projects', + hooks: { + beforeValidate: (instance: Project) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.project); + } + }, + }, +}) +export class Project extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare name: string; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/ProjectPolicy.ts b/packages/postgresdb/src/models/ProjectPolicy.ts new file mode 100644 index 00000000..94b592c8 --- /dev/null +++ b/packages/postgresdb/src/models/ProjectPolicy.ts @@ -0,0 +1,67 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; +import { Project } from './Project'; + +@Table({ + tableName: 'project_policies', + hooks: { + beforeValidate: (instance: ProjectPolicy) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.policy); + } + }, + }, +}) +export class ProjectPolicy extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @ForeignKey(() => { + return Project; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare projectId: number; + + @BelongsTo(() => { + return Project; + }) + declare project: Project; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare name: string; + + @Column({ + type: DataType.ARRAY(DataType.TEXT), + allowNull: false, + defaultValue: [], + }) + declare permissions: string[]; + + @Column({ + type: DataType.ARRAY(DataType.TEXT), + allowNull: false, + defaultValue: [], + }) + declare notPermissions: string[]; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/User.ts b/packages/postgresdb/src/models/User.ts new file mode 100644 index 00000000..542ce11c --- /dev/null +++ b/packages/postgresdb/src/models/User.ts @@ -0,0 +1,48 @@ +import { Column, DataType, Model, Table } from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; + +@Table({ + tableName: 'users', + hooks: { + beforeValidate: (instance: User) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.user); + } + }, + }, +}) +export class User extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @Column({ + type: DataType.STRING, + unique: true, + allowNull: false, + }) + declare username: string; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare passwordHash: string; + + @Column({ + type: DataType.ENUM('admin', 'user'), + allowNull: false, + defaultValue: 'user', + }) + declare role: 'admin' | 'user'; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/UserProject.ts b/packages/postgresdb/src/models/UserProject.ts new file mode 100644 index 00000000..a24e33d0 --- /dev/null +++ b/packages/postgresdb/src/models/UserProject.ts @@ -0,0 +1,57 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { Project } from './Project'; +import { ProjectPolicy } from './ProjectPolicy'; +import { User } from './User'; + +@Table({ + tableName: 'user_projects', + indexes: [{ unique: true, fields: ['user_id', 'project_id'] }], +}) +export class UserProject extends Model { + @ForeignKey(() => { + return User; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare userId: number; + + @BelongsTo(() => { + return User; + }) + declare user: User; + + @ForeignKey(() => { + return Project; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare projectId: number; + + @BelongsTo(() => { + return Project; + }) + declare project: Project; + + @ForeignKey(() => { + return ProjectPolicy; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare policyId: number; + + @BelongsTo(() => { + return ProjectPolicy; + }) + declare policy: ProjectPolicy; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/index.ts b/packages/postgresdb/src/models/index.ts index 88da204a..da667cf2 100644 --- a/packages/postgresdb/src/models/index.ts +++ b/packages/postgresdb/src/models/index.ts @@ -1,2 +1,6 @@ -export { Document } from './Document'; +export { ApiKey } from './ApiKey'; export { File } from './File'; +export { Project } from './Project'; +export { ProjectPolicy } from './ProjectPolicy'; +export { User } from './User'; +export { UserProject } from './UserProject'; diff --git a/packages/postgresdb/src/utils/publicId.ts b/packages/postgresdb/src/utils/publicId.ts new file mode 100644 index 00000000..a9364969 --- /dev/null +++ b/packages/postgresdb/src/utils/publicId.ts @@ -0,0 +1,52 @@ +import { customAlphabet } from 'nanoid'; + +/** + * Public ID prefixes for each entity type (Stripe-style) + * They can have 2 to 6 characters before the underscore. + */ +export const PUBLIC_ID_PREFIXES = { + file: 'file_', + user: 'usr_', + project: 'proj_', + policy: 'pol_', + apiKey: 'key_', +} as const; + +/** + * Prefix for raw API key values (shown once at creation, then hashed). + * Format: sk_{random} — distinguishable from JWTs (which start with 'eyJ'). + */ +export const API_KEY_RAW_PREFIX = 'sk_'; + +export type PublicIdPrefix = + (typeof PUBLIC_ID_PREFIXES)[keyof typeof PUBLIC_ID_PREFIXES]; + +/** + * Generates a Stripe-style public ID with the given prefix. + * Format: {prefix}{16-character nanoid} + * Example: file_V1StGXR8Z5jdHi6B + * + * @param prefix - The prefix for the entity type (e.g., 'file_') + * @returns A unique public ID string + */ +export const generatePublicId = (prefix: PublicIdPrefix): string => { + const nanoid = customAlphabet( + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + 16 + ); + return `${prefix}${nanoid()}`; +}; + +/** + * Validates if a string matches a public ID format + * @param id - The ID to validate + * @param prefix - Expected prefix + * @returns true if valid + */ +export const isValidPublicId = ( + id: string, + prefix: PublicIdPrefix +): boolean => { + const pattern = new RegExp(`^${prefix}[A-Za-z0-9]{16}$`); + return pattern.test(id); +}; diff --git a/packages/server/package.json b/packages/server/package.json index 418fc604..87cccecd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -11,19 +11,23 @@ }, "dependencies": { "@soat/postgresdb": "workspace:*", - "@ttoss/http-server": "^0.5.8", - "@ttoss/http-server-mcp": "^0.11.0", + "@ttoss/http-server": "^0.5.9", + "@ttoss/http-server-mcp": "^0.11.1", "@ttoss/postgresdb": "^0.8.0", + "bcryptjs": "^3.0.3", "dotenv": "^17.4.1", + "jsonwebtoken": "^9.0.3", "ollama": "^0.6.3", "pg": "^8.20.0" }, "devDependencies": { - "@testcontainers/postgresql": "^11.13.0", "@stoplight/spectral-cli": "^6.15.0", + "@testcontainers/postgresql": "^11.13.0", "@ttoss/config": "^1.37.8", "@ttoss/test-utils": "^4.2.8", + "@types/bcryptjs": "^3.0.0", "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/pg": "^8.20.0", "@types/supertest": "^7.2.0", "jest": "^30.3.0", diff --git a/packages/server/src/Context.ts b/packages/server/src/Context.ts index 644d0b9b..e7e4e3fa 100644 --- a/packages/server/src/Context.ts +++ b/packages/server/src/Context.ts @@ -1,6 +1,15 @@ import type { DB } from './db'; +export type AuthUser = { + id: number; + publicId: string; + username: string; + role: 'admin' | 'user'; + isAllowed: (projectPublicId: string, action: string) => Promise; +}; + export type Context = { db: DB; + authUser?: AuthUser; // eslint-disable-next-line @typescript-eslint/no-explicit-any } & Record; diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index e4aae3f1..782187b0 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -1,12 +1,14 @@ import { App, bodyParser, cors } from '@ttoss/http-server'; import { mcpRouter } from './mcp/server'; +import { authMiddleware } from './middleware/auth'; import { restRouter } from './rest/router'; const app = new App(); app.use(cors()); app.use(bodyParser()); +app.use(authMiddleware); app.use(restRouter.routes()); app.use(restRouter.allowedMethods()); diff --git a/packages/server/src/lib/files.ts b/packages/server/src/lib/files.ts index 7b54f108..3121dd84 100644 --- a/packages/server/src/lib/files.ts +++ b/packages/server/src/lib/files.ts @@ -1,31 +1,8 @@ import { db } from '../db'; -export const listFiles = async () => { - const allFiles = await db.File.findAll(); - return allFiles.map((file) => { - return { - id: file.id, - filename: file.filename, - contentType: file.contentType, - size: file.size, - storageType: file.storageType, - storagePath: file.storagePath, - metadata: file.metadata, - createdAt: file.createdAt, - updatedAt: file.updatedAt, - }; - }); -}; - -export const getFile = async (args: { id: string }) => { - const file = await db.File.findByPk(args.id); - - if (!file) { - return null; - } - +const mapFile = (file: InstanceType<(typeof db)['File']>) => { return { - id: file.id, + id: file.publicId, filename: file.filename, contentType: file.contentType, size: file.size, @@ -37,6 +14,21 @@ export const getFile = async (args: { id: string }) => { }; }; +export const listFiles = async () => { + const allFiles = await db.File.findAll(); + return allFiles.map(mapFile); +}; + +export const getFile = async (args: { id: string }) => { + const file = await db.File.findOne({ where: { publicId: args.id } }); + + if (!file) { + return null; + } + + return mapFile(file); +}; + export const createFile = async (args: { filename?: string; contentType?: string; @@ -46,22 +38,11 @@ export const createFile = async (args: { metadata?: string; }) => { const file = await db.File.create(args); - - return { - id: file.id, - filename: file.filename, - contentType: file.contentType, - size: file.size, - storageType: file.storageType, - storagePath: file.storagePath, - metadata: file.metadata, - createdAt: file.createdAt, - updatedAt: file.updatedAt, - }; + return mapFile(file); }; export const deleteFile = async (args: { id: string }) => { - const file = await db.File.findByPk(args.id); + const file = await db.File.findOne({ where: { publicId: args.id } }); if (!file) { return null; diff --git a/packages/server/src/lib/permissions.ts b/packages/server/src/lib/permissions.ts new file mode 100644 index 00000000..9ec5eec4 --- /dev/null +++ b/packages/server/src/lib/permissions.ts @@ -0,0 +1,71 @@ +import type { DB } from '../db'; + +export type Policy = { + permissions: string[]; + notPermissions: string[]; +}; + +const matchesPattern = (pattern: string, action: string): boolean => { + if (pattern === '*') return true; + if (pattern === action) return true; + + if (pattern.endsWith(':*')) { + const prefix = pattern.slice(0, -1); // e.g. "files:" + return action.startsWith(prefix); + } + + return false; +}; + +export const policyAllows = (policy: Policy, action: string): boolean => { + for (const pattern of policy.notPermissions) { + if (matchesPattern(pattern, action)) return false; + } + + for (const pattern of policy.permissions) { + if (matchesPattern(pattern, action)) return true; + } + + return false; +}; + +export const createApiKeyIsAllowed = (args: { + projectPublicId: string; + policy: Policy; +}) => { + return async ( + reqProjectPublicId: string, + action: string + ): Promise => { + if (reqProjectPublicId !== args.projectPublicId) return false; + return policyAllows(args.policy, action); + }; +}; + +export const createJwtIsAllowed = (args: { + role: 'admin' | 'user'; + userId: number; + db: DB; +}) => { + return async (projectPublicId: string, action: string): Promise => { + if (args.role === 'admin') return true; + const project = await args.db.Project.findOne({ + where: { publicId: projectPublicId }, + }); + if (!project) return false; + const membership = await args.db.UserProject.findOne({ + where: { userId: args.userId, projectId: project.id as number }, + include: [{ model: args.db.ProjectPolicy }], + }); + if (!membership) return false; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const policy = (membership as any).policy; + return policyAllows( + { + permissions: policy.permissions as string[], + notPermissions: policy.notPermissions as string[], + }, + action + ); + }; +}; diff --git a/packages/server/src/lib/projects.ts b/packages/server/src/lib/projects.ts new file mode 100644 index 00000000..80fbff9a --- /dev/null +++ b/packages/server/src/lib/projects.ts @@ -0,0 +1,65 @@ +import type { AuthUser } from '../Context'; +import { db } from '../db'; + +const mapProject = (project: InstanceType<(typeof db)['Project']>) => { + return { + id: project.publicId, + name: project.name, + createdAt: project.createdAt, + updatedAt: project.updatedAt, + }; +}; + +export const listProjects = async (args: { authUser: AuthUser }) => { + if (args.authUser.role === 'admin') { + const projects = await db.Project.findAll(); + return projects.map(mapProject); + } + + const userProjects = await db.UserProject.findAll({ + where: { userId: args.authUser.id }, + include: [{ model: db.Project }], + }); + + return userProjects.map((up) => { + return mapProject(up.project as InstanceType<(typeof db)['Project']>); + }); +}; + +export const getProject = async (args: { id: string; authUser: AuthUser }) => { + const project = await db.Project.findOne({ where: { publicId: args.id } }); + + if (!project) { + return 'not_found' as const; + } + + if (args.authUser.role === 'admin') { + return mapProject(project); + } + + const membership = await db.UserProject.findOne({ + where: { userId: args.authUser.id, projectId: project.id }, + }); + + if (!membership) { + return 'forbidden' as const; + } + + return mapProject(project); +}; + +export const createProject = async (args: { name: string }) => { + const project = await db.Project.create({ name: args.name }); + return mapProject(project); +}; + +export const deleteProject = async (args: { id: string }) => { + const project = await db.Project.findOne({ where: { publicId: args.id } }); + + if (!project) { + return null; + } + + await project.destroy(); + return true; +}; diff --git a/packages/server/src/lib/users.ts b/packages/server/src/lib/users.ts new file mode 100644 index 00000000..0c7edbb8 --- /dev/null +++ b/packages/server/src/lib/users.ts @@ -0,0 +1,104 @@ +import { db } from '../db'; +import { + comparePassword, + hashPassword, + signUserToken, +} from '../middleware/auth'; + +const mapUser = (user: InstanceType<(typeof db)['User']>) => { + return { + id: user.publicId, + username: user.username, + role: user.role, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; +}; + +export const listUsers = async () => { + const allUsers = await db.User.findAll(); + return allUsers.map(mapUser); +}; + +export const getUser = async (args: { id: string }) => { + const user = await db.User.findOne({ where: { publicId: args.id } }); + + if (!user) { + return null; + } + + return mapUser(user); +}; + +export const loginUser = async (args: { + username: string; + password: string; +}) => { + const user = await db.User.findOne({ where: { username: args.username } }); + + if (!user) { + return null; + } + + const valid = await comparePassword( + args.password, + user.passwordHash as string + ); + + if (!valid) { + return null; + } + + const token = signUserToken({ + publicId: user.publicId as string, + role: user.role as string, + }); + + return { ...mapUser(user), token }; +}; + +export const createUser = async (args: { + username: string; + password: string; + role?: 'admin' | 'user'; +}) => { + const passwordHash = await hashPassword(args.password); + const user = await db.User.create({ + username: args.username, + passwordHash, + role: args.role ?? 'user', + }); + + return mapUser(user); +}; + +export const createFirstAdminUser = async (args: { + username: string; + password: string; +}) => { + const count = await db.User.count(); + + if (count > 0) { + return null; + } + + const passwordHash = await hashPassword(args.password); + const user = await db.User.create({ + username: args.username, + passwordHash, + role: 'admin', + }); + + return mapUser(user); +}; + +export const deleteUser = async (args: { id: string }) => { + const user = await db.User.findOne({ where: { publicId: args.id } }); + + if (!user) { + return false; + } + + await user.destroy(); + return true; +}; diff --git a/packages/server/src/mcp/server.ts b/packages/server/src/mcp/server.ts index 6183bcf0..6fd058af 100644 --- a/packages/server/src/mcp/server.ts +++ b/packages/server/src/mcp/server.ts @@ -1,6 +1,6 @@ import { createMcpRouter, McpServer } from '@ttoss/http-server-mcp'; -import { registerTools } from './tools'; +import { registerTools } from './tools/index'; const mcpServer = new McpServer({ name: 'soat', diff --git a/packages/server/src/mcp/tools.ts b/packages/server/src/mcp/tools/files.ts similarity index 100% rename from packages/server/src/mcp/tools.ts rename to packages/server/src/mcp/tools/files.ts diff --git a/packages/server/src/mcp/tools/index.ts b/packages/server/src/mcp/tools/index.ts new file mode 100644 index 00000000..e6aa2c34 --- /dev/null +++ b/packages/server/src/mcp/tools/index.ts @@ -0,0 +1,9 @@ +import type { McpServer } from '@ttoss/http-server-mcp'; + +import { registerTools as registerFileTools } from './files'; + +const registerTools = (server: McpServer) => { + registerFileTools(server); +}; + +export { registerTools }; diff --git a/packages/server/src/middleware/auth.ts b/packages/server/src/middleware/auth.ts new file mode 100644 index 00000000..eaf70e12 --- /dev/null +++ b/packages/server/src/middleware/auth.ts @@ -0,0 +1,110 @@ +import { API_KEY_RAW_PREFIX } from '@soat/postgresdb'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; + +import type { Context } from '../Context'; +import { createApiKeyIsAllowed, createJwtIsAllowed } from '../lib/permissions'; + +// eslint-disable-next-line turbo/no-undeclared-env-vars +export const JWT_SECRET = process.env.JWT_SECRET ?? 'dev-secret'; + +export const BCRYPT_SALT_ROUNDS = 12; + +export const hashPassword = (password: string) => { + return bcrypt.hash(password, BCRYPT_SALT_ROUNDS); +}; + +export const comparePassword = (password: string, hash: string) => { + return bcrypt.compare(password, hash); +}; + +export const signUserToken = (payload: { publicId: string; role: string }) => { + return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' }); +}; + +type Next = () => Promise; + +const resolveApiKey = async (ctx: Context, rawKey: string) => { + const keyPrefix = rawKey.substring(0, 8); + + const candidates = await ctx.db.ApiKey.findAll({ + where: { keyPrefix }, + include: [ + { model: ctx.db.Project }, + { model: ctx.db.ProjectPolicy }, + { model: ctx.db.User }, + ], + }); + + for (const row of candidates) { + const match = await bcrypt.compare(rawKey, row.keyHash as string); + if (match) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const projectPublicId = (row as any).project?.publicId as string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const policyData = (row as any).policy; + const keyPolicy = { + permissions: policyData.permissions as string[], + notPermissions: policyData.notPermissions as string[], + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const keyUser = (row as any).user; + ctx.authUser = { + id: keyUser.id as number, + publicId: keyUser.publicId as string, + username: keyUser.username as string, + role: keyUser.role as 'admin' | 'user', + isAllowed: createApiKeyIsAllowed({ + projectPublicId, + policy: keyPolicy, + }), + }; + break; + } + } +}; + +const resolveJwt = async (ctx: Context, token: string) => { + let payload: { publicId: string; role: string }; + + try { + payload = jwt.verify(token, JWT_SECRET) as typeof payload; + } catch { + return; + } + + const user = await ctx.db.User.findOne({ + where: { publicId: payload.publicId }, + }); + + if (!user) { + return; + } + + const userId = user.id as number; + const role = user.role as 'admin' | 'user'; + + ctx.authUser = { + id: userId, + publicId: user.publicId as string, + username: user.username as string, + role, + isAllowed: createJwtIsAllowed({ role, userId, db: ctx.db }), + }; +}; + +export const authMiddleware = async (ctx: Context, next: Next) => { + const authHeader: string | undefined = ctx.headers?.authorization; + + if (authHeader?.startsWith('Bearer ')) { + const token = authHeader.slice(7); + + if (token.startsWith(API_KEY_RAW_PREFIX)) { + await resolveApiKey(ctx, token); + } else { + await resolveJwt(ctx, token); + } + } + + await next(); +}; diff --git a/packages/server/src/rest/openapi/v1/users.yaml b/packages/server/src/rest/openapi/v1/users.yaml new file mode 100644 index 00000000..c7870cc8 --- /dev/null +++ b/packages/server/src/rest/openapi/v1/users.yaml @@ -0,0 +1,181 @@ +openapi: 3.0.3 +info: + title: SOAT Users API + version: 1.0.0 + description: API for managing users (Users resource) + contact: + name: SOAT Team + url: https://github.com/ttoss/soat +servers: + - url: http://0.0.0.0:5047/api/v1 + description: Development server +paths: + /users: + get: + tags: + - Users + summary: List all users + description: Returns a list of all users + operationId: listUsers + responses: + '200': + description: List of users returned successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UserRecord' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Users + summary: Create a user + description: Creates a new user in the system + operationId: createUser + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - username + - password + properties: + username: + type: string + example: johndoe + password: + type: string + format: password + example: supersecret + role: + type: string + enum: [admin, user] + example: user + responses: + '201': + description: User created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UserRecord' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /users/{id}: + get: + tags: + - Users + summary: Get a user by ID + description: Returns the data of a specific user + operationId: getUser + parameters: + - name: id + in: path + required: true + description: User ID + schema: + type: string + example: usr_V1StGXR8Z5jdHi6B + responses: + '200': + description: User found + content: + application/json: + schema: + $ref: '#/components/schemas/UserRecord' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /users/bootstrap: + post: + tags: + - Users + summary: Create the first admin user + description: Creates the first admin user. Returns 409 if any user already exists. + operationId: bootstrapUser + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - username + - password + properties: + username: + type: string + example: admin + password: + type: string + format: password + example: supersecret + responses: + '201': + description: Admin user created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UserRecord' + '409': + description: Users already exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' +components: + schemas: + UserRecord: + type: object + properties: + id: + type: string + description: Public user ID (usr_ prefix) + example: usr_V1StGXR8Z5jdHi6B + username: + type: string + example: johndoe + role: + type: string + enum: [admin, user] + example: user + createdAt: + type: string + format: date-time + example: '2024-01-01T00:00:00.000Z' + updatedAt: + type: string + format: date-time + example: '2024-01-01T00:00:00.000Z' + ErrorResponse: + type: object + properties: + error: + type: string + example: 'An error occurred' diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index a53af145..8d3a947b 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -1,9 +1,13 @@ import { Router } from '@ttoss/http-server'; import { filesRouter } from './files'; +import { projectsRouter } from './projects'; +import { usersRouter } from './users'; const v1Router = new Router(); v1Router.use(filesRouter.routes()); +v1Router.use(projectsRouter.routes()); +v1Router.use(usersRouter.routes()); export { v1Router }; diff --git a/packages/server/src/rest/v1/projects.ts b/packages/server/src/rest/v1/projects.ts new file mode 100644 index 00000000..8117d6a7 --- /dev/null +++ b/packages/server/src/rest/v1/projects.ts @@ -0,0 +1,69 @@ +import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { createProject } from 'src/lib/projects'; + +const projectsRouter = new Router(); + +/** + * @openapi + * /projects: + * post: + * tags: + * - Projects + * summary: Create a project + * description: Creates a new project. Only admins can create projects. + * operationId: createProject + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * properties: + * name: + * type: string + * example: 'My Project' + * responses: + * '201': + * description: Project created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProjectRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.post('/projects', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const { name } = ctx.request.body as { name: string }; + + const project = await createProject({ name }); + + ctx.status = 201; + ctx.body = project; +}); + +export { projectsRouter }; diff --git a/packages/server/src/rest/v1/users.ts b/packages/server/src/rest/v1/users.ts new file mode 100644 index 00000000..6bf8c3ce --- /dev/null +++ b/packages/server/src/rest/v1/users.ts @@ -0,0 +1,375 @@ +import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { + createFirstAdminUser, + createUser, + deleteUser, + getUser, + listUsers, + loginUser, +} from 'src/lib/users'; + +const usersRouter = new Router(); + +/** + * @openapi + * /users: + * get: + * tags: + * - Users + * summary: List all users + * description: Returns a list of all users + * operationId: listUsers + * responses: + * '200': + * description: List of users returned successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/UserRecord' + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +usersRouter.get('/users', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await listUsers(); +}); + +/** + * @openapi + * /users/{id}: + * get: + * tags: + * - Users + * summary: Get a user by ID + * description: Returns the data of a specific user + * operationId: getUser + * parameters: + * - name: id + * in: path + * required: true + * description: User ID + * schema: + * type: string + * example: 'usr_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: User found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserRecord' + * '404': + * description: User not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +usersRouter.get('/users/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const user = await getUser({ id: ctx.params.id }); + + if (!user) { + ctx.status = 404; + ctx.body = { error: 'User not found' }; + return; + } + + ctx.body = user; +}); + +/** + * @openapi + * /users: + * post: + * tags: + * - Users + * summary: Create a user + * description: Creates a new user in the system + * operationId: createUser + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - username + * - password + * properties: + * username: + * type: string + * example: 'johndoe' + * password: + * type: string + * format: password + * example: 'supersecret' + * role: + * type: string + * enum: [admin, user] + * example: 'user' + * responses: + * '201': + * description: User created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserRecord' + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +usersRouter.post('/users', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const body = ctx.request.body as { + username: string; + password: string; + role?: 'admin' | 'user'; + }; + + const user = await createUser(body); + ctx.status = 201; + ctx.body = user; +}); + +/** + * @openapi + * /users/bootstrap: + * post: + * tags: + * - Users + * summary: Create the first admin user + * description: Creates the first admin user. Returns 409 if any user already exists. + * operationId: bootstrapUser + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - username + * - password + * properties: + * username: + * type: string + * example: 'admin' + * password: + * type: string + * format: password + * example: 'supersecret' + * responses: + * '201': + * description: Admin user created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserRecord' + * '409': + * description: Users already exist + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '500': + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +usersRouter.post('/users/bootstrap', async (ctx: Context) => { + const body = ctx.request.body as { + username: string; + password: string; + }; + + const user = await createFirstAdminUser(body); + + if (!user) { + ctx.status = 409; + ctx.body = { error: 'Users already exist' }; + return; + } + + ctx.status = 201; + ctx.body = user; +}); + +/** + * @openapi + * /users/login: + * post: + * tags: + * - Users + * summary: Login + * description: Authenticates a user and returns a JWT token + * operationId: loginUser + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - username + * - password + * properties: + * username: + * type: string + * example: 'admin' + * password: + * type: string + * format: password + * example: 'supersecret' + * responses: + * '200': + * description: Login successful + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/UserRecord' + * - type: object + * required: + * - token + * properties: + * token: + * type: string + * '401': + * description: Invalid credentials + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +usersRouter.post('/users/login', async (ctx: Context) => { + const { username, password } = ctx.request.body as { + username: string; + password: string; + }; + + const result = await loginUser({ username, password }); + + if (!result) { + ctx.status = 401; + ctx.body = { error: 'Invalid credentials' }; + return; + } + + ctx.body = result; +}); + +/** + * @openapi + * /users/{id}: + * delete: + * tags: + * - Users + * summary: Delete a user + * description: Deletes a user by ID. Admin only. + * operationId: deleteUser + * parameters: + * - name: id + * in: path + * required: true + * description: User ID + * schema: + * type: string + * example: 'usr_V1StGXR8Z5jdHi6B' + * responses: + * '204': + * description: User deleted successfully + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: User not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +usersRouter.delete('/users/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const deleted = await deleteUser({ id: ctx.params.id }); + + if (!deleted) { + ctx.status = 404; + ctx.body = { error: 'User not found' }; + return; + } + + ctx.status = 204; +}); + +export { usersRouter }; diff --git a/packages/server/tests/unit/jest.config.ts b/packages/server/tests/unit/jest.config.ts index 9468854f..156b093e 100644 --- a/packages/server/tests/unit/jest.config.ts +++ b/packages/server/tests/unit/jest.config.ts @@ -8,6 +8,6 @@ export default jestUnitConfig({ // setupFiles: ['/setupTests.ts'], setupFilesAfterEnv: ['/setupTestsAfterEnv.ts'], transformIgnorePatterns: getTransformIgnorePatterns({ - esmModules: ['@ttoss/postgresdb', '@ttoss/http-server-mcp'], + esmModules: ['@ttoss/postgresdb', '@ttoss/http-server-mcp', 'nanoid'], }), }); diff --git a/packages/server/tests/unit/testClient.ts b/packages/server/tests/unit/testClient.ts index e22365d2..63527adc 100644 --- a/packages/server/tests/unit/testClient.ts +++ b/packages/server/tests/unit/testClient.ts @@ -3,6 +3,20 @@ import request from 'supertest'; export const testClient = request(app.callback()); +/** + * Logs in with the given credentials and returns a Bearer token. + * Drives the implementation of POST /api/v1/users/login. + */ +export const loginAs = async ( + username: string, + password: string +): Promise => { + const res = await testClient + .post('/api/v1/users/login') + .send({ username, password }); + return res.body.token as string; +}; + /** * Helper to create authenticated requests with proper Origin header * Use this for requests that require JWT authentication diff --git a/packages/server/tests/unit/tests/files.test.ts b/packages/server/tests/unit/tests/files.test.ts deleted file mode 100644 index 75bc5e46..00000000 --- a/packages/server/tests/unit/tests/files.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { testClient } from '../testClient'; - -describe('GET /api/v1/files', () => { - test('should return empty list initially', async () => { - const response = await testClient.get('/api/v1/files'); - - expect(response.status).toBe(200); - expect(response.body).toEqual([]); - }); -}); - -describe('POST /api/v1/files', () => { - test('should create a file', async () => { - const payload = { - filename: 'test.txt', - contentType: 'text/plain', - size: 1024, - storageType: 'local', - storagePath: '/tmp/test.txt', - }; - - const response = await testClient.post('/api/v1/files').send(payload); - - expect(response.status).toBe(201); - expect(response.body.id).toBeDefined(); - expect(response.body.filename).toBe(payload.filename); - expect(response.body.contentType).toBe(payload.contentType); - expect(response.body.size).toBe(payload.size); - expect(response.body.storageType).toBe(payload.storageType); - expect(response.body.storagePath).toBe(payload.storagePath); - }); -}); - -describe('GET /api/v1/files/:id', () => { - test('should return a created file by id', async () => { - const payload = { - filename: 'image.png', - contentType: 'image/png', - size: 2048, - storageType: 's3', - storagePath: 's3://bucket/image.png', - }; - - const createResponse = await testClient.post('/api/v1/files').send(payload); - const { id } = createResponse.body; - - const response = await testClient.get(`/api/v1/files/${id}`); - - expect(response.status).toBe(200); - expect(response.body.id).toBe(id); - expect(response.body.filename).toBe(payload.filename); - }); - - test('should return 404 for unknown id', async () => { - const response = await testClient.get( - '/api/v1/files/00000000-0000-0000-0000-000000000000' - ); - - expect(response.status).toBe(404); - }); -}); - -describe('DELETE /api/v1/files/:id', () => { - test('should delete a file', async () => { - const payload = { - filename: 'delete-me.txt', - contentType: 'text/plain', - size: 512, - storageType: 'local', - storagePath: '/tmp/delete-me.txt', - }; - - const createResponse = await testClient.post('/api/v1/files').send(payload); - const { id } = createResponse.body; - - const deleteResponse = await testClient.delete(`/api/v1/files/${id}`); - expect(deleteResponse.status).toBe(204); - - const getResponse = await testClient.get(`/api/v1/files/${id}`); - expect(getResponse.status).toBe(404); - }); -}); diff --git a/packages/server/tests/unit/tests/projects.test.ts b/packages/server/tests/unit/tests/projects.test.ts new file mode 100644 index 00000000..6a696de4 --- /dev/null +++ b/packages/server/tests/unit/tests/projects.test.ts @@ -0,0 +1,25 @@ +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +describe('POST /api/v1/projects', () => { + let adminToken: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + }); + + test('admin can create a project', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'My Project' }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.name).toBe('My Project'); + expect(response.body.createdAt).toBeDefined(); + expect(response.body.updatedAt).toBeDefined(); + }); +}); diff --git a/packages/server/tests/unit/tests/users.test.ts b/packages/server/tests/unit/tests/users.test.ts new file mode 100644 index 00000000..aa23c91c --- /dev/null +++ b/packages/server/tests/unit/tests/users.test.ts @@ -0,0 +1,248 @@ +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +describe('POST /api/v1/users/bootstrap', () => { + test('should create the first admin user and return 201', async () => { + const response = await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.username).toBe('admin'); + expect(response.body.role).toBe('admin'); + expect(response.body.password).toBeUndefined(); + }); + + test('should return 409 if a user already exists', async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + const response = await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin2', password: 'anotherpassword' }); + + expect(response.status).toBe(409); + }); +}); + +describe('POST /api/v1/users/login', () => { + test('should return token and user data on valid credentials', async () => { + const response = await testClient + .post('/api/v1/users/login') + .send({ username: 'admin', password: 'supersecret' }); + + expect(response.status).toBe(200); + expect(response.body.token).toBeDefined(); + expect(response.body.username).toBe('admin'); + expect(response.body.role).toBe('admin'); + expect(response.body.password).toBeUndefined(); + }); + + test('should return 401 on invalid credentials', async () => { + const response = await testClient + .post('/api/v1/users/login') + .send({ username: 'admin', password: 'wrongpassword' }); + + expect(response.status).toBe(401); + }); +}); + +describe('Admin user operations', () => { + let adminToken: string; + + beforeAll(async () => { + adminToken = await loginAs('admin', 'supersecret'); + }); + + describe('POST /api/v1/users', () => { + test('admin can create a regular user', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'alice', password: 'alicepass', role: 'user' }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.username).toBe('alice'); + expect(response.body.role).toBe('user'); + expect(response.body.password).toBeUndefined(); + }); + + test('admin can create another admin', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'admin2', password: 'admin2pass', role: 'admin' }); + + expect(response.status).toBe(201); + expect(response.body.username).toBe('admin2'); + expect(response.body.role).toBe('admin'); + }); + + test('unauthenticated request cannot create a user', async () => { + const response = await testClient + .post('/api/v1/users') + .send({ username: 'hacker', password: 'pass' }); + + expect(response.status).toBe(401); + }); + + test('non-admin user cannot create a user', async () => { + const aliceToken = await loginAs('alice', 'alicepass'); + const response = await authenticatedTestClient(aliceToken) + .post('/api/v1/users') + .send({ username: 'bob', password: 'bobpass' }); + + expect(response.status).toBe(403); + }); + }); + + describe('GET /api/v1/users', () => { + test('admin can list users', async () => { + const response = + await authenticatedTestClient(adminToken).get('/api/v1/users'); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect( + response.body.some((u: { username: string }) => { + return u.username === 'admin'; + }) + ).toBe(true); + }); + + test('unauthenticated request cannot list users', async () => { + const response = await testClient.get('/api/v1/users'); + + expect(response.status).toBe(401); + }); + + test('non-admin user cannot list users', async () => { + const aliceToken = await loginAs('alice', 'alicepass'); + const response = + await authenticatedTestClient(aliceToken).get('/api/v1/users'); + + expect(response.status).toBe(403); + }); + + test('second admin can also list users', async () => { + const admin2Token = await loginAs('admin2', 'admin2pass'); + const response = + await authenticatedTestClient(admin2Token).get('/api/v1/users'); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + }); + + describe('GET /api/v1/users/:id', () => { + test('admin can get a user by id', async () => { + const listRes = + await authenticatedTestClient(adminToken).get('/api/v1/users'); + const alice = listRes.body.find((u: { username: string }) => { + return u.username === 'alice'; + }); + + const response = await authenticatedTestClient(adminToken).get( + `/api/v1/users/${alice.id}` + ); + + expect(response.status).toBe(200); + expect(response.body.username).toBe('alice'); + }); + + test('should return 404 for unknown user id', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/users/usr_nonexistent12345' + ); + + expect(response.status).toBe(404); + }); + + test('unauthenticated request cannot get a user', async () => { + const listRes = + await authenticatedTestClient(adminToken).get('/api/v1/users'); + const alice = listRes.body.find((u: { username: string }) => { + return u.username === 'alice'; + }); + + const response = await testClient.get(`/api/v1/users/${alice.id}`); + + expect(response.status).toBe(401); + }); + + test('non-admin user cannot get another user', async () => { + const aliceToken = await loginAs('alice', 'alicepass'); + const listRes = + await authenticatedTestClient(adminToken).get('/api/v1/users'); + const admin = listRes.body.find((u: { username: string }) => { + return u.username === 'admin'; + }); + + const response = await authenticatedTestClient(aliceToken).get( + `/api/v1/users/${admin.id}` + ); + + expect(response.status).toBe(403); + }); + }); + + describe('DELETE /api/v1/users/:id', () => { + test('admin can delete a user', async () => { + const createRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'todelete', password: 'pass' }); + const { id } = createRes.body; + + const deleteRes = await authenticatedTestClient(adminToken).delete( + `/api/v1/users/${id}` + ); + expect(deleteRes.status).toBe(204); + + const getRes = await authenticatedTestClient(adminToken).get( + `/api/v1/users/${id}` + ); + expect(getRes.status).toBe(404); + }); + + test('unauthenticated request cannot delete a user', async () => { + const listRes = + await authenticatedTestClient(adminToken).get('/api/v1/users'); + const alice = listRes.body.find((u: { username: string }) => { + return u.username === 'alice'; + }); + + const response = await testClient.delete(`/api/v1/users/${alice.id}`); + + expect(response.status).toBe(401); + }); + + test('non-admin user cannot delete a user', async () => { + const aliceToken = await loginAs('alice', 'alicepass'); + const listRes = + await authenticatedTestClient(adminToken).get('/api/v1/users'); + const admin = listRes.body.find((u: { username: string }) => { + return u.username === 'admin'; + }); + + const response = await authenticatedTestClient(aliceToken).delete( + `/api/v1/users/${admin.id}` + ); + + expect(response.status).toBe(403); + }); + + test('second admin can also delete a user', async () => { + const admin2Token = await loginAs('admin2', 'admin2pass'); + const createRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'todelete2', password: 'pass' }); + const { id } = createRes.body; + + const response = await authenticatedTestClient(admin2Token).delete( + `/api/v1/users/${id}` + ); + + expect(response.status).toBe(204); + }); + }); +}); diff --git a/packages/website/docs/modules/iam.md b/packages/website/docs/modules/iam.md new file mode 100644 index 00000000..d452298f --- /dev/null +++ b/packages/website/docs/modules/iam.md @@ -0,0 +1,127 @@ +# IAM Module + +The IAM (Identity and Access Management) module provides authentication and authorization primitives for SOAT-powered applications. It manages identities through **Users** and **API Keys**, and controls access to resources using **Projects**, **Policies**, and **Project Members**. + +## Overview + +The IAM module controls who can access your resources and what actions they can perform. It supports: + +- **Human users** with username/password authentication and JWT session tokens +- **Machine-to-machine access** via API keys scoped to a project and policy +- **Fine-grained authorization** through policy-based permission checks per project + +## Authentication + +### Users (JWT) + +Users authenticate via `POST /api/v1/users/login` with their username and password. On success, a signed JWT is returned. Include it as a Bearer token on subsequent requests: + +``` +Authorization: Bearer +``` + +Tokens are valid for 7 days. + +### API Keys + +API keys provide long-lived credentials for machine-to-machine access. A raw key is returned only once at creation — it is immediately hashed (bcrypt) and cannot be retrieved again. + +Key format: `sk_` (e.g., `sk_abc12345xyz`) + +Include it as a Bearer token: + +``` +Authorization: Bearer sk_abc12345xyz +``` + +An API key is scoped to a specific **Project** and **Policy**. When authenticated via an API key, the system resolves the project context and the key's effective permissions automatically. + +**Permission boundary**: An API key's effective permissions are bounded by the membership policy of the user who created it. Even if the key's policy grants broader access, permissions are intersected with the creator's membership policy at authorization time. + +## Roles + +The IAM module uses a simple role model: + +| Role | Description | +| ------- | ----------------------------------------------------------------------- | +| `admin` | Full access to all resources and projects. Policy checks are bypassed. | +| `user` | Access is scoped to projects they are members of, governed by a policy. | + +The first user in the system is bootstrapped as `admin` via `POST /api/v1/users/bootstrap`. Subsequent users are created as `user` by default. + +## Projects + +A project is the top-level resource boundary. All access control (policies, members, API keys) is scoped to a project. + +- Admins can create, list, view, and delete any project. +- Users can only list and view projects they are members of. + +## Policies + +A policy defines what actions are allowed or denied within a project. Policies are attached to project members and API keys. + +| Field | Type | Description | +| ---------------- | ---------- | ------------------------------------------------------ | +| `id` | `string` | ID (prefix: `pol_`) | +| `name` | `string` | Human-readable policy name | +| `permissions` | `string[]` | List of allowed action patterns | +| `notPermissions` | `string[]` | List of denied action patterns (deny takes precedence) | +| `projectId` | `string` | The project this policy belongs to | + +### Permission Patterns + +Permission strings follow the format `resource:action`. Wildcards are supported: + +| Pattern | Matches | +| --------------- | ---------------------------------- | +| `files:*` | Any action on the `files` resource | +| `*` | Any action on any resource | +| `files:GetFile` | Exactly the `files:GetFile` action | + +### Authorization Logic + +For a `user` role, a request is allowed if **all** of the following hold: + +1. The user is a member of the project being accessed. +2. The user's membership policy **allows** the action (`permissions` match). +3. The user's membership policy does **not deny** the action (`notPermissions` do not match). +4. If the request uses an API key, the key's policy also allows the action (intersection). + +`admin` users bypass all policy checks. + +## Project Members + +A project member is a user who has been granted access to a project under a specific policy. + +- Only admins can add, update, or remove members. +- Removing a member revokes their access to the project immediately. + +## API Keys + +| Field | Type | Description | +| ----------- | -------- | ---------------------------------------------------------- | +| `id` | `string` | Public ID (prefix: `key_`) | +| `name` | `string` | Human-readable key name | +| `policyId` | `string` | Public ID of the policy governing this key | +| `projectId` | `string` | Public ID of the scoped project | +| `key` | `string` | Raw key value — **returned only at creation, never again** | + +- Any project member can create an API key scoped to their own project. +- The key's policy must be valid within the same project. +- The key's effective permissions are bounded by the creator's membership policy. +- Members can list and delete their own keys. Admins can manage all keys. + +## Data Model + +All IAM entities are backed by the `@soat/postgresdb` package and use a `publicId` (prefixed string) as the external identifier. Internal integer primary keys are never exposed through the API. + +| Entity | `publicId` Prefix | +| ------- | ----------------- | +| User | `usr_` | +| Project | `proj_` | +| Policy | `pol_` | +| API Key | `key_` | + +## OAuth + +> **Coming soon.** OAuth 2.0 integration is planned for an upcoming release. Supported flows will include Authorization Code (interactive sign-in) and Client Credentials (server-to-server). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15f8952a..6d3a31b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: '@ttoss/postgresdb': specifier: ^0.8.0 version: 0.8.0(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2) + nanoid: + specifier: ^5.1.7 + version: 5.1.7 devDependencies: '@testcontainers/postgresql': specifier: ^11.13.0 @@ -116,17 +119,23 @@ importers: specifier: workspace:* version: link:../postgresdb '@ttoss/http-server': - specifier: ^0.5.8 - version: 0.5.8 + specifier: ^0.5.9 + version: 0.5.9 '@ttoss/http-server-mcp': - specifier: ^0.11.0 - version: 0.11.0 + specifier: ^0.11.1 + version: 0.11.1 '@ttoss/postgresdb': specifier: ^0.8.0 version: 0.8.0(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2) + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 dotenv: specifier: ^17.4.1 version: 17.4.1 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 ollama: specifier: ^0.6.3 version: 0.6.3 @@ -146,9 +155,15 @@ importers: '@ttoss/test-utils': specifier: ^4.2.8 version: 4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/bcryptjs': + specifier: ^3.0.0 + version: 3.0.0 '@types/jest': specifier: ^30.0.0 version: 30.0.0 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/pg': specifier: ^8.20.0 version: 8.20.0 @@ -3421,11 +3436,11 @@ packages: peerDependencies: eslint: '>=9.20.0' - '@ttoss/http-server-mcp@0.11.0': - resolution: {integrity: sha512-VWTWZDB4waHxhx5ZM5P4Yk8jtdR/LRDRTCGnHmkufIybaAUDvzQ8pBnCnpTw21Li18mZw7CLBZsN5Pja0uWJ/Q==} + '@ttoss/http-server-mcp@0.11.1': + resolution: {integrity: sha512-PK9IjRfsGci0O5pQhBrozo3HWogEsWy+ej8+iUz21WJO4mUs3VKVhqqLnkSfcp9YDjxWuJleeL0w3Lj1MI38CA==} - '@ttoss/http-server@0.5.8': - resolution: {integrity: sha512-Vwur4xGiFaDSGQJ6gPzryZWUE8srraND2fYLQPjJHvviTHxldRuJoTF5afJGiqpvsMLCv9JuUOeOiGDFuFl8yA==} + '@ttoss/http-server@0.5.9': + resolution: {integrity: sha512-g3gB1UM1erVOFFIa5QA6H3czNteCIDVETzAWG2yVLvmQbvNcAe0OK5ZRdIYRD5z4aPV6r380C1798VM8soV5Qw==} '@ttoss/logger@0.7.1': resolution: {integrity: sha512-PxuFk1TvIRLxVYr5PXKLDsmmfTw+lV6Dl8IP6NA2JPgmT8J7EDjv5mAOTdJteIiUZwCPL2a1Z/rrYHU8pf2LPQ==} @@ -3499,6 +3514,10 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcryptjs@3.0.0': + resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==} + deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -3600,6 +3619,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/markdown-escape@1.1.3': resolution: {integrity: sha512-JIc1+s3y5ujKnt/+N+wq6s/QdL2qZ11fP79MijrVXsAAnzSxCbT2j/3prHRouJdZ2yFLN3vkP0HytfnoCczjOw==} @@ -4460,6 +4482,10 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} @@ -4521,6 +4547,9 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -5480,6 +5509,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -7339,10 +7371,20 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} @@ -7457,6 +7499,24 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -7469,6 +7529,9 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -8080,6 +8143,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.7: + resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==} + engines: {node: ^18 || >=20} + hasBin: true + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -12879,7 +12947,7 @@ snapshots: '@commitlint/is-ignored@20.5.0': dependencies: '@commitlint/types': 20.5.0 - semver: 7.7.3 + semver: 7.7.4 '@commitlint/lint@20.5.0': dependencies: @@ -15873,9 +15941,9 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-config-turbo: 2.9.4(eslint@9.39.2(jiti@2.6.1))(turbo@2.9.4) - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-formatjs: 6.4.4(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jest: 29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2) eslint-plugin-jest-dom: 5.5.0(@testing-library/dom@10.4.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -15903,16 +15971,16 @@ snapshots: - turbo - typescript - '@ttoss/http-server-mcp@0.11.0': + '@ttoss/http-server-mcp@0.11.1': dependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) - '@ttoss/http-server': 0.5.8 + '@ttoss/http-server': 0.5.9 zod: 3.25.76 transitivePeerDependencies: - '@cfworker/json-schema' - supports-color - '@ttoss/http-server@0.5.8': + '@ttoss/http-server@0.5.9': dependencies: '@koa/bodyparser': 6.0.0(koa@3.1.1) '@koa/cors': 5.0.0 @@ -16034,6 +16102,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/bcryptjs@3.0.0': + dependencies: + bcryptjs: 3.0.3 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -16162,6 +16234,11 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 25.5.2 + '@types/markdown-escape@1.1.3': {} '@types/mdast@3.0.15': @@ -16433,7 +16510,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 minimatch: 9.0.5 - semver: 7.7.3 + semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@6.0.2) typescript: 6.0.2 @@ -16448,7 +16525,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.7.3 + semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.5.0(typescript@6.0.2) typescript: 6.0.2 @@ -17153,6 +17230,8 @@ snapshots: dependencies: tweetnacl: 0.14.5 + bcryptjs@3.0.3: {} + before-after-hook@4.0.0: {} big.js@5.2.2: {} @@ -17256,6 +17335,8 @@ snapshots: buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -17820,7 +17901,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.6) postcss-modules-values: 4.0.0(postcss@8.5.6) postcss-value-parser: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: webpack: 5.104.1 @@ -18269,6 +18350,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.267: {} @@ -18546,7 +18631,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) @@ -18557,18 +18642,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -18584,7 +18669,7 @@ snapshots: transitivePeerDependencies: - ts-jest - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -18595,7 +18680,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -19980,7 +20065,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 is-callable@1.2.7: {} @@ -20168,7 +20253,7 @@ snapshots: '@babel/parser': 7.28.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.3 + semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -20720,6 +20805,19 @@ snapshots: jsonpointer@5.0.1: {} + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -20727,6 +20825,17 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keygrip@1.1.0: dependencies: tsscmp: 1.0.6 @@ -20857,6 +20966,18 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.kebabcase@4.1.1: {} lodash.memoize@4.1.2: {} @@ -20865,6 +20986,8 @@ snapshots: lodash.mergewith@4.6.2: {} + lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {} @@ -20919,7 +21042,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 make-error@1.3.6: optional: true @@ -21920,6 +22043,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@5.1.7: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -22289,7 +22414,7 @@ snapshots: got: 12.6.1 registry-auth-token: 5.1.0 registry-url: 6.0.1 - semver: 7.7.3 + semver: 7.7.4 pako@2.1.0: {} @@ -22622,7 +22747,7 @@ snapshots: cosmiconfig: 8.3.6(typescript@6.0.2) jiti: 1.21.7 postcss: 8.5.6 - semver: 7.7.3 + semver: 7.7.4 webpack: 5.104.1 transitivePeerDependencies: - typescript @@ -23732,7 +23857,7 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 semver@6.3.1: {} diff --git a/turbo.json b/turbo.json index 209a48a3..93e2128b 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://turborepo.org/schema.json", + "$schema": "./node_modules/turbo/schema.json", "globalEnv": ["DATABASE_*"], "tasks": { "build": { From 3bd1ea09cf3b44c0644505eabd135bfdef9178b9 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Wed, 8 Apr 2026 13:17:28 -0300 Subject: [PATCH 03/27] test: flow permission tests --- .github/instructions/general.instructions.md | 9 + .github/instructions/modules.instructions.md | 44 +++ packages/postgresdb/src/models/File.ts | 21 +- .../postgresdb/src/models/ProjectPolicy.ts | 6 - packages/server/src/lib/apiKeys.ts | 75 ++++ packages/server/src/lib/files.ts | 11 +- packages/server/src/lib/permissions.ts | 9 +- packages/server/src/lib/projects.ts | 92 +++++ packages/server/src/middleware/auth.ts | 20 +- packages/server/src/rest/v1/apiKeys.ts | 326 ++++++++++++++++++ packages/server/src/rest/v1/files.ts | 84 ++++- packages/server/src/rest/v1/index.ts | 2 + packages/server/src/rest/v1/projects.ts | 261 +++++++++++++- .../tests/unit/tests/permissionsFlow.test.ts | 292 ++++++++++++++++ 14 files changed, 1238 insertions(+), 14 deletions(-) create mode 100644 .github/instructions/modules.instructions.md create mode 100644 packages/server/src/lib/apiKeys.ts create mode 100644 packages/server/src/rest/v1/apiKeys.ts create mode 100644 packages/server/tests/unit/tests/permissionsFlow.test.ts diff --git a/.github/instructions/general.instructions.md b/.github/instructions/general.instructions.md index b3dcab72..951fbb26 100644 --- a/.github/instructions/general.instructions.md +++ b/.github/instructions/general.instructions.md @@ -15,6 +15,15 @@ When defining functions, use an object for arguments instead of individual param Prefer using functions instead of classes for better simplicity and composability. +## Naming Conventions + +- User kebab-case for folder names (e.g., `my-folder`). +- Use camelCase for file names (e.g., `myFile.ts`). +- Use camelCase for variable and function names (e.g., `myVariable`, `myFunction`). +- Use PascalCase for type and interface names (e.g., `MyType`, `MyInterface`). +- Use uppercase with underscores for constants (e.g., `MY_CONSTANT`). +- For test files, use the same name as the file being tested with `.test` appended before the extension (e.g., `myFile.test.ts`). + ## Linting To fix ESLint issues in a specific file, run `pnpm eslint --fix path/to/file`. diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md new file mode 100644 index 00000000..8525c8eb --- /dev/null +++ b/.github/instructions/modules.instructions.md @@ -0,0 +1,44 @@ +--- +applyTo: '**' +description: Instructions for creating and maintaining modules across the codebase. +--- + +# Module Instructions + +A module is a named resource (e.g., `files`, `users`) that is exposed through the REST API, the MCP server, and documented in the website. Whenever a module is created or changed, **all four areas must be updated together**: + +1. **REST** — route handlers and OpenAPI spec +2. **MCP** — tool definitions in the MCP server +3. **Docs** — module documentation in the website +4. **Tests** — unit tests covering the new or changed behavior + +## Checklist for Every Module Change + +- [ ] Business logic updated in `packages/server/src/lib/.ts` +- [ ] REST routes updated in `packages/server/src/rest/v1/.ts` with `@openapi` JSDoc blocks +- [ ] Module router registered in `packages/server/src/rest/v1/index.ts` +- [ ] MCP tools updated in `packages/server/src/mcp/tools/.ts` +- [ ] Module docs updated in `packages/website/docs/modules/.md` +- [ ] Tests updated in `packages/server/tests/unit/tests/.test.ts` + +## REST + +Follow the rules in `server.instructions.md`. Each module gets its own file under `src/rest/v1/.ts` and must be mounted in `src/rest/v1/index.ts`. + +## MCP + +Each module operation must be exposed as an MCP tool in its own file at `src/mcp/tools/.ts`. The file must export a `registerTools` function that accepts a `McpServer` instance. It must then be imported and called in `src/mcp/tools/index.ts`. Tool names follow the pattern `-` (e.g., `list-files`, `create-user`). Tools call REST endpoints via `apiCall` using the same paths defined in the REST routes. + +## Docs + +Each module has a dedicated documentation page at `packages/website/docs/modules/.md`. The page must describe: + +- What the module does (overview) +- Key concepts and data model +- Any roles or access rules that apply + +Do **not** document REST endpoints in the module docs — those are covered in the auto-generated API reference. + +## Tests + +Tests live in `packages/server/tests/unit/tests/.test.ts`. Every public lib function and every REST route must have at least one test. Follow the patterns already established in `files.test.ts` and `users.test.ts`. diff --git a/packages/postgresdb/src/models/File.ts b/packages/postgresdb/src/models/File.ts index fd7fa1fe..24c6c8fa 100644 --- a/packages/postgresdb/src/models/File.ts +++ b/packages/postgresdb/src/models/File.ts @@ -1,6 +1,14 @@ -import { Column, DataType, Model, Table } from '@ttoss/postgresdb'; +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; +import { Project } from './Project'; @Table({ tableName: 'files', @@ -20,6 +28,17 @@ export class File extends Model { }) declare publicId: string; + @ForeignKey(() => { + return Project; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare projectId: number; + + @BelongsTo(() => { + return Project; + }) + declare project: Project; + @Column({ type: DataType.STRING }) declare filename?: string; diff --git a/packages/postgresdb/src/models/ProjectPolicy.ts b/packages/postgresdb/src/models/ProjectPolicy.ts index 94b592c8..ccb623bf 100644 --- a/packages/postgresdb/src/models/ProjectPolicy.ts +++ b/packages/postgresdb/src/models/ProjectPolicy.ts @@ -39,12 +39,6 @@ export class ProjectPolicy extends Model { }) declare project: Project; - @Column({ - type: DataType.STRING, - allowNull: false, - }) - declare name: string; - @Column({ type: DataType.ARRAY(DataType.TEXT), allowNull: false, diff --git a/packages/server/src/lib/apiKeys.ts b/packages/server/src/lib/apiKeys.ts new file mode 100644 index 00000000..bc513962 --- /dev/null +++ b/packages/server/src/lib/apiKeys.ts @@ -0,0 +1,75 @@ +import crypto from 'node:crypto'; + +import { API_KEY_RAW_PREFIX } from '@soat/postgresdb'; +import bcrypt from 'bcryptjs'; + +import { db } from '../db'; + +const mapApiKey = (apiKey: InstanceType<(typeof db)['ApiKey']>) => { + return { + id: apiKey.publicId, + name: apiKey.name, + keyPrefix: apiKey.keyPrefix, + createdAt: apiKey.createdAt, + updatedAt: apiKey.updatedAt, + }; +}; + +export const createApiKey = async (args: { + userId: number; + projectId: number; + policyId: number; + name: string; +}) => { + const random = crypto.randomBytes(32).toString('hex'); + const key = `${API_KEY_RAW_PREFIX}${random}`; + const keyPrefix = key.slice(0, 8); + const keyHash = await bcrypt.hash(key, 10); + + const apiKey = await db.ApiKey.create({ + ...args, + keyPrefix, + keyHash, + }); + + return { + ...mapApiKey(apiKey), + key, // Return the full key only once at creation + }; +}; + +export const getApiKey = async (args: { id: string }) => { + const apiKey = await db.ApiKey.findOne({ + where: { publicId: args.id }, + include: [ + { model: db.User, as: 'user' }, + { model: db.Project, as: 'project' }, + { model: db.ProjectPolicy, as: 'policy' }, + ], + }); + + if (!apiKey) { + return null; + } + + return { + ...mapApiKey(apiKey), + userId: apiKey.user?.publicId, + projectId: apiKey.project?.publicId, + policyId: apiKey.policy?.publicId, + }; +}; + +export const updateApiKey = async (args: { id: string; policyId: number }) => { + const apiKey = await db.ApiKey.findOne({ + where: { publicId: args.id }, + }); + + if (!apiKey) { + return null; + } + + await apiKey.update({ policyId: args.policyId }); + + return getApiKey({ id: args.id }); +}; diff --git a/packages/server/src/lib/files.ts b/packages/server/src/lib/files.ts index 3121dd84..0e280165 100644 --- a/packages/server/src/lib/files.ts +++ b/packages/server/src/lib/files.ts @@ -20,16 +20,23 @@ export const listFiles = async () => { }; export const getFile = async (args: { id: string }) => { - const file = await db.File.findOne({ where: { publicId: args.id } }); + const file = await db.File.findOne({ + where: { publicId: args.id }, + include: [{ model: db.Project, as: 'project' }], + }); if (!file) { return null; } - return mapFile(file); + return { + ...mapFile(file), + projectId: file.project?.publicId, + }; }; export const createFile = async (args: { + projectId: number; filename?: string; contentType?: string; size?: number; diff --git a/packages/server/src/lib/permissions.ts b/packages/server/src/lib/permissions.ts index 9ec5eec4..0b668094 100644 --- a/packages/server/src/lib/permissions.ts +++ b/packages/server/src/lib/permissions.ts @@ -31,14 +31,19 @@ export const policyAllows = (policy: Policy, action: string): boolean => { export const createApiKeyIsAllowed = (args: { projectPublicId: string; - policy: Policy; + userPolicy: Policy; + apiKeyPolicy: Policy; }) => { return async ( reqProjectPublicId: string, action: string ): Promise => { if (reqProjectPublicId !== args.projectPublicId) return false; - return policyAllows(args.policy, action); + // Intersection: action must be allowed by both user policy and API key policy + return ( + policyAllows(args.userPolicy, action) && + policyAllows(args.apiKeyPolicy, action) + ); }; }; diff --git a/packages/server/src/lib/projects.ts b/packages/server/src/lib/projects.ts index 80fbff9a..454f00f5 100644 --- a/packages/server/src/lib/projects.ts +++ b/packages/server/src/lib/projects.ts @@ -63,3 +63,95 @@ export const deleteProject = async (args: { id: string }) => { await project.destroy(); return true; }; + +const mapPolicy = (policy: InstanceType<(typeof db)['ProjectPolicy']>) => { + return { + id: policy.publicId, + permissions: policy.permissions, + notPermissions: policy.notPermissions, + projectId: policy.projectId, + createdAt: policy.createdAt, + updatedAt: policy.updatedAt, + }; +}; + +export const listProjectPolicies = async (args: { projectId: string }) => { + const project = await db.Project.findOne({ + where: { publicId: args.projectId }, + }); + if (!project) { + return []; + } + + const policies = await db.ProjectPolicy.findAll({ + where: { projectId: project.id }, + }); + + return policies.map(mapPolicy); +}; + +export const createProjectPolicy = async (args: { + projectId: string; + permissions: string[]; + notPermissions?: string[]; +}) => { + const project = await db.Project.findOne({ + where: { publicId: args.projectId }, + }); + if (!project) { + return null; + } + + const policy = await db.ProjectPolicy.create({ + projectId: project.id, + permissions: args.permissions, + notPermissions: args.notPermissions || [], + }); + + return mapPolicy(policy); +}; + +export const addUserToProject = async (args: { + projectId: string; + userId: string; + policyId: string; +}) => { + const project = await db.Project.findOne({ + where: { publicId: args.projectId }, + }); + if (!project) { + return null; + } + + const user = await db.User.findOne({ where: { publicId: args.userId } }); + if (!user) { + return null; + } + + const policy = await db.ProjectPolicy.findOne({ + where: { publicId: args.policyId }, + }); + if (!policy) { + return null; + } + + // Check if membership already exists + const existing = await db.UserProject.findOne({ + where: { userId: user.id, projectId: project.id }, + }); + + if (existing) { + // Update existing membership + await existing.update({ policyId: policy.id }); + return true; + } + + // Create new membership + await db.UserProject.create({ + userId: user.id, + projectId: project.id, + policyId: policy.id, + }); + + return true; +}; diff --git a/packages/server/src/middleware/auth.ts b/packages/server/src/middleware/auth.ts index eaf70e12..12b67013 100644 --- a/packages/server/src/middleware/auth.ts +++ b/packages/server/src/middleware/auth.ts @@ -49,6 +49,23 @@ const resolveApiKey = async (ctx: Context, rawKey: string) => { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const keyUser = (row as any).user; + // Get user's project policy + const project = await ctx.db.Project.findOne({ + where: { publicId: projectPublicId }, + }); + const membership = await ctx.db.UserProject.findOne({ + where: { userId: keyUser.id, projectId: project?.id }, + include: [{ model: ctx.db.ProjectPolicy }], + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const userPolicyData = (membership as any)?.policy; + const userPolicy = userPolicyData + ? { + permissions: userPolicyData.permissions as string[], + notPermissions: userPolicyData.notPermissions as string[], + } + : { permissions: [], notPermissions: [] }; + ctx.authUser = { id: keyUser.id as number, publicId: keyUser.publicId as string, @@ -56,7 +73,8 @@ const resolveApiKey = async (ctx: Context, rawKey: string) => { role: keyUser.role as 'admin' | 'user', isAllowed: createApiKeyIsAllowed({ projectPublicId, - policy: keyPolicy, + userPolicy, + apiKeyPolicy: keyPolicy, }), }; break; diff --git a/packages/server/src/rest/v1/apiKeys.ts b/packages/server/src/rest/v1/apiKeys.ts new file mode 100644 index 00000000..ff150894 --- /dev/null +++ b/packages/server/src/rest/v1/apiKeys.ts @@ -0,0 +1,326 @@ +import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { db } from 'src/db'; +import { createApiKey, getApiKey, updateApiKey } from 'src/lib/apiKeys'; + +const apiKeysRouter = new Router(); + +/** + * @openapi + * /api-keys: + * post: + * tags: + * - API Keys + * summary: Create a new API key + * description: Creates a new API key for a user in a project with specified policy + * operationId: createApiKey + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - projectId + * - policyId + * - name + * properties: + * projectId: + * type: string + * description: Project public ID + * policyId: + * type: string + * description: Policy public ID + * name: + * type: string + * description: API key name + * responses: + * '201': + * description: API key created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * key: + * type: string + * description: The full API key (shown only once) + * keyPrefix: + * type: string + * createdAt: + * type: string + * updatedAt: + * type: string + * '400': + * description: Bad request + * '403': + * description: Forbidden + * '500': + * description: Internal server error + */ +apiKeysRouter.post('/api-keys', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const { projectId, policyId, name } = ctx.request.body as { + projectId: string; + policyId: string; + name: string; + }; + + if (!projectId || !policyId || !name) { + ctx.status = 400; + ctx.body = { error: 'Missing required fields' }; + return; + } + + // Find the project + const project = await db.Project.findOne({ + where: { publicId: projectId }, + }); + + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project' }; + return; + } + + // Check if user is member of the project + const membership = await db.UserProject.findOne({ + where: { + userId: ctx.authUser.id, + projectId: project.id, + }, + }); + + if (!membership) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + // Check if policy exists and belongs to the project + const policy = await db.ProjectPolicy.findOne({ + where: { + publicId: policyId, + projectId: project.id, + }, + }); + + if (!policy) { + ctx.status = 400; + ctx.body = { error: 'Invalid policy' }; + return; + } + + const apiKey = await createApiKey({ + userId: ctx.authUser.id, + projectId: project.id, + policyId: policy.id, + name, + }); + + ctx.status = 201; + ctx.body = apiKey; +}); + +/** + * @openapi + * /api-keys/{id}: + * get: + * tags: + * - API Keys + * summary: Get an API key by ID + * description: Returns the data and metadata of a specific API key + * operationId: getApiKey + * parameters: + * - name: id + * in: path + * required: true + * description: API key ID + * schema: + * type: string + * responses: + * '200': + * description: API key found + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * keyPrefix: + * type: string + * userId: + * type: string + * projectId: + * type: string + * policyId: + * type: string + * createdAt: + * type: string + * updatedAt: + * type: string + * '404': + * description: API key not found + * '403': + * description: Forbidden + * '500': + * description: Internal server error + */ +apiKeysRouter.get('/api-keys/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const apiKey = await getApiKey({ id: ctx.params.id }); + + if (!apiKey) { + ctx.status = 404; + ctx.body = { error: 'API key not found' }; + return; + } + + // Check if user owns the API key + if (apiKey.userId !== ctx.authUser.publicId) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = apiKey; +}); + +/** + * @openapi + * /api-keys/{id}: + * put: + * tags: + * - API Keys + * summary: Update an API key + * description: Updates the policy of a specific API key + * operationId: updateApiKey + * parameters: + * - name: id + * in: path + * required: true + * description: API key ID + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - policyId + * properties: + * policyId: + * type: string + * description: New policy public ID + * responses: + * '200': + * description: API key updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * keyPrefix: + * type: string + * userId: + * type: string + * projectId: + * type: string + * policyId: + * type: string + * createdAt: + * type: string + * updatedAt: + * type: string + * '404': + * description: API key not found + * '403': + * description: Forbidden + * '500': + * description: Internal server error + */ +apiKeysRouter.put('/api-keys/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const { policyId } = ctx.request.body as { + policyId: string; + }; + + if (!policyId) { + ctx.status = 400; + ctx.body = { error: 'Missing policyId' }; + return; + } + + // Find the policy + const policy = await db.ProjectPolicy.findOne({ + where: { publicId: policyId }, + }); + + if (!policy) { + ctx.status = 400; + ctx.body = { error: 'Invalid policy' }; + return; + } + + // Check if user owns the API key + const apiKeyRecord = await db.ApiKey.findOne({ + where: { publicId: ctx.params.id }, + include: [{ model: db.User }, { model: db.Project }], + }); + + if (!apiKeyRecord) { + ctx.status = 404; + ctx.body = { error: 'API key not found' }; + return; + } + + if (apiKeyRecord.userId !== ctx.authUser.id) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + // Check if policy belongs to the same project as the API key + if (policy.projectId !== apiKeyRecord.projectId) { + ctx.status = 400; + ctx.body = { error: 'Policy does not belong to the same project' }; + return; + } + + const updatedApiKey = await updateApiKey({ + id: ctx.params.id, + policyId: policy.id, + }); + + ctx.body = updatedApiKey; +}); + +export { apiKeysRouter }; diff --git a/packages/server/src/rest/v1/files.ts b/packages/server/src/rest/v1/files.ts index 0a242092..684c3251 100644 --- a/packages/server/src/rest/v1/files.ts +++ b/packages/server/src/rest/v1/files.ts @@ -1,5 +1,6 @@ import { Router } from '@ttoss/http-server'; import type { Context } from 'src/Context'; +import { db } from 'src/db'; import { createFile, deleteFile, getFile, listFiles } from 'src/lib/files'; const filesRouter = new Router(); @@ -71,6 +72,12 @@ filesRouter.get('/files', async (ctx: Context) => { * $ref: '#/components/schemas/ErrorResponse' */ filesRouter.get('/files/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + const file = await getFile({ id: ctx.params.id }); if (!file) { @@ -79,6 +86,17 @@ filesRouter.get('/files/:id', async (ctx: Context) => { return; } + // Check if user is allowed to read files in this project + const allowed = await ctx.authUser.isAllowed( + file.projectId!, + 'files:GetFile' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + ctx.body = file; }); @@ -98,9 +116,13 @@ filesRouter.get('/files/:id', async (ctx: Context) => { * schema: * type: object * required: + * - projectId * - storageType * - storagePath * properties: + * projectId: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' * filename: * type: string * example: 'document.pdf' @@ -135,7 +157,14 @@ filesRouter.get('/files/:id', async (ctx: Context) => { * $ref: '#/components/schemas/ErrorResponse' */ filesRouter.post('/files', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + const body = ctx.request.body as { + projectId: string; filename?: string; contentType?: string; size?: number; @@ -144,7 +173,31 @@ filesRouter.post('/files', async (ctx: Context) => { metadata?: string; }; - const file = await createFile(body); + // Check if user is allowed to create files in this project + const allowed = await ctx.authUser.isAllowed( + body.projectId, + 'files:CreateFile' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + // Convert projectId to internal ID + const project = await db.Project.findOne({ + where: { publicId: body.projectId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + const file = await createFile({ + ...body, + projectId: project.id, + }); ctx.status = 201; ctx.body = file; }); @@ -183,6 +236,35 @@ filesRouter.post('/files', async (ctx: Context) => { * $ref: '#/components/schemas/ErrorResponse' */ filesRouter.delete('/files/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + // Get file to check project permission + const file = await db.File.findOne({ + where: { publicId: ctx.params.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + if (!file) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; + } + + // Check if user is allowed to delete files in this project + const allowed = await ctx.authUser.isAllowed( + file.project!.publicId, + 'files:DeleteFile' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + const result = await deleteFile({ id: ctx.params.id }); if (result === null) { diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index 8d3a947b..9a56f1d6 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -1,11 +1,13 @@ import { Router } from '@ttoss/http-server'; +import { apiKeysRouter } from './apiKeys'; import { filesRouter } from './files'; import { projectsRouter } from './projects'; import { usersRouter } from './users'; const v1Router = new Router(); +v1Router.use(apiKeysRouter.routes()); v1Router.use(filesRouter.routes()); v1Router.use(projectsRouter.routes()); v1Router.use(usersRouter.routes()); diff --git a/packages/server/src/rest/v1/projects.ts b/packages/server/src/rest/v1/projects.ts index 8117d6a7..dc95003a 100644 --- a/packages/server/src/rest/v1/projects.ts +++ b/packages/server/src/rest/v1/projects.ts @@ -1,6 +1,11 @@ import { Router } from '@ttoss/http-server'; import type { Context } from 'src/Context'; -import { createProject } from 'src/lib/projects'; +import { + addUserToProject, + createProject, + createProjectPolicy, + listProjectPolicies, +} from 'src/lib/projects'; const projectsRouter = new Router(); @@ -66,4 +71,258 @@ projectsRouter.post('/projects', async (ctx: Context) => { ctx.body = project; }); +/** + * @openapi + * /projects/{projectId}/policies: + * get: + * tags: + * - Projects + * summary: List project policies + * description: Returns a list of policies for a project. Project members can list policies. + * operationId: listProjectPolicies + * parameters: + * - name: projectId + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: List of policies returned successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ProjectPolicyRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.get('/projects/:projectId/policies', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + // Check if user is member of the project + const allowed = await ctx.authUser.isAllowed( + ctx.params.projectId, + 'projects:GetProject' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const policies = await listProjectPolicies({ + projectId: ctx.params.projectId, + }); + + ctx.body = policies; +}); + +/** + * @openapi + * /projects/{projectId}/policies: + * post: + * tags: + * - Projects + * summary: Create a project policy + * description: Creates a new policy for a project. Only admins can create policies. + * operationId: createProjectPolicy + * parameters: + * - name: projectId + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - permissions + * properties: + * permissions: + * type: array + * items: + * type: string + * example: ['files:read'] + * notPermissions: + * type: array + * items: + * type: string + * example: ['files:delete'] + * responses: + * '201': + * description: Policy created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProjectPolicyRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.post('/projects/:projectId/policies', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const { permissions, notPermissions } = ctx.request.body as { + permissions: string[]; + notPermissions?: string[]; + }; + + const policy = await createProjectPolicy({ + projectId: ctx.params.projectId, + permissions, + notPermissions, + }); + + if (!policy) { + ctx.status = 404; + ctx.body = { error: 'Project not found' }; + return; + } + + ctx.status = 201; + ctx.body = policy; +}); + +/** + * @openapi + * /projects/{projectId}/members: + * post: + * tags: + * - Projects + * summary: Add user to project + * description: Adds a user to a project with a specific policy. Only admins can add members. + * operationId: addUserToProject + * parameters: + * - name: projectId + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - userId + * - policyId + * properties: + * userId: + * type: string + * example: 'usr_V1StGXR8Z5jdHi6B' + * policyId: + * type: string + * example: 'pol_V1StGXR8Z5jdHi6B' + * responses: + * '201': + * description: User added to project successfully + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project, user, or policy not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.post('/projects/:projectId/members', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const { userId, policyId } = ctx.request.body as { + userId: string; + policyId: string; + }; + + const success = await addUserToProject({ + projectId: ctx.params.projectId, + userId, + policyId, + }); + + if (!success) { + ctx.status = 404; + ctx.body = { error: 'Project, user, or policy not found' }; + return; + } + + ctx.status = 201; +}); + export { projectsRouter }; diff --git a/packages/server/tests/unit/tests/permissionsFlow.test.ts b/packages/server/tests/unit/tests/permissionsFlow.test.ts new file mode 100644 index 00000000..58c1beda --- /dev/null +++ b/packages/server/tests/unit/tests/permissionsFlow.test.ts @@ -0,0 +1,292 @@ +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +/** + * Integration test for the full IAM permissions flow: + * + * 1. admin creates a project + * 2. admin creates a regular user + * 3. admin assigns the user to the project with a read-only policy + * 4. user can read a file using JWT + * 5. user cannot delete that file using JWT + * 6. user creates an API key scoped to the project + * 7. user assigns the deleteFile action to the API key + * 8. user can read the file using the API key + * 9. user cannot delete the file using the API key (membership policy intersection) + */ + +// ─── Group 1: Setup (Steps 1-3) ────────────────────────────────────────── + +describe('Group 1: Setup - Admin creates project, user, and assigns permissions', () => { + let adminToken: string; + let projectId: string; + let userId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + // Create project + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Test Project' }); + projectId = projectResponse.body.id; + + // Create user + const userResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'alice', password: 'alicepass' }); + userId = userResponse.body.id; + }); + + test('admin can create a project', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Test Project' }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.name).toBe('Test Project'); + }); + + test('admin can create a regular user', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'alice2', password: 'alicepass' }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.username).toBe('alice2'); + expect(response.body.role).toBe('user'); + }); + + test('admin creates a read-only policy for the project', async () => { + const response = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: ['files:DeleteFile'], + }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.permissions).toEqual(['files:GetFile']); + expect(response.body.notPermissions).toEqual(['files:DeleteFile']); + }); + + test('admin adds user to project with the read-only policy', async () => { + // First create the policy + const policyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: ['files:DeleteFile'], + }); + const policyId = policyResponse.body.id; + + const response = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ + userId, + policyId, + }); + + expect(response.status).toBe(201); + }); +}); + +// ─── Group 2: JWT Permissions (Steps 4-5) ───────────────────────────────── + +describe('Group 2: JWT Permissions - User can read but not delete file', () => { + let adminToken: string; + let projectId: string; + let userId: string; + let userToken: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + // Create project + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Test Project' }); + projectId = projectResponse.body.id; + + // Create user + const userResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'charlie', password: 'charliepass' }); + userId = userResponse.body.id; + userToken = await loginAs('charlie', 'charliepass'); + + // Create policy and add user to project + const policyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: ['files:DeleteFile'], + }); + const policyId = policyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ + userId, + policyId, + }); + + // Create file in project + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'test.txt', + storageType: 'local', + storagePath: '/tmp/test.txt', + }); + fileId = fileResponse.body.id; + }); + + test('user can read file with JWT', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files/${fileId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + expect(response.body.filename).toBe('test.txt'); + }); + + test('user cannot delete file with JWT', async () => { + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/files/${fileId}` + ); + + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); +}); + +// ─── Group 3: API Key Permissions (Steps 6-9) ───────────────────────────── + +describe('Group 3: API Key Permissions - Create key, assign permissions, test access', () => { + let adminToken: string; + let projectId: string; + let userId: string; + let userToken: string; + let apiKey: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + // Create project + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Test Project' }); + projectId = projectResponse.body.id; + + // Create user + const userResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'diana', password: 'dianapass' }); + userId = userResponse.body.id; + userToken = await loginAs('diana', 'dianapass'); + + // Create policy and add user to project + const policyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile', 'projects:GetProject'], + notPermissions: ['files:DeleteFile'], + }); + const policyId = policyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ + userId, + policyId, + }); + + // Create API key with delete permission + const newPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile', 'files:DeleteFile'], + }); + const newPolicyId = newPolicyResponse.body.id; + + const apiKeyResponse = await authenticatedTestClient(userToken) + .post('/api/v1/api-keys') + .send({ + projectId, + policyId: newPolicyId, + name: 'Test API Key', + }); + apiKey = apiKeyResponse.body.key; + + // Create file + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'test.txt', + storageType: 'local', + storagePath: '/tmp/test.txt', + }); + fileId = fileResponse.body.id; + }); + + test('user can create an API key for the project', async () => { + // First get the policy ID + const policiesResponse = await authenticatedTestClient(userToken).get( + `/api/v1/projects/${projectId}/policies` + ); + const policyId = policiesResponse.body[0].id; + + const response = await authenticatedTestClient(userToken) + .post('/api/v1/api-keys') + .send({ + projectId, + policyId, + name: 'Test API Key', + }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.name).toBe('Test API Key'); + expect(response.body.key).toMatch(/^sk_/); + expect(response.body.keyPrefix).toBe(response.body.key.slice(0, 8)); + }); + + test('user can read file using the API key', async () => { + const response = await testClient + .get(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${apiKey}`); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + expect(response.body.filename).toBe('test.txt'); + }); + + test('user cannot delete file using the API key due to policy intersection', async () => { + const response = await testClient + .delete(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${apiKey}`); + + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); +}); From c866ef861aecd435f9f34eaa884b04b1f9d32edd Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Wed, 8 Apr 2026 18:25:02 -0300 Subject: [PATCH 04/27] feat: add smoke tests --- .dockerignore | 6 + .github/instructions/general.instructions.md | 2 +- Dockerfile | 61 + docker-compose.test.yml | 49 + package.json | 1 + packages/postgresdb/src/models/ApiKey.ts | 18 +- .../postgresdb/src/models/ProjectPolicy.ts | 9 +- packages/postgresdb/src/models/UserProject.ts | 18 +- packages/server/package.json | 2 + packages/server/src/lib/files.ts | 89 + packages/server/src/lib/projects.ts | 11 +- packages/server/src/mcp/tools/files.ts | 67 +- .../server/src/rest/openapi/v1/files.yaml | 137 + packages/server/src/rest/v1/files.ts | 276 +- packages/server/src/rest/v1/projects.ts | 172 + packages/server/src/server.ts | 14 +- .../server/tests/unit/tests/apiKeys.test.ts | 241 + .../server/tests/unit/tests/files.test.ts | 286 ++ .../tests/unit/tests/permissionsFlow.test.ts | 777 ++++ .../server/tests/unit/tests/projects.test.ts | 428 +- .../blog/2019-05-28-first-blog-post.md | 2 +- .../website/blog/2019-05-29-long-blog-post.md | 2 +- .../website/blog/2021-08-26-welcome/index.md | 2 +- packages/website/docs/modules/files.md | 353 +- packages/website/docs/modules/iam.md | 148 +- packages/website/docusaurus.config.ts | 51 +- packages/website/package.json | 25 +- packages/website/sidebars.ts | 12 +- pnpm-lock.yaml | 3878 +++++++---------- tests/smoke-test.sh | 100 + 30 files changed, 4431 insertions(+), 2806 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.test.yml create mode 100644 packages/server/tests/unit/tests/apiKeys.test.ts create mode 100644 packages/server/tests/unit/tests/files.test.ts create mode 100755 tests/smoke-test.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6e4ce0ce --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +**/node_modules/ +**/dist/ +**/.turbo/ +**/coverage/ +**/*.log +.git/ diff --git a/.github/instructions/general.instructions.md b/.github/instructions/general.instructions.md index 951fbb26..12d6e966 100644 --- a/.github/instructions/general.instructions.md +++ b/.github/instructions/general.instructions.md @@ -34,4 +34,4 @@ If you need to change the database schema, read the instructions in `packages/po ## Documentation -All documentation must be written in English. +All documentation must be written in English. The target audience is developers with technical skills — write concisely and precisely, assume familiarity with REST APIs, JWT, and common backend concepts, and prefer code examples over prose descriptions. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..62337dfb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +FROM node:24-alpine AS builder + +WORKDIR /app + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy workspace manifests +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY packages/server/package.json ./packages/server/ +COPY packages/postgresdb/package.json ./packages/postgresdb/ +COPY packages/cli/package.json ./packages/cli/ + +# Use hoisted node_modules layout so devDep binaries (e.g. tsup) are resolvable +RUN echo "node-linker=hoisted" > .npmrc +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY packages/server ./packages/server +COPY packages/postgresdb ./packages/postgresdb + +# Build postgresdb first (server depends on it) +RUN pnpm --filter @soat/postgresdb build + +# Build server +RUN pnpm --filter @soat/server build + +# ---- Production image ---- +FROM node:24-alpine + +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy workspace manifests +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY packages/server/package.json ./packages/server/ +COPY packages/postgresdb/package.json ./packages/postgresdb/ +COPY packages/cli/package.json ./packages/cli/ + +# Install production dependencies only (skip lifecycle scripts like husky) +RUN pnpm install --frozen-lockfile --prod --ignore-scripts + +# Copy built artifacts +COPY --from=builder /app/packages/server/dist ./packages/server/dist +COPY --from=builder /app/packages/postgresdb/dist ./packages/postgresdb/dist + +# Mark dist/esm as ESM so Node.js parses import statements correctly +RUN echo '{"type":"module"}' > packages/server/dist/esm/package.json + +# Directory where uploaded files are persisted +ENV FILES_STORAGE_DIR=/data/files + +# Create the default storage directory +RUN mkdir -p /data/files + +VOLUME ["/data/files"] + +EXPOSE 5047 + +CMD ["node", "packages/server/dist/esm/server.js"] diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 00000000..5cfee69b --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,49 @@ +services: + database: + image: pgvector/pgvector:0.8.1-pg18-trixie + environment: + POSTGRES_DB: soat_test + POSTGRES_USER: soat_user + POSTGRES_PASSWORD: soat_password + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U soat_user -d soat_test'] + interval: 5s + timeout: 5s + retries: 10 + + server: + build: . + depends_on: + database: + condition: service_healthy + environment: + DATABASE_HOST: database + DATABASE_PORT: '5432' + DATABASE_NAME: soat_test + DATABASE_USER: soat_user + DATABASE_PASSWORD: soat_password + FILES_STORAGE_DIR: /data/files + healthcheck: + test: + - CMD + - node + - '-e' + - >- + require('http').get('http://localhost:5047/api/v1/users', + (r) => { process.exit(r.statusCode >= 200 ? 0 : 1); } + ).on('error', () => process.exit(1)) + interval: 5s + timeout: 10s + retries: 15 + start_period: 30s + + smoke-test: + image: alpine:latest + depends_on: + server: + condition: service_healthy + volumes: + - ./tests/smoke-test.sh:/smoke-test.sh:ro + environment: + SERVER_URL: 'http://server:5047' + command: sh -c "apk add --no-cache curl jq && sh /smoke-test.sh" diff --git a/package.json b/package.json index 8a04bcde..e187a2cc 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "scripts": { "prepare": "husky", + "smoke-test": "docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from smoke-test", "syncpack:fix": "syncpack fix", "syncpack:lint": "syncpack lint" }, diff --git a/packages/postgresdb/src/models/ApiKey.ts b/packages/postgresdb/src/models/ApiKey.ts index 53f29bdd..53c752e0 100644 --- a/packages/postgresdb/src/models/ApiKey.ts +++ b/packages/postgresdb/src/models/ApiKey.ts @@ -47,9 +47,12 @@ export class ApiKey extends Model { @Column({ type: DataType.INTEGER, allowNull: false }) declare projectId: number; - @BelongsTo(() => { - return Project; - }) + @BelongsTo( + () => { + return Project; + }, + { onDelete: 'CASCADE' } + ) declare project: Project; @ForeignKey(() => { @@ -58,9 +61,12 @@ export class ApiKey extends Model { @Column({ type: DataType.INTEGER, allowNull: false }) declare policyId: number; - @BelongsTo(() => { - return ProjectPolicy; - }) + @BelongsTo( + () => { + return ProjectPolicy; + }, + { onDelete: 'CASCADE' } + ) declare policy: ProjectPolicy; @Column({ diff --git a/packages/postgresdb/src/models/ProjectPolicy.ts b/packages/postgresdb/src/models/ProjectPolicy.ts index ccb623bf..86d2a3b5 100644 --- a/packages/postgresdb/src/models/ProjectPolicy.ts +++ b/packages/postgresdb/src/models/ProjectPolicy.ts @@ -34,9 +34,12 @@ export class ProjectPolicy extends Model { @Column({ type: DataType.INTEGER, allowNull: false }) declare projectId: number; - @BelongsTo(() => { - return Project; - }) + @BelongsTo( + () => { + return Project; + }, + { onDelete: 'CASCADE' } + ) declare project: Project; @Column({ diff --git a/packages/postgresdb/src/models/UserProject.ts b/packages/postgresdb/src/models/UserProject.ts index a24e33d0..07e45d3d 100644 --- a/packages/postgresdb/src/models/UserProject.ts +++ b/packages/postgresdb/src/models/UserProject.ts @@ -33,9 +33,12 @@ export class UserProject extends Model { @Column({ type: DataType.INTEGER, allowNull: false }) declare projectId: number; - @BelongsTo(() => { - return Project; - }) + @BelongsTo( + () => { + return Project; + }, + { onDelete: 'CASCADE' } + ) declare project: Project; @ForeignKey(() => { @@ -44,9 +47,12 @@ export class UserProject extends Model { @Column({ type: DataType.INTEGER, allowNull: false }) declare policyId: number; - @BelongsTo(() => { - return ProjectPolicy; - }) + @BelongsTo( + () => { + return ProjectPolicy; + }, + { onDelete: 'CASCADE' } + ) declare policy: ProjectPolicy; @Column({ type: DataType.DATE }) diff --git a/packages/server/package.json b/packages/server/package.json index 87cccecd..823e18fc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -10,10 +10,12 @@ "test": "jest --projects tests/unit" }, "dependencies": { + "@koa/multer": "^4.0.0", "@soat/postgresdb": "workspace:*", "@ttoss/http-server": "^0.5.9", "@ttoss/http-server-mcp": "^0.11.1", "@ttoss/postgresdb": "^0.8.0", + "@types/koa__multer": "^2.0.8", "bcryptjs": "^3.0.3", "dotenv": "^17.4.1", "jsonwebtoken": "^9.0.3", diff --git a/packages/server/src/lib/files.ts b/packages/server/src/lib/files.ts index 0e280165..ab30dfea 100644 --- a/packages/server/src/lib/files.ts +++ b/packages/server/src/lib/files.ts @@ -1,5 +1,17 @@ +import fs from 'node:fs'; +import path from 'node:path'; + import { db } from '../db'; +const getStorageDir = () => { + // eslint-disable-next-line turbo/no-undeclared-env-vars + const dir = process.env.FILES_STORAGE_DIR; + if (!dir) { + throw new Error('FILES_STORAGE_DIR environment variable is not set'); + } + return dir; +}; + const mapFile = (file: InstanceType<(typeof db)['File']>) => { return { id: file.publicId, @@ -35,6 +47,75 @@ export const getFile = async (args: { id: string }) => { }; }; +export const uploadFile = async (args: { + projectId: number; + fileBuffer: Buffer; + filename?: string; + contentType?: string; + metadata?: string; +}) => { + const storageDir = getStorageDir(); + fs.mkdirSync(storageDir, { recursive: true }); + + // Create DB record first to get publicId for the filename + const file = await db.File.create({ + projectId: args.projectId, + filename: args.filename, + contentType: args.contentType, + size: args.fileBuffer.length, + storageType: 'local' as const, + storagePath: '', // filled in below after we know the publicId + metadata: args.metadata, + }); + + const ext = args.filename ? path.extname(args.filename) : ''; + const storagePath = path.join(storageDir, `${file.publicId}${ext}`); + fs.writeFileSync(storagePath, args.fileBuffer); + + await file.update({ storagePath, size: args.fileBuffer.length }); + + return mapFile(file); +}; + +export const downloadFile = async (args: { id: string }) => { + const file = await db.File.findOne({ where: { publicId: args.id } }); + + if (!file) { + return null; + } + + if (file.storageType !== 'local') { + throw new Error( + `Storage type '${file.storageType}' download not supported` + ); + } + + if (!fs.existsSync(file.storagePath)) { + return null; + } + + return { + stream: fs.createReadStream(file.storagePath), + filename: file.filename, + contentType: file.contentType, + size: file.size, + }; +}; + +export const updateFileMetadata = async (args: { + id: string; + metadata: string; +}) => { + const file = await db.File.findOne({ where: { publicId: args.id } }); + + if (!file) { + return null; + } + + await file.update({ metadata: args.metadata }); + return mapFile(file); +}; + export const createFile = async (args: { projectId: number; filename?: string; @@ -55,6 +136,14 @@ export const deleteFile = async (args: { id: string }) => { return null; } + if (file.storageType === 'local' && file.storagePath) { + try { + fs.unlinkSync(file.storagePath); + } catch { + // Ignore missing file errors — record may still need to be cleaned up + } + } + await file.destroy(); return true; }; diff --git a/packages/server/src/lib/projects.ts b/packages/server/src/lib/projects.ts index 454f00f5..fd24314c 100644 --- a/packages/server/src/lib/projects.ts +++ b/packages/server/src/lib/projects.ts @@ -64,12 +64,15 @@ export const deleteProject = async (args: { id: string }) => { return true; }; -const mapPolicy = (policy: InstanceType<(typeof db)['ProjectPolicy']>) => { +const mapPolicy = ( + policy: InstanceType<(typeof db)['ProjectPolicy']>, + projectPublicId: string +) => { return { id: policy.publicId, permissions: policy.permissions, notPermissions: policy.notPermissions, - projectId: policy.projectId, + projectId: projectPublicId, createdAt: policy.createdAt, updatedAt: policy.updatedAt, }; @@ -87,7 +90,7 @@ export const listProjectPolicies = async (args: { projectId: string }) => { where: { projectId: project.id }, }); - return policies.map(mapPolicy); + return policies.map((policy) => mapPolicy(policy, project.publicId)); }; export const createProjectPolicy = async (args: { @@ -108,7 +111,7 @@ export const createProjectPolicy = async (args: { notPermissions: args.notPermissions || [], }); - return mapPolicy(policy); + return mapPolicy(policy, project.publicId); }; export const addUserToProject = async (args: { diff --git a/packages/server/src/mcp/tools/files.ts b/packages/server/src/mcp/tools/files.ts index 0552ebbb..e9dd3a5b 100644 --- a/packages/server/src/mcp/tools/files.ts +++ b/packages/server/src/mcp/tools/files.ts @@ -28,10 +28,75 @@ const registerTools = (server: McpServer) => { } ); + server.registerTool( + 'upload-file', + { + description: + 'Upload a file to the server. The file content must be provided as a base64-encoded string.', + inputSchema: { + projectId: z.string().describe('Project ID to associate the file with'), + content: z.string().describe('Base64-encoded file content'), + filename: z.string().optional().describe('Original filename'), + contentType: z + .string() + .optional() + .describe('MIME content type, e.g. text/plain'), + metadata: z + .string() + .optional() + .describe('Additional metadata as a JSON string'), + }, + }, + async ({ projectId, content, filename, contentType, metadata }) => { + const buffer = Buffer.from(content, 'base64'); + const data = await apiCall('POST', '/files/upload', { + body: { + projectId, + fileBuffer: buffer.toString('base64'), + filename, + contentType, + metadata, + }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'download-file', + { + description: 'Download a file by ID and return its content as base64', + inputSchema: { + id: z.string().describe('File ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/files/${id}/download`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'update-file-metadata', + { + description: 'Update the metadata of a file', + inputSchema: { + id: z.string().describe('File ID'), + metadata: z.string().describe('New metadata as a JSON string'), + }, + }, + async ({ id, metadata }) => { + const data = await apiCall('PATCH', `/files/${id}/metadata`, { + body: { metadata }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + server.registerTool( 'create-file', { - description: 'Create a new file record', + description: 'Create a file metadata record without uploading content', inputSchema: { storageType: z .enum(['local', 's3', 'gcs']) diff --git a/packages/server/src/rest/openapi/v1/files.yaml b/packages/server/src/rest/openapi/v1/files.yaml index 67054a2a..2bcbde3d 100644 --- a/packages/server/src/rest/openapi/v1/files.yaml +++ b/packages/server/src/rest/openapi/v1/files.yaml @@ -86,6 +86,52 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /files/upload: + post: + tags: + - Files + summary: Upload a file + description: Uploads a file to the server and stores it in the configured storage directory + operationId: uploadFile + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + - projectId + properties: + file: + type: string + format: binary + description: File content + projectId: + type: string + description: Project ID to associate the file with + example: 'proj_V1StGXR8Z5jdHi6B' + metadata: + type: string + description: Additional metadata as a JSON string + example: '{"author":"John"}' + responses: + '201': + description: File uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/FileRecord' + '400': + description: Missing file or invalid project + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' /files/{id}: get: tags: @@ -149,6 +195,84 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /files/{id}/download: + get: + tags: + - Files + summary: Download a file + description: Streams the file content to the client + operationId: downloadFile + parameters: + - name: id + in: path + required: true + description: File ID + schema: + type: string + example: 'fil_abc123' + responses: + '200': + description: File content + content: + application/octet-stream: + schema: + type: string + format: binary + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: File not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /files/{id}/metadata: + patch: + tags: + - Files + summary: Update file metadata + description: Updates the metadata field of a file + operationId: updateFileMetadata + parameters: + - name: id + in: path + required: true + description: File ID + schema: + type: string + example: 'fil_abc123' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - metadata + properties: + metadata: + type: string + description: New metadata as a JSON string + example: '{"author":"Jane","tags":["report"]}' + responses: + '200': + description: Metadata updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/FileRecord' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: File not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' components: schemas: FileRecord: @@ -198,4 +322,17 @@ components: error: type: string description: Error message + responses: + Unauthorized: + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Forbidden: + description: Insufficient permissions + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' example: 'File not found' diff --git a/packages/server/src/rest/v1/files.ts b/packages/server/src/rest/v1/files.ts index 684c3251..2fda5800 100644 --- a/packages/server/src/rest/v1/files.ts +++ b/packages/server/src/rest/v1/files.ts @@ -1,7 +1,18 @@ +import multer from '@koa/multer'; import { Router } from '@ttoss/http-server'; import type { Context } from 'src/Context'; import { db } from 'src/db'; -import { createFile, deleteFile, getFile, listFiles } from 'src/lib/files'; +import { + createFile, + deleteFile, + downloadFile, + getFile, + listFiles, + updateFileMetadata, + uploadFile, +} from 'src/lib/files'; + +const upload = multer({ storage: multer.memoryStorage() }); const filesRouter = new Router(); @@ -276,4 +287,267 @@ filesRouter.delete('/files/:id', async (ctx: Context) => { ctx.status = 204; }); +/** + * @openapi + * /files/upload: + * post: + * tags: + * - Files + * summary: Upload a file + * description: Uploads a file to the server and stores it in the configured storage directory + * operationId: uploadFile + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - file + * - projectId + * properties: + * file: + * type: string + * format: binary + * projectId: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * metadata: + * type: string + * example: '{"author":"John"}' + * responses: + * '201': + * description: File uploaded successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FileRecord' + * '400': + * description: Missing file or invalid project + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ +filesRouter.post( + '/files/upload', + upload.single('file'), + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { projectId?: string; metadata?: string }; + const file = (ctx as any).file as Express.Multer.File | undefined; + + if (!file) { + ctx.status = 400; + ctx.body = { error: 'No file provided' }; + return; + } + + if (!body.projectId) { + ctx.status = 400; + ctx.body = { error: 'projectId is required' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + body.projectId, + 'files:UploadFile' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ + where: { publicId: body.projectId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + const record = await uploadFile({ + projectId: project.id, + fileBuffer: file.buffer, + filename: file.originalname, + contentType: file.mimetype, + metadata: body.metadata, + }); + + ctx.status = 201; + ctx.body = record; + } +); + +/** + * @openapi + * /files/{id}/download: + * get: + * tags: + * - Files + * summary: Download a file + * description: Streams the file content to the client + * operationId: downloadFile + * parameters: + * - name: id + * in: path + * required: true + * description: File ID + * schema: + * type: string + * example: 'fil_abc123' + * responses: + * '200': + * description: File content + * content: + * application/octet-stream: + * schema: + * type: string + * format: binary + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: File not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.get('/files/:id/download', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const fileRecord = await getFile({ id: ctx.params.id }); + + if (!fileRecord) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + fileRecord.projectId!, + 'files:DownloadFile' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const result = await downloadFile({ id: ctx.params.id }); + + if (!result) { + ctx.status = 404; + ctx.body = { error: 'File not found on disk' }; + return; + } + + ctx.set('Content-Type', result.contentType ?? 'application/octet-stream'); + if (result.filename) { + ctx.set('Content-Disposition', `attachment; filename="${result.filename}"`); + } + if (result.size != null) { + ctx.set('Content-Length', String(result.size)); + } + ctx.body = result.stream; +}); + +/** + * @openapi + * /files/{id}/metadata: + * patch: + * tags: + * - Files + * summary: Update file metadata + * description: Updates the metadata field of a file + * operationId: updateFileMetadata + * parameters: + * - name: id + * in: path + * required: true + * description: File ID + * schema: + * type: string + * example: 'fil_abc123' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - metadata + * properties: + * metadata: + * type: string + * example: '{"author":"Jane","tags":["report"]}' + * responses: + * '200': + * description: Metadata updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FileRecord' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: File not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.patch('/files/:id/metadata', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const fileRecord = await getFile({ id: ctx.params.id }); + + if (!fileRecord) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + fileRecord.projectId!, + 'files:UpdateFileMetadata' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const body = ctx.request.body as { metadata: string }; + const updated = await updateFileMetadata({ + id: ctx.params.id, + metadata: body.metadata, + }); + + ctx.body = updated; +}); + export { filesRouter }; diff --git a/packages/server/src/rest/v1/projects.ts b/packages/server/src/rest/v1/projects.ts index dc95003a..416d2e74 100644 --- a/packages/server/src/rest/v1/projects.ts +++ b/packages/server/src/rest/v1/projects.ts @@ -4,7 +4,10 @@ import { addUserToProject, createProject, createProjectPolicy, + deleteProject, + getProject, listProjectPolicies, + listProjects, } from 'src/lib/projects'; const projectsRouter = new Router(); @@ -325,4 +328,173 @@ projectsRouter.post('/projects/:projectId/members', async (ctx: Context) => { ctx.status = 201; }); +/** + * @openapi + * /projects: + * get: + * tags: + * - Projects + * summary: List projects + * description: Admins see all projects. Members see only their own projects. + * operationId: listProjects + * responses: + * '200': + * description: List of projects returned successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ProjectRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.get('/projects', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const projects = await listProjects({ authUser: ctx.authUser }); + ctx.body = projects; +}); + +/** + * @openapi + * /projects/{id}: + * get: + * tags: + * - Projects + * summary: Get a project + * description: Admins can get any project. Members can only get projects they belong to. + * operationId: getProject + * parameters: + * - name: id + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: Project returned successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProjectRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.get('/projects/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const result = await getProject({ + id: ctx.params.id, + authUser: ctx.authUser, + }); + + if (result === 'not_found') { + ctx.status = 404; + ctx.body = { error: 'Project not found' }; + return; + } + + if (result === 'forbidden') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = result; +}); + +/** + * @openapi + * /projects/{id}: + * delete: + * tags: + * - Projects + * summary: Delete a project + * description: Only admins can delete projects. + * operationId: deleteProject + * parameters: + * - name: id + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * responses: + * '204': + * description: Project deleted successfully + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.delete('/projects/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const result = await deleteProject({ id: ctx.params.id }); + + if (!result) { + ctx.status = 404; + ctx.body = { error: 'Project not found' }; + return; + } + + ctx.status = 204; +}); + export { projectsRouter }; diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index adf8c621..3623a343 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,10 +1,8 @@ /* eslint-disable turbo/no-undeclared-env-vars */ import 'dotenv/config'; -import { models } from '@soat/postgresdb'; -import { initialize } from '@ttoss/postgresdb'; - import { app } from './app'; +import { initializeDatabase } from './db'; /** * SOAT = 5047 @@ -13,14 +11,8 @@ const SOAT_PORT = process.env.PORT || 5047; const startServer = async () => { try { - await initialize({ - models, - host: process.env.DATABASE_HOST, - port: Number(process.env.DATABASE_PORT), - database: process.env.DATABASE_NAME, - username: process.env.DATABASE_USER, - password: process.env.DATABASE_PASSWORD, - }); + const database = await initializeDatabase(app); + await database.sequelize.sync(); } catch (error) { // eslint-disable-next-line no-console console.error('Failed to connect to database:', error); diff --git a/packages/server/tests/unit/tests/apiKeys.test.ts b/packages/server/tests/unit/tests/apiKeys.test.ts new file mode 100644 index 00000000..81e1f42e --- /dev/null +++ b/packages/server/tests/unit/tests/apiKeys.test.ts @@ -0,0 +1,241 @@ +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +describe('API Keys', () => { + let adminToken: string; + let aliceToken: string; + let aliceId: string; + let bobToken: string; + let projectId: string; + let policyId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const aliceRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'alice', password: 'alicepass' }); + aliceId = aliceRes.body.id; + aliceToken = await loginAs('alice', 'alicepass'); + + await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'bob', password: 'bobpass' }); + bobToken = await loginAs('bob', 'bobpass'); + + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Test Project' }); + projectId = projectRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['files:read'] }); + policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: aliceId, policyId }); + }); + + describe('POST /api/v1/api-keys', () => { + test('returns 401 if not authenticated', async () => { + const response = await testClient + .post('/api/v1/api-keys') + .send({ projectId, policyId, name: 'My Key' }); + + expect(response.status).toBe(401); + }); + + test('returns 400 if required fields are missing', async () => { + const response = await authenticatedTestClient(aliceToken) + .post('/api/v1/api-keys') + .send({ name: 'My Key' }); + + expect(response.status).toBe(400); + }); + + test('returns 400 if project does not exist', async () => { + const response = await authenticatedTestClient(aliceToken) + .post('/api/v1/api-keys') + .send({ projectId: 'proj_nonexistent', policyId, name: 'My Key' }); + + expect(response.status).toBe(400); + }); + + test('returns 403 if user is not a member of the project', async () => { + const response = await authenticatedTestClient(bobToken) + .post('/api/v1/api-keys') + .send({ projectId, policyId, name: 'Bob Key' }); + + expect(response.status).toBe(403); + }); + + test('returns 400 if policy does not belong to the project', async () => { + const otherProjectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Other Project' }); + const otherProjectId = otherProjectRes.body.id; + + const otherPolicyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${otherProjectId}/policies`) + .send({ permissions: ['files:read'] }); + const otherPolicyId = otherPolicyRes.body.id; + + const response = await authenticatedTestClient(aliceToken) + .post('/api/v1/api-keys') + .send({ projectId, policyId: otherPolicyId, name: 'My Key' }); + + expect(response.status).toBe(400); + }); + + test('returns 201 and the full key on success', async () => { + const response = await authenticatedTestClient(aliceToken) + .post('/api/v1/api-keys') + .send({ projectId, policyId, name: 'Alice Key' }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.name).toBe('Alice Key'); + expect(response.body.key).toBeDefined(); + expect(response.body.keyPrefix).toBeDefined(); + expect(response.body.createdAt).toBeDefined(); + expect(response.body.updatedAt).toBeDefined(); + }); + }); + + describe('GET /api/v1/api-keys/:id', () => { + let apiKeyId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(aliceToken) + .post('/api/v1/api-keys') + .send({ projectId, policyId, name: 'Get Test Key' }); + apiKeyId = res.body.id; + }); + + test('returns 401 if not authenticated', async () => { + const response = await testClient.get(`/api/v1/api-keys/${apiKeyId}`); + + expect(response.status).toBe(401); + }); + + test('returns 404 if api key does not exist', async () => { + const response = await authenticatedTestClient(aliceToken).get( + '/api/v1/api-keys/key_nonexistent' + ); + + expect(response.status).toBe(404); + }); + + test('returns 403 if user does not own the api key', async () => { + const response = await authenticatedTestClient(bobToken).get( + `/api/v1/api-keys/${apiKeyId}` + ); + + expect(response.status).toBe(403); + }); + + test('returns 200 and api key data without the full key', async () => { + const response = await authenticatedTestClient(aliceToken).get( + `/api/v1/api-keys/${apiKeyId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(apiKeyId); + expect(response.body.name).toBe('Get Test Key'); + expect(response.body.keyPrefix).toBeDefined(); + expect(response.body.key).toBeUndefined(); + expect(response.body.createdAt).toBeDefined(); + expect(response.body.updatedAt).toBeDefined(); + }); + }); + + describe('PUT /api/v1/api-keys/:id', () => { + let apiKeyId: string; + let newPolicyId: string; + + beforeAll(async () => { + const keyRes = await authenticatedTestClient(aliceToken) + .post('/api/v1/api-keys') + .send({ projectId, policyId, name: 'Update Test Key' }); + apiKeyId = keyRes.body.id; + + const newPolicyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['files:write'] }); + newPolicyId = newPolicyRes.body.id; + }); + + test('returns 401 if not authenticated', async () => { + const response = await testClient + .put(`/api/v1/api-keys/${apiKeyId}`) + .send({ policyId: newPolicyId }); + + expect(response.status).toBe(401); + }); + + test('returns 400 if policyId is missing', async () => { + const response = await authenticatedTestClient(aliceToken) + .put(`/api/v1/api-keys/${apiKeyId}`) + .send({}); + + expect(response.status).toBe(400); + }); + + test('returns 400 if policy does not exist', async () => { + const response = await authenticatedTestClient(aliceToken) + .put(`/api/v1/api-keys/${apiKeyId}`) + .send({ policyId: 'policy_nonexistent' }); + + expect(response.status).toBe(400); + }); + + test('returns 404 if api key does not exist', async () => { + const response = await authenticatedTestClient(aliceToken) + .put('/api/v1/api-keys/key_nonexistent') + .send({ policyId: newPolicyId }); + + expect(response.status).toBe(404); + }); + + test('returns 403 if user does not own the api key', async () => { + const response = await authenticatedTestClient(bobToken) + .put(`/api/v1/api-keys/${apiKeyId}`) + .send({ policyId: newPolicyId }); + + expect(response.status).toBe(403); + }); + + test('returns 400 if policy belongs to a different project', async () => { + const otherProjectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Other Update Project' }); + const otherProjectId = otherProjectRes.body.id; + + const otherPolicyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${otherProjectId}/policies`) + .send({ permissions: ['files:read'] }); + const otherPolicyId = otherPolicyRes.body.id; + + const response = await authenticatedTestClient(aliceToken) + .put(`/api/v1/api-keys/${apiKeyId}`) + .send({ policyId: otherPolicyId }); + + expect(response.status).toBe(400); + }); + + test('returns 200 and the updated api key', async () => { + const response = await authenticatedTestClient(aliceToken) + .put(`/api/v1/api-keys/${apiKeyId}`) + .send({ policyId: newPolicyId }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(apiKeyId); + expect(response.body.policyId).toBe(newPolicyId); + }); + }); +}); diff --git a/packages/server/tests/unit/tests/files.test.ts b/packages/server/tests/unit/tests/files.test.ts new file mode 100644 index 00000000..001ad0c9 --- /dev/null +++ b/packages/server/tests/unit/tests/files.test.ts @@ -0,0 +1,286 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +describe('Files', () => { + let adminToken: string; + let userToken: string; + let userId: string; + let projectId: string; + let policyId: string; + let storageDir: string; + + beforeAll(async () => { + storageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'soat-files-test-')); + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.FILES_STORAGE_DIR = storageDir; + + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const createUserRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'filesuser', password: 'filespass' }); + + userId = createUserRes.body.id; + userToken = await loginAs('filesuser', 'filespass'); + + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Files Test Project' }); + projectId = projectRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: [ + 'files:UploadFile', + 'files:GetFile', + 'files:DownloadFile', + 'files:UpdateFileMetadata', + 'files:DeleteFile', + 'files:CreateFile', + ], + }); + policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + }); + + afterAll(() => { + fs.rmSync(storageDir, { recursive: true, force: true }); + }); + + describe('POST /api/v1/files/upload', () => { + test('authenticated user with permission can upload a file', async () => { + const fileContent = Buffer.from('Hello, world!'); + + const response = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { + filename: 'hello.txt', + contentType: 'text/plain', + }) + .field('projectId', projectId); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.filename).toBe('hello.txt'); + expect(response.body.contentType).toBe('text/plain'); + expect(response.body.size).toBe(fileContent.length); + }); + + test('unauthenticated request cannot upload', async () => { + const fileContent = Buffer.from('data'); + + const response = await testClient + .post('/api/v1/files/upload') + .attach('file', fileContent, { filename: 'data.txt' }) + .field('projectId', projectId); + + expect(response.status).toBe(401); + }); + + test('upload without file returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .send({ projectId }); + + expect(response.status).toBe(400); + }); + + test('upload without projectId returns 400', async () => { + const fileContent = Buffer.from('data'); + + const response = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { filename: 'data.txt' }); + + expect(response.status).toBe(400); + }); + }); + + describe('GET /api/v1/files/:id', () => { + let fileId: string; + + beforeAll(async () => { + const fileContent = Buffer.from('Get me!'); + const res = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { + filename: 'getme.txt', + contentType: 'text/plain', + }) + .field('projectId', projectId); + fileId = res.body.id; + }); + + test('user with permission can get a file by ID', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files/${fileId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + expect(response.body.filename).toBe('getme.txt'); + }); + + test('unauthenticated request cannot get a file', async () => { + const response = await testClient.get(`/api/v1/files/${fileId}`); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent file', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/files/nonexistent-file-id' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('GET /api/v1/files/:id/download', () => { + let fileId: string; + const originalContent = 'Download me, please!'; + + beforeAll(async () => { + const fileContent = Buffer.from(originalContent); + const res = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { + filename: 'download.txt', + contentType: 'text/plain', + }) + .field('projectId', projectId); + fileId = res.body.id; + }); + + test('user with permission can download a file', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files/${fileId}/download` + ); + + expect(response.status).toBe(200); + expect(response.text).toBe(originalContent); + expect(response.headers['content-type']).toMatch(/text\/plain/); + expect(response.headers['content-disposition']).toMatch( + /attachment; filename="download.txt"/ + ); + }); + + test('unauthenticated request cannot download a file', async () => { + const response = await testClient.get(`/api/v1/files/${fileId}/download`); + + expect(response.status).toBe(401); + }); + }); + + describe('PATCH /api/v1/files/:id/metadata', () => { + let fileId: string; + + beforeAll(async () => { + const fileContent = Buffer.from('Metadata target'); + const res = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { + filename: 'meta.txt', + contentType: 'text/plain', + }) + .field('projectId', projectId); + fileId = res.body.id; + }); + + test('user with permission can update file metadata', async () => { + const newMetadata = JSON.stringify({ author: 'Alice', version: 2 }); + + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/files/${fileId}/metadata`) + .send({ metadata: newMetadata }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + expect(response.body.metadata).toBe(newMetadata); + }); + + test('unauthenticated request cannot update metadata', async () => { + const response = await testClient + .patch(`/api/v1/files/${fileId}/metadata`) + .send({ metadata: '{}' }); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent file', async () => { + const response = await authenticatedTestClient(adminToken) + .patch('/api/v1/files/nonexistent-file-id/metadata') + .send({ metadata: '{}' }); + + expect(response.status).toBe(404); + }); + }); + + describe('DELETE /api/v1/files/:id', () => { + test('user with permission can delete a file and it is removed from disk', async () => { + const fileContent = Buffer.from('Delete me!'); + const uploadRes = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { + filename: 'todelete.txt', + contentType: 'text/plain', + }) + .field('projectId', projectId); + const fileId = uploadRes.body.id; + + // Verify file exists on disk + const filesOnDisk = fs.readdirSync(storageDir); + expect( + filesOnDisk.some((f) => { + return f.includes(fileId); + }) + ).toBe(true); + + const deleteRes = await authenticatedTestClient(userToken).delete( + `/api/v1/files/${fileId}` + ); + + expect(deleteRes.status).toBe(204); + + // Verify file is removed from disk + const filesAfter = fs.readdirSync(storageDir); + expect( + filesAfter.some((f) => { + return f.includes(fileId); + }) + ).toBe(false); + }); + + test('unauthenticated request cannot delete a file', async () => { + const fileContent = Buffer.from('Protected'); + const uploadRes = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { filename: 'protected.txt' }) + .field('projectId', projectId); + const fileId = uploadRes.body.id; + + const response = await testClient.delete(`/api/v1/files/${fileId}`); + + expect(response.status).toBe(401); + }); + + test('returns 404 when deleting a non-existent file', async () => { + const response = await authenticatedTestClient(adminToken).delete( + '/api/v1/files/nonexistent-file-id' + ); + + expect(response.status).toBe(404); + }); + }); +}); diff --git a/packages/server/tests/unit/tests/permissionsFlow.test.ts b/packages/server/tests/unit/tests/permissionsFlow.test.ts index 58c1beda..0c8023a9 100644 --- a/packages/server/tests/unit/tests/permissionsFlow.test.ts +++ b/packages/server/tests/unit/tests/permissionsFlow.test.ts @@ -290,3 +290,780 @@ describe('Group 3: API Key Permissions - Create key, assign permissions, test ac expect(response.body.error).toBe('Forbidden'); }); }); + +// ─── Group 4: Multiple Users with Different Policies ───────────────────── + +describe('Group 4: Two users in the same project with different policies', () => { + let adminToken: string; + let projectId: string; + let readerToken: string; + let editorToken: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Multi-User Project' }); + projectId = projectResponse.body.id; + + const readerUserResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'frank', password: 'frankpass' }); + readerToken = await loginAs('frank', 'frankpass'); + + const editorUserResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'grace', password: 'gracepass' }); + editorToken = await loginAs('grace', 'gracepass'); + + const readPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: [], + }); + const readPolicyId = readPolicyResponse.body.id; + + const editPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile', 'files:DeleteFile'], + notPermissions: [], + }); + const editPolicyId = editPolicyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: readerUserResponse.body.id, policyId: readPolicyId }); + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: editorUserResponse.body.id, policyId: editPolicyId }); + + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'shared.txt', + storageType: 'local', + storagePath: '/tmp/shared.txt', + }); + fileId = fileResponse.body.id; + }); + + test('reader user can read the file', async () => { + const response = await authenticatedTestClient(readerToken).get( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('reader user cannot delete the file', async () => { + const response = await authenticatedTestClient(readerToken).delete( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); + + test('editor user can read the file', async () => { + const response = await authenticatedTestClient(editorToken).get( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('editor user can delete the file', async () => { + const response = await authenticatedTestClient(editorToken).delete( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(204); + }); +}); + +// ─── Group 5: User with Multiple API Keys ───────────────────────────────── + +describe('Group 5: User with multiple API keys scoped to different permissions', () => { + let adminToken: string; + let projectId: string; + let userToken: string; + let readOnlyApiKey: string; + let deleteOnlyApiKey: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Multiple API Keys Project' }); + projectId = projectResponse.body.id; + + const userResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'howard', password: 'howardpass' }); + userToken = await loginAs('howard', 'howardpass'); + + // User membership policy: allows both read and delete + const memberPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile', 'files:DeleteFile'], + notPermissions: [], + }); + const memberPolicyId = memberPolicyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: userResponse.body.id, policyId: memberPolicyId }); + + // API key policy 1: read-only + const readKeyPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: [], + }); + const readKeyPolicyId = readKeyPolicyResponse.body.id; + + // API key policy 2: delete-only + const deleteKeyPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:DeleteFile'], + notPermissions: [], + }); + const deleteKeyPolicyId = deleteKeyPolicyResponse.body.id; + + const readKeyResponse = await authenticatedTestClient(userToken) + .post('/api/v1/api-keys') + .send({ projectId, policyId: readKeyPolicyId, name: 'Read Key' }); + readOnlyApiKey = readKeyResponse.body.key; + + const deleteKeyResponse = await authenticatedTestClient(userToken) + .post('/api/v1/api-keys') + .send({ projectId, policyId: deleteKeyPolicyId, name: 'Delete Key' }); + deleteOnlyApiKey = deleteKeyResponse.body.key; + + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'target.txt', + storageType: 'local', + storagePath: '/tmp/target.txt', + }); + fileId = fileResponse.body.id; + }); + + test('read-only API key can read the file', async () => { + const response = await testClient + .get(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${readOnlyApiKey}`); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('read-only API key cannot delete the file', async () => { + const response = await testClient + .delete(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${readOnlyApiKey}`); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); + + test('delete-only API key cannot read the file', async () => { + const response = await testClient + .get(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${deleteOnlyApiKey}`); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); + + test('delete-only API key can delete the file', async () => { + const response = await testClient + .delete(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${deleteOnlyApiKey}`); + expect(response.status).toBe(204); + }); +}); + +// ─── Group 6: API Key Project Isolation ────────────────────────────────── + +describe('Group 6: API key cannot access files in a different project', () => { + let adminToken: string; + let projectAId: string; + let projectBId: string; + let userToken: string; + let apiKey: string; + let fileInProjectA: string; + let fileInProjectB: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const projectAResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Project Alpha' }); + projectAId = projectAResponse.body.id; + + const projectBResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Project Beta' }); + projectBId = projectBResponse.body.id; + + const userResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'ivan', password: 'ivanpass' }); + userToken = await loginAs('ivan', 'ivanpass'); + + const policyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectAId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: [], + }); + const policyId = policyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectAId}/members`) + .send({ userId: userResponse.body.id, policyId }); + + const apiKeyResponse = await authenticatedTestClient(userToken) + .post('/api/v1/api-keys') + .send({ projectId: projectAId, policyId, name: 'Project A Key' }); + apiKey = apiKeyResponse.body.key; + + const fileAResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId: projectAId, + filename: 'alpha.txt', + storageType: 'local', + storagePath: '/tmp/alpha.txt', + }); + fileInProjectA = fileAResponse.body.id; + + const fileBResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId: projectBId, + filename: 'beta.txt', + storageType: 'local', + storagePath: '/tmp/beta.txt', + }); + fileInProjectB = fileBResponse.body.id; + }); + + test('API key can read a file from its own project', async () => { + const response = await testClient + .get(`/api/v1/files/${fileInProjectA}`) + .set('Authorization', `Bearer ${apiKey}`); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileInProjectA); + }); + + test('API key cannot read a file from a different project', async () => { + const response = await testClient + .get(`/api/v1/files/${fileInProjectB}`) + .set('Authorization', `Bearer ${apiKey}`); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); +}); + +// ─── Group 7: Wildcard * Policy ─────────────────────────────────────────── + +describe('Group 7: Policy with wildcard * grants all permissions', () => { + let adminToken: string; + let projectId: string; + let userToken: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Wildcard Project' }); + projectId = projectResponse.body.id; + + const userResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'julia', password: 'juliapass' }); + userToken = await loginAs('julia', 'juliapass'); + + const policyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['*'], + notPermissions: [], + }); + const policyId = policyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: userResponse.body.id, policyId }); + + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'wildcard.txt', + storageType: 'local', + storagePath: '/tmp/wildcard.txt', + }); + fileId = fileResponse.body.id; + }); + + test('user with wildcard * policy can read the file', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('user with wildcard * policy can delete the file', async () => { + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(204); + }); +}); + +// ─── Group 8: Namespace Wildcard files:* ────────────────────────────────── + +describe('Group 8: Policy with files:* grants all file-namespace permissions', () => { + let adminToken: string; + let projectId: string; + let userToken: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Namespace Wildcard Project' }); + projectId = projectResponse.body.id; + + const userResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'kevin', password: 'kevinpass' }); + userToken = await loginAs('kevin', 'kevinpass'); + + const policyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:*'], + notPermissions: [], + }); + const policyId = policyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: userResponse.body.id, policyId }); + + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'namespace.txt', + storageType: 'local', + storagePath: '/tmp/namespace.txt', + }); + fileId = fileResponse.body.id; + }); + + test('user with files:* policy can read the file', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('user with files:* policy can delete the file', async () => { + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(204); + }); +}); + +// ─── Group 9: notPermissions Takes Precedence ───────────────────────────── + +describe('Group 9: notPermissions overrides permissions when action appears in both', () => { + let adminToken: string; + let projectId: string; + let userToken: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Conflict Policy Project' }); + projectId = projectResponse.body.id; + + const userResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'luna', password: 'lunapass' }); + userToken = await loginAs('luna', 'lunapass'); + + // Same action appears in both permissions and notPermissions — deny wins + const policyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile', 'files:DeleteFile'], + notPermissions: ['files:DeleteFile'], + }); + const policyId = policyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: userResponse.body.id, policyId }); + + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'conflict.txt', + storageType: 'local', + storagePath: '/tmp/conflict.txt', + }); + fileId = fileResponse.body.id; + }); + + test('user can read the file (allowed by permissions, not blocked by notPermissions)', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('user cannot delete the file even though it is in permissions (notPermissions wins)', async () => { + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); +}); + +// ─── Group 10: User Without Project Membership ──────────────────────────── + +describe('Group 10: User without project membership is denied access to all file operations', () => { + let adminToken: string; + let projectId: string; + let outsiderToken: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Private Project' }); + projectId = projectResponse.body.id; + + // Create outsider user — deliberately NOT added to the project + await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'mark', password: 'markpass' }); + outsiderToken = await loginAs('mark', 'markpass'); + + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'private.txt', + storageType: 'local', + storagePath: '/tmp/private.txt', + }); + fileId = fileResponse.body.id; + }); + + test('outsider user cannot read the file (no project membership)', async () => { + const response = await authenticatedTestClient(outsiderToken).get( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); + + test('outsider user cannot delete the file (no project membership)', async () => { + const response = await authenticatedTestClient(outsiderToken).delete( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); +}); + +// ─── Group 11: Multiple Admins ──────────────────────────────────────────── + +describe('Group 11: Multiple admins can all manage projects and bypass policy checks', () => { + let admin1Token: string; + let admin2Token: string; + let projectId: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + admin1Token = await loginAs('admin', 'supersecret'); + + // Admin1 promotes a second user to admin role + await authenticatedTestClient(admin1Token) + .post('/api/v1/users') + .send({ username: 'nina', password: 'ninapass', role: 'admin' }); + admin2Token = await loginAs('nina', 'ninapass'); + + const projectResponse = await authenticatedTestClient(admin1Token) + .post('/api/v1/projects') + .send({ name: 'Admin1 Project' }); + projectId = projectResponse.body.id; + + const fileResponse = await authenticatedTestClient(admin1Token) + .post('/api/v1/files') + .send({ + projectId, + filename: 'admin-file.txt', + storageType: 'local', + storagePath: '/tmp/admin-file.txt', + }); + fileId = fileResponse.body.id; + }); + + test('admin2 can create a project independently', async () => { + const response = await authenticatedTestClient(admin2Token) + .post('/api/v1/projects') + .send({ name: 'Admin2 Project' }); + expect(response.status).toBe(201); + expect(response.body.name).toBe('Admin2 Project'); + }); + + test('admin2 can read files in a project created by admin1', async () => { + const response = await authenticatedTestClient(admin2Token).get( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('admin2 can create policies in a project created by admin1', async () => { + const response = await authenticatedTestClient(admin2Token) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: [], + }); + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + }); + + test('admin2 can delete files in a project created by admin1', async () => { + const response = await authenticatedTestClient(admin2Token).delete( + `/api/v1/files/${fileId}` + ); + expect(response.status).toBe(204); + }); +}); + +// ─── Group 12: Multiple Users with Multiple API Keys ────────────────────── + +describe('Group 12: Multiple users each with multiple API keys in the same project', () => { + let adminToken: string; + let projectId: string; + let user1ReadKey: string; + let user1DeleteKey: string; + let user2Key: string; + let fileId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const projectResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Multi-User API Keys Project' }); + projectId = projectResponse.body.id; + + // User1 membership: full permissions + const user1Response = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'oscar', password: 'oscarpass' }); + const user1Token = await loginAs('oscar', 'oscarpass'); + + const fullPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile', 'files:DeleteFile'], + notPermissions: [], + }); + const fullPolicyId = fullPolicyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: user1Response.body.id, policyId: fullPolicyId }); + + // User2 membership: read-only + const user2Response = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'patricia', password: 'patriciapass' }); + const user2Token = await loginAs('patricia', 'patriciapass'); + + const readOnlyPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: [], + }); + const readOnlyPolicyId = readOnlyPolicyResponse.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: user2Response.body.id, policyId: readOnlyPolicyId }); + + // API key policies + const readKeyPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile'], + notPermissions: [], + }); + const readKeyPolicyId = readKeyPolicyResponse.body.id; + + const deleteKeyPolicyResponse = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:GetFile', 'files:DeleteFile'], + notPermissions: [], + }); + const deleteKeyPolicyId = deleteKeyPolicyResponse.body.id; + + // User1 creates two API keys: one read-only, one full + const u1ReadKeyResponse = await authenticatedTestClient(user1Token) + .post('/api/v1/api-keys') + .send({ projectId, policyId: readKeyPolicyId, name: 'Oscar Read Key' }); + user1ReadKey = u1ReadKeyResponse.body.key; + + const u1DeleteKeyResponse = await authenticatedTestClient(user1Token) + .post('/api/v1/api-keys') + .send({ + projectId, + policyId: deleteKeyPolicyId, + name: 'Oscar Delete Key', + }); + user1DeleteKey = u1DeleteKeyResponse.body.key; + + // User2 creates a key with full key policy — but membership is read-only, + // so the effective permission (intersection) is still read-only + const u2KeyResponse = await authenticatedTestClient(user2Token) + .post('/api/v1/api-keys') + .send({ + projectId, + policyId: deleteKeyPolicyId, + name: 'Patricia Key', + }); + user2Key = u2KeyResponse.body.key; + + const fileResponse = await authenticatedTestClient(adminToken) + .post('/api/v1/files') + .send({ + projectId, + filename: 'multi.txt', + storageType: 'local', + storagePath: '/tmp/multi.txt', + }); + fileId = fileResponse.body.id; + }); + + test('user1 read key can read the file', async () => { + const response = await testClient + .get(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${user1ReadKey}`); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('user1 read key cannot delete the file (key policy is read-only)', async () => { + const response = await testClient + .delete(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${user1ReadKey}`); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); + + test('user2 key can read the file', async () => { + const response = await testClient + .get(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${user2Key}`); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('user2 key cannot delete the file (membership policy is read-only, intersection blocks delete)', async () => { + const response = await testClient + .delete(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${user2Key}`); + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + }); + + test('user1 delete key can read the file', async () => { + const response = await testClient + .get(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${user1DeleteKey}`); + expect(response.status).toBe(200); + expect(response.body.id).toBe(fileId); + }); + + test('user1 delete key can delete the file', async () => { + const response = await testClient + .delete(`/api/v1/files/${fileId}`) + .set('Authorization', `Bearer ${user1DeleteKey}`); + expect(response.status).toBe(204); + }); +}); diff --git a/packages/server/tests/unit/tests/projects.test.ts b/packages/server/tests/unit/tests/projects.test.ts index 6a696de4..f96ed4b3 100644 --- a/packages/server/tests/unit/tests/projects.test.ts +++ b/packages/server/tests/unit/tests/projects.test.ts @@ -1,7 +1,9 @@ import { authenticatedTestClient, loginAs, testClient } from '../testClient'; -describe('POST /api/v1/projects', () => { +describe('Projects', () => { let adminToken: string; + let userToken: string; + let userId: string; beforeAll(async () => { await testClient @@ -9,17 +11,423 @@ describe('POST /api/v1/projects', () => { .send({ username: 'admin', password: 'supersecret' }); adminToken = await loginAs('admin', 'supersecret'); + + const createUserRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'alice', password: 'alicepass' }); + + userId = createUserRes.body.id; + userToken = await loginAs('alice', 'alicepass'); + }); + + describe('POST /api/v1/projects', () => { + test('admin can create a project', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'My Project' }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.name).toBe('My Project'); + expect(response.body.createdAt).toBeDefined(); + expect(response.body.updatedAt).toBeDefined(); + }); + + test('unauthenticated request cannot create a project', async () => { + const response = await testClient + .post('/api/v1/projects') + .send({ name: 'Unauthorized Project' }); + + expect(response.status).toBe(401); + }); + + test('non-admin user cannot create a project', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/projects') + .send({ name: 'Forbidden Project' }); + + expect(response.status).toBe(403); + }); + }); + + describe('GET /api/v1/projects', () => { + test('admin can list all projects', async () => { + const response = + await authenticatedTestClient(adminToken).get('/api/v1/projects'); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('unauthenticated request cannot list projects', async () => { + const response = await testClient.get('/api/v1/projects'); + + expect(response.status).toBe(401); + }); + + test('user only sees projects they are a member of', async () => { + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Member Project' }); + const memberProjectId = projectRes.body.id; + + await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Other Project' }); + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${memberProjectId}/policies`) + .send({ permissions: ['projects:GetProject'] }); + const policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${memberProjectId}/members`) + .send({ userId, policyId }); + + const response = + await authenticatedTestClient(userToken).get('/api/v1/projects'); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect( + response.body.some((p: { id: string }) => { + return p.id === memberProjectId; + }) + ).toBe(true); + expect(response.body.length).toBe(1); + }); + }); + + describe('GET /api/v1/projects/:id', () => { + let projectId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Gettable Project' }); + projectId = res.body.id; + }); + + test('admin can get any project', async () => { + const response = await authenticatedTestClient(adminToken).get( + `/api/v1/projects/${projectId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(projectId); + expect(response.body.name).toBe('Gettable Project'); + }); + + test('unauthenticated request cannot get a project', async () => { + const response = await testClient.get(`/api/v1/projects/${projectId}`); + + expect(response.status).toBe(401); + }); + + test('user cannot get a project they are not a member of', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/projects/${projectId}` + ); + + expect(response.status).toBe(403); + }); + + test('user can get a project they are a member of', async () => { + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['projects:GetProject'] }); + const policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + + const response = await authenticatedTestClient(userToken).get( + `/api/v1/projects/${projectId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(projectId); + }); + + test('returns 404 for unknown project id', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/projects/proj_nonexistent12345' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('DELETE /api/v1/projects/:id', () => { + test('admin can delete a project', async () => { + const createRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'To Delete' }); + const { id } = createRes.body; + + const deleteRes = await authenticatedTestClient(adminToken).delete( + `/api/v1/projects/${id}` + ); + expect(deleteRes.status).toBe(204); + + const getRes = await authenticatedTestClient(adminToken).get( + `/api/v1/projects/${id}` + ); + expect(getRes.status).toBe(404); + }); + + test('unauthenticated request cannot delete a project', async () => { + const createRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Not Deletable Unauth' }); + const { id } = createRes.body; + + const response = await testClient.delete(`/api/v1/projects/${id}`); + + expect(response.status).toBe(401); + }); + + test('non-admin user cannot delete a project', async () => { + const createRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Not Deletable User' }); + const { id } = createRes.body; + + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/projects/${id}` + ); + + expect(response.status).toBe(403); + }); + + test('returns 404 when deleting non-existent project', async () => { + const response = await authenticatedTestClient(adminToken).delete( + '/api/v1/projects/proj_nonexistent12345' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('POST /api/v1/projects/:projectId/policies', () => { + let projectId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Policy Project' }); + projectId = res.body.id; + }); + + test('admin can create a project policy', async () => { + const response = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['files:read', 'files:write'] }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.permissions).toEqual(['files:read', 'files:write']); + expect(response.body.projectId).toBe(projectId); + }); + + test('admin can create a policy with notPermissions', async () => { + const response = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['files:read'], + notPermissions: ['files:delete'], + }); + + expect(response.status).toBe(201); + expect(response.body.notPermissions).toEqual(['files:delete']); + }); + + test('unauthenticated request cannot create a policy', async () => { + const response = await testClient + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['files:read'] }); + + expect(response.status).toBe(401); + }); + + test('non-admin user cannot create a policy', async () => { + const response = await authenticatedTestClient(userToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['files:read'] }); + + expect(response.status).toBe(403); + }); + + test('returns 404 for non-existent project', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/projects/proj_nonexistent12345/policies') + .send({ permissions: ['files:read'] }); + + expect(response.status).toBe(404); + }); + }); + + describe('GET /api/v1/projects/:projectId/policies', () => { + let projectId: string; + let memberUserToken: string; + let memberUserId: string; + + beforeAll(async () => { + const projRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'List Policies Project' }); + projectId = projRes.body.id; + + const userRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'policyuser', password: 'policypass' }); + memberUserId = userRes.body.id; + memberUserToken = await loginAs('policyuser', 'policypass'); + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['projects:GetProject'] }); + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: memberUserId, policyId: policyRes.body.id }); + }); + + test('admin can list project policies', async () => { + const response = await authenticatedTestClient(adminToken).get( + `/api/v1/projects/${projectId}/policies` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThan(0); + }); + + test('project member can list policies', async () => { + const response = await authenticatedTestClient(memberUserToken).get( + `/api/v1/projects/${projectId}/policies` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('unauthenticated request cannot list policies', async () => { + const response = await testClient.get( + `/api/v1/projects/${projectId}/policies` + ); + + expect(response.status).toBe(401); + }); + + test('non-member user cannot list policies', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/projects/${projectId}/policies` + ); + + expect(response.status).toBe(403); + }); }); - test('admin can create a project', async () => { - const response = await authenticatedTestClient(adminToken) - .post('/api/v1/projects') - .send({ name: 'My Project' }); + describe('POST /api/v1/projects/:projectId/members', () => { + let projectId: string; + let policyId: string; + + beforeAll(async () => { + const projRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Members Project' }); + projectId = projRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['projects:GetProject'] }); + policyId = policyRes.body.id; + }); + + test('admin can add a user to a project', async () => { + const response = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + + expect(response.status).toBe(201); + }); + + test('unauthenticated request cannot add a member', async () => { + const response = await testClient + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + + expect(response.status).toBe(401); + }); + + test('non-admin user cannot add a member', async () => { + const response = await authenticatedTestClient(userToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + + expect(response.status).toBe(403); + }); + + test('returns 404 for non-existent project', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/projects/proj_nonexistent12345/members') + .send({ userId, policyId }); + + expect(response.status).toBe(404); + }); + }); + + describe('cascade deletion when a project is deleted', () => { + test('deleting a project removes its policies, memberships, and api keys', async () => { + const projRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Cascade Test Project' }); + expect(projRes.status).toBe(201); + const projectId = projRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ permissions: ['projects:GetProject'] }); + expect(policyRes.status).toBe(201); + const policyId = policyRes.body.id; + + const memberRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + expect(memberRes.status).toBe(201); + + const apiKeyRes = await authenticatedTestClient(userToken) + .post('/api/v1/api-keys') + .send({ projectId, policyId, name: 'Cascade Test Key' }); + expect(apiKeyRes.status).toBe(201); + const apiKeyId = apiKeyRes.body.id; + + const deleteRes = await authenticatedTestClient(adminToken).delete( + `/api/v1/projects/${projectId}` + ); + expect(deleteRes.status).toBe(204); + + // Policies cascade-deleted: project no longer found, so list returns [] + const policiesRes = await authenticatedTestClient(adminToken).get( + `/api/v1/projects/${projectId}/policies` + ); + expect(policiesRes.body).toEqual([]); + + // UserProject cascade-deleted: deleted project no longer in alice's project list + const projectsRes = + await authenticatedTestClient(userToken).get('/api/v1/projects'); + const projectIds = projectsRes.body.map((p: { id: string }) => { + return p.id; + }); + expect(projectIds).not.toContain(projectId); - expect(response.status).toBe(201); - expect(response.body.id).toBeDefined(); - expect(response.body.name).toBe('My Project'); - expect(response.body.createdAt).toBeDefined(); - expect(response.body.updatedAt).toBeDefined(); + // ApiKey cascade-deleted: key no longer found + const apiKeyGetRes = await authenticatedTestClient(userToken).get( + `/api/v1/api-keys/${apiKeyId}` + ); + expect(apiKeyGetRes.status).toBe(404); + }); }); }); diff --git a/packages/website/blog/2019-05-28-first-blog-post.md b/packages/website/blog/2019-05-28-first-blog-post.md index d3032efb..986f0429 100644 --- a/packages/website/blog/2019-05-28-first-blog-post.md +++ b/packages/website/blog/2019-05-28-first-blog-post.md @@ -7,6 +7,6 @@ tags: [hola, docusaurus] Lorem ipsum dolor sit amet... - +{/_ truncate _/} ...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/packages/website/blog/2019-05-29-long-blog-post.md b/packages/website/blog/2019-05-29-long-blog-post.md index eb4435de..3cd8c101 100644 --- a/packages/website/blog/2019-05-29-long-blog-post.md +++ b/packages/website/blog/2019-05-29-long-blog-post.md @@ -9,7 +9,7 @@ This is the summary of a very long blog post, Use a `` comment to limit blog post size in the list view. - +{/_ truncate _/} Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/packages/website/blog/2021-08-26-welcome/index.md b/packages/website/blog/2021-08-26-welcome/index.md index 349ea075..7cc2a6ab 100644 --- a/packages/website/blog/2021-08-26-welcome/index.md +++ b/packages/website/blog/2021-08-26-welcome/index.md @@ -9,7 +9,7 @@ tags: [facebook, hello, docusaurus] Here are a few tips you might find useful. - +{/_ truncate _/} Simply add Markdown files (or folders) to the `blog` directory. diff --git a/packages/website/docs/modules/files.md b/packages/website/docs/modules/files.md index f0fe1def..a4aaaca6 100644 --- a/packages/website/docs/modules/files.md +++ b/packages/website/docs/modules/files.md @@ -1,328 +1,87 @@ # Files Module -The Files module (`@soat/files-core`) provides a flexible file storage and management system with support for multiple storage backends. It handles file uploads, retrieval, deletion, and metadata tracking through a unified interface. +The Files module provides file upload, download, metadata management, and deletion through a local filesystem storage backend. Files are stored in a configurable directory and tracked in PostgreSQL. ## Overview -The Files module abstracts storage operations across different backends, allowing you to switch between local filesystem, AWS S3, and Google Cloud Storage without changing your application code. It automatically tracks file metadata in PostgreSQL for efficient querying and management. +Files are associated with a project and stored at `{FILES_STORAGE_DIR}/{publicId}{ext}` on the server's local filesystem. Every file record exposes a `publicId` as its `id` — the internal database primary key is never returned. -## Installation +## Configuration -```bash -pnpm add @soat/files-core -``` - -## Storage Backends - -### Local Storage - -Store files on the local filesystem. - -```typescript -const config: StorageConfig = { - type: 'local', - local: { - path: '/path/to/storage', - }, -}; -``` - -### AWS S3 - -Store files in Amazon S3 buckets. - -```typescript -const config: StorageConfig = { - type: 's3', - s3: { - bucket: 'my-bucket', - region: 'us-east-1', - accessKeyId: process.env.AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, - }, -}; -``` - -### Google Cloud Storage - -Store files in Google Cloud Storage buckets. - -```typescript -const config: StorageConfig = { - type: 'gcs', - gcs: { - bucket: 'my-bucket', - projectId: 'my-project-id', - keyFilename: '/path/to/service-account-key.json', // optional - }, -}; -``` - -## Core Functions - -### saveFile - -Save content directly to storage with optional metadata. - -```typescript -import { saveFile } from '@soat/files-core'; - -const file = await saveFile({ - config, - content: 'Hello, world!', // string or Buffer - options: { - contentType: 'text/plain', - metadata: { - author: 'John Doe', - tags: ['greeting', 'example'], - }, - }, -}); - -console.log(file.id); // UUID of saved file -``` - -**Parameters:** - -- `config`: Storage configuration object -- `content`: String or Buffer containing file data -- `options` (optional): - - `contentType`: MIME type of the content - - `metadata`: Key-value pairs for custom metadata - -**Returns:** `FileData` object with `id` and `content` - -### uploadFile - -Upload a file from the local filesystem to storage. - -```typescript -import { uploadFile } from '@soat/files-core'; - -const file = await uploadFile({ - config, - filePath: '/path/to/local/file.pdf', - options: { - contentType: 'application/pdf', - metadata: { - filename: 'document.pdf', - category: 'invoices', - }, - }, -}); -``` - -**Parameters:** - -- `config`: Storage configuration object -- `filePath`: Absolute path to the file to upload -- `options` (optional): Same as `saveFile` - -**Returns:** `FileData` object with `id` and `content` - -### retrieveFileById - -Retrieve a file's content by its ID. - -```typescript -import { retrieveFileById } from '@soat/files-core'; - -const file = await retrieveFileById({ - config, - id: 'file-uuid-here', -}); - -if (file) { - console.log(file.content); // Buffer or string -} else { - console.log('File not found'); -} -``` - -**Parameters:** +| Environment Variable | Required | Description | +| -------------------- | -------- | ------------------------------------------------------------------------------------------------------- | +| `FILES_STORAGE_DIR` | Yes | Absolute path to the directory where uploaded files are stored. Must be writable by the server process. | -- `config`: Storage configuration object -- `id`: UUID of the file to retrieve +When running via Docker, mount a volume at this path to persist files across container restarts: -**Returns:** `FileData | null` +```yaml +services: + server: + image: soat-server + environment: + FILES_STORAGE_DIR: /data/files + volumes: + - files-data:/data/files -### deleteFile - -Delete a file from both storage and database. - -```typescript -import { deleteFile } from '@soat/files-core'; - -const deleted = await deleteFile({ - config, - id: 'file-uuid-here', -}); - -console.log(deleted ? 'File deleted' : 'File not found'); -``` - -**Parameters:** - -- `config`: Storage configuration object -- `id`: UUID of the file to delete - -**Returns:** `boolean` indicating success - -### listFileRecords - -List all file records from the database with metadata. - -```typescript -import { listFileRecords } from '@soat/files-core'; - -const files = await listFileRecords(); - -files.forEach((file) => { - console.log(file.id, file.filename, file.size, file.createdAt); -}); -``` - -**Returns:** Array of `FileRecord` objects - -### getFileRecord - -Get a specific file's metadata record. - -```typescript -import { getFileRecord } from '@soat/files-core'; - -const record = await getFileRecord('file-uuid-here'); - -if (record) { - console.log(record.filename, record.contentType, record.size); -} +volumes: + files-data: ``` -**Parameters:** - -- `id`: UUID of the file - -**Returns:** `FileRecord | null` - -## Type Definitions - -### StorageConfig - -Configuration for storage backend selection. - -```typescript -interface StorageConfig { - type: 'local' | 's3' | 'gcs'; - local?: { - path: string; - }; - s3?: { - bucket: string; - region: string; - accessKeyId: string; - secretAccessKey: string; - }; - gcs?: { - bucket: string; - keyFilename?: string; - projectId?: string; - }; -} -``` - -### UploadOptions - -Options for file upload operations. - -```typescript -interface UploadOptions { - contentType?: string; - metadata?: Record; -} -``` - -### FileData - -Returned data after file operations. - -```typescript -interface FileData { - id: string; - content: string | Buffer; -} -``` - -### FileRecord - -Database record for file metadata. - -```typescript -interface FileRecord { - id: string; - filename?: string; - contentType?: string; - size?: number; - storageType: 'local' | 's3' | 'gcs'; - storagePath: string; - metadata?: Record; - createdAt: Date; - updatedAt: Date; -} -``` +## Data Model -## Usage with Server +| Field | Type | Description | +| ------------- | ------------------------ | ------------------------------------------------------------------ | +| `id` | string | Public identifier (exposed as `id`, maps to `publicId` internally) | +| `filename` | string | Original filename | +| `contentType` | string | MIME type | +| `size` | number | File size in bytes | +| `storageType` | `local` \| `s3` \| `gcs` | Storage backend (currently `local`) | +| `storagePath` | string | Absolute path on disk | +| `metadata` | string | Arbitrary JSON string for custom metadata | +| `projectId` | string | ID of the owning project | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | -The Files module is integrated into the SOAT server REST API at `/v1/files`: +## Permissions -- `GET /v1/files` - List all files -- `POST /v1/files/upload` - Upload a file -- `GET /v1/files/:id` - Retrieve file by ID -- `DELETE /v1/files/:id` - Delete file by ID +File operations are governed by per-project policies. Grant the following permissions to allow a user to perform each action: -See the [API documentation](/docs/api/files/soat-files-api) for details. +| Action | Permission string | +| ----------------------------- | -------------------------- | +| Upload a file | `files:UploadFile` | +| Get file metadata | `files:GetFile` | +| Download file content | `files:DownloadFile` | +| Update metadata | `files:UpdateFileMetadata` | +| Create a metadata-only record | `files:CreateFile` | +| Delete a file | `files:DeleteFile` | -## Best Practices +## Operations -### Security +### Upload a file -- **Never commit credentials** - Use environment variables for S3/GCS credentials -- **Validate file types** - Check `contentType` before processing -- **Limit file sizes** - Implement size limits at the API level -- **Sanitize metadata** - Validate and sanitize user-provided metadata +`POST /api/v1/files/upload` — Multipart form-data. The server stores the file on disk and creates a database record. -### Performance +Required fields: `file` (binary), `projectId`. +Optional fields: `metadata` (JSON string). -- **Use buffers for binary data** - More efficient than base64 strings -- **Stream large files** - For files >10MB, consider streaming implementations -- **Cache metadata** - File records are in PostgreSQL, suitable for caching +### Download a file -### Storage Selection +`GET /api/v1/files/{id}/download` — Returns the raw file bytes with the original `Content-Type` and `Content-Disposition: attachment` header. -- **Local**: Development, small-scale deployments, low latency requirements -- **S3**: Production, scalable, CDN integration, global availability -- **GCS**: Google Cloud ecosystem, machine learning pipelines +### Get file metadata -## Error Handling +`GET /api/v1/files/{id}` — Returns the file record without transferring the file content. -All functions may throw errors for: +### Update metadata -- Invalid configuration -- Storage backend connectivity issues -- Filesystem permission errors -- Database connection failures +`PATCH /api/v1/files/{id}/metadata` — Replaces the `metadata` field on an existing file record. -Wrap operations in try-catch blocks: +Request body: -```typescript -try { - const file = await saveFile({ config, content: data }); - console.log('Saved:', file.id); -} catch (error) { - console.error('Failed to save file:', error.message); -} +```json +{ "metadata": "{\"author\":\"Alice\",\"tags\":[\"report\"]}" } ``` -## Next Steps +### Delete a file -- [Getting Started](/docs/getting-started) - Set up SOAT server with file storage -- [API Reference](/docs/api/files/soat-files-api) - REST API endpoints for files +`DELETE /api/v1/files/{id}` — Removes the file from disk and deletes the database record. Returns `204 No Content`. diff --git a/packages/website/docs/modules/iam.md b/packages/website/docs/modules/iam.md index d452298f..f57fcf15 100644 --- a/packages/website/docs/modules/iam.md +++ b/packages/website/docs/modules/iam.md @@ -1,127 +1,115 @@ # IAM Module -The IAM (Identity and Access Management) module provides authentication and authorization primitives for SOAT-powered applications. It manages identities through **Users** and **API Keys**, and controls access to resources using **Projects**, **Policies**, and **Project Members**. +The IAM (Identity and Access Management) module controls who can access SOAT and what they are allowed to do. It provides user authentication, project-based organization, fine-grained permission policies, and API keys for programmatic access. ## Overview -The IAM module controls who can access your resources and what actions they can perform. It supports: +Every resource in SOAT (files, projects, and more) is protected by IAM. Before any action is performed, IAM verifies: -- **Human users** with username/password authentication and JWT session tokens -- **Machine-to-machine access** via API keys scoped to a project and policy -- **Fine-grained authorization** through policy-based permission checks per project +1. **Who are you?** — Authentication via a login session (JWT) or an API key. +2. **Are you allowed to do that?** — Authorization via policies attached to your project membership. ## Authentication -### Users (JWT) +### Login (JWT) -Users authenticate via `POST /api/v1/users/login` with their username and password. On success, a signed JWT is returned. Include it as a Bearer token on subsequent requests: +Human users authenticate by logging in with their credentials. SOAT returns a session token that you include in every subsequent request. ``` -Authorization: Bearer +POST /api/v1/users/login ``` -Tokens are valid for 7 days. +```json +{ + "email": "you@example.com", + "password": "your-password" +} +``` -### API Keys +Include the token in the `Authorization` header: -API keys provide long-lived credentials for machine-to-machine access. A raw key is returned only once at creation — it is immediately hashed (bcrypt) and cannot be retrieved again. +``` +Authorization: Bearer +``` -Key format: `sk_` (e.g., `sk_abc12345xyz`) +Session tokens expire after 7 days. -Include it as a Bearer token: +### API Keys + +For scripts, integrations, or any machine-to-machine access, use an API key instead of a login token. API keys are long-lived and can be scoped to a specific set of permissions. + +Create a key via `POST /api/v1/api-keys`. The full key value is returned **only once** at creation — store it securely. Use it exactly like a session token: ``` -Authorization: Bearer sk_abc12345xyz +Authorization: Bearer sk_ ``` -An API key is scoped to a specific **Project** and **Policy**. When authenticated via an API key, the system resolves the project context and the key's effective permissions automatically. - -**Permission boundary**: An API key's effective permissions are bounded by the membership policy of the user who created it. Even if the key's policy grants broader access, permissions are intersected with the creator's membership policy at authorization time. +An API key's permissions are always limited to what its creator is allowed to do. A key can never grant more access than the user who created it. ## Roles -The IAM module uses a simple role model: - -| Role | Description | -| ------- | ----------------------------------------------------------------------- | -| `admin` | Full access to all resources and projects. Policy checks are bypassed. | -| `user` | Access is scoped to projects they are members of, governed by a policy. | +| Role | Description | +| ------- | ------------------------------------------------------- | +| `admin` | Full access to all resources and management operations. | +| `user` | Access governed by project membership and policies. | -The first user in the system is bootstrapped as `admin` via `POST /api/v1/users/bootstrap`. Subsequent users are created as `user` by default. +The first user registered via `POST /api/v1/users/bootstrap` becomes the admin. All subsequent users have the `user` role. ## Projects -A project is the top-level resource boundary. All access control (policies, members, API keys) is scoped to a project. +A project is the top-level organizational unit in SOAT. All resources — files, policies, and API keys — belong to a project. -- Admins can create, list, view, and delete any project. -- Users can only list and view projects they are members of. +- **Admins** can create, manage, and delete any project. +- **Users** can access only the projects they are members of. +- Each project membership carries a **policy** that defines what that member is allowed to do. ## Policies -A policy defines what actions are allowed or denied within a project. Policies are attached to project members and API keys. +A policy defines what actions a member (or API key) is allowed or denied within a project. -| Field | Type | Description | -| ---------------- | ---------- | ------------------------------------------------------ | -| `id` | `string` | ID (prefix: `pol_`) | -| `name` | `string` | Human-readable policy name | -| `permissions` | `string[]` | List of allowed action patterns | -| `notPermissions` | `string[]` | List of denied action patterns (deny takes precedence) | -| `projectId` | `string` | The project this policy belongs to | - -### Permission Patterns - -Permission strings follow the format `resource:action`. Wildcards are supported: - -| Pattern | Matches | -| --------------- | ---------------------------------- | -| `files:*` | Any action on the `files` resource | -| `*` | Any action on any resource | -| `files:GetFile` | Exactly the `files:GetFile` action | - -### Authorization Logic +```json +{ + "name": "read-only", + "projectId": "proj_...", + "permissions": ["files:*"], + "notPermissions": ["files:DeleteFile"] +} +``` -For a `user` role, a request is allowed if **all** of the following hold: +| Field | Description | +| ---------------- | -------------------------------------------------------------------------- | +| `permissions` | Actions the policy allows. Supports wildcards (`files:*`, `*`). | +| `notPermissions` | Actions the policy explicitly denies. Denials take precedence over allows. | -1. The user is a member of the project being accessed. -2. The user's membership policy **allows** the action (`permissions` match). -3. The user's membership policy does **not deny** the action (`notPermissions` do not match). -4. If the request uses an API key, the key's policy also allows the action (intersection). +### Permission Actions -`admin` users bypass all policy checks. +Permission strings follow the format `resource:Action`. The table below lists all available actions and the REST endpoint each one protects. -## Project Members +| Permission Action | REST Endpoint | What it controls | +| --------------------- | -------------------------- | ------------------------- | +| `files:GetFile` | `GET /api/v1/files/:id` | Retrieve a specific file | +| `files:CreateFile` | `POST /api/v1/files` | Upload a new file | +| `files:DeleteFile` | `DELETE /api/v1/files/:id` | Delete a file | +| `projects:GetProject` | `GET /api/v1/projects/:id` | View a project's policies | -A project member is a user who has been granted access to a project under a specific policy. +Use wildcards to grant broader access: -- Only admins can add, update, or remove members. -- Removing a member revokes their access to the project immediately. +| Pattern | Effect | +| --------------- | --------------------------- | +| `*` | Allow everything | +| `files:*` | Allow all file operations | +| `files:GetFile` | Allow only retrieving files | ## API Keys -| Field | Type | Description | -| ----------- | -------- | ---------------------------------------------------------- | -| `id` | `string` | Public ID (prefix: `key_`) | -| `name` | `string` | Human-readable key name | -| `policyId` | `string` | Public ID of the policy governing this key | -| `projectId` | `string` | Public ID of the scoped project | -| `key` | `string` | Raw key value — **returned only at creation, never again** | - -- Any project member can create an API key scoped to their own project. -- The key's policy must be valid within the same project. -- The key's effective permissions are bounded by the creator's membership policy. -- Members can list and delete their own keys. Admins can manage all keys. - -## Data Model - -All IAM entities are backed by the `@soat/postgresdb` package and use a `publicId` (prefixed string) as the external identifier. Internal integer primary keys are never exposed through the API. +Each API key is attached to a policy, which limits the key to a specific set of actions. When a request uses an API key, both the key's policy and the owner's membership policy must allow the action. -| Entity | `publicId` Prefix | -| ------- | ----------------- | -| User | `usr_` | -| Project | `proj_` | -| Policy | `pol_` | -| API Key | `key_` | +| Field | Description | +| ---------- | ---------------------------------------------------------------------------- | +| `name` | A human-readable label for the key | +| `policyId` | The policy that governs this key's permissions | +| `key` | The raw key value — **shown only once at creation, never retrievable again** | ## OAuth -> **Coming soon.** OAuth 2.0 integration is planned for an upcoming release. Supported flows will include Authorization Code (interactive sign-in) and Client Credentials (server-to-server). +> **Coming soon.** OAuth 2.0 support (Authorization Code and Client Credentials flows) is planned for a future release. diff --git a/packages/website/docusaurus.config.ts b/packages/website/docusaurus.config.ts index a1b99968..5a905c33 100644 --- a/packages/website/docusaurus.config.ts +++ b/packages/website/docusaurus.config.ts @@ -1,9 +1,33 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + import type * as Preset from '@docusaurus/preset-classic'; import type { Config } from '@docusaurus/types'; import { themes as prismThemes } from 'prism-react-renderer'; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) +const buildOpenApiConfig = () => { + const specsDir = path.resolve(__dirname, '../server/src/rest/openapi/v1'); + const files = fs.readdirSync(specsDir).filter((f) => { + return f.endsWith('.yaml'); + }); + return Object.fromEntries( + files.map((file) => { + const name = path.basename(file, '.yaml'); + return [ + name, + { + specPath: `../server/src/rest/openapi/v1/${file}`, + outputDir: `docs/api/${name}`, + sidebarOptions: { groupPathsBy: 'tag' }, + hideSendButton: false, + }, + ]; + }) + ); +}; + const config: Config = { title: 'SOAT', tagline: 'Persistent Memory for AI Agents', @@ -73,24 +97,7 @@ const config: Config = { { id: 'api', docsPluginId: 'classic', - config: { - files: { - specPath: '../server/src/rest/openapi/v1/files.yaml', - outputDir: 'docs/api/files', - sidebarOptions: { - groupPathsBy: 'tag', - }, - hideSendButton: false, - }, - documents: { - specPath: '../server/src/rest/openapi/v1/documents.yaml', - outputDir: 'docs/api/documents', - sidebarOptions: { - groupPathsBy: 'tag', - }, - hideSendButton: false, - }, - }, + config: buildOpenApiConfig(), }, ], ], @@ -113,16 +120,16 @@ const config: Config = { { type: 'docSidebar', sidebarId: 'tutorialSidebar', - position: 'right', + position: 'left', label: 'Docs', }, { type: 'docSidebar', sidebarId: 'apiSidebar', - position: 'right', + position: 'left', label: 'API', }, - { to: '/blog', label: 'Blog', position: 'right' }, + { to: '/blog', label: 'Blog', position: 'left' }, { href: 'https://github.com/ttoss/soat', label: 'GitHub', @@ -138,7 +145,7 @@ const config: Config = { items: [ { label: 'Documentation', - to: '/docs/intro', + to: '/docs/Introduction', }, ], }, diff --git a/packages/website/package.json b/packages/website/package.json index 64038b4f..b7e2562a 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -17,20 +17,21 @@ "clean-api-docs": "docusaurus clean-api-docs all" }, "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/preset-classic": "3.9.2", - "@mdx-js/react": "^3.0.0", - "clsx": "^2.0.0", - "prism-react-renderer": "^2.3.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "docusaurus-plugin-openapi-docs": "^4.0.0", - "docusaurus-theme-openapi-docs": "^4.0.0" + "@docusaurus/core": "3.10.0", + "@docusaurus/faster": "^3.10.0", + "@docusaurus/preset-classic": "3.10.0", + "@mdx-js/react": "^3.1.1", + "clsx": "^2.1.1", + "docusaurus-plugin-openapi-docs": "^5.0.0", + "docusaurus-theme-openapi-docs": "^5.0.0", + "prism-react-renderer": "^2.4.1", + "react": "^19.2.5", + "react-dom": "^19.2.5" }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/tsconfig": "3.9.2", - "@docusaurus/types": "3.9.2", + "@docusaurus/module-type-aliases": "3.10.0", + "@docusaurus/tsconfig": "3.10.0", + "@docusaurus/types": "3.10.0", "typescript": "~6.0.2" }, "browserslist": { diff --git a/packages/website/sidebars.ts b/packages/website/sidebars.ts index 6122652b..1f71ba11 100644 --- a/packages/website/sidebars.ts +++ b/packages/website/sidebars.ts @@ -1,7 +1,7 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; -import documentsSidebar from './docs/api/documents/sidebar'; import filesSidebar from './docs/api/files/sidebar'; +import usersSidebar from './docs/api/users/sidebar'; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) @@ -18,7 +18,7 @@ import filesSidebar from './docs/api/files/sidebar'; const sidebars: SidebarsConfig = { // By default, Docusaurus generates a sidebar from the docs folder structure tutorialSidebar: [ - 'intro', + 'Introduction', 'getting-started', { type: 'category', @@ -36,13 +36,13 @@ const sidebars: SidebarsConfig = { apiSidebar: [ { type: 'category', - label: 'Documents API', - items: documentsSidebar, + label: 'Files API', + items: filesSidebar, }, { type: 'category', - label: 'Files API', - items: filesSidebar, + label: 'Users API', + items: usersSidebar, }, ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d3a31b5..538efe02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 1.37.8 '@ttoss/eslint-config': specifier: ^1.26.14 - version: 1.26.14(@testing-library/dom@10.4.1)(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(prettier@3.8.1)(turbo@2.9.4)(typescript@6.0.2) + version: 1.26.14(@testing-library/dom@10.4.1)(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(prettier@3.8.1)(turbo@2.9.4)(typescript@6.0.2) '@ttoss/monorepo': specifier: ^1.29.8 version: 1.29.8 @@ -74,10 +74,10 @@ importers: version: 30.0.0 jest: specifier: ^30.3.0 - version: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + version: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 8.5.1(@swc/core@1.15.24)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -102,19 +102,22 @@ importers: version: 0.2.8 '@ttoss/test-utils': specifier: ^4.2.8 - version: 4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@types/node': specifier: ^25.5.2 version: 25.5.2 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 8.5.1(@swc/core@1.15.24)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) typescript: specifier: ~6.0.2 version: 6.0.2 packages/server: dependencies: + '@koa/multer': + specifier: ^4.0.0 + version: 4.0.0(koa@3.1.1)(multer@2.0.2) '@soat/postgresdb': specifier: workspace:* version: link:../postgresdb @@ -127,6 +130,9 @@ importers: '@ttoss/postgresdb': specifier: ^0.8.0 version: 0.8.0(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2) + '@types/koa__multer': + specifier: ^2.0.8 + version: 2.0.8 bcryptjs: specifier: ^3.0.3 version: 3.0.3 @@ -154,7 +160,7 @@ importers: version: 1.37.8 '@ttoss/test-utils': specifier: ^4.2.8 - version: 4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@types/bcryptjs': specifier: ^3.0.0 version: 3.0.0 @@ -172,13 +178,13 @@ importers: version: 7.2.0 jest: specifier: ^30.3.0 - version: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + version: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) supertest: specifier: ^7.2.2 version: 7.2.2 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 8.5.1(@swc/core@1.15.24)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -186,42 +192,45 @@ importers: packages/website: dependencies: '@docusaurus/core': - specifier: 3.9.2 - version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + specifier: 3.10.0 + version: 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/faster': + specifier: ^3.10.0 + version: 3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@docusaurus/preset-classic': - specifier: 3.9.2 - version: 3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@6.0.2) + specifier: 3.10.0 + version: 3.10.0(@algolia/client-search@5.46.2)(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(search-insights@2.17.3)(typescript@6.0.2) '@mdx-js/react': - specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.7)(react@19.2.3) + specifier: ^3.1.1 + version: 3.1.1(@types/react@19.2.7)(react@19.2.5) clsx: - specifier: ^2.0.0 + specifier: ^2.1.1 version: 2.1.1 docusaurus-plugin-openapi-docs: - specifier: ^4.0.0 - version: 4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3) + specifier: ^5.0.0 + version: 5.0.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@docusaurus/utils-validation@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@docusaurus/utils@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/json-schema@7.0.15)(encoding@0.1.13)(react@19.2.5) docusaurus-theme-openapi-docs: - specifier: ^4.0.0 - version: 4.5.1(@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(sass@1.97.2)(webpack@5.104.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1) + specifier: ^5.0.0 + version: 5.0.0(255c652d32e779b619b35f7801205cba) prism-react-renderer: - specifier: ^2.3.0 - version: 2.4.1(react@19.2.3) + specifier: ^2.4.1 + version: 2.4.1(react@19.2.5) react: - specifier: ^19.0.0 - version: 19.2.3 + specifier: ^19.2.5 + version: 19.2.5 react-dom: - specifier: ^19.0.0 - version: 19.2.3(react@19.2.3) + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) devDependencies: '@docusaurus/module-type-aliases': - specifier: 3.9.2 - version: 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 3.10.0 + version: 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@docusaurus/tsconfig': - specifier: 3.9.2 - version: 3.9.2 + specifier: 3.10.0 + version: 3.10.0 '@docusaurus/types': - specifier: 3.9.2 - version: 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 3.10.0 + version: 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) typescript: specifier: ~6.0.2 version: 6.0.2 @@ -330,9 +339,11 @@ packages: resolution: {integrity: sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==} engines: {node: '>= 14.0.0'} - '@apidevtools/json-schema-ref-parser@11.9.3': - resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} - engines: {node: '>= 16'} + '@apidevtools/json-schema-ref-parser@15.3.5': + resolution: {integrity: sha512-orNOYXw3hYXxxisXMldjzjBzqqTLBPbwOtHg7ovBPvfBHDue1qM9YJENZ3W2BQuS+7z4ThogMbEzEsov57Itkg==} + engines: {node: '>=20'} + peerDependencies: + '@types/json-schema': ^7.0.15 '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -402,11 +413,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-define-polyfill-provider@0.6.5': - resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - '@babel/helper-define-polyfill-provider@0.6.8': resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} peerDependencies: @@ -532,12 +538,6 @@ packages: peerDependencies: '@babel/core': ^7.13.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3': - resolution: {integrity: sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} engines: {node: '>=6.9.0'} @@ -588,12 +588,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.27.1': - resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.28.6': resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} engines: {node: '>=6.9.0'} @@ -688,24 +682,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.28.0': - resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.29.0': resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-to-generator@7.27.1': - resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-to-generator@7.28.6': resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} engines: {node: '>=6.9.0'} @@ -718,60 +700,30 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.28.5': - resolution: {integrity: sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.28.6': resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.27.1': - resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.28.6': resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.28.3': - resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - '@babel/plugin-transform-class-static-block@7.28.6': resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.28.4': - resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-classes@7.28.6': resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.27.1': - resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.28.6': resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} engines: {node: '>=6.9.0'} @@ -784,12 +736,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-dotall-regex@7.27.1': - resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-dotall-regex@7.28.6': resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} engines: {node: '>=6.9.0'} @@ -802,12 +748,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1': - resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} engines: {node: '>=6.9.0'} @@ -820,24 +760,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-explicit-resource-management@7.28.0': - resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-explicit-resource-management@7.28.6': resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-exponentiation-operator@7.28.5': - resolution: {integrity: sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-exponentiation-operator@7.28.6': resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} engines: {node: '>=6.9.0'} @@ -862,12 +790,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-json-strings@7.27.1': - resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-json-strings@7.28.6': resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} engines: {node: '>=6.9.0'} @@ -880,12 +802,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-logical-assignment-operators@7.28.5': - resolution: {integrity: sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-logical-assignment-operators@7.28.6': resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} engines: {node: '>=6.9.0'} @@ -916,12 +832,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.28.5': - resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.29.0': resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==} engines: {node: '>=6.9.0'} @@ -934,12 +844,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': - resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} engines: {node: '>=6.9.0'} @@ -952,36 +856,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': - resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-numeric-separator@7.27.1': - resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-numeric-separator@7.28.6': resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.28.4': - resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.28.6': resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} engines: {node: '>=6.9.0'} @@ -994,12 +880,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-catch-binding@7.27.1': - resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-catch-binding@7.28.6': resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} engines: {node: '>=6.9.0'} @@ -1024,24 +904,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.27.1': - resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.28.6': resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-property-in-object@7.27.1': - resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-property-in-object@7.28.6': resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} engines: {node: '>=6.9.0'} @@ -1084,24 +952,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.28.4': - resolution: {integrity: sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.29.0': resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regexp-modifiers@7.27.1': - resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/plugin-transform-regexp-modifiers@7.28.6': resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} engines: {node: '>=6.9.0'} @@ -1126,12 +982,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.27.1': - resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.28.6': resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} engines: {node: '>=6.9.0'} @@ -1168,12 +1018,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-property-regex@7.27.1': - resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-property-regex@7.28.6': resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} engines: {node: '>=6.9.0'} @@ -1186,24 +1030,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-sets-regex@7.27.1': - resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/plugin-transform-unicode-sets-regex@7.28.6': resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.28.5': - resolution: {integrity: sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/preset-env@7.29.2': resolution: {integrity: sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==} engines: {node: '>=6.9.0'} @@ -1227,10 +1059,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime-corejs3@7.28.4': - resolution: {integrity: sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} @@ -1692,12 +1520,12 @@ packages: search-insights: optional: true - '@docusaurus/babel@3.9.2': - resolution: {integrity: sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==} + '@docusaurus/babel@3.10.0': + resolution: {integrity: sha512-mqCJhCZNZUDg0zgDEaPTM4DnRsisa24HdqTy/qn/MQlbwhTb4WVaZg6ZyX6yIVKqTz8fS1hBMgM+98z+BeJJDg==} engines: {node: '>=20.0'} - '@docusaurus/bundler@3.9.2': - resolution: {integrity: sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==} + '@docusaurus/bundler@3.10.0': + resolution: {integrity: sha512-iONUGZGgp+lAkw/cJZH6irONcF4p8+278IsdRlq8lYhxGjkoNUs0w7F4gVXBYSNChq5KG5/JleTSsdJySShxow==} engines: {node: '>=20.0'} peerDependencies: '@docusaurus/faster': '*' @@ -1705,106 +1533,116 @@ packages: '@docusaurus/faster': optional: true - '@docusaurus/core@3.9.2': - resolution: {integrity: sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==} + '@docusaurus/core@3.10.0': + resolution: {integrity: sha512-mgLdQsO8xppnQZc3LPi+Mf+PkPeyxJeIx11AXAq/14fsaMefInQiMEZUUmrc7J+956G/f7MwE7tn8KZgi3iRcA==} engines: {node: '>=20.0'} hasBin: true peerDependencies: + '@docusaurus/faster': '*' '@mdx-js/react': ^3.0.0 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@docusaurus/faster': + optional: true + + '@docusaurus/cssnano-preset@3.10.0': + resolution: {integrity: sha512-qzSshTO1DB3TYW+dPUal5KHM7XPc5YQfzF3Kdb2NDACJUyGbNcFtw3tGkCJlYwhNCRKbZcmwraKUS1i5dcHdGg==} + engines: {node: '>=20.0'} - '@docusaurus/cssnano-preset@3.9.2': - resolution: {integrity: sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==} + '@docusaurus/faster@3.10.0': + resolution: {integrity: sha512-GNPtVH14ISjHfSwnHu3KiFGf86ICmJSQDeSv/QaanpBgiZGOtgZaslnC5q8WiguxM1EVkwcGxPuD8BXF4eggKw==} engines: {node: '>=20.0'} + peerDependencies: + '@docusaurus/types': '*' - '@docusaurus/logger@3.9.2': - resolution: {integrity: sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==} + '@docusaurus/logger@3.10.0': + resolution: {integrity: sha512-9jrZzFuBH1LDRlZ7cznAhCLmAZ3HSDqgwdrSSZdGHq9SPUOQgXXu8mnxe2ZRB9NS1PCpMTIOVUqDtZPIhMafZg==} engines: {node: '>=20.0'} - '@docusaurus/mdx-loader@3.9.2': - resolution: {integrity: sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==} + '@docusaurus/mdx-loader@3.10.0': + resolution: {integrity: sha512-mQQV97080AH4PYNs087l202NMDqRopZA4mg5W76ZZyTFrmWhJ3mHg+8A+drJVENxw5/Q+wHMHLgsx+9z1nEs0A==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/module-type-aliases@3.9.2': - resolution: {integrity: sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==} + '@docusaurus/module-type-aliases@3.10.0': + resolution: {integrity: sha512-/1O0Zg8w3DFrYX/I6Fbss7OJrtZw1QoyjDhegiFNHVi9A9Y0gQ3jUAytVxF6ywpAWpLyLxch8nN8H/V3XfzdJQ==} peerDependencies: react: '*' react-dom: '*' - '@docusaurus/plugin-content-blog@3.9.2': - resolution: {integrity: sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==} + '@docusaurus/plugin-content-blog@3.10.0': + resolution: {integrity: sha512-RuTz68DhB7CL96QO5UsFbciD7GPYq6QV+YMfF9V0+N4ZgLhJIBgpVAr8GobrKF6NRe5cyWWETU5z5T834piG9g==} engines: {node: '>=20.0'} peerDependencies: '@docusaurus/plugin-content-docs': '*' react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/plugin-content-docs@3.9.2': - resolution: {integrity: sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==} + '@docusaurus/plugin-content-docs@3.10.0': + resolution: {integrity: sha512-9BjHhf15ct8Z7TThTC0xRndKDVvMKmVsAGAN7W9FpNRzfMdScOGcXtLmcCWtJGvAezjOJIm6CxOYCy3Io5+RnQ==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/plugin-content-pages@3.9.2': - resolution: {integrity: sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==} + '@docusaurus/plugin-content-pages@3.10.0': + resolution: {integrity: sha512-5amX8kEJI+nIGtuLVjYk59Y5utEJ3CHETFOPEE4cooIRLA4xM4iBsA6zFgu4ljcopeYwvBzFEWf5g2I6Yb9SkA==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/plugin-css-cascade-layers@3.9.2': - resolution: {integrity: sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==} + '@docusaurus/plugin-css-cascade-layers@3.10.0': + resolution: {integrity: sha512-6q1vtt5FJcg5osgkHeM1euErECNqEZ5Z1j69yiNx2luEBIso+nxCkS9nqj8w+MK5X7rvKEToGhFfOFWncs51pQ==} engines: {node: '>=20.0'} - '@docusaurus/plugin-debug@3.9.2': - resolution: {integrity: sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==} + '@docusaurus/plugin-debug@3.10.0': + resolution: {integrity: sha512-XcljKN+G+nmmK69uQA1d9BlYU3ZftG3T3zpK8/7Hf/wrOlV7TA4Ampdrdwkg0jElKdKAoSnPhCO0/U3bQGsVQQ==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/plugin-google-analytics@3.9.2': - resolution: {integrity: sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==} + '@docusaurus/plugin-google-analytics@3.10.0': + resolution: {integrity: sha512-hTEoodatpBZnUat5nFExbuTGA1lhWGy7vZGuTew5Q3QDtGKFpSJLYmZJhdTjvCFwv1+qQ67hgAVlKdJOB8TXow==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/plugin-google-gtag@3.9.2': - resolution: {integrity: sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==} + '@docusaurus/plugin-google-gtag@3.10.0': + resolution: {integrity: sha512-iB/Zzjv/eelJRbdULZqzWCbgMgJ7ht4ONVjXtN3+BI/muil6S87gQ1OJyPwlXD+ELdKkitC7bWv5eJdYOZLhrQ==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/plugin-google-tag-manager@3.9.2': - resolution: {integrity: sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==} + '@docusaurus/plugin-google-tag-manager@3.10.0': + resolution: {integrity: sha512-FEjZxqKgLHa+Wez/EgKxRwvArNCWIScfyEQD95rot7jkxp6nonjI5XIbGfO/iYhM5Qinwe8aIEQHP2KZtpqVuA==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/plugin-sitemap@3.9.2': - resolution: {integrity: sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==} + '@docusaurus/plugin-sitemap@3.10.0': + resolution: {integrity: sha512-DVTSLjB97hIjmayGnGcBfognCeI7ZuUKgEnU7Oz81JYqXtVg94mVTthDjq3QHTylYNeCUbkaW8VF0FDLcc8pPw==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/plugin-svgr@3.9.2': - resolution: {integrity: sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==} + '@docusaurus/plugin-svgr@3.10.0': + resolution: {integrity: sha512-lNljBESaETZqVBMPqkrGchr+UPT1eZzEPLmJhz8I76BxbjqgsUnRvrq6lQJ9sYjgmgX52KB7kkgczqd2yzoswQ==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/preset-classic@3.9.2': - resolution: {integrity: sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==} + '@docusaurus/preset-classic@3.10.0': + resolution: {integrity: sha512-kw/Ye02Hc6xP1OdTswy8yxQEHg0fdPpyWAQRxr5b2x3h7LlG2Zgbb5BDFROnXDDMpUxB7YejlocJIE5HIEfpNA==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 @@ -1815,51 +1653,51 @@ packages: peerDependencies: react: '*' - '@docusaurus/theme-classic@3.9.2': - resolution: {integrity: sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==} + '@docusaurus/theme-classic@3.10.0': + resolution: {integrity: sha512-9msCAsRdN+UG+RwPwCFb0uKy4tGoPh5YfBozXeGUtIeAgsMdn6f3G/oY861luZ3t8S2ET8S9Y/1GnpJAGWytww==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/theme-common@3.9.2': - resolution: {integrity: sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==} + '@docusaurus/theme-common@3.10.0': + resolution: {integrity: sha512-Dkp1YXKn16ByCJAdIjbDIOpVb4Z66MsVD694/ilX1vAAHaVEMrVsf/NPd9VgreyFx08rJ9GqV1MtzsbTcU73Kg==} engines: {node: '>=20.0'} peerDependencies: '@docusaurus/plugin-content-docs': '*' react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/theme-search-algolia@3.9.2': - resolution: {integrity: sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==} + '@docusaurus/theme-search-algolia@3.10.0': + resolution: {integrity: sha512-f5FPKI08e3JRG63vR/o4qeuUVHUHzFzM0nnF+AkB67soAZgNsKJRf2qmUZvlQkGwlV+QFkKe4D0ANMh1jToU3g==} engines: {node: '>=20.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/theme-translations@3.9.2': - resolution: {integrity: sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==} + '@docusaurus/theme-translations@3.10.0': + resolution: {integrity: sha512-L9IbFLwTc5+XdgH45iQYufLn0SVZd6BUNelDbKIFlH+E4hhjuj/XHWAFMX/w2K59rfy8wak9McOaei7BSUfRPA==} engines: {node: '>=20.0'} - '@docusaurus/tsconfig@3.9.2': - resolution: {integrity: sha512-j6/Fp4Rlpxsc632cnRnl5HpOWeb6ZKssDj6/XzzAzVGXXfm9Eptx3rxCC+fDzySn9fHTS+CWJjPineCR1bB5WQ==} + '@docusaurus/tsconfig@3.10.0': + resolution: {integrity: sha512-TXdC3WXuPrdQAexLvjUJfnYf3YKEgEqAs5nK0Q88pRBCW7t7oN4ILvWYb3A5Z1wlSXyXGWW/mCUmLEhdWsjnDQ==} - '@docusaurus/types@3.9.2': - resolution: {integrity: sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==} + '@docusaurus/types@3.10.0': + resolution: {integrity: sha512-F0dOt3FOoO20rRaFK7whGFQZ3ggyrWEdQc/c8/UiRuzhtg4y1w9FspXH5zpCT07uMnJKBPGh+qNazbNlCQqvSw==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - '@docusaurus/utils-common@3.9.2': - resolution: {integrity: sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==} + '@docusaurus/utils-common@3.10.0': + resolution: {integrity: sha512-JyL7sb9QVDgYvudIS81Dv0lsWm7le0vGZSDwsztxWam1SPBqrnkvBy9UYL/amh6pbybkyYTd3CMTkO24oMlCSw==} engines: {node: '>=20.0'} - '@docusaurus/utils-validation@3.9.2': - resolution: {integrity: sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==} + '@docusaurus/utils-validation@3.10.0': + resolution: {integrity: sha512-c+6n2+ZPOJtWWc8Bb/EYdpSDfjYEScdCu9fB/SNjOmSCf1IdVnGf2T53o0tsz0gDRtCL90tifTL0JE/oMuP1Mw==} engines: {node: '>=20.0'} - '@docusaurus/utils@3.9.2': - resolution: {integrity: sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==} + '@docusaurus/utils@3.10.0': + resolution: {integrity: sha512-T3B0WTigsIthe0D4LQa2k+7bJY+c3WS+Wq2JhcznOSpn1lSN64yNtHQXboCj3QnUs1EuAZszQG1SHKu5w5ZrlA==} engines: {node: '>=20.0'} '@emnapi/core@1.7.1': @@ -2573,9 +2411,6 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@jsdevtools/ono@7.1.3': - resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - '@jsep-plugin/assignment@1.3.0': resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} engines: {node: '>= 10.16.0'} @@ -2731,9 +2566,30 @@ packages: '@cfworker/json-schema': optional: true + '@module-federation/error-codes@0.22.0': + resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} + + '@module-federation/runtime-core@0.22.0': + resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} + + '@module-federation/runtime-tools@0.22.0': + resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} + + '@module-federation/runtime@0.22.0': + resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} + + '@module-federation/sdk@0.22.0': + resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} + + '@module-federation/webpack-bundler-runtime@0.22.0': + resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.0.7': + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -2981,21 +2837,24 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@redocly/ajv@8.17.1': - resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==} + '@redocly/ajv@8.18.0': + resolution: {integrity: sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==} - '@redocly/config@0.22.2': - resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + '@redocly/ajv@8.18.3': + resolution: {integrity: sha512-l42u0of3hY98sN2A+M4qTX1O/KrpgGH32Hu9kP2GtHyD5Dfqq86PKFLe5dwaD8DEnNmlOlll2BAmeEtf0DaySg==} - '@redocly/openapi-core@1.34.6': - resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==} - engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@redocly/config@0.46.1': + resolution: {integrity: sha512-dSdkB2wRLtvl3f7ayRu9vqVhUMjjRaxZlHgRbgOtPPXxn4uI/ciDO87h4CJb7Iet+OVpevpAU6gU8bo5qVbQxg==} - '@reduxjs/toolkit@1.9.7': - resolution: {integrity: sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==} + '@redocly/openapi-core@2.26.0': + resolution: {integrity: sha512-BjTPzSV1Gv430W9S/7i5T/dEZDK00GFk6ILCNTI+31pA9lEFJOXc0XRJT+V3v+m3nXIgGoo6GgqeLdAiM10rNg==} + engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} + + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.0.2 + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 peerDependenciesMeta: react: optional: true @@ -3135,6 +2994,74 @@ packages: cpu: [x64] os: [win32] + '@rspack/binding-darwin-arm64@1.7.11': + resolution: {integrity: sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@1.7.11': + resolution: {integrity: sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@1.7.11': + resolution: {integrity: sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-arm64-musl@1.7.11': + resolution: {integrity: sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rspack/binding-linux-x64-gnu@1.7.11': + resolution: {integrity: sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-x64-musl@1.7.11': + resolution: {integrity: sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rspack/binding-wasm32-wasi@1.7.11': + resolution: {integrity: sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@1.7.11': + resolution: {integrity: sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@1.7.11': + resolution: {integrity: sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@1.7.11': + resolution: {integrity: sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==} + cpu: [x64] + os: [win32] + + '@rspack/binding@1.7.11': + resolution: {integrity: sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==} + + '@rspack/core@1.7.11': + resolution: {integrity: sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@rspack/lite-tapable@1.1.0': + resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -3201,6 +3128,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@stoplight/better-ajv-errors@1.0.3': resolution: {integrity: sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==} engines: {node: ^12.20 || >= 14.13} @@ -3376,54 +3306,229 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@szmarczak/http-timer@5.0.1': - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} - - '@testcontainers/postgresql@11.13.0': - resolution: {integrity: sha512-Y+HLf+IEu9+h3MgxRhnFunw9ldk3jxN2PO6zyejN5AiDd1aSeBQ7hfpc/4MlMm65St2DdLGf39oE/vdW1+hc/Q==} + '@swc/core-darwin-arm64@1.15.24': + resolution: {integrity: sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} + '@swc/core-darwin-x64@1.15.24': + resolution: {integrity: sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@swc/core-linux-arm-gnueabihf@1.15.24': + resolution: {integrity: sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true + '@swc/core-linux-arm64-gnu@1.15.24': + resolution: {integrity: sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' + '@swc/core-linux-arm64-musl@1.15.24': + resolution: {integrity: sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] - '@trysound/sax@0.2.0': - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} + '@swc/core-linux-ppc64-gnu@1.15.24': + resolution: {integrity: sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + '@swc/core-linux-s390x-gnu@1.15.24': + resolution: {integrity: sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + '@swc/core-linux-x64-gnu@1.15.24': + resolution: {integrity: sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + '@swc/core-linux-x64-musl@1.15.24': + resolution: {integrity: sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.24': + resolution: {integrity: sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.24': + resolution: {integrity: sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.24': + resolution: {integrity: sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.24': + resolution: {integrity: sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/html-darwin-arm64@1.15.24': + resolution: {integrity: sha512-2yH5kkeBM6mcSajWdIvh482HZDthvWM+SkH17CAzmgDgP2WGZ3IpdeIQxdV8Jj9kRdJaI0VqdXGT0qRRt6zw4A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/html-darwin-x64@1.15.24': + resolution: {integrity: sha512-1k4Wl1eExT9yal3fX6MGcrpWOvYo+f7jnzw+ksg+8ifpYqpcrcy6Rv6cB78SgXzZJRpx8zBY1luk+zYyoDlrWA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/html-linux-arm-gnueabihf@1.15.24': + resolution: {integrity: sha512-XbqWgyBE6tukUs+0zwzW+Xo3N/P6SoiJJ44QfB3RCb5Naz/1vwJbNgn9erFDgoq7CChmCooFuMfNnmh/E/Orsg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/html-linux-arm64-gnu@1.15.24': + resolution: {integrity: sha512-GqJgkJHTlLM0tzJHX0tmU0ZAU4rIfMYZ2yJwCBwnFaLw4NacpimyWnWGJxH83SViVZ33DfLD2LG/dHN8xDAmRA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/html-linux-arm64-musl@1.15.24': + resolution: {integrity: sha512-+7Xw69Y4p/LwhudMJZOQ++mKeXWTnh3vpNv5Ar+X1x8kfPBHKRXI3sRKf5JqE0oJqJXTgFP5xByzmO/KBee3sQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/html-linux-ppc64-gnu@1.15.24': + resolution: {integrity: sha512-ZKxckgQkOY2a54jiCnIBs5TkMNx7zvuKbe1WsM/WV0BiTfMfw5iMmtCKAIuYCz/PJRXVK0dY4VH3DS7jabBvwg==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/html-linux-s390x-gnu@1.15.24': + resolution: {integrity: sha512-y0WBjqDZALqOzasxrEOlgHq6SX34nAE4+0MATufmSoFEdiQIBYkm9m4C8XQNCNHv52ERCu/EPGK3Q8RfXaBLhQ==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/html-linux-x64-gnu@1.15.24': + resolution: {integrity: sha512-U//u302yBSgh6vFfJmrw17Xm7k9a17m/E3AcHK4w12CZOFtsKHQnxE3i9uFWhNbW5F70w2A9QENml5b0Us8XMg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/html-linux-x64-musl@1.15.24': + resolution: {integrity: sha512-U9gsAQCPiCROWKhLhSnW4JzkkOY6X4q0ZP/nA6UeKoahDdw4E8onPujtRSivt4ZxwdJKfAnsxeJY07V9YLZu9Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/html-win32-arm64-msvc@1.15.24': + resolution: {integrity: sha512-AETh78z9ig4e1eAlx8a02BnIS5iNIJ7C43swQsxMraSDZvZuBxnvEXHqnt94jRlw7fzmJRRpJdVcInQ21u/xGA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/html-win32-ia32-msvc@1.15.24': + resolution: {integrity: sha512-ymJkEATvFF1+So41/SkulPBoRzRXP6HxUGfvdSJ29qeYejxWMrIWyjDE1+vAalo4IAR0cWFE2Ef2A2Qeg8QbGA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/html-win32-x64-msvc@1.15.24': + resolution: {integrity: sha512-l+Gv0+jcSaDILljpEMC8pQE+ubRoZcft+woUgKTTlJQEFS+MgxKKLQjNCXx3hzhuru5/Yo8x71Ng/aVT7PwprA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/html@1.15.24': + resolution: {integrity: sha512-2kWRCU09lBBg3bZLz8Kc37azQ6sBwiV1P7VDvqwKEJC2CtREe5y1XgLLd78kqSpFli52hZ6l3CNPDqkaX6ceAg==} + engines: {node: '>=14'} + + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@testcontainers/postgresql@11.13.0': + resolution: {integrity: sha512-Y+HLf+IEu9+h3MgxRhnFunw9ldk3jxN2PO6zyejN5AiDd1aSeBQ7hfpc/4MlMm65St2DdLGf39oE/vdW1+hc/Q==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} @@ -3496,6 +3601,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/accepts@1.3.7': + resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -3533,9 +3641,15 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/content-disposition@0.5.9': + resolution: {integrity: sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==} + '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cookies@0.9.2': + resolution: {integrity: sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -3569,11 +3683,8 @@ packages: '@types/express@4.17.25': resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - '@types/gtag.js@0.0.12': - resolution: {integrity: sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==} - - '@types/hast@2.3.10': - resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + '@types/gtag.js@0.0.20': + resolution: {integrity: sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==} '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -3581,14 +3692,12 @@ packages: '@types/history@4.7.11': resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - '@types/hoist-non-react-statics@3.3.7': - resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} - peerDependencies: - '@types/react': '*' - '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + '@types/http-assert@1.5.6': + resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} + '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -3622,12 +3731,21 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/keygrip@1.0.6': + resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} + + '@types/koa-compose@3.2.9': + resolution: {integrity: sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==} + + '@types/koa@3.0.2': + resolution: {integrity: sha512-7TRzVOBcH/q8CfPh9AmHBQ8TZtimT4Sn+rw8//hXveI6+F41z93W8a+0B0O8L7apKQv+vKBIEZSECiL0Oo1JFA==} + + '@types/koa__multer@2.0.8': + resolution: {integrity: sha512-XkD9YzmGKzKfOkxwHepueQkP61okAeu6ibAieL4NSqwM1lyGOkgbT2XmP+xROd2agQVsewPqsFOsfgVmlEqJmg==} + '@types/markdown-escape@1.1.3': resolution: {integrity: sha512-JIc1+s3y5ujKnt/+N+wq6s/QdL2qZ11fP79MijrVXsAAnzSxCbT2j/3prHRouJdZ2yFLN3vkP0HytfnoCczjOw==} - '@types/mdast@3.0.15': - resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -3671,9 +3789,6 @@ packages: resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. - '@types/parse5@6.0.3': - resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} - '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -3683,18 +3798,12 @@ packages: '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-redux@7.1.34': - resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==} - '@types/react-relay@18.2.1': resolution: {integrity: sha512-KgmFapsxAylhxcFfaAv5GZZJhTHnDvV8IDZVsUm5afpJUvgZC1Y68ssfOGsFfiFY/2EhxHM/YPfpdKbfmF3Ecg==} @@ -3770,6 +3879,9 @@ packages: '@types/urijs@1.19.26': resolution: {integrity: sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -4141,9 +4253,6 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - ajv@8.11.0: - resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} - ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -4301,22 +4410,12 @@ packages: async-lock@1.4.1: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} - async@3.2.2: - resolution: {integrity: sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==} - - async@3.2.4: - resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} - async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - author-regex@1.0.0: resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} engines: {node: '>=0.8'} @@ -4375,11 +4474,6 @@ packages: resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - babel-plugin-polyfill-corejs2@0.4.14: - resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-corejs2@0.4.17: resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} peerDependencies: @@ -4395,11 +4489,6 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-regenerator@0.6.5: - resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-regenerator@0.6.8: resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} peerDependencies: @@ -4778,10 +4867,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -5000,15 +5085,9 @@ packages: peerDependencies: webpack: ^5.1.0 - core-js-compat@3.47.0: - resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} - core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} - core-js-pure@3.47.0: - resolution: {integrity: sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==} - core-js@3.47.0: resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} @@ -5361,6 +5440,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -5387,10 +5470,6 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -5415,13 +5494,13 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - docusaurus-plugin-openapi-docs@4.5.1: - resolution: {integrity: sha512-3I6Sjz19D/eM86a24/nVkYfqNkl/zuXSP04XVo7qm/vlPeCpHVM4li2DLj7PzElr6dlS9RbaS4HVIQhEOPGBRQ==} + docusaurus-plugin-openapi-docs@5.0.0: + resolution: {integrity: sha512-G77fm6cu61VuwFMSOgy7KRHbATPNqNenO2VXuULAO8AmiAMVJ0fx9hTy+UDzr+U9pzCE5ZRJRPngf6F2klFaLA==} engines: {node: '>=14'} peerDependencies: - '@docusaurus/plugin-content-docs': ^3.5.0 - '@docusaurus/utils': ^3.5.0 - '@docusaurus/utils-validation': ^3.5.0 + '@docusaurus/plugin-content-docs': ^3.10.0 + '@docusaurus/utils': ^3.10.0 + '@docusaurus/utils-validation': ^3.10.0 react: ^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0 docusaurus-plugin-sass@0.2.6: @@ -5430,12 +5509,12 @@ packages: '@docusaurus/core': ^2.0.0-beta || ^3.0.0-alpha sass: ^1.30.0 - docusaurus-theme-openapi-docs@4.5.1: - resolution: {integrity: sha512-C7mYh9JC3l9jjRtqJVu0EIyOgxHB08jE0Tp5NSkNkrrBak4A13SrXCisNjvt1eaNjS+tsz7qD0bT3aI5hsRvWA==} + docusaurus-theme-openapi-docs@5.0.0: + resolution: {integrity: sha512-5JXaBUCopCFsq0rpcl79UFziHuj7+FhhYxuqwf0GsyDa513Ns7db3d5ULUgfUg1qehCj9jJ9JIQpZUQEnX7aJw==} engines: {node: '>=14'} peerDependencies: - '@docusaurus/theme-common': ^3.5.0 - docusaurus-plugin-openapi-docs: ^4.0.0 + '@docusaurus/theme-common': ^3.10.0 + docusaurus-plugin-openapi-docs: ^5.0.0 docusaurus-plugin-sass: ^0.2.3 react: ^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -6213,18 +6292,10 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-extra@11.3.2: - resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} - engines: {node: '>=14.14'} - fs-extra@11.3.4: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} - fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - fs-minipass@3.0.3: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -6453,21 +6524,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-from-parse5@7.1.2: - resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} - hast-util-from-parse5@8.0.3: resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} - hast-util-parse-selector@3.1.1: - resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} - hast-util-raw@7.2.3: - resolution: {integrity: sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==} - hast-util-raw@9.1.0: resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} @@ -6477,21 +6539,12 @@ packages: hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} - hast-util-to-parse5@7.1.0: - resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==} - hast-util-to-parse5@8.0.1: resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} - hast-util-whitespace@2.0.1: - resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} - hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hastscript@7.2.0: - resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} - hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} @@ -6551,8 +6604,8 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} - html-void-elements@2.0.1: - resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -6686,6 +6739,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -6757,9 +6813,6 @@ packages: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} - inline-style-parser@0.1.1: - resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -6815,10 +6868,6 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} - is-buffer@2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} @@ -7325,6 +7374,10 @@ packages: resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==} engines: {node: '>=12.0.0'} + json-schema-to-ts@2.7.2: + resolution: {integrity: sha512-R1JfqKqbBR4qE8UyBR56Ms30LL62/nlhoz+1UkfI/VE7p54Awu919FZ6ZUPG8zIa3XB65usPJgr1ONVncUGSaQ==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -7400,10 +7453,6 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - koa-compose@4.1.0: resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} @@ -7445,6 +7494,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -7550,6 +7673,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -7627,60 +7753,33 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mdast-util-definitions@5.1.2: - resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} - mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} - mdast-util-find-and-replace@2.2.2: - resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} - mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - mdast-util-from-markdown@1.3.1: - resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} - mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} mdast-util-frontmatter@2.0.1: resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} - mdast-util-gfm-autolink-literal@1.0.3: - resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} - mdast-util-gfm-autolink-literal@2.0.1: resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} - mdast-util-gfm-footnote@1.0.2: - resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} - mdast-util-gfm-footnote@2.1.0: resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} - mdast-util-gfm-strikethrough@1.0.3: - resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} - mdast-util-gfm-strikethrough@2.0.0: resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - mdast-util-gfm-table@1.0.7: - resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} - mdast-util-gfm-table@2.0.0: resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - mdast-util-gfm-task-list-item@1.0.2: - resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} - mdast-util-gfm-task-list-item@2.0.0: resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - mdast-util-gfm@2.0.2: - resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} - mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} @@ -7696,27 +7795,15 @@ packages: mdast-util-mdxjs-esm@2.0.1: resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - mdast-util-phrasing@3.0.1: - resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} - mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - mdast-util-to-hast@12.3.0: - resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} - mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - mdast-util-to-markdown@1.5.0: - resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} - mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - mdast-util-to-string@3.2.0: - resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} - mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} @@ -7759,9 +7846,6 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - micromark-core-commonmark@1.1.0: - resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} - micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -7771,45 +7855,24 @@ packages: micromark-extension-frontmatter@2.0.0: resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} - micromark-extension-gfm-autolink-literal@1.0.5: - resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} - micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} - micromark-extension-gfm-footnote@1.1.2: - resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} - micromark-extension-gfm-footnote@2.1.0: resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} - micromark-extension-gfm-strikethrough@1.0.7: - resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} - micromark-extension-gfm-strikethrough@2.1.0: resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} - micromark-extension-gfm-table@1.0.7: - resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} - micromark-extension-gfm-table@2.1.1: resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} - micromark-extension-gfm-tagfilter@1.0.2: - resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} - micromark-extension-gfm-tagfilter@2.0.0: resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - micromark-extension-gfm-task-list-item@1.0.5: - resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} - micromark-extension-gfm-task-list-item@2.1.0: resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} - micromark-extension-gfm@2.0.3: - resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} - micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} @@ -7828,15 +7891,9 @@ packages: micromark-extension-mdxjs@3.0.0: resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} - micromark-factory-destination@1.1.0: - resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} - micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - micromark-factory-label@1.1.0: - resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} - micromark-factory-label@2.0.1: resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} @@ -7849,15 +7906,9 @@ packages: micromark-factory-space@2.0.1: resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - micromark-factory-title@1.1.0: - resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} - micromark-factory-title@2.0.1: resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - micromark-factory-whitespace@1.1.0: - resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} - micromark-factory-whitespace@2.0.1: resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} @@ -7867,72 +7918,39 @@ packages: micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - micromark-util-chunked@1.1.0: - resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} - micromark-util-chunked@2.0.1: resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - micromark-util-classify-character@1.1.0: - resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} - micromark-util-classify-character@2.0.1: resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - micromark-util-combine-extensions@1.1.0: - resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} - micromark-util-combine-extensions@2.0.1: resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - micromark-util-decode-numeric-character-reference@1.1.0: - resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} - micromark-util-decode-numeric-character-reference@2.0.2: resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - micromark-util-decode-string@1.1.0: - resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} - micromark-util-decode-string@2.0.1: resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - micromark-util-encode@1.1.0: - resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} - micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} micromark-util-events-to-acorn@2.0.3: resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} - micromark-util-html-tag-name@1.2.0: - resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} - micromark-util-html-tag-name@2.0.1: resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - micromark-util-normalize-identifier@1.1.0: - resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} - micromark-util-normalize-identifier@2.0.1: resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - micromark-util-resolve-all@1.1.0: - resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} - micromark-util-resolve-all@2.0.1: resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - micromark-util-sanitize-uri@1.2.0: - resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} - micromark-util-sanitize-uri@2.0.1: resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - micromark-util-subtokenize@1.1.0: - resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} - micromark-util-subtokenize@2.1.0: resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} @@ -7948,9 +7966,6 @@ packages: micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - micromark@3.2.0: - resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} - micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} @@ -7970,8 +7985,8 @@ packages: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-format@2.0.1: - resolution: {integrity: sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg==} + mime-format@2.0.2: + resolution: {integrity: sha512-Y5ERWVcyh3sby9Fx2U5F1yatiTFjNsqF5NltihTWI9QgNtr5o3dbCZdcKa1l2wyfhnwwoP9HGNxga7LqZLA6gw==} mime-types@2.1.18: resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} @@ -7995,6 +8010,11 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -8102,10 +8122,6 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -8373,11 +8389,14 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - openapi-to-postmanv2@4.25.0: - resolution: {integrity: sha512-sIymbkQby0gzxt2Yez8YKB6hoISEel05XwGwNrAhr6+vxJWXNxkmssQc/8UEtVkuJ9ZfUXLkip9PYACIpfPDWg==} - engines: {node: '>=8'} + openapi-to-postmanv2@6.0.0: + resolution: {integrity: sha512-PU676AZh/7A1CFi7MOsIRTnasEauK1EQL1c4ktAOZIknoPh7OZ3yC1NoRHfogoTfS/9WFXV+uOP1mg+402fuoQ==} + engines: {node: '>=18'} hasBin: true + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -8503,9 +8522,6 @@ packages: parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -8569,9 +8585,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - path@0.12.7: - resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -8624,6 +8637,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -9080,16 +9097,16 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - postman-code-generators@1.14.2: - resolution: {integrity: sha512-qZAyyowfQAFE4MSCu2KtMGGQE/+oG1JhMZMJNMdZHYCSfQiVVeKxgk3oI4+KJ3d1y5rrm2D6C6x+Z+7iyqm+fA==} - engines: {node: '>=12'} + postman-code-generators@2.1.1: + resolution: {integrity: sha512-+egQK1Jf9a92QP23vRTKcDLOthIQmI7WI4czEsZq/wgguLMnVHJ26KlT8AVtpAdVw28hqUbHwicerYxRWCfjoA==} + engines: {node: '>=18'} - postman-collection@4.5.0: - resolution: {integrity: sha512-152JSW9pdbaoJihwjc7Q8lc3nPg/PC9lPTHdMk7SHnHhu/GBJB7b2yb9zG7Qua578+3PxkQ/HYBuXpDSvsf7GQ==} - engines: {node: '>=10'} + postman-collection@5.3.0: + resolution: {integrity: sha512-PMa5vRheqDFfS1bkRg8WBidWxunRA80sT5YNLP27YC5+ycyfiLMCwPnqQd1zfvxkGk04Pr9UronWmmgsbpsVyQ==} + engines: {node: '>=18'} - postman-url-encoder@3.0.5: - resolution: {integrity: sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA==} + postman-url-encoder@3.0.8: + resolution: {integrity: sha512-EOgUMBazo7JNP4TDrd64TsooCiWzzo4143Ws8E8WYGEpn2PKpq+S4XRTDhuRTYHm3VKOpUZs7ZYZq7zSDuesqA==} engines: {node: '>=10'} prelude-ls@1.2.1: @@ -9176,9 +9193,6 @@ packages: resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} engines: {node: '>=18'} - property-information@6.5.0: - resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -9213,10 +9227,6 @@ packages: pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - qs@6.14.1: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} @@ -9251,10 +9261,10 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-dom@19.2.3: - resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: - react: ^19.2.3 + react: ^19.2.5 react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} @@ -9290,8 +9300,8 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' - react-loadable-ssr-addon-v5-slorber@1.0.1: - resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} + react-loadable-ssr-addon-v5-slorber@1.0.3: + resolution: {integrity: sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==} engines: {node: '>=10.13.0'} peerDependencies: react-loadable: '*' @@ -9300,11 +9310,11 @@ packages: react-magic-dropzone@1.0.1: resolution: {integrity: sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA==} - react-markdown@8.0.7: - resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} peerDependencies: - '@types/react': '>=16' - react: '>=16' + '@types/react': '>=18' + react: '>=18' react-modal@3.16.3: resolution: {integrity: sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==} @@ -9312,16 +9322,16 @@ packages: react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19 react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19 - react-redux@7.2.9: - resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} peerDependencies: - react: ^16.8.3 || ^17 || ^18 - react-dom: '*' - react-native: '*' + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 peerDependenciesMeta: - react-dom: + '@types/react': optional: true - react-native: + redux: optional: true react-router-config@5.1.1: @@ -9340,8 +9350,8 @@ packages: peerDependencies: react: '>=15' - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} readable-stream@2.3.8: @@ -9388,13 +9398,13 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - redux-thunk@2.4.2: - resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: - redux: ^4 + redux: ^5.0.0 - redux@4.2.1: - resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -9440,9 +9450,6 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true - rehype-raw@6.1.1: - resolution: {integrity: sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==} - rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -9469,24 +9476,15 @@ packages: remark-frontmatter@5.0.0: resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} - remark-gfm@3.0.1: - resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} - remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} remark-mdx@3.1.1: resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} - remark-parse@10.0.2: - resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} - remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - remark-rehype@10.1.0: - resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} - remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} @@ -9518,8 +9516,8 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - reselect@4.1.8: - resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} reserved@0.1.2: resolution: {integrity: sha512-/qO54MWj5L8WCBP9/UNe2iefJc+L9yETbH32xO/ft/EYPOTCR5k+azvDUgdCOKwZH8hXwPd0b8XBL78Nn2U69g==} @@ -9617,10 +9615,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -9714,8 +9708,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true @@ -9790,8 +9784,8 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serve-handler@6.1.6: - resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} + serve-handler@6.1.7: + resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} serve-index@1.9.1: resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} @@ -10172,9 +10166,6 @@ packages: style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} - style-to-object@0.4.4: - resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} - style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} @@ -10224,6 +10215,12 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + swc-loader@0.2.7: + resolution: {integrity: sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==} + peerDependencies: + '@swc/core': ^1.2.147 + webpack: '>=2' + swr@2.3.8: resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} peerDependencies: @@ -10442,6 +10439,9 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-algebra@1.2.2: + resolution: {integrity: sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -10630,9 +10630,6 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} - unified@10.1.2: - resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -10648,39 +10645,21 @@ packages: resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} engines: {node: '>=12'} - unist-util-generated@2.0.1: - resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} - - unist-util-is@5.2.1: - resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} unist-util-position-from-estree@2.0.0: resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} - unist-util-position@4.0.4: - resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} - unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - unist-util-stringify-position@3.0.3: - resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} - unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - unist-util-visit-parents@5.1.3: - resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} - unist-util-visit-parents@6.0.2: resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - unist-util-visit@4.1.2: - resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} - unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} @@ -10741,9 +10720,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - util@0.10.4: - resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} - utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} @@ -10763,11 +10739,6 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - uvu@0.5.6: - resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} - engines: {node: '>=8'} - hasBin: true - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -10811,21 +10782,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vfile-location@4.1.0: - resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} - vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} - vfile-message@3.1.4: - resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} - vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - vfile@5.3.7: - resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} - vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -11054,9 +11016,9 @@ packages: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} - xml-formatter@2.6.1: - resolution: {integrity: sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw==} - engines: {node: '>= 10'} + xml-formatter@3.7.0: + resolution: {integrity: sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==} + engines: {node: '>= 16'} xml-js@1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} @@ -11066,9 +11028,9 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - xml-parser-xo@3.2.0: - resolution: {integrity: sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==} - engines: {node: '>= 10'} + xml-parser-xo@4.1.5: + resolution: {integrity: sha512-TxyRxk9sTOUg3glxSIY6f0nfuqRll2OEF8TspLgh5mZkLuBgheCn3zClcDSGJ58TvNmiwyCCuat4UajPud/5Og==} + engines: {node: '>= 16'} xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -11189,12 +11151,12 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.120(react@19.2.3)(zod@4.1.13)': + '@ai-sdk/react@2.0.120(react@19.2.5)(zod@4.1.13)': dependencies: '@ai-sdk/provider-utils': 3.0.20(zod@4.1.13) ai: 5.0.118(zod@4.1.13) - react: 19.2.3 - swr: 2.3.8(react@19.2.3) + react: 19.2.5 + swr: 2.3.8(react@19.2.5) throttleit: 2.1.0 optionalDependencies: zod: 4.1.13 @@ -11307,9 +11269,8 @@ snapshots: dependencies: '@algolia/client-common': 5.46.2 - '@apidevtools/json-schema-ref-parser@11.9.3': + '@apidevtools/json-schema-ref-parser@15.3.5(@types/json-schema@7.0.15)': dependencies: - '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 js-yaml: 4.1.1 @@ -11417,19 +11378,6 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.5 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11456,13 +11404,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - regexpu-core: 6.4.0 - semver: 6.3.1 - '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11470,17 +11411,6 @@ snapshots: regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.3 - lodash.debounce: 4.0.8 - resolve: 1.22.11 - transitivePeerDependencies: - - supports-color - '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11550,15 +11480,6 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.28.3 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11568,15 +11489,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11634,14 +11546,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11650,35 +11554,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11688,14 +11573,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11713,10 +11590,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11746,15 +11619,10 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': dependencies: @@ -11841,37 +11709,17 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11881,15 +11729,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11899,34 +11738,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11935,14 +11756,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11951,18 +11764,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-globals': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11975,26 +11776,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/template': 7.27.2 - '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/template': 7.28.6 - '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12003,58 +11790,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12063,34 +11820,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12099,72 +11838,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12173,14 +11875,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12197,16 +11891,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12217,14 +11901,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12233,59 +11909,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.5) - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12297,14 +11941,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12313,24 +11949,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-optional-chaining@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-optional-chaining@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12347,24 +11970,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12373,15 +11983,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12391,38 +11992,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12430,17 +12014,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) - '@babel/types': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12452,80 +12025,45 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-runtime@7.28.5(@babel/core@7.28.5)': + '@babel/plugin-transform-runtime@7.28.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-plugin-utils': 7.27.1 - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.5) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.5) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12534,47 +12072,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12586,128 +12098,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/preset-env@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/compat-data': 7.28.5 - '@babel/core': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.28.5) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.28.5) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5) - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.5) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.5) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-block-scoping': 7.28.5(@babel/core@7.28.5) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.28.5) - '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.5) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) - '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.5) - '@babel/plugin-transform-exponentiation-operator': 7.28.5(@babel/core@7.28.5) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-logical-assignment-operators': 7.28.5(@babel/core@7.28.5) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.28.5) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.28.5) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.5) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.5) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.28.5) - '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.5) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.5) - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.5) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.5) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.5) - core-js-compat: 3.47.0 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/preset-env@7.29.2(@babel/core@7.29.0)': dependencies: '@babel/compat-data': 7.29.0 @@ -12784,13 +12197,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.5 - esutils: 2.0.3 - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12798,18 +12204,6 @@ snapshots: '@babel/types': 7.28.5 esutils: 2.0.3 - '@babel/preset-react@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/preset-react@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12822,17 +12216,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.28.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12844,10 +12227,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime-corejs3@7.28.4': - dependencies: - core-js-pure: 3.47.0 - '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': @@ -13342,19 +12721,19 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/core@4.4.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docsearch/core@4.4.0(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': optionalDependencies: '@types/react': 19.2.7 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) '@docsearch/css@4.4.0': {} - '@docsearch/react@4.4.0(@algolia/client-search@5.46.2)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)': + '@docsearch/react@4.4.0(@algolia/client-search@5.46.2)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(search-insights@2.17.3)': dependencies: - '@ai-sdk/react': 2.0.120(react@19.2.3)(zod@4.1.13) + '@ai-sdk/react': 2.0.120(react@19.2.5)(zod@4.1.13) '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3) - '@docsearch/core': 4.4.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docsearch/core': 4.4.0(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@docsearch/css': 4.4.0 ai: 5.0.118(zod@4.1.13) algoliasearch: 5.46.2 @@ -13362,28 +12741,27 @@ snapshots: zod: 4.1.13 optionalDependencies: '@types/react': 19.2.7 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) search-insights: 2.17.3 transitivePeerDependencies: - '@algolia/client-search' - '@docusaurus/babel@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/babel@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@babel/core': 7.28.5 - '@babel/generator': 7.28.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-transform-runtime': 7.28.5(@babel/core@7.28.5) - '@babel/preset-env': 7.28.5(@babel/core@7.28.5) - '@babel/preset-react': 7.28.5(@babel/core@7.28.5) - '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.28.5(@babel/core@7.29.0) + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + '@babel/preset-react': 7.28.5(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@babel/runtime': 7.28.4 - '@babel/runtime-corejs3': 7.28.4 - '@babel/traverse': 7.28.5 - '@docusaurus/logger': 3.9.2 - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@babel/traverse': 7.29.0 + '@docusaurus/logger': 3.10.0 + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) babel-plugin-dynamic-import-node: 2.3.3 - fs-extra: 11.3.2 + fs-extra: 11.3.4 tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' @@ -13394,32 +12772,34 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/bundler@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@babel/core': 7.28.5 - '@docusaurus/babel': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/cssnano-preset': 3.9.2 - '@docusaurus/logger': 3.9.2 - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.104.1) + '@babel/core': 7.29.0 + '@docusaurus/babel': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/cssnano-preset': 3.10.0 + '@docusaurus/logger': 3.10.0 + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + babel-loader: 9.2.1(@babel/core@7.29.0)(webpack@5.104.1(@swc/core@1.15.24)) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.104.1) - css-loader: 6.11.0(webpack@5.104.1) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.104.1) + copy-webpack-plugin: 11.0.0(webpack@5.104.1(@swc/core@1.15.24)) + css-loader: 6.11.0(@rspack/core@1.7.11)(webpack@5.104.1(@swc/core@1.15.24)) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.104.1(@swc/core@1.15.24)) cssnano: 6.1.2(postcss@8.5.6) - file-loader: 6.2.0(webpack@5.104.1) + file-loader: 6.2.0(webpack@5.104.1(@swc/core@1.15.24)) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.9.4(webpack@5.104.1) - null-loader: 4.0.1(webpack@5.104.1) + mini-css-extract-plugin: 2.9.4(webpack@5.104.1(@swc/core@1.15.24)) + null-loader: 4.0.1(webpack@5.104.1(@swc/core@1.15.24)) postcss: 8.5.6 - postcss-loader: 7.3.4(postcss@8.5.6)(typescript@6.0.2)(webpack@5.104.1) + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@6.0.2)(webpack@5.104.1(@swc/core@1.15.24)) postcss-preset-env: 10.6.0(postcss@8.5.6) - terser-webpack-plugin: 5.3.16(webpack@5.104.1) + terser-webpack-plugin: 5.3.16(@swc/core@1.15.24)(webpack@5.104.1(@swc/core@1.15.24)) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) - webpack: 5.104.1 - webpackbar: 6.0.1(webpack@5.104.1) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1(@swc/core@1.15.24)))(webpack@5.104.1(@swc/core@1.15.24)) + webpack: 5.104.1(@swc/core@1.15.24) + webpackbar: 6.0.1(webpack@5.104.1(@swc/core@1.15.24)) + optionalDependencies: + '@docusaurus/faster': 3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -13435,16 +12815,16 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/core@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@docusaurus/babel': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/bundler': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/logger': 3.9.2 - '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.3) + '@docusaurus/babel': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/bundler': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/logger': 3.10.0 + '@docusaurus/mdx-loader': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-common': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.5) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -13457,33 +12837,34 @@ snapshots: eta: 2.2.0 eval: 0.1.8 execa: 5.1.1 - fs-extra: 11.3.2 + fs-extra: 11.3.4 html-tags: 3.3.1 - html-webpack-plugin: 5.6.5(webpack@5.104.1) + html-webpack-plugin: 5.6.5(@rspack/core@1.7.11)(webpack@5.104.1(@swc/core@1.15.24)) leven: 3.1.0 - lodash: 4.17.21 + lodash: 4.17.23 open: 8.4.2 p-map: 4.0.0 prompts: 2.4.2 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)' - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.3)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.3))(webpack@5.104.1) - react-router: 5.3.4(react@19.2.3) - react-router-config: 5.1.1(react-router@5.3.4(react@19.2.3))(react@19.2.3) - react-router-dom: 5.3.4(react@19.2.3) - semver: 7.7.3 - serve-handler: 6.1.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.5)' + react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.5))(webpack@5.104.1(@swc/core@1.15.24)) + react-router: 5.3.4(react@19.2.5) + react-router-config: 5.1.1(react-router@5.3.4(react@19.2.5))(react@19.2.5) + react-router-dom: 5.3.4(react@19.2.5) + semver: 7.7.4 + serve-handler: 6.1.7 tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.2(webpack@5.104.1) + webpack-dev-server: 5.2.2(webpack@5.104.1(@swc/core@1.15.24)) webpack-merge: 6.0.1 + optionalDependencies: + '@docusaurus/faster': 3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) transitivePeerDependencies: - - '@docusaurus/faster' - '@parcel/css' - '@rspack/core' - '@swc/core' @@ -13499,34 +12880,52 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/cssnano-preset@3.9.2': + '@docusaurus/cssnano-preset@3.10.0': dependencies: cssnano-preset-advanced: 6.1.2(postcss@8.5.6) postcss: 8.5.6 postcss-sort-media-queries: 5.2.0(postcss@8.5.6) tslib: 2.8.1 - '@docusaurus/logger@3.9.2': + '@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': + dependencies: + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rspack/core': 1.7.11 + '@swc/core': 1.15.24 + '@swc/html': 1.15.24 + browserslist: 4.28.1 + lightningcss: 1.32.0 + semver: 7.7.4 + swc-loader: 0.2.7(@swc/core@1.15.24)(webpack@5.104.1(@swc/core@1.15.24)) + tslib: 2.8.1 + webpack: 5.104.1(@swc/core@1.15.24) + transitivePeerDependencies: + - '@swc/helpers' + - esbuild + - uglify-js + - webpack-cli + + '@docusaurus/logger@3.10.0': dependencies: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/mdx-loader@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@docusaurus/logger': 3.9.2 - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/logger': 3.10.0 + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@mdx-js/mdx': 3.1.1 '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.104.1) - fs-extra: 11.3.2 + file-loader: 6.2.0(webpack@5.104.1(@swc/core@1.15.24)) + fs-extra: 11.3.4 image-size: 2.0.2 mdast-util-mdx: 3.0.0 mdast-util-to-string: 4.0.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) rehype-raw: 7.0.0 remark-directive: 3.0.1 remark-emoji: 4.0.1 @@ -13536,9 +12935,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1(@swc/core@1.15.24)))(webpack@5.104.1(@swc/core@1.15.24)) vfile: 6.0.3 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) transitivePeerDependencies: - '@swc/core' - esbuild @@ -13546,17 +12945,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/module-type-aliases@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@types/history': 4.7.11 '@types/react': 19.2.7 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)' - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.3)' + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.5)' transitivePeerDependencies: - '@swc/core' - esbuild @@ -13564,29 +12963,30 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': - dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/logger': 3.9.2 - '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/plugin-content-blog@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': + dependencies: + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/logger': 3.10.0 + '@docusaurus/mdx-loader': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/plugin-content-docs': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/theme-common': 3.10.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-common': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) cheerio: 1.0.0-rc.12 + combine-promises: 1.2.0 feed: 4.2.2 - fs-extra: 11.3.2 - lodash: 4.17.21 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + fs-extra: 11.3.4 + lodash: 4.17.23 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) schema-dts: 1.1.5 srcset: 4.0.0 tslib: 2.8.1 unist-util-visit: 5.0.0 utility-types: 3.11.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13605,28 +13005,28 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': - dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/logger': 3.9.2 - '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': + dependencies: + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/logger': 3.10.0 + '@docusaurus/mdx-loader': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/module-type-aliases': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/theme-common': 3.10.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-common': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 - fs-extra: 11.3.2 + fs-extra: 11.3.4 js-yaml: 4.1.1 - lodash: 4.17.21 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + lodash: 4.17.23 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13645,18 +13045,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/plugin-content-pages@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - fs-extra: 11.3.2 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/mdx-loader': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + fs-extra: 11.3.4 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13675,12 +13075,12 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/plugin-css-cascade-layers@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -13702,15 +13102,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/plugin-debug@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - fs-extra: 11.3.2 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-json-view-lite: 2.5.0(react@19.2.3) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + fs-extra: 11.3.4 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-json-view-lite: 2.5.0(react@19.2.5) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -13730,13 +13130,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/plugin-google-analytics@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -13756,14 +13156,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/plugin-google-gtag@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@types/gtag.js': 0.0.12 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/gtag.js': 0.0.20 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -13783,13 +13183,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/plugin-google-tag-manager@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -13809,17 +13209,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': - dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/logger': 3.9.2 - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - fs-extra: 11.3.2 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@docusaurus/plugin-sitemap@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': + dependencies: + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/logger': 3.10.0 + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-common': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + fs-extra: 11.3.4 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) sitemap: 7.1.2 tslib: 2.8.1 transitivePeerDependencies: @@ -13840,18 +13240,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@docusaurus/plugin-svgr@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@svgr/core': 8.1.0(typescript@6.0.2) '@svgr/webpack': 8.1.0(typescript@6.0.2) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13870,25 +13270,25 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@6.0.2)': - dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@6.0.2) - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@docusaurus/preset-classic@3.10.0(@algolia/client-search@5.46.2)(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(search-insights@2.17.3)(typescript@6.0.2)': + dependencies: + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-content-blog': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-content-docs': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-content-pages': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-css-cascade-layers': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-debug': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-google-analytics': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-google-gtag': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-google-tag-manager': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-sitemap': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-svgr': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/theme-classic': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@rspack/core@1.7.11)(@swc/core@1.15.24)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/theme-common': 3.10.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/theme-search-algolia': 3.10.0(@algolia/client-search@5.46.2)(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(search-insights@2.17.3)(typescript@6.0.2) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) transitivePeerDependencies: - '@algolia/client-search' - '@docusaurus/faster' @@ -13910,37 +13310,38 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/react-loadable@6.0.0(react@19.2.3)': + '@docusaurus/react-loadable@6.0.0(react@19.2.5)': dependencies: '@types/react': 19.2.7 - react: 19.2.3 - - '@docusaurus/theme-classic@3.9.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': - dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/logger': 3.9.2 - '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/theme-translations': 3.9.2 - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.3) + react: 19.2.5 + + '@docusaurus/theme-classic@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@rspack/core@1.7.11)(@swc/core@1.15.24)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': + dependencies: + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/logger': 3.10.0 + '@docusaurus/mdx-loader': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/module-type-aliases': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/plugin-content-blog': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-content-docs': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/plugin-content-pages': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/theme-common': 3.10.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/theme-translations': 3.10.0 + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-common': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.5) clsx: 2.1.1 + copy-text-to-clipboard: 3.2.2 infima: 0.2.0-alpha.45 - lodash: 4.17.21 + lodash: 4.17.23 nprogress: 0.2.0 postcss: 8.5.6 - prism-react-renderer: 2.4.1(react@19.2.3) + prism-react-renderer: 2.4.1(react@19.2.5) prismjs: 1.30.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-router-dom: 5.3.4(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router-dom: 5.3.4(react@19.2.5) rtlcss: 4.3.0 tslib: 2.8.1 utility-types: 3.11.0 @@ -13962,21 +13363,21 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/theme-common@3.10.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/mdx-loader': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/module-type-aliases': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/plugin-content-docs': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-common': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@types/history': 4.7.11 '@types/react': 19.2.7 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 - prism-react-renderer: 2.4.1(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + prism-react-renderer: 2.4.1(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 utility-types: 3.11.0 transitivePeerDependencies: @@ -13986,24 +13387,25 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.2)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@6.0.2)': + '@docusaurus/theme-search-algolia@3.10.0(@algolia/client-search@5.46.2)(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(search-insights@2.17.3)(typescript@6.0.2)': dependencies: - '@docsearch/react': 4.4.0(@algolia/client-search@5.46.2)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3) - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/logger': 3.9.2 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/theme-translations': 3.9.2 - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3) + '@docsearch/react': 4.4.0(@algolia/client-search@5.46.2)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(search-insights@2.17.3) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/logger': 3.10.0 + '@docusaurus/plugin-content-docs': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/theme-common': 3.10.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/theme-translations': 3.10.0 + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) algoliasearch: 5.46.2 algoliasearch-helper: 3.27.0(algoliasearch@5.46.2) clsx: 2.1.1 eta: 2.2.0 - fs-extra: 11.3.2 - lodash: 4.17.21 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + fs-extra: 11.3.4 + lodash: 4.17.23 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 utility-types: 3.11.0 transitivePeerDependencies: @@ -14027,14 +13429,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-translations@3.9.2': + '@docusaurus/theme-translations@3.10.0': dependencies: - fs-extra: 11.3.2 + fs-extra: 11.3.4 tslib: 2.8.1 - '@docusaurus/tsconfig@3.9.2': {} + '@docusaurus/tsconfig@3.10.0': {} - '@docusaurus/types@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@mdx-js/mdx': 3.1.1 '@types/history': 4.7.11 @@ -14042,11 +13444,11 @@ snapshots: '@types/react': 19.2.7 commander: 5.1.0 joi: 17.13.3 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)' + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)' utility-types: 3.11.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -14055,9 +13457,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/utils-common@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' @@ -14068,15 +13470,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/utils-validation@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@docusaurus/logger': 3.9.2 - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - fs-extra: 11.3.2 + '@docusaurus/logger': 3.10.0 + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-common': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + fs-extra: 11.3.4 joi: 17.13.3 js-yaml: 4.1.1 - lodash: 4.17.21 + lodash: 4.17.23 tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' @@ -14087,29 +13489,29 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@docusaurus/utils@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@docusaurus/logger': 3.9.2 - '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/logger': 3.10.0 + '@docusaurus/types': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-common': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.104.1) - fs-extra: 11.3.2 + file-loader: 6.2.0(webpack@5.104.1(@swc/core@1.15.24)) + fs-extra: 11.3.4 github-slugger: 1.5.0 globby: 11.1.0 gray-matter: 4.0.3 jiti: 1.21.7 js-yaml: 4.1.1 - lodash: 4.17.21 + lodash: 4.17.23 micromatch: 4.0.8 p-queue: 6.6.2 prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1(@swc/core@1.15.24)))(webpack@5.104.1(@swc/core@1.15.24)) utility-types: 3.11.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) transitivePeerDependencies: - '@swc/core' - esbuild @@ -14450,11 +13852,11 @@ snapshots: dependencies: hono: 4.12.12 - '@hookform/error-message@2.0.1(react-dom@19.2.3(react@19.2.3))(react-hook-form@7.70.0(react@19.2.3))(react@19.2.3)': + '@hookform/error-message@2.0.1(react-dom@19.2.5(react@19.2.5))(react-hook-form@7.70.0(react@19.2.5))(react@19.2.5)': dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-hook-form: 7.70.0(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-hook-form: 7.70.0(react@19.2.5) '@humanfs/core@0.19.1': {} @@ -14548,7 +13950,7 @@ snapshots: jest-util: 30.3.0 slash: 3.0.0 - '@jest/core@30.3.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2))': + '@jest/core@30.3.0(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2))': dependencies: '@jest/console': 30.3.0 '@jest/pattern': 30.0.1 @@ -14563,7 +13965,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.3.0 - jest-config: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + jest-config: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) jest-haste-map: 30.3.0 jest-message-util: 30.3.0 jest-regex-util: 30.0.1 @@ -14804,8 +14206,6 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@jsdevtools/ono@7.1.3': {} - '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': dependencies: jsep: 1.4.0 @@ -15056,11 +14456,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3)': + '@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5)': dependencies: '@types/mdx': 2.0.13 '@types/react': 19.2.7 - react: 19.2.3 + react: 19.2.5 '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: @@ -15084,6 +14484,31 @@ snapshots: transitivePeerDependencies: - supports-color + '@module-federation/error-codes@0.22.0': {} + + '@module-federation/runtime-core@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/runtime-tools@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/webpack-bundler-runtime': 0.22.0 + + '@module-federation/runtime@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/runtime-core': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/sdk@0.22.0': {} + + '@module-federation/webpack-bundler-runtime@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/sdk': 0.22.0 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.7.1 @@ -15091,6 +14516,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.0.7': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -15329,38 +14761,48 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@redocly/ajv@8.17.1': + '@redocly/ajv@8.18.0': + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + '@redocly/ajv@8.18.3': dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - '@redocly/config@0.22.2': {} + '@redocly/config@0.46.1': + dependencies: + json-schema-to-ts: 2.7.2 - '@redocly/openapi-core@1.34.6': + '@redocly/openapi-core@2.26.0': dependencies: - '@redocly/ajv': 8.17.1 - '@redocly/config': 0.22.2 + '@redocly/ajv': 8.18.3 + '@redocly/config': 0.46.1 + ajv: '@redocly/ajv@8.18.0' + ajv-formats: 3.0.1(@redocly/ajv@8.18.0) colorette: 1.4.0 - https-proxy-agent: 7.0.6 js-levenshtein: 1.1.6 js-yaml: 4.1.1 - minimatch: 5.1.6 + picomatch: 4.0.4 pluralize: 8.0.0 yaml-ast-parser: 0.0.43 - transitivePeerDependencies: - - supports-color - '@reduxjs/toolkit@1.9.7(react-redux@7.2.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.5)(redux@5.0.1))(react@19.2.5)': dependencies: - immer: 9.0.21 - redux: 4.2.1 - redux-thunk: 2.4.2(redux@4.2.1) - reselect: 4.1.8 + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 optionalDependencies: - react: 19.2.3 - react-redux: 7.2.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.5 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.5)(redux@5.0.1) '@rollup/plugin-commonjs@22.0.2(rollup@2.79.2)': dependencies: @@ -15446,6 +14888,59 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true + '@rspack/binding-darwin-arm64@1.7.11': + optional: true + + '@rspack/binding-darwin-x64@1.7.11': + optional: true + + '@rspack/binding-linux-arm64-gnu@1.7.11': + optional: true + + '@rspack/binding-linux-arm64-musl@1.7.11': + optional: true + + '@rspack/binding-linux-x64-gnu@1.7.11': + optional: true + + '@rspack/binding-linux-x64-musl@1.7.11': + optional: true + + '@rspack/binding-wasm32-wasi@1.7.11': + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + + '@rspack/binding-win32-arm64-msvc@1.7.11': + optional: true + + '@rspack/binding-win32-ia32-msvc@1.7.11': + optional: true + + '@rspack/binding-win32-x64-msvc@1.7.11': + optional: true + + '@rspack/binding@1.7.11': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.7.11 + '@rspack/binding-darwin-x64': 1.7.11 + '@rspack/binding-linux-arm64-gnu': 1.7.11 + '@rspack/binding-linux-arm64-musl': 1.7.11 + '@rspack/binding-linux-x64-gnu': 1.7.11 + '@rspack/binding-linux-x64-musl': 1.7.11 + '@rspack/binding-wasm32-wasi': 1.7.11 + '@rspack/binding-win32-arm64-msvc': 1.7.11 + '@rspack/binding-win32-ia32-msvc': 1.7.11 + '@rspack/binding-win32-x64-msvc': 1.7.11 + + '@rspack/core@1.7.11': + dependencies: + '@module-federation/runtime-tools': 0.22.0 + '@rspack/binding': 1.7.11 + '@rspack/lite-tapable': 1.1.0 + + '@rspack/lite-tapable@1.1.0': {} + '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -15489,13 +14984,13 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@slorber/react-helmet-async@1.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.28.4 invariant: 2.2.4 prop-types: 15.8.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) react-fast-compare: 3.2.2 shallowequal: 1.1.0 @@ -15507,6 +15002,8 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} + '@stoplight/better-ajv-errors@1.0.3(ajv@8.17.1)': dependencies: ajv: 8.17.1 @@ -15761,54 +15258,54 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-preset@8.1.0(@babel/core@7.28.5)': + '@svgr/babel-preset@8.1.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.5) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.0) '@svgr/core@8.1.0(typescript@6.0.2)': dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) camelcase: 6.3.0 cosmiconfig: 8.3.6(typescript@6.0.2) snake-case: 3.0.4 @@ -15818,13 +15315,13 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@6.0.2))': dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) '@svgr/core': 8.1.0(typescript@6.0.2) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 @@ -15842,11 +15339,11 @@ snapshots: '@svgr/webpack@8.1.0(typescript@6.0.2)': dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.28.5) - '@babel/preset-env': 7.28.5(@babel/core@7.28.5) - '@babel/preset-react': 7.28.5(@babel/core@7.28.5) - '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.29.0) + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + '@babel/preset-react': 7.28.5(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@svgr/core': 8.1.0(typescript@6.0.2) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@6.0.2)) '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@6.0.2))(typescript@6.0.2) @@ -15854,6 +15351,119 @@ snapshots: - supports-color - typescript + '@swc/core-darwin-arm64@1.15.24': + optional: true + + '@swc/core-darwin-x64@1.15.24': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.24': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.24': + optional: true + + '@swc/core-linux-arm64-musl@1.15.24': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.24': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.24': + optional: true + + '@swc/core-linux-x64-gnu@1.15.24': + optional: true + + '@swc/core-linux-x64-musl@1.15.24': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.24': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.24': + optional: true + + '@swc/core-win32-x64-msvc@1.15.24': + optional: true + + '@swc/core@1.15.24': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.24 + '@swc/core-darwin-x64': 1.15.24 + '@swc/core-linux-arm-gnueabihf': 1.15.24 + '@swc/core-linux-arm64-gnu': 1.15.24 + '@swc/core-linux-arm64-musl': 1.15.24 + '@swc/core-linux-ppc64-gnu': 1.15.24 + '@swc/core-linux-s390x-gnu': 1.15.24 + '@swc/core-linux-x64-gnu': 1.15.24 + '@swc/core-linux-x64-musl': 1.15.24 + '@swc/core-win32-arm64-msvc': 1.15.24 + '@swc/core-win32-ia32-msvc': 1.15.24 + '@swc/core-win32-x64-msvc': 1.15.24 + + '@swc/counter@0.1.3': {} + + '@swc/html-darwin-arm64@1.15.24': + optional: true + + '@swc/html-darwin-x64@1.15.24': + optional: true + + '@swc/html-linux-arm-gnueabihf@1.15.24': + optional: true + + '@swc/html-linux-arm64-gnu@1.15.24': + optional: true + + '@swc/html-linux-arm64-musl@1.15.24': + optional: true + + '@swc/html-linux-ppc64-gnu@1.15.24': + optional: true + + '@swc/html-linux-s390x-gnu@1.15.24': + optional: true + + '@swc/html-linux-x64-gnu@1.15.24': + optional: true + + '@swc/html-linux-x64-musl@1.15.24': + optional: true + + '@swc/html-win32-arm64-msvc@1.15.24': + optional: true + + '@swc/html-win32-ia32-msvc@1.15.24': + optional: true + + '@swc/html-win32-x64-msvc@1.15.24': + optional: true + + '@swc/html@1.15.24': + dependencies: + '@swc/counter': 0.1.3 + optionalDependencies: + '@swc/html-darwin-arm64': 1.15.24 + '@swc/html-darwin-x64': 1.15.24 + '@swc/html-linux-arm-gnueabihf': 1.15.24 + '@swc/html-linux-arm64-gnu': 1.15.24 + '@swc/html-linux-arm64-musl': 1.15.24 + '@swc/html-linux-ppc64-gnu': 1.15.24 + '@swc/html-linux-s390x-gnu': 1.15.24 + '@swc/html-linux-x64-gnu': 1.15.24 + '@swc/html-linux-x64-musl': 1.15.24 + '@swc/html-win32-arm64-msvc': 1.15.24 + '@swc/html-win32-ia32-msvc': 1.15.24 + '@swc/html-win32-x64-msvc': 1.15.24 + + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + '@szmarczak/http-timer@5.0.1': dependencies: defer-to-connect: 2.0.1 @@ -15887,12 +15497,12 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.28.4 '@testing-library/dom': 10.4.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.7 @@ -15932,7 +15542,7 @@ snapshots: - supports-color - ts-jest - '@ttoss/eslint-config@1.26.14(@testing-library/dom@10.4.1)(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(prettier@3.8.1)(turbo@2.9.4)(typescript@6.0.2)': + '@ttoss/eslint-config@1.26.14(@testing-library/dom@10.4.1)(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(prettier@3.8.1)(turbo@2.9.4)(typescript@6.0.2)': dependencies: '@eslint/compat': 2.0.4(eslint@9.39.2(jiti@2.6.1)) '@eslint/eslintrc': 3.3.5 @@ -15944,7 +15554,7 @@ snapshots: eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-formatjs: 6.4.4(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-jest: 29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2) + eslint-plugin-jest: 29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2) eslint-plugin-jest-dom: 5.5.0(@testing-library/dom@10.4.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-prefer-arrow-functions: 3.9.1(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) @@ -16026,19 +15636,19 @@ snapshots: - supports-color - tedious - '@ttoss/test-utils@4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@ttoss/test-utils@4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@emotion/jest': 11.14.2(@types/jest@30.0.0) '@faker-js/faker': 10.4.0 '@testing-library/dom': 10.4.1 '@testing-library/jest-dom': 6.9.1 - '@testing-library/react': 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@testing-library/react': 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@types/relay-test-utils': 19.0.0 - jest: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + jest: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) jest-environment-jsdom: 30.2.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) relay-test-utils: 20.1.1(encoding@0.1.13) resize-observer-polyfill: 1.5.1 transitivePeerDependencies: @@ -16075,6 +15685,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/accepts@1.3.7': + dependencies: + '@types/node': 25.5.2 + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -16129,8 +15743,17 @@ snapshots: dependencies: '@types/node': 25.5.2 + '@types/content-disposition@0.5.9': {} + '@types/cookiejar@2.1.5': {} + '@types/cookies@0.9.2': + dependencies: + '@types/connect': 3.4.38 + '@types/express': 4.17.25 + '@types/keygrip': 1.0.6 + '@types/node': 25.5.2 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -16182,11 +15805,7 @@ snapshots: '@types/qs': 6.14.0 '@types/serve-static': 1.15.10 - '@types/gtag.js@0.0.12': {} - - '@types/hast@2.3.10': - dependencies: - '@types/unist': 2.0.11 + '@types/gtag.js@0.0.20': {} '@types/hast@3.0.4': dependencies: @@ -16194,13 +15813,10 @@ snapshots: '@types/history@4.7.11': {} - '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.7)': - dependencies: - '@types/react': 19.2.7 - hoist-non-react-statics: 3.3.2 - '@types/html-minifier-terser@6.1.0': {} + '@types/http-assert@1.5.6': {} + '@types/http-cache-semantics@4.0.4': {} '@types/http-errors@2.0.5': {} @@ -16239,11 +15855,28 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 25.5.2 - '@types/markdown-escape@1.1.3': {} + '@types/keygrip@1.0.6': {} - '@types/mdast@3.0.15': + '@types/koa-compose@3.2.9': dependencies: - '@types/unist': 2.0.11 + '@types/koa': 3.0.2 + + '@types/koa@3.0.2': + dependencies: + '@types/accepts': 1.3.7 + '@types/content-disposition': 0.5.9 + '@types/cookies': 0.9.2 + '@types/http-assert': 1.5.6 + '@types/http-errors': 2.0.5 + '@types/keygrip': 1.0.6 + '@types/koa-compose': 3.2.9 + '@types/node': 25.5.2 + + '@types/koa__multer@2.0.8': + dependencies: + '@types/koa': 3.0.2 + + '@types/markdown-escape@1.1.3': {} '@types/mdast@4.0.4': dependencies: @@ -16285,8 +15918,6 @@ snapshots: dependencies: parse-path: 7.1.0 - '@types/parse5@6.0.3': {} - '@types/pg@8.20.0': dependencies: '@types/node': 25.5.2 @@ -16297,19 +15928,10 @@ snapshots: '@types/prismjs@1.26.5': {} - '@types/prop-types@15.7.15': {} - '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} - '@types/react-redux@7.1.34': - dependencies: - '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.7) - '@types/react': 19.2.7 - hoist-non-react-statics: 3.3.2 - redux: 4.2.1 - '@types/react-relay@18.2.1': dependencies: '@types/react': 19.2.7 @@ -16410,6 +16032,8 @@ snapshots: '@types/urijs@1.19.26': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/validator@13.15.10': {} '@types/ws@8.18.1': @@ -16756,10 +16380,6 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.1.13 - ajv-draft-04@1.0.0(ajv@8.11.0): - optionalDependencies: - ajv: 8.11.0 - ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -16768,21 +16388,21 @@ snapshots: dependencies: ajv: 8.17.1 - ajv-formats@2.1.1(ajv@8.11.0): - optionalDependencies: - ajv: 8.11.0 - ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(@redocly/ajv@8.18.0): + optionalDependencies: + ajv: '@redocly/ajv@8.18.0' + ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 - ajv-keywords@3.5.2(ajv@6.12.6): + ajv-keywords@3.5.2(ajv@6.14.0): dependencies: - ajv: 6.12.6 + ajv: 6.14.0 ajv-keywords@5.1.0(ajv@8.17.1): dependencies: @@ -16803,13 +16423,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.11.0: - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -17013,16 +16626,10 @@ snapshots: async-lock@1.4.1: {} - async@3.2.2: {} - - async@3.2.4: {} - async@3.2.6: {} asynckit@0.4.0: {} - at-least-node@1.0.0: {} - author-regex@1.0.0: {} autoprefixer@10.4.23(postcss@8.5.6): @@ -17057,12 +16664,12 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.104.1): + babel-loader@9.2.1(@babel/core@7.29.0)(webpack@5.104.1(@swc/core@1.15.24)): dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -17098,15 +16705,6 @@ snapshots: dependencies: '@types/babel__core': 7.20.5 - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.5): - dependencies: - '@babel/compat-data': 7.28.5 - '@babel/core': 7.28.5 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.5) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): dependencies: '@babel/compat-data': 7.29.0 @@ -17116,11 +16714,11 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.5): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.5 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.5) - core-js-compat: 3.47.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color @@ -17132,13 +16730,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.5): - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -17254,7 +16845,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.0 + qs: 6.14.1 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 @@ -17571,8 +17162,6 @@ snapshots: clone@1.0.4: {} - clsx@1.2.1: {} - clsx@2.1.1: {} co-body@6.2.0: @@ -17780,7 +17369,7 @@ snapshots: copy-text-to-clipboard@3.2.2: {} - copy-webpack-plugin@11.0.0(webpack@5.104.1): + copy-webpack-plugin@11.0.0(webpack@5.104.1(@swc/core@1.15.24)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -17788,18 +17377,12 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.104.1 - - core-js-compat@3.47.0: - dependencies: - browserslist: 4.28.1 + webpack: 5.104.1(@swc/core@1.15.24) core-js-compat@3.49.0: dependencies: browserslist: 4.28.1 - core-js-pure@3.47.0: {} - core-js@3.47.0: {} core-util-is@1.0.3: {} @@ -17892,7 +17475,7 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.104.1): + css-loader@6.11.0(@rspack/core@1.7.11)(webpack@5.104.1(@swc/core@1.15.24)): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -17903,9 +17486,10 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - webpack: 5.104.1 + '@rspack/core': 1.7.11 + webpack: 5.104.1(@swc/core@1.15.24) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.104.1): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.104.1(@swc/core@1.15.24)): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 6.1.2(postcss@8.5.6) @@ -17913,7 +17497,7 @@ snapshots: postcss: 8.5.6 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) optionalDependencies: clean-css: 5.3.3 @@ -18132,6 +17716,8 @@ snapshots: detect-libc@1.0.3: optional: true + detect-libc@2.1.2: {} + detect-newline@3.1.0: {} detect-node@2.1.0: {} @@ -18159,8 +17745,6 @@ snapshots: diff@4.0.2: optional: true - diff@5.2.0: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -18198,81 +17782,82 @@ snapshots: dependencies: esutils: 2.0.3 - docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3): + docusaurus-plugin-openapi-docs@5.0.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@docusaurus/utils-validation@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@docusaurus/utils@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/json-schema@7.0.15)(encoding@0.1.13)(react@19.2.5): dependencies: - '@apidevtools/json-schema-ref-parser': 11.9.3 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) - '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@redocly/openapi-core': 1.34.6 + '@apidevtools/json-schema-ref-parser': 15.3.5(@types/json-schema@7.0.15) + '@docusaurus/plugin-content-docs': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@docusaurus/utils': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@docusaurus/utils-validation': 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@redocly/openapi-core': 2.26.0 allof-merge: 0.6.7 - chalk: 4.1.2 - clsx: 1.2.1 - fs-extra: 9.1.0 + chalk: 5.6.2 + clsx: 2.1.1 + fs-extra: 11.3.4 json-pointer: 0.6.2 json5: 2.2.3 lodash: 4.17.21 mustache: 4.2.0 - openapi-to-postmanv2: 4.25.0(encoding@0.1.13) - postman-collection: 4.5.0 - react: 19.2.3 + openapi-to-postmanv2: 6.0.0(encoding@0.1.13) + postman-collection: 5.3.0 + react: 19.2.5 slugify: 1.6.6 swagger2openapi: 7.0.8(encoding@0.1.13) - xml-formatter: 2.6.1 + xml-formatter: 3.7.0 transitivePeerDependencies: + - '@types/json-schema' - encoding - - supports-color - docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(sass@1.97.2)(webpack@5.104.1): + docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@rspack/core@1.7.11)(sass@1.97.2)(webpack@5.104.1(@swc/core@1.15.24)): dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + '@docusaurus/core': 3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) sass: 1.97.2 - sass-loader: 16.0.6(sass@1.97.2)(webpack@5.104.1) + sass-loader: 16.0.6(@rspack/core@1.7.11)(sass@1.97.2)(webpack@5.104.1(@swc/core@1.15.24)) transitivePeerDependencies: - '@rspack/core' - node-sass - sass-embedded - webpack - docusaurus-theme-openapi-docs@4.5.1(@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(docusaurus-plugin-openapi-docs@4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(sass@1.97.2)(webpack@5.104.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1): + docusaurus-theme-openapi-docs@5.0.0(255c652d32e779b619b35f7801205cba): dependencies: - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@hookform/error-message': 2.0.1(react-dom@19.2.3(react@19.2.3))(react-hook-form@7.70.0(react@19.2.3))(react@19.2.3) - '@reduxjs/toolkit': 1.9.7(react-redux@7.2.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@docusaurus/theme-common': 3.10.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@hookform/error-message': 2.0.1(react-dom@19.2.5(react@19.2.5))(react-hook-form@7.70.0(react@19.2.5))(react@19.2.5) + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.5)(redux@5.0.1))(react@19.2.5) allof-merge: 0.6.7 buffer: 6.0.3 - clsx: 1.2.1 + clsx: 2.1.1 copy-text-to-clipboard: 3.2.2 crypto-js: 4.2.0 - docusaurus-plugin-openapi-docs: 4.5.1(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(encoding@0.1.13)(react@19.2.3) - docusaurus-plugin-sass: 0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(sass@1.97.2)(webpack@5.104.1) + docusaurus-plugin-openapi-docs: 5.0.0(@docusaurus/plugin-content-docs@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@docusaurus/utils-validation@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@docusaurus/utils@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/json-schema@7.0.15)(encoding@0.1.13)(react@19.2.5) + docusaurus-plugin-sass: 0.2.6(@docusaurus/core@3.10.0(@docusaurus/faster@3.10.0(@docusaurus/types@3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.5))(@rspack/core@1.7.11)(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2))(@rspack/core@1.7.11)(sass@1.97.2)(webpack@5.104.1(@swc/core@1.15.24)) file-saver: 2.0.5 lodash: 4.17.21 pako: 2.1.0 - postman-code-generators: 1.14.2 - postman-collection: 4.5.0 - prism-react-renderer: 2.4.1(react@19.2.3) + path-browserify: 1.0.1 + postman-code-generators: 2.1.1 + postman-collection: 5.3.0 + prism-react-renderer: 2.4.1(react@19.2.5) process: 0.11.10 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-hook-form: 7.70.0(react@19.2.3) - react-live: 4.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-hook-form: 7.70.0(react@19.2.5) + react-live: 4.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-magic-dropzone: 1.0.1 - react-markdown: 8.0.7(@types/react@19.2.7)(react@19.2.3) - react-modal: 3.16.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react-redux: 7.2.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - rehype-raw: 6.1.1 - remark-gfm: 3.0.1 + react-markdown: 10.1.0(@types/react@19.2.7)(react@19.2.5) + react-modal: 3.16.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.5)(redux@5.0.1) + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 sass: 1.97.2 - sass-loader: 16.0.6(sass@1.97.2)(webpack@5.104.1) + sass-loader: 16.0.6(@rspack/core@1.7.11)(sass@1.97.2)(webpack@5.104.1(@swc/core@1.15.24)) unist-util-visit: 5.0.0 url: 0.11.4 - xml-formatter: 2.6.1 + xml-formatter: 3.7.0 transitivePeerDependencies: - '@rspack/core' - '@types/react' - node-sass - - react-native + - redux - sass-embedded - supports-color - webpack @@ -18706,13 +18291,13 @@ snapshots: optionalDependencies: '@testing-library/dom': 10.4.1 - eslint-plugin-jest@29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2): + eslint-plugin-jest@29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2): dependencies: '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) optionalDependencies: '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) - jest: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + jest: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -19058,7 +18643,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.14.1 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.2 @@ -19208,11 +18793,11 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.104.1): + file-loader@6.2.0(webpack@5.104.1(@swc/core@1.15.24)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) file-saver@2.0.5: {} @@ -19329,25 +18914,12 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 - fs-extra@11.3.2: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.0 universalify: 2.0.1 - fs-extra@9.1.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - fs-minipass@3.0.3: dependencies: minipass: 7.1.2 @@ -19602,16 +19174,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-from-parse5@7.1.2: - dependencies: - '@types/hast': 2.3.10 - '@types/unist': 2.0.11 - hastscript: 7.2.0 - property-information: 6.5.0 - vfile: 5.3.7 - vfile-location: 4.1.0 - web-namespaces: 2.0.1 - hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 @@ -19623,28 +19185,10 @@ snapshots: vfile-location: 5.0.3 web-namespaces: 2.0.1 - hast-util-parse-selector@3.1.1: - dependencies: - '@types/hast': 2.3.10 - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 - hast-util-raw@7.2.3: - dependencies: - '@types/hast': 2.3.10 - '@types/parse5': 6.0.3 - hast-util-from-parse5: 7.1.2 - hast-util-to-parse5: 7.1.0 - html-void-elements: 2.0.1 - parse5: 6.0.1 - unist-util-position: 4.0.4 - unist-util-visit: 4.1.2 - vfile: 5.3.7 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - hast-util-raw@9.1.0: dependencies: '@types/hast': 3.0.4 @@ -19702,15 +19246,6 @@ snapshots: transitivePeerDependencies: - supports-color - hast-util-to-parse5@7.1.0: - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 2.0.3 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - hast-util-to-parse5@8.0.1: dependencies: '@types/hast': 3.0.4 @@ -19721,20 +19256,10 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 - hast-util-whitespace@2.0.1: {} - hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 - hastscript@7.2.0: - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 3.1.1 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - hastscript@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -19811,19 +19336,20 @@ snapshots: html-tags@3.3.1: {} - html-void-elements@2.0.1: {} + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.5(webpack@5.104.1): + html-webpack-plugin@5.6.5(@rspack/core@1.7.11)(webpack@5.104.1(@swc/core@1.15.24)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 - lodash: 4.17.21 + lodash: 4.17.23 pretty-error: 4.0.0 tapable: 2.3.0 optionalDependencies: - webpack: 5.104.1 + '@rspack/core': 1.7.11 + webpack: 5.104.1(@swc/core@1.15.24) htmlparser2@6.1.0: dependencies: @@ -19952,6 +19478,8 @@ snapshots: image-size@2.0.2: {} + immer@11.1.4: {} + immer@9.0.21: {} immutable@5.1.4: {} @@ -19999,8 +19527,6 @@ snapshots: ini@6.0.0: {} - inline-style-parser@0.1.1: {} - inline-style-parser@0.2.7: {} internal-slot@1.1.0: @@ -20057,8 +19583,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-buffer@2.0.5: {} - is-builtin-module@5.0.0: dependencies: builtin-modules: 5.0.0 @@ -20323,15 +19847,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)): + jest-cli@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)): dependencies: - '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + '@jest/core': 30.3.0(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) '@jest/test-result': 30.3.0 '@jest/types': 30.3.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + jest-config: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) jest-util: 30.3.0 jest-validate: 30.3.0 yargs: 17.7.2 @@ -20342,7 +19866,7 @@ snapshots: - supports-color - ts-node - jest-config@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)): + jest-config@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -20369,7 +19893,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 25.5.2 - ts-node: 10.9.2(@types/node@25.5.2)(typescript@6.0.2) + ts-node: 10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -20664,12 +20188,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)): + jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)): dependencies: - '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + '@jest/core': 30.3.0(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) '@jest/types': 30.3.0 import-local: 3.2.0 - jest-cli: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2)) + jest-cli: 30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -20763,6 +20287,12 @@ snapshots: json-schema-compare: 0.2.2 lodash: 4.17.21 + json-schema-to-ts@2.7.2: + dependencies: + '@babel/runtime': 7.28.4 + '@types/json-schema': 7.0.15 + ts-algebra: 1.2.2 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -20848,8 +20378,6 @@ snapshots: kleur@3.0.3: {} - kleur@4.1.5: {} - koa-compose@4.1.0: {} koa-send@5.0.1: @@ -20914,6 +20442,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -21000,6 +20577,8 @@ snapshots: lodash@4.17.21: {} + lodash@4.17.23: {} + log-update@6.1.0: dependencies: ansi-escapes: 7.2.0 @@ -21081,12 +20660,6 @@ snapshots: math-intrinsics@1.1.0: {} - mdast-util-definitions@5.1.2: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - unist-util-visit: 4.1.2 - mdast-util-directive@3.1.0: dependencies: '@types/mdast': 4.0.4 @@ -21101,36 +20674,12 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-find-and-replace@2.2.2: - dependencies: - '@types/mdast': 3.0.15 - escape-string-regexp: 5.0.0 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - - mdast-util-find-and-replace@3.0.2: - dependencies: - '@types/mdast': 4.0.4 - escape-string-regexp: 5.0.0 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - mdast-util-from-markdown@1.3.1: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - decode-named-character-reference: 1.2.0 - mdast-util-to-string: 3.2.0 - micromark: 3.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-decode-string: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-stringify-position: 3.0.3 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 mdast-util-from-markdown@2.0.2: dependencies: @@ -21160,13 +20709,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-autolink-literal@1.0.3: - dependencies: - '@types/mdast': 3.0.15 - ccount: 2.0.1 - mdast-util-find-and-replace: 2.2.2 - micromark-util-character: 1.2.0 - mdast-util-gfm-autolink-literal@2.0.1: dependencies: '@types/mdast': 4.0.4 @@ -21175,12 +20717,6 @@ snapshots: mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@1.0.2: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - micromark-util-normalize-identifier: 1.1.0 - mdast-util-gfm-footnote@2.1.0: dependencies: '@types/mdast': 4.0.4 @@ -21191,11 +20727,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@1.0.3: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - mdast-util-gfm-strikethrough@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -21204,15 +20735,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-table@1.0.7: - dependencies: - '@types/mdast': 3.0.15 - markdown-table: 3.0.4 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - mdast-util-gfm-table@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -21223,11 +20745,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@1.0.2: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - mdast-util-gfm-task-list-item@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -21237,18 +20754,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm@2.0.2: - dependencies: - mdast-util-from-markdown: 1.3.1 - mdast-util-gfm-autolink-literal: 1.0.3 - mdast-util-gfm-footnote: 1.0.2 - mdast-util-gfm-strikethrough: 1.0.3 - mdast-util-gfm-table: 1.0.7 - mdast-util-gfm-task-list-item: 1.0.2 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - mdast-util-gfm@3.1.0: dependencies: mdast-util-from-markdown: 2.0.2 @@ -21310,27 +20815,11 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-phrasing@3.0.1: - dependencies: - '@types/mdast': 3.0.15 - unist-util-is: 5.2.1 - mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 unist-util-is: 6.0.1 - mdast-util-to-hast@12.3.0: - dependencies: - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-definitions: 5.1.2 - micromark-util-sanitize-uri: 1.2.0 - trim-lines: 3.0.1 - unist-util-generated: 2.0.1 - unist-util-position: 4.0.4 - unist-util-visit: 4.1.2 - mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -21343,17 +20832,6 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 - mdast-util-to-markdown@1.5.0: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - longest-streak: 3.1.0 - mdast-util-phrasing: 3.0.1 - mdast-util-to-string: 3.2.0 - micromark-util-decode-string: 1.1.0 - unist-util-visit: 4.1.2 - zwitch: 2.0.4 - mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -21366,10 +20844,6 @@ snapshots: unist-util-visit: 5.0.0 zwitch: 2.0.4 - mdast-util-to-string@3.2.0: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-string@4.0.0: dependencies: '@types/mdast': 4.0.4 @@ -21403,25 +20877,6 @@ snapshots: methods@1.1.2: {} - micromark-core-commonmark@1.1.0: - dependencies: - decode-named-character-reference: 1.2.0 - micromark-factory-destination: 1.1.0 - micromark-factory-label: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-factory-title: 1.1.0 - micromark-factory-whitespace: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-chunked: 1.1.0 - micromark-util-classify-character: 1.1.0 - micromark-util-html-tag-name: 1.2.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-subtokenize: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -21458,13 +20913,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm-autolink-literal@1.0.5: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-extension-gfm-autolink-literal@2.1.0: dependencies: micromark-util-character: 2.1.1 @@ -21472,17 +20920,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm-footnote@1.1.2: - dependencies: - micromark-core-commonmark: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-extension-gfm-footnote@2.1.0: dependencies: devlop: 1.1.0 @@ -21494,15 +20931,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm-strikethrough@1.0.7: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-classify-character: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-extension-gfm-strikethrough@2.1.0: dependencies: devlop: 1.1.0 @@ -21512,14 +20940,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm-table@1.0.7: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-extension-gfm-table@2.1.1: dependencies: devlop: 1.1.0 @@ -21528,22 +20948,10 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm-tagfilter@1.0.2: - dependencies: - micromark-util-types: 1.1.0 - micromark-extension-gfm-tagfilter@2.0.0: dependencies: micromark-util-types: 2.0.2 - micromark-extension-gfm-task-list-item@1.0.5: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-extension-gfm-task-list-item@2.1.0: dependencies: devlop: 1.1.0 @@ -21552,17 +20960,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm@2.0.3: - dependencies: - micromark-extension-gfm-autolink-literal: 1.0.5 - micromark-extension-gfm-footnote: 1.1.2 - micromark-extension-gfm-strikethrough: 1.0.7 - micromark-extension-gfm-table: 1.0.7 - micromark-extension-gfm-tagfilter: 1.0.2 - micromark-extension-gfm-task-list-item: 1.0.5 - micromark-util-combine-extensions: 1.1.0 - micromark-util-types: 1.1.0 - micromark-extension-gfm@3.0.0: dependencies: micromark-extension-gfm-autolink-literal: 2.1.0 @@ -21625,25 +21022,12 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-destination@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-label@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-factory-label@2.0.1: dependencies: devlop: 1.1.0 @@ -21673,13 +21057,6 @@ snapshots: micromark-util-character: 2.1.1 micromark-util-types: 2.0.2 - micromark-factory-title@1.1.0: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-title@2.0.1: dependencies: micromark-factory-space: 2.0.1 @@ -21687,13 +21064,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-whitespace@1.1.0: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-whitespace@2.0.1: dependencies: micromark-factory-space: 2.0.1 @@ -21711,51 +21081,25 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-chunked@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-chunked@2.0.1: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-classify-character@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-util-classify-character@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-combine-extensions@1.1.0: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-types: 1.1.0 - micromark-util-combine-extensions@2.0.1: dependencies: micromark-util-chunked: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-decode-numeric-character-reference@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-decode-numeric-character-reference@2.0.2: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-decode-string@1.1.0: - dependencies: - decode-named-character-reference: 1.2.0 - micromark-util-character: 1.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-decode-string@2.0.1: dependencies: decode-named-character-reference: 1.2.0 @@ -21763,8 +21107,6 @@ snapshots: micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 - micromark-util-encode@1.1.0: {} - micromark-util-encode@2.0.1: {} micromark-util-events-to-acorn@2.0.3: @@ -21777,45 +21119,22 @@ snapshots: micromark-util-types: 2.0.2 vfile-message: 4.0.3 - micromark-util-html-tag-name@1.2.0: {} - micromark-util-html-tag-name@2.0.1: {} - micromark-util-normalize-identifier@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-normalize-identifier@2.0.1: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-resolve-all@1.1.0: - dependencies: - micromark-util-types: 1.1.0 - micromark-util-resolve-all@2.0.1: dependencies: micromark-util-types: 2.0.2 - micromark-util-sanitize-uri@1.2.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-encode: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-sanitize-uri@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-encode: 2.0.1 micromark-util-symbol: 2.0.1 - micromark-util-subtokenize@1.1.0: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-util-subtokenize@2.1.0: dependencies: devlop: 1.1.0 @@ -21831,28 +21150,6 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@3.2.0: - dependencies: - '@types/debug': 4.1.12 - debug: 4.4.3 - decode-named-character-reference: 1.2.0 - micromark-core-commonmark: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-chunked: 1.1.0 - micromark-util-combine-extensions: 1.1.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-encode: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-subtokenize: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - micromark@4.0.2: dependencies: '@types/debug': 4.1.12 @@ -21886,7 +21183,7 @@ snapshots: mime-db@1.54.0: {} - mime-format@2.0.1: + mime-format@2.0.2: dependencies: charset: 1.0.1 @@ -21906,6 +21203,8 @@ snapshots: mime@2.6.0: {} + mime@3.0.0: {} + mimic-fn@2.1.0: {} mimic-function@5.0.1: {} @@ -21916,11 +21215,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.4(webpack@5.104.1): + mini-css-extract-plugin@2.9.4(webpack@5.104.1(@swc/core@1.15.24)): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) minimalistic-assert@1.0.1: {} @@ -22005,8 +21304,6 @@ snapshots: moment@2.30.1: {} - mri@1.2.0: {} - mrmime@2.0.1: {} ms@2.0.0: {} @@ -22177,11 +21474,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.104.1): + null-loader@4.0.1(webpack@5.104.1(@swc/core@1.15.24)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) nwsapi@2.2.23: {} @@ -22306,12 +21603,12 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openapi-to-postmanv2@4.25.0(encoding@0.1.13): + openapi-to-postmanv2@6.0.0(encoding@0.1.13): dependencies: - ajv: 8.11.0 - ajv-draft-04: 1.0.0(ajv@8.11.0) - ajv-formats: 2.1.1(ajv@8.11.0) - async: 3.2.4 + ajv: 8.17.1 + ajv-draft-04: 1.0.0(ajv@8.17.1) + ajv-formats: 2.1.1(ajv@8.17.1) + async: 3.2.6 commander: 2.20.3 graphlib: 2.1.8 js-yaml: 4.1.0 @@ -22321,13 +21618,16 @@ snapshots: neotraverse: 0.6.15 oas-resolver-browser: 2.5.6 object-hash: 3.0.0 + openapi-types: 12.1.3 path-browserify: 1.0.1 - postman-collection: 4.5.0 + postman-collection: 5.3.0 swagger2openapi: 7.0.8(encoding@0.1.13) yaml: 1.10.2 transitivePeerDependencies: - encoding + openapi-types@12.1.3: {} + opener@1.5.2: {} optionator@0.9.4: @@ -22466,8 +21766,6 @@ snapshots: domhandler: 5.0.3 parse5: 7.3.0 - parse5@6.0.1: {} - parse5@7.3.0: dependencies: entities: 6.0.1 @@ -22517,11 +21815,6 @@ snapshots: path-type@4.0.0: {} - path@0.12.7: - dependencies: - process: 0.11.10 - util: 0.10.4 - pathe@2.0.3: {} pg-cloudflare@1.3.0: @@ -22567,6 +21860,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pirates@4.0.7: {} pkce-challenge@5.0.1: {} @@ -22742,13 +22037,13 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - postcss-loader@7.3.4(postcss@8.5.6)(typescript@6.0.2)(webpack@5.104.1): + postcss-loader@7.3.4(postcss@8.5.6)(typescript@6.0.2)(webpack@5.104.1(@swc/core@1.15.24)): dependencies: cosmiconfig: 8.3.6(typescript@6.0.2) jiti: 1.21.7 postcss: 8.5.6 semver: 7.7.4 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) transitivePeerDependencies: - typescript @@ -23051,30 +22346,29 @@ snapshots: dependencies: xtend: 4.0.2 - postman-code-generators@1.14.2: + postman-code-generators@2.1.1: dependencies: - async: 3.2.2 + async: 3.2.6 detect-package-manager: 3.0.2 lodash: 4.17.21 - path: 0.12.7 - postman-collection: 4.5.0 + postman-collection: 5.3.0 shelljs: 0.8.5 - postman-collection@4.5.0: + postman-collection@5.3.0: dependencies: '@faker-js/faker': 5.5.3 file-type: 3.9.0 http-reasons: 0.1.0 iconv-lite: 0.6.3 liquid-json: 0.3.1 - lodash: 4.17.21 - mime-format: 2.0.1 - mime-types: 2.1.35 - postman-url-encoder: 3.0.5 - semver: 7.6.3 + lodash: 4.17.23 + mime: 3.0.0 + mime-format: 2.0.2 + postman-url-encoder: 3.0.8 + semver: 7.7.1 uuid: 8.3.2 - postman-url-encoder@3.0.5: + postman-url-encoder@3.0.8: dependencies: punycode: 2.3.1 @@ -23100,7 +22394,7 @@ snapshots: pretty-error@4.0.0: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 renderkid: 3.0.0 pretty-format@27.5.1: @@ -23129,11 +22423,11 @@ snapshots: printable-characters@1.0.42: {} - prism-react-renderer@2.4.1(react@19.2.3): + prism-react-renderer@2.4.1(react@19.2.5): dependencies: '@types/prismjs': 1.26.5 clsx: 2.1.1 - react: 19.2.3 + react: 19.2.5 prismjs@1.30.0: {} @@ -23176,8 +22470,6 @@ snapshots: transitivePeerDependencies: - supports-color - property-information@6.5.0: {} - property-information@7.1.0: {} proto-list@1.2.4: {} @@ -23219,10 +22511,6 @@ snapshots: pure-rand@7.0.1: {} - qs@6.14.0: - dependencies: - side-channel: 1.1.0 - qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -23260,16 +22548,16 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-dom@19.2.3(react@19.2.3): + react-dom@19.2.5(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 scheduler: 0.27.0 react-fast-compare@3.2.2: {} - react-hook-form@7.70.0(react@19.2.3): + react-hook-form@7.70.0(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 react-is@16.13.1: {} @@ -23277,89 +22565,82 @@ snapshots: react-is@18.3.1: {} - react-json-view-lite@2.5.0(react@19.2.3): + react-json-view-lite@2.5.0(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 react-lifecycles-compat@3.0.4: {} - react-live@4.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-live@4.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - prism-react-renderer: 2.4.1(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + prism-react-renderer: 2.4.1(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) sucrase: 3.35.1 - use-editable: 2.3.3(react@19.2.3) + use-editable: 2.3.3(react@19.2.5) - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.3))(webpack@5.104.1): + react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.5))(webpack@5.104.1(@swc/core@1.15.24)): dependencies: '@babel/runtime': 7.28.4 - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.3)' - webpack: 5.104.1 + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.5)' + webpack: 5.104.1(@swc/core@1.15.24) react-magic-dropzone@1.0.1: {} - react-markdown@8.0.7(@types/react@19.2.7)(react@19.2.3): + react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.5): dependencies: - '@types/hast': 2.3.10 - '@types/prop-types': 15.7.15 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 '@types/react': 19.2.7 - '@types/unist': 2.0.11 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 2.0.1 - prop-types: 15.8.1 - property-information: 6.5.0 - react: 19.2.3 - react-is: 18.3.1 - remark-parse: 10.0.2 - remark-rehype: 10.1.0 - space-separated-tokens: 2.0.2 - style-to-object: 0.4.4 - unified: 10.1.2 - unist-util-visit: 4.1.2 - vfile: 5.3.7 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.5 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 transitivePeerDependencies: - supports-color - react-modal@3.16.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-modal@3.16.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: exenv: 1.2.2 prop-types: 15.8.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) react-lifecycles-compat: 3.0.4 warning: 4.0.3 - react-redux@7.2.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-redux@9.2.0(@types/react@19.2.7)(react@19.2.5)(redux@5.0.1): dependencies: - '@babel/runtime': 7.28.4 - '@types/react-redux': 7.1.34 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 19.2.3 - react-is: 17.0.2 + '@types/use-sync-external-store': 0.0.6 + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: - react-dom: 19.2.3(react@19.2.3) + '@types/react': 19.2.7 + redux: 5.0.1 - react-router-config@5.1.1(react-router@5.3.4(react@19.2.3))(react@19.2.3): + react-router-config@5.1.1(react-router@5.3.4(react@19.2.5))(react@19.2.5): dependencies: '@babel/runtime': 7.28.4 - react: 19.2.3 - react-router: 5.3.4(react@19.2.3) + react: 19.2.5 + react-router: 5.3.4(react@19.2.5) - react-router-dom@5.3.4(react@19.2.3): + react-router-dom@5.3.4(react@19.2.5): dependencies: '@babel/runtime': 7.28.4 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.3 - react-router: 5.3.4(react@19.2.3) + react: 19.2.5 + react-router: 5.3.4(react@19.2.5) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react-router@5.3.4(react@19.2.3): + react-router@5.3.4(react@19.2.5): dependencies: '@babel/runtime': 7.28.4 history: 4.10.1 @@ -23367,12 +22648,12 @@ snapshots: loose-envify: 1.4.0 path-to-regexp: 1.9.0 prop-types: 15.8.1 - react: 19.2.3 + react: 19.2.5 react-is: 16.13.1 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react@19.2.3: {} + react@19.2.5: {} readable-stream@2.3.8: dependencies: @@ -23446,13 +22727,11 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 - redux-thunk@2.4.2(redux@4.2.1): + redux-thunk@3.1.0(redux@5.0.1): dependencies: - redux: 4.2.1 + redux: 5.0.1 - redux@4.2.1: - dependencies: - '@babel/runtime': 7.28.4 + redux@5.0.1: {} reflect-metadata@0.2.2: {} @@ -23509,12 +22788,6 @@ snapshots: dependencies: jsesc: 3.1.0 - rehype-raw@6.1.1: - dependencies: - '@types/hast': 2.3.10 - hast-util-raw: 7.2.3 - unified: 10.1.2 - rehype-raw@7.0.0: dependencies: '@types/hast': 3.0.4 @@ -23574,15 +22847,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-gfm@3.0.1: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-gfm: 2.0.2 - micromark-extension-gfm: 2.0.3 - unified: 10.1.2 - transitivePeerDependencies: - - supports-color - remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -23601,14 +22865,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-parse@10.0.2: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-from-markdown: 1.3.1 - unified: 10.1.2 - transitivePeerDependencies: - - supports-color - remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -23618,13 +22874,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-rehype@10.1.0: - dependencies: - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-to-hast: 12.3.0 - unified: 10.1.2 - remark-rehype@11.1.2: dependencies: '@types/hast': 3.0.4 @@ -23644,7 +22893,7 @@ snapshots: css-select: 4.3.0 dom-converter: 0.2.0 htmlparser2: 6.1.0 - lodash: 4.17.21 + lodash: 4.17.23 strip-ansi: 6.0.1 repeat-string@1.6.1: {} @@ -23659,7 +22908,7 @@ snapshots: requires-port@1.0.0: {} - reselect@4.1.8: {} + reselect@5.1.1: {} reserved@0.1.2: {} @@ -23772,10 +23021,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - sade@1.8.1: - dependencies: - mri: 1.2.0 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -23803,12 +23048,13 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@16.0.6(sass@1.97.2)(webpack@5.104.1): + sass-loader@16.0.6(@rspack/core@1.7.11)(sass@1.97.2)(webpack@5.104.1(@swc/core@1.15.24)): dependencies: neo-async: 2.6.2 optionalDependencies: + '@rspack/core': 1.7.11 sass: 1.97.2 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) sass@1.97.2: dependencies: @@ -23831,8 +23077,8 @@ snapshots: schema-utils@3.3.0: dependencies: '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) schema-utils@4.3.3: dependencies: @@ -23861,7 +23107,7 @@ snapshots: semver@6.3.1: {} - semver@7.6.3: {} + semver@7.7.1: {} semver@7.7.3: {} @@ -23943,12 +23189,12 @@ snapshots: dependencies: randombytes: 2.1.0 - serve-handler@6.1.6: + serve-handler@6.1.7: dependencies: bytes: 3.0.0 content-disposition: 0.5.2 mime-types: 2.1.18 - minimatch: 3.1.2 + minimatch: 3.1.5 path-is-inside: 1.0.2 path-to-regexp: 3.3.0 range-parser: 1.2.0 @@ -24405,10 +23651,6 @@ snapshots: dependencies: style-to-object: 1.0.14 - style-to-object@0.4.4: - dependencies: - inline-style-parser: 0.1.1 - style-to-object@1.0.14: dependencies: inline-style-parser: 0.2.7 @@ -24491,11 +23733,17 @@ snapshots: transitivePeerDependencies: - encoding - swr@2.3.8(react@19.2.3): + swc-loader@0.2.7(@swc/core@1.15.24)(webpack@5.104.1(@swc/core@1.15.24)): + dependencies: + '@swc/core': 1.15.24 + '@swc/counter': 0.1.3 + webpack: 5.104.1(@swc/core@1.15.24) + + swr@2.3.8(react@19.2.5): dependencies: dequal: 2.0.3 - react: 19.2.3 - use-sync-external-store: 1.6.0(react@19.2.3) + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) symbol-tree@3.2.4: {} @@ -24588,14 +23836,16 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser-webpack-plugin@5.3.16(webpack@5.104.1): + terser-webpack-plugin@5.3.16(@swc/core@1.15.24)(webpack@5.104.1(@swc/core@1.15.24)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) + optionalDependencies: + '@swc/core': 1.15.24 terser@5.44.1: dependencies: @@ -24714,6 +23964,8 @@ snapshots: trough@2.2.0: {} + ts-algebra@1.2.2: {} + ts-api-utils@2.1.0(typescript@6.0.2): dependencies: typescript: 6.0.2 @@ -24724,7 +23976,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@25.5.2)(typescript@6.0.2): + ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -24741,6 +23993,8 @@ snapshots: typescript: 6.0.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.24 optional: true tsconfig-paths@3.15.0: @@ -24756,7 +24010,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): + tsup@8.5.1(@swc/core@1.15.24)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.1) cac: 6.7.14 @@ -24776,6 +24030,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: + '@swc/core': 1.15.24 postcss: 8.5.6 typescript: 6.0.2 transitivePeerDependencies: @@ -24914,16 +24169,6 @@ snapshots: unicorn-magic@0.3.0: {} - unified@10.1.2: - dependencies: - '@types/unist': 2.0.11 - bail: 2.0.2 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 5.3.7 - unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -24946,12 +24191,6 @@ snapshots: dependencies: crypto-random-string: 4.0.0 - unist-util-generated@2.0.1: {} - - unist-util-is@5.2.1: - dependencies: - '@types/unist': 2.0.11 - unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -24960,38 +24199,19 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-position@4.0.4: - dependencies: - '@types/unist': 2.0.11 - unist-util-position@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-stringify-position@3.0.3: - dependencies: - '@types/unist': 2.0.11 - unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@5.1.3: - dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - unist-util-visit-parents@6.0.2: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.1 - unist-util-visit@4.1.2: - dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 @@ -25047,7 +24267,7 @@ snapshots: is-yarn-global: 0.4.1 latest-version: 7.0.0 pupa: 3.3.0 - semver: 7.7.3 + semver: 7.7.4 semver-diff: 4.0.0 xdg-basedir: 5.1.0 @@ -25057,34 +24277,30 @@ snapshots: urijs@1.19.11: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1(@swc/core@1.15.24)))(webpack@5.104.1(@swc/core@1.15.24)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) optionalDependencies: - file-loader: 6.2.0(webpack@5.104.1) + file-loader: 6.2.0(webpack@5.104.1(@swc/core@1.15.24)) url@0.11.4: dependencies: punycode: 1.4.1 - qs: 6.14.0 + qs: 6.14.1 - use-editable@2.3.3(react@19.2.3): + use-editable@2.3.3(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 - use-sync-external-store@1.6.0(react@19.2.3): + use-sync-external-store@1.6.0(react@19.2.5): dependencies: - react: 19.2.3 + react: 19.2.5 util-deprecate@1.0.2: {} - util@0.10.4: - dependencies: - inherits: 2.0.3 - utila@0.4.0: {} utility-types@3.11.0: {} @@ -25095,13 +24311,6 @@ snapshots: uuid@8.3.2: {} - uvu@0.5.6: - dependencies: - dequal: 2.0.3 - diff: 5.2.0 - kleur: 4.1.5 - sade: 1.8.1 - v8-compile-cache-lib@3.0.1: optional: true @@ -25143,33 +24352,16 @@ snapshots: vary@1.1.2: {} - vfile-location@4.1.0: - dependencies: - '@types/unist': 2.0.11 - vfile: 5.3.7 - vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 vfile: 6.0.3 - vfile-message@3.1.4: - dependencies: - '@types/unist': 2.0.11 - unist-util-stringify-position: 3.0.3 - vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - vfile@5.3.7: - dependencies: - '@types/unist': 2.0.11 - is-buffer: 2.0.5 - unist-util-stringify-position: 3.0.3 - vfile-message: 3.1.4 - vfile@6.0.3: dependencies: '@types/unist': 3.0.3 @@ -25226,7 +24418,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(webpack@5.104.1): + webpack-dev-middleware@7.4.5(webpack@5.104.1(@swc/core@1.15.24)): dependencies: colorette: 2.0.20 memfs: 4.51.1 @@ -25235,9 +24427,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) - webpack-dev-server@5.2.2(webpack@5.104.1): + webpack-dev-server@5.2.2(webpack@5.104.1(@swc/core@1.15.24)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -25265,10 +24457,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.104.1) + webpack-dev-middleware: 7.4.5(webpack@5.104.1(@swc/core@1.15.24)) ws: 8.18.3 optionalDependencies: - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) transitivePeerDependencies: - bufferutil - debug @@ -25289,7 +24481,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.104.1: + webpack@5.104.1(@swc/core@1.15.24): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -25313,7 +24505,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.104.1) + terser-webpack-plugin: 5.3.16(@swc/core@1.15.24)(webpack@5.104.1(@swc/core@1.15.24)) watchpack: 2.5.0 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -25321,7 +24513,7 @@ snapshots: - esbuild - uglify-js - webpackbar@6.0.1(webpack@5.104.1): + webpackbar@6.0.1(webpack@5.104.1(@swc/core@1.15.24)): dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -25330,7 +24522,7 @@ snapshots: markdown-table: 2.0.0 pretty-time: 1.1.0 std-env: 3.10.0 - webpack: 5.104.1 + webpack: 5.104.1(@swc/core@1.15.24) wrap-ansi: 7.0.0 websocket-driver@0.7.4: @@ -25480,9 +24672,9 @@ snapshots: xdg-basedir@5.1.0: {} - xml-formatter@2.6.1: + xml-formatter@3.7.0: dependencies: - xml-parser-xo: 3.2.0 + xml-parser-xo: 4.1.5 xml-js@1.6.11: dependencies: @@ -25490,7 +24682,7 @@ snapshots: xml-name-validator@5.0.0: {} - xml-parser-xo@3.2.0: {} + xml-parser-xo@4.1.5: {} xmlchars@2.2.0: {} diff --git a/tests/smoke-test.sh b/tests/smoke-test.sh new file mode 100755 index 00000000..687cc8b9 --- /dev/null +++ b/tests/smoke-test.sh @@ -0,0 +1,100 @@ +#!/bin/sh +set -e + +BASE_URL="${SERVER_URL:-http://localhost:5047}/api/v1" + +echo "=== Smoke test started ===" + +# 1. Bootstrap admin user (201 on first run, 409 if already exists) +echo "--- Bootstrapping admin user ---" +BOOTSTRAP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/users/bootstrap" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"Admin1234!"}') +if [ "$BOOTSTRAP_STATUS" != "201" ] && [ "$BOOTSTRAP_STATUS" != "409" ]; then + echo "ERROR: Bootstrap returned $BOOTSTRAP_STATUS" >&2 + exit 1 +fi +echo "Bootstrap status: $BOOTSTRAP_STATUS" + +# 2. Login to get JWT token +echo "--- Logging in ---" +LOGIN_RESP=$(curl -sf -X POST "$BASE_URL/users/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"Admin1234!"}') +TOKEN=$(echo "$LOGIN_RESP" | jq -r '.token') +echo "Token: ${TOKEN:0:20}..." + +# 3. Create a project +echo "--- Creating project ---" +PROJECT_RESP=$(curl -sf -X POST "$BASE_URL/projects" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"name":"smoke-test-project"}') +PROJECT_PUBLIC_ID=$(echo "$PROJECT_RESP" | jq -r '.id') +echo "Project id: $PROJECT_PUBLIC_ID" + +# 4. Upload a file via multipart form +echo "--- Uploading file ---" +echo "Hello, smoke test!" > /tmp/smoke.txt +UPLOAD_RESP=$(curl -sf -X POST "$BASE_URL/files/upload" \ + -H "Authorization: Bearer $TOKEN" \ + -F "file=@/tmp/smoke.txt;type=text/plain" \ + -F "projectId=$PROJECT_PUBLIC_ID") +FILE_ID=$(echo "$UPLOAD_RESP" | jq -r '.id') +echo "File id: $FILE_ID" + +# 5. Get file metadata +echo "--- Getting file metadata ---" +GET_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/files/$FILE_ID" \ + -H "Authorization: Bearer $TOKEN") +if [ "$GET_STATUS" != "200" ]; then + echo "ERROR: GET file returned $GET_STATUS, expected 200" >&2 + exit 1 +fi +echo "GET status: $GET_STATUS" + +# 6. Download file and verify content +echo "--- Downloading file ---" +CONTENT=$(curl -sf "$BASE_URL/files/$FILE_ID/download" \ + -H "Authorization: Bearer $TOKEN") +EXPECTED="Hello, smoke test!" +if [ "$CONTENT" != "$EXPECTED" ]; then + echo "ERROR: Content mismatch. Got '$CONTENT', expected '$EXPECTED'" >&2 + exit 1 +fi +echo "Content matches." + +# 7. Update metadata +echo "--- Updating metadata ---" +PATCH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH "$BASE_URL/files/$FILE_ID/metadata" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"metadata":"smoke-tested"}') +if [ "$PATCH_STATUS" != "200" ]; then + echo "ERROR: PATCH metadata returned $PATCH_STATUS, expected 200" >&2 + exit 1 +fi +echo "PATCH status: $PATCH_STATUS" + +# 8. Delete file +echo "--- Deleting file ---" +DELETE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE_URL/files/$FILE_ID" \ + -H "Authorization: Bearer $TOKEN") +if [ "$DELETE_STATUS" != "204" ]; then + echo "ERROR: DELETE returned $DELETE_STATUS, expected 204" >&2 + exit 1 +fi +echo "DELETE status: $DELETE_STATUS" + +# 9. Verify file is gone (404) +echo "--- Verifying deletion ---" +AFTER_DELETE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/files/$FILE_ID" \ + -H "Authorization: Bearer $TOKEN") +if [ "$AFTER_DELETE_STATUS" != "404" ]; then + echo "ERROR: Expected 404 after deletion, got $AFTER_DELETE_STATUS" >&2 + exit 1 +fi +echo "File correctly returns 404 after deletion." + +echo "" +echo "=== All smoke tests passed! ===" From 31e2cfc132410b9a734fefda48e0dbfd1b40ee82 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Wed, 8 Apr 2026 21:23:18 -0300 Subject: [PATCH 05/27] feat: documents first implementation --- docker-compose.test.yml | 28 +- eslint.config.mjs | 9 +- packages/postgresdb/src/models/Document.ts | 53 +++ packages/postgresdb/src/models/index.ts | 1 + packages/postgresdb/src/utils/publicId.ts | 1 + packages/server/.env.example | 14 +- packages/server/src/app.ts | 4 +- packages/server/src/lib/documents.ts | 169 +++++++ packages/server/src/lib/embedding.ts | 23 + packages/server/src/lib/files.ts | 1 - packages/server/src/mcp/tools/documents.ts | 92 ++++ packages/server/src/mcp/tools/index.ts | 2 + .../server/src/rest/openapi/v1/documents.yaml | 274 +++++++++++ packages/server/src/rest/v1/documents.ts | 445 ++++++++++++++++++ packages/server/src/rest/v1/index.ts | 2 + .../server/tests/unit/tests/documents.test.ts | 280 +++++++++++ packages/website/docs/modules/documents.md | 140 ++++++ .../website/docs/tutorials/connect-mcp.md | 63 --- .../website/docs/tutorials/storing-memory.md | 46 -- packages/website/sidebars.ts | 7 +- tests/smoke-test.sh | 47 ++ 21 files changed, 1580 insertions(+), 121 deletions(-) create mode 100644 packages/postgresdb/src/models/Document.ts create mode 100644 packages/server/src/lib/documents.ts create mode 100644 packages/server/src/lib/embedding.ts create mode 100644 packages/server/src/mcp/tools/documents.ts create mode 100644 packages/server/src/rest/openapi/v1/documents.yaml create mode 100644 packages/server/src/rest/v1/documents.ts create mode 100644 packages/server/tests/unit/tests/documents.test.ts create mode 100644 packages/website/docs/modules/documents.md delete mode 100644 packages/website/docs/tutorials/connect-mcp.md delete mode 100644 packages/website/docs/tutorials/storing-memory.md diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 5cfee69b..0318d6c2 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -11,11 +11,28 @@ services: timeout: 5s retries: 10 + ollama: + image: ollama/ollama:latest + volumes: + - ollama_cache:/root/.ollama + entrypoint: + - /bin/sh + - -c + - 'ollama serve & sleep 5 && ollama pull qwen3-embedding:0.6b && wait' + healthcheck: + test: ['CMD-SHELL', 'ollama list | grep qwen3-embedding'] + interval: 10s + timeout: 30s + retries: 30 + start_period: 30s + server: build: . depends_on: database: condition: service_healthy + ollama: + condition: service_healthy environment: DATABASE_HOST: database DATABASE_PORT: '5432' @@ -23,14 +40,18 @@ services: DATABASE_USER: soat_user DATABASE_PASSWORD: soat_password FILES_STORAGE_DIR: /data/files + OLLAMA_BASE_URL: http://ollama:11434 + EMBEDDING_PROVIDER: ollama + EMBEDDING_MODEL: qwen3-embedding:0.6b + EMBEDDING_DIMENSIONS: '1024' healthcheck: test: - CMD - node - '-e' - >- - require('http').get('http://localhost:5047/api/v1/users', - (r) => { process.exit(r.statusCode >= 200 ? 0 : 1); } + require('http').get('http://localhost:5047/health', + (r) => { process.exit(r.statusCode === 200 ? 0 : 1); } ).on('error', () => process.exit(1)) interval: 5s timeout: 10s @@ -47,3 +68,6 @@ services: environment: SERVER_URL: 'http://server:5047' command: sh -c "apk add --no-cache curl jq && sh /smoke-test.sh" + +volumes: + ollama_cache: diff --git a/eslint.config.mjs b/eslint.config.mjs index 57cf2322..1e8e7865 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,3 +1,10 @@ import ttossEslintConfig from '@ttoss/eslint-config'; -export default [...ttossEslintConfig]; +export default [ + ...ttossEslintConfig, + { + rules: { + 'turbo/no-undeclared-env-vars': 'off', + }, + }, +]; diff --git a/packages/postgresdb/src/models/Document.ts b/packages/postgresdb/src/models/Document.ts new file mode 100644 index 00000000..06213938 --- /dev/null +++ b/packages/postgresdb/src/models/Document.ts @@ -0,0 +1,53 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; +import { File } from './File'; + +@Table({ + tableName: 'documents', + hooks: { + beforeValidate: (instance: Document) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.document); + } + }, + }, +}) +export class Document extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @ForeignKey(() => { + return File; + }) + @Column({ type: DataType.INTEGER, allowNull: false, unique: true }) + declare fileId: number; + + @BelongsTo(() => { + return File; + }) + declare file: File; + + @Column({ + type: DataType.VECTOR(1024), + allowNull: true, + }) + declare embedding: number[] | null; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/index.ts b/packages/postgresdb/src/models/index.ts index da667cf2..89877943 100644 --- a/packages/postgresdb/src/models/index.ts +++ b/packages/postgresdb/src/models/index.ts @@ -1,4 +1,5 @@ export { ApiKey } from './ApiKey'; +export { Document } from './Document'; export { File } from './File'; export { Project } from './Project'; export { ProjectPolicy } from './ProjectPolicy'; diff --git a/packages/postgresdb/src/utils/publicId.ts b/packages/postgresdb/src/utils/publicId.ts index a9364969..4caef7bf 100644 --- a/packages/postgresdb/src/utils/publicId.ts +++ b/packages/postgresdb/src/utils/publicId.ts @@ -10,6 +10,7 @@ export const PUBLIC_ID_PREFIXES = { project: 'proj_', policy: 'pol_', apiKey: 'key_', + document: 'doc_', } as const; /** diff --git a/packages/server/.env.example b/packages/server/.env.example index 9a32f497..d65047db 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -6,4 +6,16 @@ DATABASE_USER=soat_user DATABASE_PASSWORD=soat_password # Server Configuration -PORT=5047 \ No newline at end of file +PORT=5047 + +# Embedding Configuration +EMBEDDING_PROVIDER=ollama +EMBEDDING_MODEL=qwen3-embedding:0.6b +EMBEDDING_DIMENSIONS=1024 +OLLAMA_BASE_URL=http://localhost:11434 + +# Embedding Configuration +EMBEDDING_PROVIDER=ollama +EMBEDDING_MODEL=qwen3-embedding:0.6b +EMBEDDING_DIMENSIONS=1024 +OLLAMA_BASE_URL=http://localhost:11434 \ No newline at end of file diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 782187b0..b5856946 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -1,4 +1,4 @@ -import { App, bodyParser, cors } from '@ttoss/http-server'; +import { App, addHealthCheck, bodyParser, cors } from '@ttoss/http-server'; import { mcpRouter } from './mcp/server'; import { authMiddleware } from './middleware/auth'; @@ -6,6 +6,8 @@ import { restRouter } from './rest/router'; const app = new App(); +addHealthCheck({ app }); + app.use(cors()); app.use(bodyParser()); app.use(authMiddleware); diff --git a/packages/server/src/lib/documents.ts b/packages/server/src/lib/documents.ts new file mode 100644 index 00000000..751cafb2 --- /dev/null +++ b/packages/server/src/lib/documents.ts @@ -0,0 +1,169 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { db } from '../db'; +import { getEmbedding } from './embedding'; + +const getStorageDir = () => { + const dir = process.env.FILES_STORAGE_DIR; + if (!dir) { + throw new Error('FILES_STORAGE_DIR environment variable is not set'); + } + return dir; +}; + +const mapDocument = ( + doc: InstanceType<(typeof db)['Document']> & { + file?: InstanceType<(typeof db)['File']> & { + project?: InstanceType<(typeof db)['Project']>; + }; + } +) => { + return { + id: doc.publicId, + fileId: doc.file?.publicId, + projectId: doc.file?.project?.publicId, + filename: doc.file?.filename, + size: doc.file?.size, + createdAt: doc.createdAt, + updatedAt: doc.updatedAt, + }; +}; + +export const listDocuments = async (args: { projectId: number }) => { + const documents = await db.Document.findAll({ + include: [ + { + model: db.File, + as: 'file', + where: { projectId: args.projectId }, + include: [{ model: db.Project, as: 'project' }], + }, + ], + }); + return documents.map(mapDocument); +}; + +export const getDocument = async (args: { id: string }) => { + const doc = await db.Document.findOne({ + where: { publicId: args.id }, + include: [ + { + model: db.File, + as: 'file', + include: [{ model: db.Project, as: 'project' }], + }, + ], + }); + + if (!doc) { + return null; + } + + const mapped = mapDocument(doc); + + if (doc.file?.storagePath && fs.existsSync(doc.file.storagePath)) { + const content = fs.readFileSync(doc.file.storagePath, 'utf-8'); + return { ...mapped, content }; + } + + return { ...mapped, content: null }; +}; + +export const createDocument = async (args: { + projectId: number; + content: string; + filename?: string; +}) => { + const storageDir = getStorageDir(); + fs.mkdirSync(storageDir, { recursive: true }); + + const file = await db.File.create({ + projectId: args.projectId, + filename: args.filename ?? 'document.txt', + contentType: 'text/plain', + size: Buffer.byteLength(args.content, 'utf-8'), + storageType: 'local' as const, + storagePath: '', + }); + + const storagePath = path.join(storageDir, `${file.publicId}.txt`); + fs.writeFileSync(storagePath, args.content, 'utf-8'); + await file.update({ + storagePath, + size: Buffer.byteLength(args.content, 'utf-8'), + }); + + const embedding = await getEmbedding({ text: args.content }); + + const doc = await db.Document.create({ + fileId: file.id, + embedding, + }); + + const created = await db.Document.findOne({ + where: { id: doc.id }, + include: [ + { + model: db.File, + as: 'file', + include: [{ model: db.Project, as: 'project' }], + }, + ], + }); + + return mapDocument(created!); +}; + +export const deleteDocument = async (args: { id: string }) => { + const doc = await db.Document.findOne({ + where: { publicId: args.id }, + include: [{ model: db.File, as: 'file' }], + }); + + if (!doc) { + return null; + } + + if (doc.file?.storagePath) { + try { + fs.unlinkSync(doc.file.storagePath); + } catch { + // Ignore missing file errors + } + } + + await doc.destroy(); + if (doc.file) { + await doc.file.destroy(); + } + + return true; +}; + +export const searchDocuments = async (args: { + projectId: number; + query: string; + limit?: number; +}) => { + const embedding = await getEmbedding({ text: args.query }); + const limit = args.limit ?? 10; + const embeddingLiteral = `[${embedding.join(',')}]`; + + const documents = await db.Document.findAll({ + include: [ + { + model: db.File, + as: 'file', + where: { projectId: args.projectId }, + include: [{ model: db.Project, as: 'project' }], + }, + ], + order: db.Document.sequelize!.literal( + `embedding <=> '${embeddingLiteral}'` + ), + limit, + }); + + return documents.map(mapDocument); +}; diff --git a/packages/server/src/lib/embedding.ts b/packages/server/src/lib/embedding.ts new file mode 100644 index 00000000..b5da90ab --- /dev/null +++ b/packages/server/src/lib/embedding.ts @@ -0,0 +1,23 @@ +import { Ollama } from 'ollama'; + +export const getEmbedding = async (args: { + text: string; +}): Promise => { + const provider = process.env.EMBEDDING_PROVIDER; + const model = process.env.EMBEDDING_MODEL; + + if (!provider || !model) { + throw new Error( + 'EMBEDDING_PROVIDER and EMBEDDING_MODEL environment variables must be set' + ); + } + + if (provider === 'ollama') { + const host = process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434'; + const ollama = new Ollama({ host }); + const response = await ollama.embed({ model, input: args.text }); + return response.embeddings[0]; + } + + throw new Error(`Unsupported embedding provider: ${provider}`); +}; diff --git a/packages/server/src/lib/files.ts b/packages/server/src/lib/files.ts index ab30dfea..7c2ab42d 100644 --- a/packages/server/src/lib/files.ts +++ b/packages/server/src/lib/files.ts @@ -4,7 +4,6 @@ import path from 'node:path'; import { db } from '../db'; const getStorageDir = () => { - // eslint-disable-next-line turbo/no-undeclared-env-vars const dir = process.env.FILES_STORAGE_DIR; if (!dir) { throw new Error('FILES_STORAGE_DIR environment variable is not set'); diff --git a/packages/server/src/mcp/tools/documents.ts b/packages/server/src/mcp/tools/documents.ts new file mode 100644 index 00000000..91a4e0d3 --- /dev/null +++ b/packages/server/src/mcp/tools/documents.ts @@ -0,0 +1,92 @@ +import type { McpServer } from '@ttoss/http-server-mcp'; +import { apiCall, z } from '@ttoss/http-server-mcp'; + +const registerTools = (server: McpServer) => { + server.registerTool( + 'list-documents', + { + description: 'List all documents in a project', + inputSchema: { + projectId: z.string().describe('Project ID'), + }, + }, + async ({ projectId }) => { + const data = await apiCall( + 'GET', + `/documents?projectId=${encodeURIComponent(projectId)}` + ); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'get-document', + { + description: 'Get a document by ID including its text content', + inputSchema: { + id: z.string().describe('Document ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/documents/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'create-document', + { + description: + 'Create a new text document with an embedding vector for semantic search', + inputSchema: { + projectId: z.string().describe('Project ID'), + content: z.string().describe('Text content of the document'), + filename: z.string().optional().describe('Optional filename'), + }, + }, + async ({ projectId, content, filename }) => { + const data = await apiCall('POST', '/documents', { + body: { projectId, content, filename }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'delete-document', + { + description: 'Delete a document and its underlying file', + inputSchema: { + id: z.string().describe('Document ID'), + }, + }, + async ({ id }) => { + await apiCall('DELETE', `/documents/${id}`); + return { content: [{ type: 'text', text: 'Document deleted' }] }; + } + ); + + server.registerTool( + 'search-documents', + { + description: + 'Perform semantic search over documents in a project using cosine similarity', + inputSchema: { + projectId: z.string().describe('Project ID'), + query: z.string().describe('Natural language search query'), + limit: z + .number() + .optional() + .describe('Maximum number of results (default: 10)'), + }, + }, + async ({ projectId, query, limit }) => { + const data = await apiCall('POST', '/documents/search', { + body: { projectId, query, limit }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); +}; + +export { registerTools }; diff --git a/packages/server/src/mcp/tools/index.ts b/packages/server/src/mcp/tools/index.ts index e6aa2c34..31b1d44a 100644 --- a/packages/server/src/mcp/tools/index.ts +++ b/packages/server/src/mcp/tools/index.ts @@ -1,8 +1,10 @@ import type { McpServer } from '@ttoss/http-server-mcp'; +import { registerTools as registerDocumentTools } from './documents'; import { registerTools as registerFileTools } from './files'; const registerTools = (server: McpServer) => { + registerDocumentTools(server); registerFileTools(server); }; diff --git a/packages/server/src/rest/openapi/v1/documents.yaml b/packages/server/src/rest/openapi/v1/documents.yaml new file mode 100644 index 00000000..c7a186af --- /dev/null +++ b/packages/server/src/rest/openapi/v1/documents.yaml @@ -0,0 +1,274 @@ +openapi: 3.0.3 +info: + title: SOAT Documents API + version: 1.0.0 + description: API for managing AI-indexed text documents with semantic search + contact: + name: SOAT Team + url: https://github.com/ttoss/soat +servers: + - url: http://0.0.0.0:5047/api/v1 + description: Development server +paths: + /documents: + get: + tags: + - Documents + summary: List documents in a project + description: Returns all documents belonging to a project + operationId: listDocuments + parameters: + - name: projectId + in: query + required: true + description: Project public ID + schema: + type: string + example: 'proj_V1StGXR8Z5jdHi6B' + responses: + '200': + description: List of documents + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DocumentRecord' + '400': + description: Missing projectId + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Documents + summary: Create a document + description: Creates a new text document and generates an embedding vector for semantic search + operationId: createDocument + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - projectId + - content + properties: + projectId: + type: string + example: 'proj_V1StGXR8Z5jdHi6B' + content: + type: string + example: 'The quick brown fox jumps over the lazy dog.' + filename: + type: string + example: 'my-doc.txt' + responses: + '201': + description: Document created + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentRecord' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /documents/{id}: + get: + tags: + - Documents + summary: Get a document by ID + description: Returns a document with its text content + operationId: getDocument + parameters: + - name: id + in: path + required: true + description: Document ID + schema: + type: string + example: 'doc_V1StGXR8Z5jdHi6B' + responses: + '200': + description: Document found + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentRecord' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - Documents + summary: Delete a document + description: Deletes a document and its underlying file + operationId: deleteDocument + parameters: + - name: id + in: path + required: true + description: Document ID + schema: + type: string + example: 'doc_V1StGXR8Z5jdHi6B' + responses: + '204': + description: Document deleted + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /documents/search: + post: + tags: + - Documents + summary: Semantic search over documents + description: Embeds the query text and returns the most similar documents using cosine distance + operationId: searchDocuments + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - projectId + - query + properties: + projectId: + type: string + example: 'proj_V1StGXR8Z5jdHi6B' + query: + type: string + example: 'What is the capital of France?' + limit: + type: integer + example: 5 + responses: + '200': + description: Search results + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DocumentRecord' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' +components: + schemas: + DocumentRecord: + type: object + properties: + id: + type: string + description: Document public ID + example: 'doc_V1StGXR8Z5jdHi6B' + fileId: + type: string + description: Underlying file public ID + example: 'file_V1StGXR8Z5jdHi6B' + projectId: + type: string + description: Project public ID + example: 'proj_V1StGXR8Z5jdHi6B' + filename: + type: string + description: Original filename + example: 'my-doc.txt' + size: + type: integer + description: File size in bytes + example: 42 + content: + type: string + nullable: true + description: Text content (only present on getDocument) + example: 'The quick brown fox jumps over the lazy dog.' + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + ErrorResponse: + type: object + properties: + error: + type: string + example: 'Document not found' diff --git a/packages/server/src/rest/v1/documents.ts b/packages/server/src/rest/v1/documents.ts new file mode 100644 index 00000000..30c1a4b3 --- /dev/null +++ b/packages/server/src/rest/v1/documents.ts @@ -0,0 +1,445 @@ +import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { db } from 'src/db'; +import { + createDocument, + deleteDocument, + getDocument, + listDocuments, + searchDocuments, +} from 'src/lib/documents'; + +const documentsRouter = new Router(); + +/** + * @openapi + * /documents: + * get: + * tags: + * - Documents + * summary: List documents in a project + * description: Returns all documents belonging to a project + * operationId: listDocuments + * parameters: + * - name: projectId + * in: query + * required: true + * description: Project public ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: List of documents + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/DocumentRecord' + * '400': + * description: Missing projectId + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.get('/documents', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const projectId = ctx.query.projectId as string; + + if (!projectId) { + ctx.status = 400; + ctx.body = { error: 'projectId query parameter is required' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + projectId, + 'documents:ListDocuments' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ where: { publicId: projectId } }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + ctx.body = await listDocuments({ projectId: project.id }); +}); + +/** + * @openapi + * /documents/{id}: + * get: + * tags: + * - Documents + * summary: Get a document by ID + * description: Returns a document with its text content + * operationId: getDocument + * parameters: + * - name: id + * in: path + * required: true + * description: Document ID + * schema: + * type: string + * example: 'doc_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: Document found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DocumentRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Document not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.get('/documents/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const doc = await getDocument({ id: ctx.params.id }); + + if (!doc) { + ctx.status = 404; + ctx.body = { error: 'Document not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + doc.projectId!, + 'documents:GetDocument' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = doc; +}); + +/** + * @openapi + * /documents: + * post: + * tags: + * - Documents + * summary: Create a document + * description: Creates a new text document with an embedding vector + * operationId: createDocument + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - projectId + * - content + * properties: + * projectId: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * content: + * type: string + * example: 'The quick brown fox jumps over the lazy dog.' + * filename: + * type: string + * example: 'my-doc.txt' + * responses: + * '201': + * description: Document created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DocumentRecord' + * '400': + * description: Invalid request body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.post('/documents', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { + projectId: string; + content: string; + filename?: string; + }; + + if (!body.projectId || !body.content) { + ctx.status = 400; + ctx.body = { error: 'projectId and content are required' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + body.projectId, + 'documents:CreateDocument' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ + where: { publicId: body.projectId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + const doc = await createDocument({ + projectId: project.id, + content: body.content, + filename: body.filename, + }); + + ctx.status = 201; + ctx.body = doc; +}); + +/** + * @openapi + * /documents/{id}: + * delete: + * tags: + * - Documents + * summary: Delete a document + * description: Deletes a document and its underlying file + * operationId: deleteDocument + * parameters: + * - name: id + * in: path + * required: true + * description: Document ID + * schema: + * type: string + * example: 'doc_V1StGXR8Z5jdHi6B' + * responses: + * '204': + * description: Document deleted + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Document not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.delete('/documents/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const doc = await getDocument({ id: ctx.params.id }); + + if (!doc) { + ctx.status = 404; + ctx.body = { error: 'Document not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + doc.projectId!, + 'documents:DeleteDocument' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const result = await deleteDocument({ id: ctx.params.id }); + + if (result === null) { + ctx.status = 404; + ctx.body = { error: 'Document not found' }; + return; + } + + ctx.status = 204; +}); + +/** + * @openapi + * /documents/search: + * post: + * tags: + * - Documents + * summary: Semantic search over documents + * description: Embeds the query text and returns the most similar documents using cosine distance + * operationId: searchDocuments + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - projectId + * - query + * properties: + * projectId: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * query: + * type: string + * example: 'What is the capital of France?' + * limit: + * type: integer + * example: 5 + * responses: + * '200': + * description: Search results + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/DocumentRecord' + * '400': + * description: Invalid request body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.post('/documents/search', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { + projectId: string; + query: string; + limit?: number; + }; + + if (!body.projectId || !body.query) { + ctx.status = 400; + ctx.body = { error: 'projectId and query are required' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + body.projectId, + 'documents:SearchDocuments' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ + where: { publicId: body.projectId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + const results = await searchDocuments({ + projectId: project.id, + query: body.query, + limit: body.limit, + }); + + ctx.body = results; +}); + +export { documentsRouter }; diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index 9a56f1d6..fa670dae 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -1,6 +1,7 @@ import { Router } from '@ttoss/http-server'; import { apiKeysRouter } from './apiKeys'; +import { documentsRouter } from './documents'; import { filesRouter } from './files'; import { projectsRouter } from './projects'; import { usersRouter } from './users'; @@ -8,6 +9,7 @@ import { usersRouter } from './users'; const v1Router = new Router(); v1Router.use(apiKeysRouter.routes()); +v1Router.use(documentsRouter.routes()); v1Router.use(filesRouter.routes()); v1Router.use(projectsRouter.routes()); v1Router.use(usersRouter.routes()); diff --git a/packages/server/tests/unit/tests/documents.test.ts b/packages/server/tests/unit/tests/documents.test.ts new file mode 100644 index 00000000..efb73fde --- /dev/null +++ b/packages/server/tests/unit/tests/documents.test.ts @@ -0,0 +1,280 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +jest.mock('src/lib/embedding', () => { + return { + getEmbedding: jest.fn().mockResolvedValue(Array(1024).fill(0.1)), + }; +}); + +describe('Documents', () => { + let adminToken: string; + let userToken: string; + let userId: string; + let projectId: string; + let policyId: string; + let storageDir: string; + + beforeAll(async () => { + storageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'soat-docs-test-')); + + process.env.FILES_STORAGE_DIR = storageDir; + + process.env.EMBEDDING_PROVIDER = 'ollama'; + + process.env.EMBEDDING_MODEL = 'qwen3-embedding:0.6b'; + + process.env.EMBEDDING_DIMENSIONS = '1024'; + + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const createUserRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'docsuser', password: 'docspass' }); + + userId = createUserRes.body.id; + userToken = await loginAs('docsuser', 'docspass'); + + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Docs Test Project' }); + projectId = projectRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: [ + 'documents:ListDocuments', + 'documents:GetDocument', + 'documents:CreateDocument', + 'documents:DeleteDocument', + 'documents:SearchDocuments', + ], + }); + policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + }); + + afterAll(() => { + fs.rmSync(storageDir, { recursive: true, force: true }); + }); + + describe('POST /api/v1/documents', () => { + test('authenticated user with permission can create a document', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ + projectId, + content: 'Hello, world! This is a test document.', + filename: 'hello.txt', + }); + + expect(response.status).toBe(201); + expect(response.body.id).toMatch(/^doc_/); + expect(response.body.filename).toBe('hello.txt'); + expect(response.body.projectId).toBe(projectId); + expect(response.body.size).toBeGreaterThan(0); + expect(response.body.content).toBeUndefined(); + }); + + test('unauthenticated request cannot create a document', async () => { + const response = await testClient.post('/api/v1/documents').send({ + projectId, + content: 'Secret', + }); + + expect(response.status).toBe(401); + }); + + test('missing projectId returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ content: 'No project' }); + + expect(response.status).toBe(400); + }); + + test('missing content returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ projectId }); + + expect(response.status).toBe(400); + }); + }); + + describe('GET /api/v1/documents', () => { + test('authenticated user with permission can list documents', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/documents?projectId=${projectId}` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('unauthenticated request cannot list documents', async () => { + const response = await testClient.get( + `/api/v1/documents?projectId=${projectId}` + ); + + expect(response.status).toBe(401); + }); + + test('missing projectId returns 400', async () => { + const response = + await authenticatedTestClient(userToken).get('/api/v1/documents'); + + expect(response.status).toBe(400); + }); + }); + + describe('GET /api/v1/documents/:id', () => { + let documentId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ + projectId, + content: 'Fetch this document back.', + filename: 'fetch-me.txt', + }); + documentId = res.body.id; + }); + + test('user with permission can get a document by ID including content', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/documents/${documentId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(documentId); + expect(response.body.content).toBe('Fetch this document back.'); + }); + + test('unauthenticated request cannot get a document', async () => { + const response = await testClient.get(`/api/v1/documents/${documentId}`); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent document', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/documents/doc_nonexistent' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('POST /api/v1/documents/search', () => { + beforeAll(async () => { + await authenticatedTestClient(userToken).post('/api/v1/documents').send({ + projectId, + content: 'The capital of France is Paris.', + filename: 'france.txt', + }); + }); + + test('user with permission can search documents', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents/search') + .send({ projectId, query: 'capital of France' }); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('search with limit returns at most limit results', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents/search') + .send({ projectId, query: 'test content', limit: 1 }); + + expect(response.status).toBe(200); + expect(response.body.length).toBeLessThanOrEqual(1); + }); + + test('unauthenticated request cannot search documents', async () => { + const response = await testClient + .post('/api/v1/documents/search') + .send({ projectId, query: 'test' }); + + expect(response.status).toBe(401); + }); + + test('missing projectId returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents/search') + .send({ query: 'no project' }); + + expect(response.status).toBe(400); + }); + + test('missing query returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents/search') + .send({ projectId }); + + expect(response.status).toBe(400); + }); + }); + + describe('DELETE /api/v1/documents/:id', () => { + test('user with permission can delete a document and file is removed from disk', async () => { + const createRes = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ + projectId, + content: 'Delete me please.', + filename: 'todelete.txt', + }); + const documentId = createRes.body.id; + + const filesOnDisk = fs.readdirSync(storageDir); + expect(filesOnDisk.length).toBeGreaterThan(0); + + const deleteRes = await authenticatedTestClient(userToken).delete( + `/api/v1/documents/${documentId}` + ); + + expect(deleteRes.status).toBe(204); + + const getRes = await authenticatedTestClient(userToken).get( + `/api/v1/documents/${documentId}` + ); + expect(getRes.status).toBe(404); + }); + + test('unauthenticated request cannot delete a document', async () => { + const createRes = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ projectId, content: 'Protected.' }); + const documentId = createRes.body.id; + + const response = await testClient.delete( + `/api/v1/documents/${documentId}` + ); + + expect(response.status).toBe(401); + }); + + test('returns 404 when deleting a non-existent document', async () => { + const response = await authenticatedTestClient(adminToken).delete( + '/api/v1/documents/doc_nonexistent' + ); + + expect(response.status).toBe(404); + }); + }); +}); diff --git a/packages/website/docs/modules/documents.md b/packages/website/docs/modules/documents.md new file mode 100644 index 00000000..6181aae2 --- /dev/null +++ b/packages/website/docs/modules/documents.md @@ -0,0 +1,140 @@ +# Documents Module + +The Documents module stores plain-text documents along with an embedding vector in PostgreSQL, enabling semantic (vector) search across project content. Under the hood each document is backed by a Files record stored on disk. + +## Overview + +A Document IS a File — it always uses `.txt` format and is associated with a project. When a document is created, its text content is passed to a configured embedding provider (currently Ollama only), and the resulting vector is stored alongside the text. This allows cosine-similarity search at query time without an external vector database. + +Documents are identified by `publicId` prefixed with `doc_`. The internal database primary key is never returned. + +## Configuration + +| Environment Variable | Required | Description | +| ---------------------- | -------- | ------------------------------------------------------------ | +| `FILES_STORAGE_DIR` | Yes | Directory where `.txt` files are written (shared with Files) | +| `EMBEDDING_PROVIDER` | Yes | Embedding backend — only `ollama` is supported | +| `EMBEDDING_MODEL` | Yes | Model name, e.g. `qwen3-embedding:0.6b` | +| `EMBEDDING_DIMENSIONS` | Yes | Vector dimensions — must match the model output, e.g. `1024` | +| `OLLAMA_BASE_URL` | No | Ollama server URL, defaults to `http://localhost:11434` | + +### Ollama setup example + +```bash +# Pull the embedding model +ollama pull qwen3-embedding:0.6b + +# Verify it's running +ollama list +``` + +Set the server environment variables: + +```env +EMBEDDING_PROVIDER=ollama +EMBEDDING_MODEL=qwen3-embedding:0.6b +EMBEDDING_DIMENSIONS=1024 +OLLAMA_BASE_URL=http://localhost:11434 +``` + +## Data Model + +| Field | Type | Description | +| ----------- | ------ | ------------------------------------------------------------- | +| `id` | string | Public identifier prefixed with `doc_` | +| `fileId` | string | Public ID of the underlying File record | +| `projectId` | string | Public ID of the owning project | +| `filename` | string | Original filename (`.txt` extension) | +| `size` | number | File size in bytes | +| `content` | string | Text content — only present in `GET /documents/:id` responses | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +The `embedding` column (pgvector `vector(N)`) is stored in the database but never returned via the API. + +## Permissions + +Document operations are governed by per-project policies. Grant the following permissions: + +| Action | Permission string | +| ----------------- | --------------------------- | +| List documents | `documents:ListDocuments` | +| Get a document | `documents:GetDocument` | +| Create a document | `documents:CreateDocument` | +| Delete a document | `documents:DeleteDocument` | +| Semantic search | `documents:SearchDocuments` | + +## Operations + +### List documents + +Returns all documents in a project (no content field): + +```http +GET /api/v1/documents?projectId=proj_xxx +Authorization: Bearer +``` + +### Get a document + +Returns the document record including its full text `content`: + +```http +GET /api/v1/documents/doc_xxx +Authorization: Bearer +``` + +### Create a document + +Writes the text to disk, generates an embedding, and persists both: + +```http +POST /api/v1/documents +Authorization: Bearer +Content-Type: application/json + +{ + "projectId": "proj_xxx", + "content": "The quick brown fox jumps over the lazy dog.", + "filename": "my-doc.txt" +} +``` + +### Delete a document + +Deletes the Document record, the underlying File record, and removes the file from disk: + +```http +DELETE /api/v1/documents/doc_xxx +Authorization: Bearer +``` + +### Semantic search + +Embeds the query and returns the closest documents by cosine distance: + +```http +POST /api/v1/documents/search +Authorization: Bearer +Content-Type: application/json + +{ + "projectId": "proj_xxx", + "query": "What is the capital of France?", + "limit": 5 +} +``` + +The `limit` field defaults to `10` when omitted. + +## MCP Tools + +The following MCP tools are available for AI assistants: + +| Tool name | Description | +| ------------------ | --------------------------------------------------- | +| `list-documents` | List all documents in a project | +| `get-document` | Retrieve a document including its text content | +| `create-document` | Create a new text document with automatic embedding | +| `delete-document` | Delete a document and its underlying file | +| `search-documents` | Semantic search over project documents | diff --git a/packages/website/docs/tutorials/connect-mcp.md b/packages/website/docs/tutorials/connect-mcp.md deleted file mode 100644 index 6b82ec61..00000000 --- a/packages/website/docs/tutorials/connect-mcp.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Connect with MCP - -The **Model Context Protocol (MCP)** is the easiest way to consume SOAT. It allows AI clients (like Claude Desktop) to automatically discover and use the memory tools provided by your server. - -## Integrating with Claude Desktop - -1. **Locate your Claude Config** - - Find the `claude_desktop_config.json` file on your machine: - - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` - - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` - -2. **Add SOAT Server** - - Add the following configuration to the `mcpServers` object. We use `stdio` (standard input/output) for local development or `sse` (Server-Sent Events) for remote servers. - - _If you are running the server locally via Docker on port 3000, you will likely use the SSE transport._ - - ```json - { - "mcpServers": { - "soat": { - "command": "node", - "args": ["path/to/soat/packages/server/dist/index.js"], - "env": { - "DATABASE_URL": "postgres://soat_user:soat_password@localhost:5432/soat_db" - } - } - } - } - ``` - - > **Wait!** The example above assumes you are running the MCP server directly via `node`. - > If you are using the **Docker container** we set up in "Getting Started", you need an MCP Client that supports HTTP/SSE. - > - > _Currently, Claude Desktop creates a local process. You might need to run a local "bridge" script or run the node process directly as shown above._ - - **Recommended for Local Source Usage:** - If you cloned the repo, the easiest way currently is to point Claude directly to the built server file: - - ```bash - # First, ensure you have built the project - pnpm install - pnpm build - ``` - - Then update your config to point to the absolute path of the built file. - -3. **Restart Claude** - - Restart the Claude Desktop application. You should see a 🔌 icon indicating the MCP server is connected. - -4. **Test It** - - Ask Claude: _"Please save this conversation to my memory"_ or _"What do you know about my project SOAT?"_. - -## Using with Cursor - -Cursor also supports MCP. Go to **Cursor Settings > Features > MCP** and add a new server. diff --git a/packages/website/docs/tutorials/storing-memory.md b/packages/website/docs/tutorials/storing-memory.md deleted file mode 100644 index 9d5fe3e4..00000000 --- a/packages/website/docs/tutorials/storing-memory.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Storing & Retrieving Memory - -Once connected via MCP, your agent has access to specific tools to interact with the memory database. - -## Available Tools - -The SOAT Server exposes the following tools to the agent: - -### `add_memory` - -Stores a piece of text into the database. It automatically generates a vector embedding for semantic search later. - -- **Input**: `text` (string), `metadata` (optional JSON) -- **Example**: "The user prefers TypeScript over JavaScript for all new projects." - -### `search_memory` - -Retrieves relevant memories based on a query. It compares the vector embedding of the query with stored memories. - -- **Input**: `query` (string) -- **Example**: "What are the user's coding preferences?" -- **Output**: A list of matching text snippets with similarity scores. - -## Example Workflow - -Here is how an interaction might look between you and an agent using SOAT: - -1. **User**: "My API key for the weather service is `12345`. Remember that." -2. **Agent**: _Calls `add_memory` with text "Weather service API key is 12345"._ -3. **Agent**: "I have stored that API key in your memory." - -... _Days later_ ... - -1. **User**: "I need to check the weather, what credential should I use?" -2. **Agent**: _Calls `search_memory` with "weather credential API key"._ -3. **Soat Server**: _Returns the memory stored earlier._ -4. **Agent**: "You should use the API key `12345`." - -## Best Practices - -- **Be Specific**: Agents perform best when memories are atomic and self-contained. -- **Use Context**: When asking the agent to remember something, explicitly say "save this to memory". diff --git a/packages/website/sidebars.ts b/packages/website/sidebars.ts index 1f71ba11..5bf5c1db 100644 --- a/packages/website/sidebars.ts +++ b/packages/website/sidebars.ts @@ -20,15 +20,10 @@ const sidebars: SidebarsConfig = { tutorialSidebar: [ 'Introduction', 'getting-started', - { - type: 'category', - label: 'Tutorials', - items: [{ type: 'autogenerated', dirName: 'tutorials' }], - }, { type: 'category', label: 'Modules', - items: ['modules/files'], + items: [{ type: 'autogenerated', dirName: 'modules' }], }, ], diff --git a/tests/smoke-test.sh b/tests/smoke-test.sh index 687cc8b9..8f884aef 100755 --- a/tests/smoke-test.sh +++ b/tests/smoke-test.sh @@ -96,5 +96,52 @@ if [ "$AFTER_DELETE_STATUS" != "404" ]; then fi echo "File correctly returns 404 after deletion." +# 10. Create first document +echo "--- Creating first document ---" +DOC1_RESP=$(curl -sf -X POST "$BASE_URL/documents" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d "{\"projectId\":\"$PROJECT_PUBLIC_ID\",\"content\":\"The quick brown fox jumps over the lazy dog\",\"filename\":\"fox.txt\"}") +DOC1_ID=$(echo "$DOC1_RESP" | jq -r '.id') +echo "Document 1 id: $DOC1_ID" + +# 11. Create second document +echo "--- Creating second document ---" +DOC2_RESP=$(curl -sf -X POST "$BASE_URL/documents" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d "{\"projectId\":\"$PROJECT_PUBLIC_ID\",\"content\":\"Machine learning models require large amounts of training data\",\"filename\":\"ml.txt\"}") +DOC2_ID=$(echo "$DOC2_RESP" | jq -r '.id') +echo "Document 2 id: $DOC2_ID" + +# 12. Search documents +echo "--- Searching documents ---" +SEARCH_RESP=$(curl -sf -X POST "$BASE_URL/documents/search" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d "{\"projectId\":\"$PROJECT_PUBLIC_ID\",\"query\":\"fox animal jumping\",\"limit\":5}") +SEARCH_COUNT=$(echo "$SEARCH_RESP" | jq 'length') +if [ "$SEARCH_COUNT" -lt 1 ]; then + echo "ERROR: Document search returned $SEARCH_COUNT results, expected at least 1" >&2 + exit 1 +fi +echo "Search returned $SEARCH_COUNT result(s)." + +# 13. Delete documents +echo "--- Deleting documents ---" +DELETE_DOC1=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE_URL/documents/$DOC1_ID" \ + -H "Authorization: Bearer $TOKEN") +if [ "$DELETE_DOC1" != "204" ]; then + echo "ERROR: DELETE document 1 returned $DELETE_DOC1, expected 204" >&2 + exit 1 +fi +DELETE_DOC2=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE_URL/documents/$DOC2_ID" \ + -H "Authorization: Bearer $TOKEN") +if [ "$DELETE_DOC2" != "204" ]; then + echo "ERROR: DELETE document 2 returned $DELETE_DOC2, expected 204" >&2 + exit 1 +fi +echo "Documents deleted." + echo "" echo "=== All smoke tests passed! ===" From 76c2afb561f85e5f9cbdf87f84cdc7c05b88cc11 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Sat, 11 Apr 2026 15:39:28 -0300 Subject: [PATCH 06/27] feat: actors --- .github/workflows/main.yml | 90 +++++ .github/workflows/pr.yml | 25 ++ carlin.yml | 0 docs/prd-actors-conversations.md | 245 ++++++++++++ packages/postgresdb/src/models/Actor.ts | 62 ++++ packages/postgresdb/src/models/index.ts | 1 + packages/postgresdb/src/utils/publicId.ts | 1 + packages/server/package.json | 1 - packages/server/src/Context.ts | 12 + packages/server/src/lib/actors.ts | 89 +++++ packages/server/src/lib/documents.ts | 22 +- packages/server/src/mcp/tools/actors.ts | 90 +++++ packages/server/src/mcp/tools/documents.ts | 24 +- packages/server/src/mcp/tools/index.ts | 2 + packages/server/src/middleware/auth.ts | 56 ++- .../server/src/rest/openapi/v1/actors.yaml | 223 +++++++++++ .../server/src/rest/openapi/v1/documents.yaml | 22 +- packages/server/src/rest/v1/actors.ts | 350 ++++++++++++++++++ packages/server/src/rest/v1/documents.ts | 98 +++-- packages/server/src/rest/v1/files.ts | 8 +- packages/server/src/rest/v1/index.ts | 2 + .../server/tests/unit/tests/actors.test.ts | 245 ++++++++++++ .../server/tests/unit/tests/documents.test.ts | 58 ++- packages/website/docs/modules/actors.md | 44 +++ packages/website/docs/modules/documents.md | 30 +- turbo.json | 4 + 26 files changed, 1695 insertions(+), 109 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/pr.yml create mode 100644 carlin.yml create mode 100644 docs/prd-actors-conversations.md create mode 100644 packages/postgresdb/src/models/Actor.ts create mode 100644 packages/server/src/lib/actors.ts create mode 100644 packages/server/src/mcp/tools/actors.ts create mode 100644 packages/server/src/rest/openapi/v1/actors.yaml create mode 100644 packages/server/src/rest/v1/actors.ts create mode 100644 packages/server/tests/unit/tests/actors.test.ts create mode 100644 packages/website/docs/modules/actors.md diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..bc4e320f --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,90 @@ +name: Main + +on: + push: + branches: + - main + +jobs: + test-and-build: + name: Test and Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Test and build + run: pnpm turbo run build test + + deploy-docker: + name: Deploy Docker Image + runs-on: ubuntu-latest + needs: test-and-build + permissions: + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ghcr.io/${{ github.repository }}:latest + + deploy-website: + name: Deploy Website + runs-on: ubuntu-latest + needs: test-and-build + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build server (required for OpenAPI docs generation) + run: pnpm --filter @soat/postgresdb build && pnpm --filter @soat/server build + + - name: Configure git credentials + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git + + - name: Deploy website + run: pnpm --filter website deploy + env: + GIT_USER: ${{ github.actor }} + USE_SSH: 'false' + CURRENT_BRANCH: main + DEPLOYMENT_BRANCH: gh-pages diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..842e313b --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,25 @@ +name: PR + +on: + pull_request: + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build and test + run: pnpm turbo run build test diff --git a/carlin.yml b/carlin.yml new file mode 100644 index 00000000..e69de29b diff --git a/docs/prd-actors-conversations.md b/docs/prd-actors-conversations.md new file mode 100644 index 00000000..23349a56 --- /dev/null +++ b/docs/prd-actors-conversations.md @@ -0,0 +1,245 @@ +# PRD: Actors & Conversations + +**Status**: In Development +**Branch**: `feat-files` +**Date**: April 10, 2026 + +--- + +## Overview + +This document describes the design and implementation of two new modules — **Actors** and **Conversations** — added to the Soat platform. These modules allow projects to model participants and their ordered interactions with documents stored in the system. + +--- + +## Problem Statement + +Projects that use Soat to manage documents often represent real-world conversations — support tickets, AI chat sessions, interview transcripts, onboarding flows — but the platform had no way to: + +1. Represent the **participant** (human, AI agent, service) who drives a conversation. +2. Group and **sequence documents** that belong to a single conversational exchange. +3. Track whether a conversation is **active or concluded**. + +Without these primitives, consumers must maintain that structure in their own databases, losing the ability to query, filter, and act on conversations server-side. + +--- + +## Goals + +- Introduce a first-class `Actor` entity scoped to a project. +- Introduce a first-class `Conversation` entity that ties together an actor, a project, and an ordered sequence of documents. +- Expose full CRUD over both entities via the REST API, MCP tools, and documentation. +- Fit naturally into the existing auth model (project policies, `isAllowed` permission checks). + +--- + +## Non-Goals + +- Real-time messaging or WebSocket streaming. +- Conversation history versioning or audit trails. +- Multi-actor conversations (a conversation has exactly one actor in this version). +- Document content modification through conversation endpoints. + +--- + +## Data Model + +### Actor + +Represents a participant — a human user, an AI agent, or any named entity that can own a conversation. + +| Field | Type | Notes | +| ----------- | ---------------- | --------------------------------------------- | +| `id` | `string` | Public ID, prefixed `act_` | +| `projectId` | `string` | Public ID of the owning project | +| `name` | `string` | Display name (required) | +| `type` | `string \| null` | Optional label, e.g. `human`, `ai`, `service` | +| `createdAt` | `string` | ISO 8601 | +| `updatedAt` | `string` | ISO 8601 | + +**Database table**: `actors` +**Cascade**: deleted when parent project is deleted (`onDelete: CASCADE`). + +--- + +### Conversation + +Represents a single conversational session, owned by one actor, containing an ordered list of documents. + +| Field | Type | Notes | +| ----------- | -------------------- | ------------------------------------------------- | +| `id` | `string` | Public ID, prefixed `conv_` | +| `projectId` | `string` | Public ID of the owning project | +| `actorId` | `string` | Public ID of the actor who owns this conversation | +| `status` | `"open" \| "closed"` | `open` by default | +| `createdAt` | `string` | ISO 8601 | +| `updatedAt` | `string` | ISO 8601 | + +**Database table**: `conversations` +**Cascade**: deleted when parent project or actor is deleted. + +--- + +### ConversationDocument (junction) + +Associates a document with a conversation at a specific position in the sequence. + +| Field | Type | Notes | +| ---------------- | -------- | ---------------------------------------------------- | +| `conversationId` | `number` | Internal FK to `conversations` | +| `documentId` | `number` | Internal FK to `documents` | +| `position` | `number` | Zero-based integer; auto-incremented if not provided | + +**Database table**: `conversation_documents` +**Unique constraint**: `(conversation_id, document_id)` — a document can appear only once per conversation. +**Ordering**: queries return entries sorted by `position ASC`. + +--- + +## Permission Model + +Both modules integrate with the existing project policy system. The following action strings are checked via `ctx.authUser.isAllowed(projectId, action)`: + +| Action | Endpoint | +| ---------------------------------- | -------------------------------------------- | +| `actors:ListActors` | `GET /actors` | +| `actors:GetActor` | `GET /actors/:id` | +| `actors:CreateActor` | `POST /actors` | +| `actors:DeleteActor` | `DELETE /actors/:id` | +| `conversations:ListConversations` | `GET /conversations` | +| `conversations:GetConversation` | `GET /conversations/:id` | +| `conversations:GetConversation` | `GET /conversations/:id/documents` | +| `conversations:CreateConversation` | `POST /conversations` | +| `conversations:UpdateConversation` | `PATCH /conversations/:id` | +| `conversations:UpdateConversation` | `POST /conversations/:id/documents` | +| `conversations:UpdateConversation` | `DELETE /conversations/:id/documents/:docId` | +| `conversations:DeleteConversation` | `DELETE /conversations/:id` | + +--- + +## REST API + +### Actors + +``` +GET /actors?projectId= List actors +GET /actors/:id Get actor +POST /actors Create actor +DELETE /actors/:id Delete actor +``` + +**POST /actors** body: + +```json +{ + "projectId": "proj_...", + "name": "Support Bot", + "type": "ai" +} +``` + +--- + +### Conversations + +``` +GET /conversations?projectId= List conversations +GET /conversations/:id Get conversation +POST /conversations Create conversation +PATCH /conversations/:id Update status +DELETE /conversations/:id Delete conversation +GET /conversations/:id/documents List documents in order +POST /conversations/:id/documents Add document to conversation +DELETE /conversations/:id/documents/:documentId Remove document +``` + +**POST /conversations** body: + +```json +{ + "projectId": "proj_...", + "actorId": "act_...", + "status": "open" +} +``` + +**PATCH /conversations/:id** body: + +```json +{ "status": "closed" } +``` + +**POST /conversations/:id/documents** body: + +```json +{ + "documentId": "doc_...", + "position": 2 +} +``` + +If `position` is omitted, the document is appended at `MAX(position) + 1` (or position `0` if the conversation is empty). + +--- + +## MCP Tools + +All operations are also exposed as MCP tools for AI agent consumption: + +| Tool name | Description | +| ------------------------------ | ------------------------------------------ | +| `list-actors` | List actors in a project | +| `get-actor` | Get actor by ID | +| `create-actor` | Create an actor | +| `delete-actor` | Delete an actor | +| `list-conversations` | List conversations in a project | +| `get-conversation` | Get conversation by ID | +| `create-conversation` | Create a conversation | +| `update-conversation-status` | Set status to `open` or `closed` | +| `delete-conversation` | Delete a conversation | +| `list-conversation-documents` | List documents in a conversation (ordered) | +| `add-conversation-document` | Add a document at a given position | +| `remove-conversation-document` | Remove a document from a conversation | + +--- + +## Implementation Architecture + +``` +packages/postgresdb/src/models/ + Actor.ts ← DB model, publicId: act_ + Conversation.ts ← DB model, publicId: conv_ + ConversationDocument.ts ← Junction table model + +packages/server/src/lib/ + actors.ts ← listActors, getActor, createActor, deleteActor + conversations.ts ← listConversations, getConversation, createConversation, + updateConversationStatus, deleteConversation, + listConversationDocuments, addDocumentToConversation, + removeDocumentFromConversation + +packages/server/src/rest/v1/ + actors.ts ← REST routes registered in index.ts + conversations.ts ← REST routes registered in index.ts + +packages/server/src/mcp/tools/ + actors.ts ← MCP tools registered in index.ts + conversations.ts ← MCP tools registered in index.ts +``` + +--- + +## Remaining Work + +- [ ] OpenAPI YAML component schemas (`ActorRecord`, `ConversationRecord`, `ConversationDocumentRecord`) in `src/rest/openapi/v1/` +- [ ] Module docs page at `packages/website/docs/modules/actors.md` and `conversations.md` +- [ ] Unit tests at `packages/server/tests/unit/tests/actors.test.ts` and `conversations.test.ts` +- [ ] Database migration / sync for `actors`, `conversations`, `conversation_documents` tables + +--- + +## Open Questions + +1. **Multi-actor conversations** — should a future version support multiple actors per conversation (e.g. a thread between a human and an AI)? +2. **Position gaps** — if a document is removed from the middle, positions are not re-indexed. Should a reorder endpoint be added? +3. **Conversation filters** — should `GET /conversations` support filtering by `status` or `actorId`? diff --git a/packages/postgresdb/src/models/Actor.ts b/packages/postgresdb/src/models/Actor.ts new file mode 100644 index 00000000..07d796e9 --- /dev/null +++ b/packages/postgresdb/src/models/Actor.ts @@ -0,0 +1,62 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; +import { Project } from './Project'; + +@Table({ + tableName: 'actors', + hooks: { + beforeValidate: (instance: Actor) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.actor); + } + }, + }, + indexes: [ + { + unique: true, + fields: ['project_id', 'external_id'], + }, + ], +}) +export class Actor extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @ForeignKey(() => { + return Project; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare projectId: number; + + @BelongsTo(() => { + return Project; + }) + declare project: Project; + + @Column({ type: DataType.STRING, allowNull: false }) + declare name: string; + + @Column({ type: DataType.STRING }) + declare type?: string; + + @Column({ type: DataType.STRING }) + declare externalId?: string; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/index.ts b/packages/postgresdb/src/models/index.ts index 89877943..8b1ce7e2 100644 --- a/packages/postgresdb/src/models/index.ts +++ b/packages/postgresdb/src/models/index.ts @@ -1,3 +1,4 @@ +export { Actor } from './Actor'; export { ApiKey } from './ApiKey'; export { Document } from './Document'; export { File } from './File'; diff --git a/packages/postgresdb/src/utils/publicId.ts b/packages/postgresdb/src/utils/publicId.ts index 4caef7bf..6090576b 100644 --- a/packages/postgresdb/src/utils/publicId.ts +++ b/packages/postgresdb/src/utils/publicId.ts @@ -11,6 +11,7 @@ export const PUBLIC_ID_PREFIXES = { policy: 'pol_', apiKey: 'key_', document: 'doc_', + actor: 'act_', } as const; /** diff --git a/packages/server/package.json b/packages/server/package.json index 823e18fc..272ffce6 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -10,7 +10,6 @@ "test": "jest --projects tests/unit" }, "dependencies": { - "@koa/multer": "^4.0.0", "@soat/postgresdb": "workspace:*", "@ttoss/http-server": "^0.5.9", "@ttoss/http-server-mcp": "^0.11.1", diff --git a/packages/server/src/Context.ts b/packages/server/src/Context.ts index e7e4e3fa..696725ee 100644 --- a/packages/server/src/Context.ts +++ b/packages/server/src/Context.ts @@ -6,6 +6,18 @@ export type AuthUser = { username: string; role: 'admin' | 'user'; isAllowed: (projectPublicId: string, action: string) => Promise; + /** + * Resolves the internal project IDs the caller may access for the given action. + * - Explicit projectPublicId: verifies permission and returns [id], or null if forbidden/not found. + * - API key (no explicit id): infers from the key's scoped project. + * - JWT admin (no explicit id): returns undefined (no filter — all projects). + * - JWT user (no explicit id): enumerates all projects the user has access to. + */ + resolveProjectIds: (args: { + projectPublicId?: string; + action: string; + }) => Promise; + apiKeyProjectId?: string; }; export type Context = { diff --git a/packages/server/src/lib/actors.ts b/packages/server/src/lib/actors.ts new file mode 100644 index 00000000..bd9aabb8 --- /dev/null +++ b/packages/server/src/lib/actors.ts @@ -0,0 +1,89 @@ +import { db } from '../db'; + +const mapActor = ( + actor: InstanceType<(typeof db)['Actor']> & { + project?: InstanceType<(typeof db)['Project']>; + } +) => { + return { + id: actor.publicId, + projectId: actor.project?.publicId, + name: actor.name, + type: actor.type ?? undefined, + externalId: actor.externalId ?? undefined, + createdAt: actor.createdAt, + updatedAt: actor.updatedAt, + }; +}; + +export const listActors = async (args: { + projectIds?: number[]; + externalId?: string; +}) => { + if (args.projectIds !== undefined && args.projectIds.length === 0) { + return []; + } + + const where: Record = {}; + + if (args.projectIds !== undefined) { + where.projectId = args.projectIds; + } + + if (args.externalId !== undefined) { + where.externalId = args.externalId; + } + + const actors = await db.Actor.findAll({ + where: Object.keys(where).length > 0 ? where : undefined, + include: [{ model: db.Project, as: 'project' }], + }); + + return actors.map(mapActor); +}; + +export const getActor = async (args: { id: string }) => { + const actor = await db.Actor.findOne({ + where: { publicId: args.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + if (!actor) { + return null; + } + + return mapActor(actor); +}; + +export const createActor = async (args: { + projectId: number; + name: string; + type?: string; + externalId?: string; +}) => { + const actor = await db.Actor.create({ + projectId: args.projectId, + name: args.name, + type: args.type, + externalId: args.externalId, + }); + + const actorWithProject = await db.Actor.findOne({ + where: { id: actor.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + return mapActor(actorWithProject!); +}; + +export const deleteActor = async (args: { id: string }) => { + const actor = await db.Actor.findOne({ where: { publicId: args.id } }); + + if (!actor) { + return null; + } + + await actor.destroy(); + + return { id: args.id }; +}; diff --git a/packages/server/src/lib/documents.ts b/packages/server/src/lib/documents.ts index 751cafb2..cc785877 100644 --- a/packages/server/src/lib/documents.ts +++ b/packages/server/src/lib/documents.ts @@ -30,13 +30,20 @@ const mapDocument = ( }; }; -export const listDocuments = async (args: { projectId: number }) => { +export const listDocuments = async (args: { projectIds?: number[] }) => { + if (args.projectIds !== undefined && args.projectIds.length === 0) { + return []; + } + + const fileWhere = + args.projectIds !== undefined ? { projectId: args.projectIds } : undefined; + const documents = await db.Document.findAll({ include: [ { model: db.File, as: 'file', - where: { projectId: args.projectId }, + where: fileWhere, include: [{ model: db.Project, as: 'project' }], }, ], @@ -142,20 +149,27 @@ export const deleteDocument = async (args: { id: string }) => { }; export const searchDocuments = async (args: { - projectId: number; + projectIds?: number[]; query: string; limit?: number; }) => { + if (args.projectIds !== undefined && args.projectIds.length === 0) { + return []; + } + const embedding = await getEmbedding({ text: args.query }); const limit = args.limit ?? 10; const embeddingLiteral = `[${embedding.join(',')}]`; + const fileWhere = + args.projectIds !== undefined ? { projectId: args.projectIds } : undefined; + const documents = await db.Document.findAll({ include: [ { model: db.File, as: 'file', - where: { projectId: args.projectId }, + where: fileWhere, include: [{ model: db.Project, as: 'project' }], }, ], diff --git a/packages/server/src/mcp/tools/actors.ts b/packages/server/src/mcp/tools/actors.ts new file mode 100644 index 00000000..996a778e --- /dev/null +++ b/packages/server/src/mcp/tools/actors.ts @@ -0,0 +1,90 @@ +import type { McpServer } from '@ttoss/http-server-mcp'; +import { apiCall, z } from '@ttoss/http-server-mcp'; + +const registerTools = (server: McpServer) => { + server.registerTool( + 'list-actors', + { + description: + 'List actors. If projectId is omitted, returns all actors accessible to the caller. Optionally filter by externalId (e.g. WhatsApp phone number).', + inputSchema: { + projectId: z.string().optional().describe('Project ID (optional)'), + externalId: z + .string() + .optional() + .describe('External ID to filter by (e.g. WhatsApp phone number)'), + }, + }, + async ({ projectId, externalId }) => { + const params = new URLSearchParams(); + if (projectId) params.set('projectId', projectId); + if (externalId) params.set('externalId', externalId); + const qs = params.toString() ? `?${params.toString()}` : ''; + const data = await apiCall('GET', `/actors${qs}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'get-actor', + { + description: 'Get an actor by ID', + inputSchema: { + id: z.string().describe('Actor ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/actors/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'create-actor', + { + description: + 'Create a new actor. API keys infer the project automatically; JWT callers must supply projectId.', + inputSchema: { + projectId: z + .string() + .optional() + .describe( + 'Project ID (required for JWT auth, optional for API keys)' + ), + name: z.string().describe('Actor name'), + type: z + .string() + .optional() + .describe("Optional actor type (e.g. 'customer', 'agent')"), + externalId: z + .string() + .optional() + .describe( + 'Optional external identifier (e.g. WhatsApp phone number). Must be unique within the project.' + ), + }, + }, + async ({ projectId, name, type, externalId }) => { + const data = await apiCall('POST', '/actors', { + body: { projectId, name, type, externalId }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'delete-actor', + { + description: 'Delete an actor by ID', + inputSchema: { + id: z.string().describe('Actor ID'), + }, + }, + async ({ id }) => { + await apiCall('DELETE', `/actors/${id}`); + return { content: [{ type: 'text', text: 'Actor deleted' }] }; + } + ); +}; + +export { registerTools }; diff --git a/packages/server/src/mcp/tools/documents.ts b/packages/server/src/mcp/tools/documents.ts index 91a4e0d3..09e72c14 100644 --- a/packages/server/src/mcp/tools/documents.ts +++ b/packages/server/src/mcp/tools/documents.ts @@ -5,16 +5,15 @@ const registerTools = (server: McpServer) => { server.registerTool( 'list-documents', { - description: 'List all documents in a project', + description: + 'List documents. If projectId is omitted, returns all documents accessible to the caller.', inputSchema: { - projectId: z.string().describe('Project ID'), + projectId: z.string().optional().describe('Project ID (optional)'), }, }, async ({ projectId }) => { - const data = await apiCall( - 'GET', - `/documents?projectId=${encodeURIComponent(projectId)}` - ); + const qs = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ''; + const data = await apiCall('GET', `/documents${qs}`); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } ); @@ -37,9 +36,14 @@ const registerTools = (server: McpServer) => { 'create-document', { description: - 'Create a new text document with an embedding vector for semantic search', + 'Create a new text document with an embedding vector for semantic search. API keys infer the project automatically; JWT callers must supply projectId.', inputSchema: { - projectId: z.string().describe('Project ID'), + projectId: z + .string() + .optional() + .describe( + 'Project ID (required for JWT auth, optional for API keys)' + ), content: z.string().describe('Text content of the document'), filename: z.string().optional().describe('Optional filename'), }, @@ -70,9 +74,9 @@ const registerTools = (server: McpServer) => { 'search-documents', { description: - 'Perform semantic search over documents in a project using cosine similarity', + 'Perform semantic search over documents using cosine similarity. If projectId is omitted, searches across all accessible projects.', inputSchema: { - projectId: z.string().describe('Project ID'), + projectId: z.string().optional().describe('Project ID (optional)'), query: z.string().describe('Natural language search query'), limit: z .number() diff --git a/packages/server/src/mcp/tools/index.ts b/packages/server/src/mcp/tools/index.ts index 31b1d44a..e460d7a7 100644 --- a/packages/server/src/mcp/tools/index.ts +++ b/packages/server/src/mcp/tools/index.ts @@ -1,9 +1,11 @@ import type { McpServer } from '@ttoss/http-server-mcp'; +import { registerTools as registerActorTools } from './actors'; import { registerTools as registerDocumentTools } from './documents'; import { registerTools as registerFileTools } from './files'; const registerTools = (server: McpServer) => { + registerActorTools(server); registerDocumentTools(server); registerFileTools(server); }; diff --git a/packages/server/src/middleware/auth.ts b/packages/server/src/middleware/auth.ts index 12b67013..fb068d08 100644 --- a/packages/server/src/middleware/auth.ts +++ b/packages/server/src/middleware/auth.ts @@ -5,7 +5,6 @@ import jwt from 'jsonwebtoken'; import type { Context } from '../Context'; import { createApiKeyIsAllowed, createJwtIsAllowed } from '../lib/permissions'; -// eslint-disable-next-line turbo/no-undeclared-env-vars export const JWT_SECRET = process.env.JWT_SECRET ?? 'dev-secret'; export const BCRYPT_SALT_ROUNDS = 12; @@ -66,16 +65,29 @@ const resolveApiKey = async (ctx: Context, rawKey: string) => { } : { permissions: [], notPermissions: [] }; + const apiKeyIsAllowed = createApiKeyIsAllowed({ + projectPublicId, + userPolicy, + apiKeyPolicy: keyPolicy, + }); + ctx.authUser = { id: keyUser.id as number, publicId: keyUser.publicId as string, username: keyUser.username as string, role: keyUser.role as 'admin' | 'user', - isAllowed: createApiKeyIsAllowed({ - projectPublicId, - userPolicy, - apiKeyPolicy: keyPolicy, - }), + apiKeyProjectId: projectPublicId, + isAllowed: apiKeyIsAllowed, + resolveProjectIds: async ({ projectPublicId: reqId, action }) => { + const targetId = reqId ?? projectPublicId; + const allowed = await apiKeyIsAllowed(targetId, action); + if (!allowed) return null; + const proj = await ctx.db.Project.findOne({ + where: { publicId: targetId }, + }); + if (!proj) return null; + return [proj.id as number]; + }, }; break; } @@ -102,12 +114,42 @@ const resolveJwt = async (ctx: Context, token: string) => { const userId = user.id as number; const role = user.role as 'admin' | 'user'; + const jwtIsAllowed = createJwtIsAllowed({ role, userId, db: ctx.db }); + ctx.authUser = { id: userId, publicId: user.publicId as string, username: user.username as string, role, - isAllowed: createJwtIsAllowed({ role, userId, db: ctx.db }), + isAllowed: jwtIsAllowed, + resolveProjectIds: async ({ projectPublicId, action }) => { + if (projectPublicId) { + const allowed = await jwtIsAllowed(projectPublicId, action); + if (!allowed) return null; + const proj = await ctx.db.Project.findOne({ + where: { publicId: projectPublicId }, + }); + if (!proj) return null; + return [proj.id as number]; + } + if (role === 'admin') return undefined; + const memberships = await ctx.db.UserProject.findAll({ + where: { userId }, + include: [{ model: ctx.db.Project }], + }); + const accessible: number[] = []; + for (const membership of memberships) { + const proj = ( + membership as unknown as { + project: InstanceType<(typeof ctx.db)['Project']>; + } + ).project; + if (!proj) continue; + const allowed = await jwtIsAllowed(proj.publicId as string, action); + if (allowed) accessible.push(proj.id as number); + } + return accessible; + }, }; }; diff --git a/packages/server/src/rest/openapi/v1/actors.yaml b/packages/server/src/rest/openapi/v1/actors.yaml new file mode 100644 index 00000000..e5c672fb --- /dev/null +++ b/packages/server/src/rest/openapi/v1/actors.yaml @@ -0,0 +1,223 @@ +openapi: 3.0.3 +info: + title: SOAT Actors API + version: 1.0.0 + description: API for managing actors (e.g. WhatsApp contacts) associated with projects + contact: + name: SOAT Team + url: https://github.com/ttoss/soat +servers: + - url: http://0.0.0.0:5047/api/v1 + description: Development server +paths: + /actors: + get: + tags: + - Actors + summary: List actors + description: Returns all actors the caller has access to. If projectId is provided, returns only actors in that project. API keys are scoped to a single project automatically. JWT users without projectId receive actors across all their accessible projects. + operationId: listActors + parameters: + - name: projectId + in: query + required: false + description: Project public ID (optional) + schema: + type: string + example: 'proj_V1StGXR8Z5jdHi6B' + - name: externalId + in: query + required: false + description: External ID to filter by (e.g. WhatsApp phone number) + schema: + type: string + example: '+15551234567' + responses: + '200': + description: List of actors + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ActorRecord' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Actors + summary: Create an actor + description: Creates a new actor. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + operationId: createActor + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + projectId: + type: string + description: Project public ID. Required for JWT auth; omit when using an API key. + example: 'proj_V1StGXR8Z5jdHi6B' + name: + type: string + example: 'Alice' + type: + type: string + description: Optional actor type (e.g. 'customer', 'agent') + example: 'customer' + externalId: + type: string + description: Optional external identifier (e.g. WhatsApp phone number). Must be unique within a project. + example: '+15551234567' + responses: + '201': + description: Actor created + content: + application/json: + schema: + $ref: '#/components/schemas/ActorRecord' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /actors/{id}: + get: + tags: + - Actors + summary: Get an actor by ID + description: Returns an actor by its public ID + operationId: getActor + parameters: + - name: id + in: path + required: true + description: Actor ID + schema: + type: string + example: 'act_V1StGXR8Z5jdHi6B' + responses: + '200': + description: Actor found + content: + application/json: + schema: + $ref: '#/components/schemas/ActorRecord' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Actor not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - Actors + summary: Delete an actor + description: Deletes an actor by its public ID + operationId: deleteActor + parameters: + - name: id + in: path + required: true + description: Actor ID + schema: + type: string + example: 'act_V1StGXR8Z5jdHi6B' + responses: + '204': + description: Actor deleted + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Actor not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' +components: + schemas: + ActorRecord: + type: object + properties: + id: + type: string + description: Actor public ID + example: 'act_V1StGXR8Z5jdHi6B' + projectId: + type: string + description: Project public ID + example: 'proj_V1StGXR8Z5jdHi6B' + name: + type: string + example: 'Alice' + type: + type: string + nullable: true + description: Actor type (e.g. 'customer', 'agent') + example: 'customer' + externalId: + type: string + nullable: true + description: External identifier (e.g. WhatsApp phone number) + example: '+15551234567' + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + ErrorResponse: + type: object + properties: + error: + type: string + example: 'Actor not found' diff --git a/packages/server/src/rest/openapi/v1/documents.yaml b/packages/server/src/rest/openapi/v1/documents.yaml index c7a186af..2dd1287d 100644 --- a/packages/server/src/rest/openapi/v1/documents.yaml +++ b/packages/server/src/rest/openapi/v1/documents.yaml @@ -14,14 +14,14 @@ paths: get: tags: - Documents - summary: List documents in a project - description: Returns all documents belonging to a project + summary: List documents + description: Returns all documents the caller has access to. If projectId is provided, returns only documents in that project. API keys are scoped to a single project automatically. JWT users without projectId receive documents across all their accessible projects. operationId: listDocuments parameters: - name: projectId in: query - required: true - description: Project public ID + required: false + description: Project public ID (optional) schema: type: string example: 'proj_V1StGXR8Z5jdHi6B' @@ -34,12 +34,6 @@ paths: type: array items: $ref: '#/components/schemas/DocumentRecord' - '400': - description: Missing projectId - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' '401': description: Unauthorized content: @@ -56,7 +50,7 @@ paths: tags: - Documents summary: Create a document - description: Creates a new text document and generates an embedding vector for semantic search + description: Creates a new text document and generates an embedding vector for semantic search. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. operationId: createDocument requestBody: required: true @@ -65,11 +59,11 @@ paths: schema: type: object required: - - projectId - content properties: projectId: type: string + description: Project public ID. Required for JWT auth; omit when using an API key. example: 'proj_V1StGXR8Z5jdHi6B' content: type: string @@ -182,7 +176,7 @@ paths: tags: - Documents summary: Semantic search over documents - description: Embeds the query text and returns the most similar documents using cosine distance + description: Embeds the query text and returns the most similar documents using cosine distance. If projectId is omitted, searches across all projects the caller has access to. operationId: searchDocuments requestBody: required: true @@ -191,11 +185,11 @@ paths: schema: type: object required: - - projectId - query properties: projectId: type: string + description: Project public ID (optional). Omit to search across all accessible projects. example: 'proj_V1StGXR8Z5jdHi6B' query: type: string diff --git a/packages/server/src/rest/v1/actors.ts b/packages/server/src/rest/v1/actors.ts new file mode 100644 index 00000000..f0a68fdc --- /dev/null +++ b/packages/server/src/rest/v1/actors.ts @@ -0,0 +1,350 @@ +import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { db } from 'src/db'; +import { createActor, deleteActor, getActor, listActors } from 'src/lib/actors'; + +const actorsRouter = new Router(); + +/** + * @openapi + * /actors: + * get: + * tags: + * - Actors + * summary: List actors + * description: Returns all actors the caller has access to. If projectId is provided, returns only actors in that project. API keys are scoped to a single project automatically. JWT users without projectId receive actors across all their accessible projects. + * operationId: listActors + * parameters: + * - name: projectId + * in: query + * required: false + * description: Project public ID (optional) + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * - name: externalId + * in: query + * required: false + * description: External ID to filter by (e.g. WhatsApp phone number) + * schema: + * type: string + * example: '+15551234567' + * responses: + * '200': + * description: List of actors + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ActorRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +actorsRouter.get('/actors', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const projectPublicId = ctx.query.projectId as string | undefined; + const externalId = ctx.query.externalId as string | undefined; + + const projectIds = await ctx.authUser.resolveProjectIds({ + projectPublicId, + action: 'actors:ListActors', + }); + + if (projectIds === null) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await listActors({ projectIds, externalId }); +}); + +/** + * @openapi + * /actors/{id}: + * get: + * tags: + * - Actors + * summary: Get an actor by ID + * description: Returns an actor by its public ID + * operationId: getActor + * parameters: + * - name: id + * in: path + * required: true + * description: Actor ID + * schema: + * type: string + * example: 'act_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: Actor found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ActorRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Actor not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +actorsRouter.get('/actors/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const actor = await getActor({ id: ctx.params.id }); + + if (!actor) { + ctx.status = 404; + ctx.body = { error: 'Actor not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + actor.projectId!, + 'actors:GetActor' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = actor; +}); + +/** + * @openapi + * /actors: + * post: + * tags: + * - Actors + * summary: Create an actor + * description: Creates a new actor. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + * operationId: createActor + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * properties: + * projectId: + * type: string + * description: Project public ID. Required for JWT auth; omit when using an API key. + * example: 'proj_V1StGXR8Z5jdHi6B' + * name: + * type: string + * example: 'Alice' + * type: + * type: string + * description: Optional actor type (e.g. 'customer', 'agent') + * example: 'customer' + * externalId: + * type: string + * description: Optional external identifier (e.g. WhatsApp phone number). Must be unique within a project. + * example: '+15551234567' + * responses: + * '201': + * description: Actor created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ActorRecord' + * '400': + * description: Invalid request body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +actorsRouter.post('/actors', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { + projectId?: string; + name: string; + type?: string; + externalId?: string; + }; + + if (!body.name) { + ctx.status = 400; + ctx.body = { error: 'name is required' }; + return; + } + + let resolvedProjectPublicId = body.projectId; + if (!resolvedProjectPublicId) { + if (ctx.authUser.apiKeyProjectId) { + resolvedProjectPublicId = ctx.authUser.apiKeyProjectId; + } else { + ctx.status = 400; + ctx.body = { error: 'projectId is required' }; + return; + } + } + + const allowed = await ctx.authUser.isAllowed( + resolvedProjectPublicId, + 'actors:CreateActor' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ + where: { publicId: resolvedProjectPublicId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + try { + const actor = await createActor({ + projectId: project.id, + name: body.name, + type: body.type, + externalId: body.externalId, + }); + + ctx.status = 201; + ctx.body = actor; + } catch (error) { + if ( + (error as { name?: string }).name === 'SequelizeUniqueConstraintError' + ) { + ctx.status = 409; + ctx.body = { + error: 'An actor with this externalId already exists in the project', + }; + return; + } + throw error; + } +}); + +/** + * @openapi + * /actors/{id}: + * delete: + * tags: + * - Actors + * summary: Delete an actor + * description: Deletes an actor by its public ID + * operationId: deleteActor + * parameters: + * - name: id + * in: path + * required: true + * description: Actor ID + * schema: + * type: string + * example: 'act_V1StGXR8Z5jdHi6B' + * responses: + * '204': + * description: Actor deleted + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Actor not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +actorsRouter.delete('/actors/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const actor = await getActor({ id: ctx.params.id }); + + if (!actor) { + ctx.status = 404; + ctx.body = { error: 'Actor not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + actor.projectId!, + 'actors:DeleteActor' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + await deleteActor({ id: ctx.params.id }); + ctx.status = 204; +}); + +export { actorsRouter }; diff --git a/packages/server/src/rest/v1/documents.ts b/packages/server/src/rest/v1/documents.ts index 30c1a4b3..1d94b0b4 100644 --- a/packages/server/src/rest/v1/documents.ts +++ b/packages/server/src/rest/v1/documents.ts @@ -17,14 +17,14 @@ const documentsRouter = new Router(); * get: * tags: * - Documents - * summary: List documents in a project - * description: Returns all documents belonging to a project + * summary: List documents + * description: Returns all documents the caller has access to. If projectId is provided, returns only documents in that project. API keys are scoped to a single project automatically. JWT users without projectId receive documents across all their accessible projects. * operationId: listDocuments * parameters: * - name: projectId * in: query - * required: true - * description: Project public ID + * required: false + * description: Project public ID (optional) * schema: * type: string * example: 'proj_V1StGXR8Z5jdHi6B' @@ -37,12 +37,6 @@ const documentsRouter = new Router(); * type: array * items: * $ref: '#/components/schemas/DocumentRecord' - * '400': - * description: Missing projectId - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ErrorResponse' * '401': * description: Unauthorized * content: @@ -63,32 +57,20 @@ documentsRouter.get('/documents', async (ctx: Context) => { return; } - const projectId = ctx.query.projectId as string; + const projectPublicId = ctx.query.projectId as string | undefined; - if (!projectId) { - ctx.status = 400; - ctx.body = { error: 'projectId query parameter is required' }; - return; - } + const projectIds = await ctx.authUser!.resolveProjectIds({ + projectPublicId, + action: 'documents:ListDocuments', + }); - const allowed = await ctx.authUser.isAllowed( - projectId, - 'documents:ListDocuments' - ); - if (!allowed) { + if (projectIds === null) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; return; } - const project = await db.Project.findOne({ where: { publicId: projectId } }); - if (!project) { - ctx.status = 400; - ctx.body = { error: 'Invalid project ID' }; - return; - } - - ctx.body = await listDocuments({ projectId: project.id }); + ctx.body = await listDocuments({ projectIds }); }); /** @@ -169,7 +151,7 @@ documentsRouter.get('/documents/:id', async (ctx: Context) => { * tags: * - Documents * summary: Create a document - * description: Creates a new text document with an embedding vector + * description: Creates a new text document with an embedding vector. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. * operationId: createDocument * requestBody: * required: true @@ -178,11 +160,11 @@ documentsRouter.get('/documents/:id', async (ctx: Context) => { * schema: * type: object * required: - * - projectId * - content * properties: * projectId: * type: string + * description: Project public ID. Required for JWT auth; omit when using an API key. * example: 'proj_V1StGXR8Z5jdHi6B' * content: * type: string @@ -224,19 +206,31 @@ documentsRouter.post('/documents', async (ctx: Context) => { } const body = ctx.request.body as { - projectId: string; + projectId?: string; content: string; filename?: string; }; - if (!body.projectId || !body.content) { + if (!body.content) { ctx.status = 400; - ctx.body = { error: 'projectId and content are required' }; + ctx.body = { error: 'content is required' }; return; } + // Resolve projectId: use explicit value, infer from API key, or error for JWT + let resolvedProjectPublicId = body.projectId; + if (!resolvedProjectPublicId) { + if (ctx.authUser.apiKeyProjectId) { + resolvedProjectPublicId = ctx.authUser.apiKeyProjectId; + } else { + ctx.status = 400; + ctx.body = { error: 'projectId is required' }; + return; + } + } + const allowed = await ctx.authUser.isAllowed( - body.projectId, + resolvedProjectPublicId, 'documents:CreateDocument' ); if (!allowed) { @@ -246,7 +240,7 @@ documentsRouter.post('/documents', async (ctx: Context) => { } const project = await db.Project.findOne({ - where: { publicId: body.projectId }, + where: { publicId: resolvedProjectPublicId }, }); if (!project) { ctx.status = 400; @@ -346,7 +340,7 @@ documentsRouter.delete('/documents/:id', async (ctx: Context) => { * tags: * - Documents * summary: Semantic search over documents - * description: Embeds the query text and returns the most similar documents using cosine distance + * description: Embeds the query text and returns the most similar documents using cosine distance. If projectId is omitted, searches across all projects the caller has access to. * operationId: searchDocuments * requestBody: * required: true @@ -355,11 +349,11 @@ documentsRouter.delete('/documents/:id', async (ctx: Context) => { * schema: * type: object * required: - * - projectId * - query * properties: * projectId: * type: string + * description: Project public ID (optional). Omit to search across all accessible projects. * example: 'proj_V1StGXR8Z5jdHi6B' * query: * type: string @@ -403,38 +397,30 @@ documentsRouter.post('/documents/search', async (ctx: Context) => { } const body = ctx.request.body as { - projectId: string; + projectId?: string; query: string; limit?: number; }; - if (!body.projectId || !body.query) { + if (!body.query) { ctx.status = 400; - ctx.body = { error: 'projectId and query are required' }; + ctx.body = { error: 'query is required' }; return; } - const allowed = await ctx.authUser.isAllowed( - body.projectId, - 'documents:SearchDocuments' - ); - if (!allowed) { + const projectIds = await ctx.authUser!.resolveProjectIds({ + projectPublicId: body.projectId, + action: 'documents:SearchDocuments', + }); + + if (projectIds === null) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; return; } - const project = await db.Project.findOne({ - where: { publicId: body.projectId }, - }); - if (!project) { - ctx.status = 400; - ctx.body = { error: 'Invalid project ID' }; - return; - } - const results = await searchDocuments({ - projectId: project.id, + projectIds, query: body.query, limit: body.limit, }); diff --git a/packages/server/src/rest/v1/files.ts b/packages/server/src/rest/v1/files.ts index 2fda5800..d9dec782 100644 --- a/packages/server/src/rest/v1/files.ts +++ b/packages/server/src/rest/v1/files.ts @@ -1,5 +1,5 @@ -import multer from '@koa/multer'; -import { Router } from '@ttoss/http-server'; +import type { MulterFile } from '@ttoss/http-server'; +import { multer, Router } from '@ttoss/http-server'; import type { Context } from 'src/Context'; import { db } from 'src/db'; import { @@ -343,8 +343,8 @@ filesRouter.post( return; } - const body = ctx.request.body as { projectId?: string; metadata?: string }; - const file = (ctx as any).file as Express.Multer.File | undefined; + const body = ctx.request.body as { projectId: string; metadata?: string }; + const file = ctx.file as MulterFile | undefined; if (!file) { ctx.status = 400; diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index fa670dae..a26f541e 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -1,5 +1,6 @@ import { Router } from '@ttoss/http-server'; +import { actorsRouter } from './actors'; import { apiKeysRouter } from './apiKeys'; import { documentsRouter } from './documents'; import { filesRouter } from './files'; @@ -8,6 +9,7 @@ import { usersRouter } from './users'; const v1Router = new Router(); +v1Router.use(actorsRouter.routes()); v1Router.use(apiKeysRouter.routes()); v1Router.use(documentsRouter.routes()); v1Router.use(filesRouter.routes()); diff --git a/packages/server/tests/unit/tests/actors.test.ts b/packages/server/tests/unit/tests/actors.test.ts new file mode 100644 index 00000000..fb2db280 --- /dev/null +++ b/packages/server/tests/unit/tests/actors.test.ts @@ -0,0 +1,245 @@ +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +describe('Actors', () => { + let adminToken: string; + let userToken: string; + let userId: string; + let projectId: string; + let policyId: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const createUserRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'actorsuser', password: 'actorspass' }); + + userId = createUserRes.body.id; + userToken = await loginAs('actorsuser', 'actorspass'); + + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Actors Test Project' }); + projectId = projectRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: [ + 'actors:ListActors', + 'actors:GetActor', + 'actors:CreateActor', + 'actors:DeleteActor', + ], + }); + policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + }); + + describe('POST /api/v1/actors', () => { + test('authenticated user with permission can create an actor', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'Alice' }); + + expect(response.status).toBe(201); + expect(response.body.id).toMatch(/^act_/); + expect(response.body.name).toBe('Alice'); + expect(response.body.projectId).toBe(projectId); + expect(response.body.type).toBeUndefined(); + expect(response.body.externalId).toBeUndefined(); + }); + + test('can create an actor with type and externalId', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ + projectId, + name: 'Bob', + type: 'customer', + externalId: '+15550001111', + }); + + expect(response.status).toBe(201); + expect(response.body.name).toBe('Bob'); + expect(response.body.type).toBe('customer'); + expect(response.body.externalId).toBe('+15550001111'); + }); + + test('duplicate externalId within same project returns 409', async () => { + await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'Charlie', externalId: '+15559999999' }); + + const response = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'Charlie2', externalId: '+15559999999' }); + + expect(response.status).toBe(409); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .post('/api/v1/actors') + .send({ projectId, name: 'Anon' }); + + expect(response.status).toBe(401); + }); + + test('missing name returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId }); + + expect(response.status).toBe(400); + }); + + test('missing projectId returns 400 for JWT users', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ name: 'NoProject' }); + + expect(response.status).toBe(400); + }); + }); + + describe('GET /api/v1/actors', () => { + beforeAll(async () => { + await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'ListActor1' }); + await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'ListActor2' }); + }); + + test('authenticated user with permission can list actors', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/actors?projectId=${projectId}` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThanOrEqual(2); + }); + + test('listing without projectId returns all accessible actors', async () => { + const response = + await authenticatedTestClient(userToken).get('/api/v1/actors'); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('can filter by externalId', async () => { + await authenticatedTestClient(userToken).post('/api/v1/actors').send({ + projectId, + name: 'ExternalFiltered', + externalId: '+15558887777', + }); + + const response = await authenticatedTestClient(userToken).get( + `/api/v1/actors?externalId=%2B15558887777` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect( + response.body.some((a: { externalId: string }) => { + return a.externalId === '+15558887777'; + }) + ).toBe(true); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get( + `/api/v1/actors?projectId=${projectId}` + ); + + expect(response.status).toBe(401); + }); + }); + + describe('GET /api/v1/actors/:id', () => { + let actorId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'FetchActor', type: 'agent' }); + actorId = res.body.id; + }); + + test('user with permission can get an actor by ID', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/actors/${actorId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(actorId); + expect(response.body.name).toBe('FetchActor'); + expect(response.body.type).toBe('agent'); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get(`/api/v1/actors/${actorId}`); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent actor', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/actors/act_nonexistent' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('DELETE /api/v1/actors/:id', () => { + test('user with permission can delete an actor', async () => { + const createRes = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'ToDelete' }); + const actorId = createRes.body.id; + + const deleteRes = await authenticatedTestClient(userToken).delete( + `/api/v1/actors/${actorId}` + ); + + expect(deleteRes.status).toBe(204); + + const getRes = await authenticatedTestClient(userToken).get( + `/api/v1/actors/${actorId}` + ); + expect(getRes.status).toBe(404); + }); + + test('unauthenticated request returns 401', async () => { + const createRes = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'ToDeleteAnon' }); + + const response = await testClient.delete( + `/api/v1/actors/${createRes.body.id}` + ); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent actor', async () => { + const response = await authenticatedTestClient(adminToken).delete( + '/api/v1/actors/act_nonexistent' + ); + + expect(response.status).toBe(404); + }); + }); +}); diff --git a/packages/server/tests/unit/tests/documents.test.ts b/packages/server/tests/unit/tests/documents.test.ts index efb73fde..c4dede30 100644 --- a/packages/server/tests/unit/tests/documents.test.ts +++ b/packages/server/tests/unit/tests/documents.test.ts @@ -131,11 +131,12 @@ describe('Documents', () => { expect(response.status).toBe(401); }); - test('missing projectId returns 400', async () => { + test('listing without projectId returns all accessible documents', async () => { const response = await authenticatedTestClient(userToken).get('/api/v1/documents'); - expect(response.status).toBe(400); + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); }); }); @@ -213,12 +214,13 @@ describe('Documents', () => { expect(response.status).toBe(401); }); - test('missing projectId returns 400', async () => { + test('search without projectId returns results across accessible projects', async () => { const response = await authenticatedTestClient(userToken) .post('/api/v1/documents/search') .send({ query: 'no project' }); - expect(response.status).toBe(400); + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); }); test('missing query returns 400', async () => { @@ -277,4 +279,52 @@ describe('Documents', () => { expect(response.status).toBe(404); }); }); + + describe('API key access', () => { + let apiKey: string; + + beforeAll(async () => { + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['documents:ListDocuments', 'documents:SearchDocuments'], + }); + const apiKeyPolicyId = policyRes.body.id; + + const apiKeyRes = await authenticatedTestClient(userToken) + .post('/api/v1/api-keys') + .send({ + projectId, + policyId: apiKeyPolicyId, + name: 'Docs Test API Key', + }); + apiKey = apiKeyRes.body.key; + + await authenticatedTestClient(userToken).post('/api/v1/documents').send({ + projectId, + content: 'API key test document.', + filename: 'apikey-doc.txt', + }); + }); + + test('API key can list documents without providing projectId', async () => { + const response = await testClient + .get('/api/v1/documents') + .set('Authorization', `Bearer ${apiKey}`); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThan(0); + }); + + test('API key can search documents without providing projectId', async () => { + const response = await testClient + .post('/api/v1/documents/search') + .set('Authorization', `Bearer ${apiKey}`) + .send({ query: 'API key test' }); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + }); }); diff --git a/packages/website/docs/modules/actors.md b/packages/website/docs/modules/actors.md new file mode 100644 index 00000000..e2f0b9c7 --- /dev/null +++ b/packages/website/docs/modules/actors.md @@ -0,0 +1,44 @@ +# Actors Module + +The Actors module represents entities (people, bots, or other participants) that interact within a project. A common use case is storing WhatsApp contacts, where `externalId` holds the phone number. + +## Overview + +An Actor belongs to a project and has a display name, an optional type, and an optional `externalId`. The `externalId` is unique within a project and is designed for correlating actors with external systems — for example, mapping a WhatsApp phone number to a known contact. + +Actors are identified by `publicId` prefixed with `act_`. The internal database primary key is never returned. + +## Data Model + +| Field | Type | Description | +| ------------ | ------ | ------------------------------------------------------------------------------------ | +| `id` | string | Public identifier prefixed with `act_` | +| `projectId` | string | Public ID of the owning project | +| `name` | string | Display name of the actor | +| `type` | string | Optional actor type (e.g. `customer`, `agent`) | +| `externalId` | string | Optional external identifier (e.g. WhatsApp phone number). Unique within the project | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +## Key Concepts + +### externalId + +`externalId` is a free-form string that lets you correlate an Actor with a record in an external system. It is enforced unique per project at the database level — two actors in the same project cannot share the same `externalId`. Across different projects, the same `externalId` value is allowed. + +A `null` / absent `externalId` is never considered a duplicate — PostgreSQL's NULL semantics are preserved. + +### Filtering + +`GET /actors` accepts an optional `externalId` query parameter. This lets you look up an actor by their external identifier (e.g. resolve a WhatsApp number to an Actor record) without knowing their `act_` ID. + +## Permissions + +Actor operations are governed by per-project policies. Grant the following permissions: + +| Action | Permission string | +| --------------- | -------------------- | +| List actors | `actors:ListActors` | +| Get actor by ID | `actors:GetActor` | +| Create actor | `actors:CreateActor` | +| Delete actor | `actors:DeleteActor` | diff --git a/packages/website/docs/modules/documents.md b/packages/website/docs/modules/documents.md index 6181aae2..c0bd7c98 100644 --- a/packages/website/docs/modules/documents.md +++ b/packages/website/docs/modules/documents.md @@ -68,7 +68,7 @@ Document operations are governed by per-project policies. Grant the following pe ### List documents -Returns all documents in a project (no content field): +Returns documents visible to the caller (no content field). `projectId` is optional — omit it to retrieve documents across all accessible projects: ```http GET /api/v1/documents?projectId=proj_xxx @@ -111,7 +111,7 @@ Authorization: Bearer ### Semantic search -Embeds the query and returns the closest documents by cosine distance: +Embeds the query and returns the closest documents by cosine distance. `projectId` is optional — omit it to search across all accessible projects: ```http POST /api/v1/documents/search @@ -127,14 +127,26 @@ Content-Type: application/json The `limit` field defaults to `10` when omitted. +## Project ID Resolution + +For endpoints that accept `projectId`, the field is optional. When omitted, the server resolves accessible projects based on the caller's identity: + +| Caller type | Behavior when `projectId` is omitted | +| ----------- | ---------------------------------------------------------------------------- | +| API key | Infers the project from the key's own scope (single project) | +| JWT admin | No project filter — returns results across all projects | +| JWT user | Enumerates all projects the user is a member of with the required permission | + +If `projectId` is supplied but the caller lacks permission for that project, the request returns `403 Forbidden`. + ## MCP Tools The following MCP tools are available for AI assistants: -| Tool name | Description | -| ------------------ | --------------------------------------------------- | -| `list-documents` | List all documents in a project | -| `get-document` | Retrieve a document including its text content | -| `create-document` | Create a new text document with automatic embedding | -| `delete-document` | Delete a document and its underlying file | -| `search-documents` | Semantic search over project documents | +| Tool name | Description | +| ------------------ | -------------------------------------------------------------------------- | +| `list-documents` | List documents; omit `projectId` to retrieve all accessible documents | +| `get-document` | Retrieve a document including its text content | +| `create-document` | Create a new text document with automatic embedding | +| `delete-document` | Delete a document and its underlying file | +| `search-documents` | Semantic search; omit `projectId` to search across all accessible projects | diff --git a/turbo.json b/turbo.json index 93e2128b..553af844 100644 --- a/turbo.json +++ b/turbo.json @@ -5,6 +5,10 @@ "build": { "dependsOn": ["^build"], "outputs": ["dist/**", "build/**"] + }, + "test": { + "dependsOn": ["^build"], + "outputs": [] } } } From 3859912d3cc7c72925ef593b412035671222199e Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Sat, 11 Apr 2026 20:49:41 -0300 Subject: [PATCH 07/27] chore: add dev container --- .github/instructions/website.instructions.md | 5 + .prettierrc.js => .prettierrc.cjs | 2 +- docker-compose.dev.yml | 64 ++ package.json | 1 + .../postgresdb/src/models/Conversation.ts | 61 ++ .../src/models/ConversationMessage.ts | 59 ++ packages/postgresdb/src/models/index.ts | 2 + packages/postgresdb/src/utils/publicId.ts | 1 + packages/server/package.json | 1 + packages/server/src/lib/conversations.ts | 293 +++++++ .../server/src/mcp/tools/conversations.ts | 174 ++++ packages/server/src/mcp/tools/index.ts | 4 + packages/server/src/mcp/tools/projects.ts | 32 + .../src/rest/openapi/v1/conversations.yaml | 508 +++++++++++ packages/server/src/rest/v1/conversations.ts | 807 ++++++++++++++++++ packages/server/src/rest/v1/index.ts | 2 + .../tests/unit/tests/conversations.test.ts | 506 +++++++++++ .../website/docs/modules/conversations.md | 58 ++ packages/website/docs/modules/iam.md | 27 +- pnpm-lock.yaml | 19 +- 20 files changed, 2607 insertions(+), 19 deletions(-) rename .prettierrc.js => .prettierrc.cjs (60%) create mode 100644 docker-compose.dev.yml create mode 100644 packages/postgresdb/src/models/Conversation.ts create mode 100644 packages/postgresdb/src/models/ConversationMessage.ts create mode 100644 packages/server/src/lib/conversations.ts create mode 100644 packages/server/src/mcp/tools/conversations.ts create mode 100644 packages/server/src/mcp/tools/projects.ts create mode 100644 packages/server/src/rest/openapi/v1/conversations.yaml create mode 100644 packages/server/src/rest/v1/conversations.ts create mode 100644 packages/server/tests/unit/tests/conversations.test.ts create mode 100644 packages/website/docs/modules/conversations.md diff --git a/.github/instructions/website.instructions.md b/.github/instructions/website.instructions.md index f1697b4d..5b1d9b3c 100644 --- a/.github/instructions/website.instructions.md +++ b/.github/instructions/website.instructions.md @@ -22,6 +22,11 @@ Agents working on the website package must adhere to the following guidelines to - **Tone and Voice**: Write in a technical, confident, and concise manner as specified in the BRANDBOOK.md. Avoid unnecessary jargon while maintaining technical accuracy. - **Navigation**: Ensure all new pages and sections are properly integrated into the navigation system and sidebars. +## Module Documentation + +- **Permission Actions**: `iam.md` is the single source of truth for all permission action strings. Do not duplicate permission tables in module docs. Instead, reference the relevant action inline when describing each endpoint — e.g., "Requires the `files:GetFile` permission action (see [IAM](../modules/iam.md#permission-actions))." +- **Keep in sync**: When a new permission action is added to the server, update the Permission Actions table in `iam.md` and add inline references in the corresponding module doc. + ## Technical Requirements - **API Documentation**: When generating or updating API documentation, ensure it is comprehensive and follows the standards outlined in the website package. diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 60% rename from .prettierrc.js rename to .prettierrc.cjs index 4cb0c80e..bf46d885 100644 --- a/.prettierrc.js +++ b/.prettierrc.cjs @@ -1,3 +1,3 @@ const { prettierConfig } = require('@ttoss/config'); -module.exports = prettierConfig(); \ No newline at end of file +module.exports = prettierConfig(); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..0bbc8781 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,64 @@ +services: + database: + image: pgvector/pgvector:0.8.1-pg18-trixie + container_name: soat-database-dev + environment: + POSTGRES_DB: ${DATABASE_NAME:-soat_dev} + POSTGRES_USER: ${DATABASE_USER:-soat_user} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-soat_password} + volumes: + - postgres_data:/var/lib/postgresql + healthcheck: + test: + [ + 'CMD-SHELL', + 'pg_isready -U ${DATABASE_USER:-soat_user} -d ${DATABASE_NAME:-soat_dev}', + ] + interval: 10s + timeout: 5s + retries: 5 + + ollama: + image: ollama/ollama:latest + container_name: soat-ollama-dev + volumes: + - ollama_cache:/root/.ollama + entrypoint: + - /bin/sh + - -c + - 'ollama serve & sleep 5 && ollama pull qwen3-embedding:0.6b && wait' + healthcheck: + test: ['CMD-SHELL', 'ollama list | grep qwen3-embedding'] + interval: 10s + timeout: 30s + retries: 30 + start_period: 30s + + server: + build: . + container_name: soat-server-dev + depends_on: + database: + condition: service_healthy + ollama: + condition: service_healthy + ports: + - '5047:5047' + environment: + DATABASE_HOST: database + DATABASE_PORT: '5432' + DATABASE_NAME: ${DATABASE_NAME:-soat_dev} + DATABASE_USER: ${DATABASE_USER:-soat_user} + DATABASE_PASSWORD: ${DATABASE_PASSWORD:-soat_password} + FILES_STORAGE_DIR: /data/files + OLLAMA_BASE_URL: http://ollama:11434 + EMBEDDING_PROVIDER: ollama + EMBEDDING_MODEL: qwen3-embedding:0.6b + EMBEDDING_DIMENSIONS: '1024' + volumes: + - files_data:/data/files + +volumes: + postgres_data: + ollama_cache: + files_data: diff --git a/package.json b/package.json index e187a2cc..1c518959 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "scripts": { "prepare": "husky", "smoke-test": "docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from smoke-test", + "dev": "docker compose -f docker-compose.dev.yml up --build", "syncpack:fix": "syncpack fix", "syncpack:lint": "syncpack lint" }, diff --git a/packages/postgresdb/src/models/Conversation.ts b/packages/postgresdb/src/models/Conversation.ts new file mode 100644 index 00000000..8b2f3315 --- /dev/null +++ b/packages/postgresdb/src/models/Conversation.ts @@ -0,0 +1,61 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + HasMany, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; +import { ConversationMessage } from './ConversationMessage'; +import { Project } from './Project'; + +@Table({ + tableName: 'conversations', + hooks: { + beforeValidate: (instance: Conversation) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.conversation); + } + }, + }, +}) +export class Conversation extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @ForeignKey(() => { + return Project; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare projectId: number; + + @BelongsTo(() => { + return Project; + }) + declare project: Project; + + @Column({ + type: DataType.STRING, + allowNull: false, + defaultValue: 'open', + }) + declare status: string; + + @HasMany(() => { + return ConversationMessage; + }) + declare messages: ConversationMessage[]; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/ConversationMessage.ts b/packages/postgresdb/src/models/ConversationMessage.ts new file mode 100644 index 00000000..db451909 --- /dev/null +++ b/packages/postgresdb/src/models/ConversationMessage.ts @@ -0,0 +1,59 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { Actor } from './Actor'; +import { Conversation } from './Conversation'; +import { Document } from './Document'; + +@Table({ + tableName: 'conversation_messages', + indexes: [ + { + unique: true, + fields: ['conversation_id', 'document_id'], + }, + ], +}) +export class ConversationMessage extends Model { + @ForeignKey(() => { + return Conversation; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare conversationId: number; + + @BelongsTo(() => { + return Conversation; + }) + declare conversation: Conversation; + + @ForeignKey(() => { + return Document; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare documentId: number; + + @BelongsTo(() => { + return Document; + }) + declare document: Document; + + @ForeignKey(() => { + return Actor; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare actorId: number; + + @BelongsTo(() => { + return Actor; + }) + declare actor: Actor; + + @Column({ type: DataType.INTEGER, allowNull: false }) + declare position: number; +} diff --git a/packages/postgresdb/src/models/index.ts b/packages/postgresdb/src/models/index.ts index 8b1ce7e2..37ae33f3 100644 --- a/packages/postgresdb/src/models/index.ts +++ b/packages/postgresdb/src/models/index.ts @@ -1,5 +1,7 @@ export { Actor } from './Actor'; export { ApiKey } from './ApiKey'; +export { Conversation } from './Conversation'; +export { ConversationMessage } from './ConversationMessage'; export { Document } from './Document'; export { File } from './File'; export { Project } from './Project'; diff --git a/packages/postgresdb/src/utils/publicId.ts b/packages/postgresdb/src/utils/publicId.ts index 6090576b..cd24a359 100644 --- a/packages/postgresdb/src/utils/publicId.ts +++ b/packages/postgresdb/src/utils/publicId.ts @@ -12,6 +12,7 @@ export const PUBLIC_ID_PREFIXES = { apiKey: 'key_', document: 'doc_', actor: 'act_', + conversation: 'conv_', } as const; /** diff --git a/packages/server/package.json b/packages/server/package.json index 272ffce6..5dcd53ac 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -9,6 +9,7 @@ "pretest": "pnpm run --filter @soat/postgresdb build", "test": "jest --projects tests/unit" }, + "type": "module", "dependencies": { "@soat/postgresdb": "workspace:*", "@ttoss/http-server": "^0.5.9", diff --git a/packages/server/src/lib/conversations.ts b/packages/server/src/lib/conversations.ts new file mode 100644 index 00000000..d6abcd17 --- /dev/null +++ b/packages/server/src/lib/conversations.ts @@ -0,0 +1,293 @@ +import { db } from '../db'; +import { createDocument } from './documents'; + +const mapConversation = ( + conversation: InstanceType<(typeof db)['Conversation']> & { + project?: InstanceType<(typeof db)['Project']>; + } +) => { + return { + id: conversation.publicId, + projectId: conversation.project?.publicId, + status: conversation.status, + createdAt: conversation.createdAt, + updatedAt: conversation.updatedAt, + }; +}; + +const mapMessage = ( + message: InstanceType<(typeof db)['ConversationMessage']> & { + document?: InstanceType<(typeof db)['Document']>; + actor?: InstanceType<(typeof db)['Actor']>; + } +) => { + return { + documentId: message.document?.publicId, + actorId: message.actor?.publicId, + position: message.position, + }; +}; + +export const listConversations = async (args: { + projectIds?: number[]; + actorId?: string; +}) => { + if (args.projectIds !== undefined && args.projectIds.length === 0) { + return []; + } + + const where: Record = {}; + + if (args.projectIds !== undefined) { + where.projectId = args.projectIds; + } + + if (args.actorId !== undefined) { + const actor = await db.Actor.findOne({ where: { publicId: args.actorId } }); + if (!actor) { + return []; + } + const messages = await db.ConversationMessage.findAll({ + where: { actorId: actor.id }, + attributes: ['conversationId'], + group: ['conversationId'], + }); + const conversationIds = messages.map((m) => m.conversationId); + where.id = conversationIds; + } + + const conversations = await db.Conversation.findAll({ + where: Object.keys(where).length > 0 ? where : undefined, + include: [{ model: db.Project, as: 'project' }], + }); + + return conversations.map(mapConversation); +}; + +export const getConversation = async (args: { id: string }) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + if (!conversation) { + return null; + } + + return mapConversation(conversation); +}; + +export const createConversation = async (args: { + projectId: number; + status?: string; +}) => { + const conversation = await db.Conversation.create({ + projectId: args.projectId, + status: args.status ?? 'open', + }); + + const conversationWithAssociations = await db.Conversation.findOne({ + where: { id: conversation.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + return mapConversation(conversationWithAssociations!); +}; + +export const updateConversationStatus = async (args: { + id: string; + status: string; +}) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.id }, + }); + + if (!conversation) { + return null; + } + + await conversation.update({ status: args.status }); + + const updatedConversation = await db.Conversation.findOne({ + where: { publicId: args.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + return mapConversation(updatedConversation!); +}; + +export const deleteConversation = async (args: { id: string }) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.id }, + }); + + if (!conversation) { + return null; + } + + await conversation.destroy(); + + return { id: args.id }; +}; + +export const listConversationMessages = async (args: { + conversationId: string; +}) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.conversationId }, + }); + + if (!conversation) { + return null; + } + + const messages = await db.ConversationMessage.findAll({ + where: { conversationId: conversation.id }, + include: [ + { model: db.Document, as: 'document' }, + { model: db.Actor, as: 'actor' }, + ], + order: [['position', 'ASC']], + }); + + return messages.map(mapMessage); +}; + +export const addConversationMessage = async (args: { + conversationId: string; + message: string; + actorId: string; + position?: number; +}) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.conversationId }, + }); + + if (!conversation) { + return null; + } + + const actor = await db.Actor.findOne({ where: { publicId: args.actorId } }); + + if (!actor) { + return null; + } + + const createdDoc = await createDocument({ + projectId: conversation.projectId, + content: args.message, + }); + + const document = await db.Document.findOne({ + where: { publicId: createdDoc.id }, + }); + + if (!document) { + return null; + } + + let position = args.position; + + if (position === undefined) { + const maxMessage = await db.ConversationMessage.findOne({ + where: { conversationId: conversation.id }, + order: [['position', 'DESC']], + }); + position = maxMessage ? maxMessage.position + 1 : 0; + } + + const message = await db.ConversationMessage.create({ + conversationId: conversation.id, + documentId: document.id, + actorId: actor.id, + position, + }); + + const messageWithAssociations = await db.ConversationMessage.findOne({ + where: { id: message.id }, + include: [ + { model: db.Document, as: 'document' }, + { model: db.Actor, as: 'actor' }, + ], + }); + + return mapMessage(messageWithAssociations!); +}; + +export const removeConversationMessage = async (args: { + conversationId: string; + documentId: string; +}) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.conversationId }, + }); + + if (!conversation) { + return null; + } + + const document = await db.Document.findOne({ + where: { publicId: args.documentId }, + }); + + if (!document) { + return null; + } + + const message = await db.ConversationMessage.findOne({ + where: { + conversationId: conversation.id, + documentId: document.id, + }, + }); + + if (!message) { + return null; + } + + await message.destroy(); + + return { conversationId: args.conversationId, documentId: args.documentId }; +}; + +export const listConversationActors = async (args: { + conversationId: string; +}) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.conversationId }, + }); + + if (!conversation) { + return null; + } + + const messages = await db.ConversationMessage.findAll({ + where: { conversationId: conversation.id }, + include: [ + { + model: db.Actor, + as: 'actor', + include: [{ model: db.Project, as: 'project' }], + }, + ], + }); + + const seen = new Set(); + const actors = []; + for (const msg of messages) { + if (!seen.has(msg.actorId)) { + seen.add(msg.actorId); + actors.push(msg.actor); + } + } + + return actors.map((actor) => ({ + id: actor.publicId, + projectId: actor.project?.publicId, + name: actor.name, + type: actor.type ?? undefined, + externalId: actor.externalId ?? undefined, + createdAt: actor.createdAt, + updatedAt: actor.updatedAt, + })); +}; diff --git a/packages/server/src/mcp/tools/conversations.ts b/packages/server/src/mcp/tools/conversations.ts new file mode 100644 index 00000000..cfc43053 --- /dev/null +++ b/packages/server/src/mcp/tools/conversations.ts @@ -0,0 +1,174 @@ +import type { McpServer } from '@ttoss/http-server-mcp'; +import { apiCall, z } from '@ttoss/http-server-mcp'; + +const registerTools = (server: McpServer) => { + server.registerTool( + 'list-conversations', + { + description: + 'List conversations. If projectId is omitted, returns all conversations accessible to the caller. Optionally filter by actorId.', + inputSchema: { + projectId: z.string().optional().describe('Project ID (optional)'), + actorId: z + .string() + .optional() + .describe('Actor ID to filter conversations by'), + }, + }, + async ({ projectId, actorId }) => { + const params = new URLSearchParams(); + if (projectId) params.set('projectId', projectId); + if (actorId) params.set('actorId', actorId); + const qs = params.toString() ? `?${params.toString()}` : ''; + const data = await apiCall('GET', `/conversations${qs}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'get-conversation', + { + description: 'Get a conversation by ID', + inputSchema: { + id: z.string().describe('Conversation ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/conversations/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'create-conversation', + { + description: + 'Create a new conversation. API keys infer the project automatically; JWT callers must supply projectId.', + inputSchema: { + projectId: z + .string() + .optional() + .describe( + 'Project ID (required for JWT auth, optional for API keys)' + ), + status: z + .string() + .optional() + .describe( + "Initial status, either 'open' or 'closed'. Defaults to 'open'." + ), + }, + }, + async ({ projectId, status }) => { + const data = await apiCall('POST', '/conversations', { + body: { projectId, status }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'update-conversation', + { + description: "Update a conversation's status", + inputSchema: { + id: z.string().describe('Conversation ID'), + status: z.string().describe("New status, either 'open' or 'closed'"), + }, + }, + async ({ id, status }) => { + const data = await apiCall('PATCH', `/conversations/${id}`, { + body: { status }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'delete-conversation', + { + description: 'Delete a conversation by ID', + inputSchema: { + id: z.string().describe('Conversation ID'), + }, + }, + async ({ id }) => { + await apiCall('DELETE', `/conversations/${id}`); + return { content: [{ type: 'text', text: 'Conversation deleted' }] }; + } + ); + + server.registerTool( + 'list-conversation-messages', + { + description: + 'List all messages (documents) in a conversation, ordered by position', + inputSchema: { + id: z.string().describe('Conversation ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/conversations/${id}/messages`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'add-conversation-message', + { + description: + 'Add a message to a conversation. The message content is saved as a document internally. If position is omitted, the message is appended at the end.', + inputSchema: { + id: z.string().describe('Conversation ID'), + message: z.string().describe('Message text content to send'), + actorId: z.string().describe('Actor ID who is sending this message'), + position: z + .number() + .optional() + .describe( + 'Zero-based position in the conversation. Defaults to MAX+1 (append).' + ), + }, + }, + async ({ id, message, actorId, position }) => { + const data = await apiCall('POST', `/conversations/${id}/messages`, { + body: { message, actorId, position }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'list-conversation-actors', + { + description: + 'List all distinct actors who have sent at least one message in a conversation', + inputSchema: { + id: z.string().describe('Conversation ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/conversations/${id}/actors`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'remove-conversation-message', + { + description: 'Remove a document from a conversation', + inputSchema: { + id: z.string().describe('Conversation ID'), + documentId: z.string().describe('Document ID to remove'), + }, + }, + async ({ id, documentId }) => { + await apiCall('DELETE', `/conversations/${id}/messages/${documentId}`); + return { + content: [{ type: 'text', text: 'Message removed from conversation' }], + }; + } + ); +}; + +export { registerTools }; diff --git a/packages/server/src/mcp/tools/index.ts b/packages/server/src/mcp/tools/index.ts index e460d7a7..4a236d8e 100644 --- a/packages/server/src/mcp/tools/index.ts +++ b/packages/server/src/mcp/tools/index.ts @@ -1,13 +1,17 @@ import type { McpServer } from '@ttoss/http-server-mcp'; import { registerTools as registerActorTools } from './actors'; +import { registerTools as registerConversationTools } from './conversations'; import { registerTools as registerDocumentTools } from './documents'; import { registerTools as registerFileTools } from './files'; +import { registerTools as registerProjectTools } from './projects'; const registerTools = (server: McpServer) => { registerActorTools(server); + registerConversationTools(server); registerDocumentTools(server); registerFileTools(server); + registerProjectTools(server); }; export { registerTools }; diff --git a/packages/server/src/mcp/tools/projects.ts b/packages/server/src/mcp/tools/projects.ts new file mode 100644 index 00000000..43ec177e --- /dev/null +++ b/packages/server/src/mcp/tools/projects.ts @@ -0,0 +1,32 @@ +import type { McpServer } from '@ttoss/http-server-mcp'; +import { apiCall, z } from '@ttoss/http-server-mcp'; + +const registerTools = (server: McpServer) => { + server.registerTool( + 'list-projects', + { + description: 'List all projects accessible to the current user', + inputSchema: {}, + }, + async () => { + const data = await apiCall('GET', '/projects'); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'get-project', + { + description: 'Get a project by ID', + inputSchema: { + id: z.string().describe('Project ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/projects/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); +}; + +export { registerTools }; diff --git a/packages/server/src/rest/openapi/v1/conversations.yaml b/packages/server/src/rest/openapi/v1/conversations.yaml new file mode 100644 index 00000000..5a9023f8 --- /dev/null +++ b/packages/server/src/rest/openapi/v1/conversations.yaml @@ -0,0 +1,508 @@ +openapi: 3.0.3 +info: + title: SOAT Conversations API + version: 1.0.0 + description: API for managing conversations and their messages + contact: + name: SOAT Team + url: https://github.com/ttoss/soat +servers: + - url: http://0.0.0.0:5047/api/v1 + description: Development server +paths: + /conversations: + get: + tags: + - Conversations + summary: List conversations + description: Returns all conversations the caller has access to. If projectId is provided, returns only conversations in that project. API keys are scoped to a single project automatically. + operationId: listConversations + parameters: + - name: projectId + in: query + required: false + description: Project public ID (optional) + schema: + type: string + example: 'proj_V1StGXR8Z5jdHi6B' + - name: actorId + in: query + required: false + description: Filter by actor public ID + schema: + type: string + example: 'act_V1StGXR8Z5jdHi6B' + responses: + '200': + description: List of conversations + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ConversationRecord' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Conversations + summary: Create a conversation + description: Creates a new conversation. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + operationId: createConversation + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + projectId: + type: string + description: Project public ID. Required for JWT auth; omit when using an API key. + example: 'proj_V1StGXR8Z5jdHi6B' + status: + type: string + enum: [open, closed] + default: open + description: Initial conversation status + responses: + '201': + description: Conversation created + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationRecord' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /conversations/{id}: + get: + tags: + - Conversations + summary: Get a conversation by ID + description: Returns a conversation by its public ID + operationId: getConversation + parameters: + - name: id + in: path + required: true + description: Conversation ID + schema: + type: string + example: 'conv_V1StGXR8Z5jdHi6B' + responses: + '200': + description: Conversation found + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationRecord' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Conversation not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + patch: + tags: + - Conversations + summary: Update a conversation + description: Updates the status of a conversation + operationId: updateConversation + parameters: + - name: id + in: path + required: true + description: Conversation ID + schema: + type: string + example: 'conv_V1StGXR8Z5jdHi6B' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - status + properties: + status: + type: string + enum: [open, closed] + description: New conversation status + responses: + '200': + description: Conversation updated + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationRecord' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Conversation not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - Conversations + summary: Delete a conversation + description: Deletes a conversation by its public ID + operationId: deleteConversation + parameters: + - name: id + in: path + required: true + description: Conversation ID + schema: + type: string + example: 'conv_V1StGXR8Z5jdHi6B' + responses: + '204': + description: Conversation deleted + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Conversation not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /conversations/{id}/messages: + get: + tags: + - Conversations + summary: List conversation messages + description: Returns all messages (documents) attached to a conversation, ordered by position + operationId: listConversationMessages + parameters: + - name: id + in: path + required: true + description: Conversation ID + schema: + type: string + example: 'conv_V1StGXR8Z5jdHi6B' + responses: + '200': + description: List of messages + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ConversationMessageRecord' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Conversation not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Conversations + summary: Add a message to a conversation + description: Creates a document from the message text and attaches it to the conversation at the given position. If position is omitted, it is appended at the end. + operationId: addConversationMessage + parameters: + - name: id + in: path + required: true + description: Conversation ID + schema: + type: string + example: 'conv_V1StGXR8Z5jdHi6B' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - message + - actorId + properties: + message: + type: string + description: Message text content to add to the conversation + example: 'Hello, how can I help you?' + actorId: + type: string + description: Actor public ID who is sending this message + example: 'act_V1StGXR8Z5jdHi6B' + position: + type: integer + description: Zero-based position. Defaults to MAX+1 (append). + example: 0 + responses: + '201': + description: Message added + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationMessageRecord' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Conversation or actor not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /conversations/{id}/actors: + get: + tags: + - Conversations + summary: List actors in a conversation + description: Returns all distinct actors who have sent at least one message in the conversation + operationId: listConversationActors + parameters: + - name: id + in: path + required: true + description: Conversation ID + schema: + type: string + example: 'conv_V1StGXR8Z5jdHi6B' + responses: + '200': + description: List of actors + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ConversationActorRecord' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Conversation not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /conversations/{id}/messages/{documentId}: + delete: + tags: + - Conversations + summary: Remove a message from a conversation + description: Removes a document from a conversation + operationId: removeConversationMessage + parameters: + - name: id + in: path + required: true + description: Conversation ID + schema: + type: string + example: 'conv_V1StGXR8Z5jdHi6B' + - name: documentId + in: path + required: true + description: Document public ID + schema: + type: string + example: 'doc_V1StGXR8Z5jdHi6B' + responses: + '204': + description: Message removed + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Conversation or message not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' +components: + schemas: + ConversationRecord: + type: object + properties: + id: + type: string + description: Conversation public ID + example: 'conv_V1StGXR8Z5jdHi6B' + projectId: + type: string + description: Project public ID + example: 'proj_V1StGXR8Z5jdHi6B' + status: + type: string + enum: [open, closed] + description: Conversation status + example: 'open' + createdAt: + type: string + format: date-time + description: Creation timestamp + updatedAt: + type: string + format: date-time + description: Last update timestamp + ConversationMessageRecord: + type: object + properties: + documentId: + type: string + description: Document public ID + example: 'doc_V1StGXR8Z5jdHi6B' + actorId: + type: string + description: Actor public ID who sent this message + example: 'act_V1StGXR8Z5jdHi6B' + position: + type: integer + description: Zero-based position in the conversation + example: 0 + ConversationActorRecord: + type: object + properties: + id: + type: string + description: Actor public ID + example: 'act_V1StGXR8Z5jdHi6B' + projectId: + type: string + description: Project public ID + example: 'proj_V1StGXR8Z5jdHi6B' + name: + type: string + description: Actor name + example: 'Alice' + type: + type: string + description: Actor type + example: 'human' + externalId: + type: string + description: External identifier + example: 'ext_123' + createdAt: + type: string + format: date-time + description: Creation timestamp + updatedAt: + type: string + format: date-time + description: Last update timestamp + ErrorResponse: + type: object + properties: + error: + type: string + description: Error message + example: 'Conversation not found' diff --git a/packages/server/src/rest/v1/conversations.ts b/packages/server/src/rest/v1/conversations.ts new file mode 100644 index 00000000..9ac1031e --- /dev/null +++ b/packages/server/src/rest/v1/conversations.ts @@ -0,0 +1,807 @@ +import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { db } from 'src/db'; +import { + addConversationMessage, + createConversation, + deleteConversation, + getConversation, + listConversationActors, + listConversationMessages, + listConversations, + removeConversationMessage, + updateConversationStatus, +} from 'src/lib/conversations'; + +const conversationsRouter = new Router(); + +/** + * @openapi + * /conversations: + * get: + * tags: + * - Conversations + * summary: List conversations + * description: Returns all conversations the caller has access to. If projectId is provided, returns only conversations in that project. API keys are scoped to a single project automatically. + * operationId: listConversations + * parameters: + * - name: projectId + * in: query + * required: false + * description: Project public ID (optional) + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * - name: actorId + * in: query + * required: false + * description: Filter by actor public ID + * schema: + * type: string + * example: 'act_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: List of conversations + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ConversationRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.get('/conversations', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const projectPublicId = ctx.query.projectId as string | undefined; + const actorId = ctx.query.actorId as string | undefined; + + const projectIds = await ctx.authUser.resolveProjectIds({ + projectPublicId, + action: 'conversations:ListConversations', + }); + + if (projectIds === null) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await listConversations({ projectIds, actorId }); +}); + +/** + * @openapi + * /conversations/{id}: + * get: + * tags: + * - Conversations + * summary: Get a conversation by ID + * description: Returns a conversation by its public ID + * operationId: getConversation + * parameters: + * - name: id + * in: path + * required: true + * description: Conversation ID + * schema: + * type: string + * example: 'conv_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: Conversation found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ConversationRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Conversation not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.get('/conversations/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + conversation.projectId!, + 'conversations:GetConversation' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = conversation; +}); + +/** + * @openapi + * /conversations: + * post: + * tags: + * - Conversations + * summary: Create a conversation + * description: Creates a new conversation. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + * operationId: createConversation + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * projectId: + * type: string + * description: Project public ID. Required for JWT auth; omit when using an API key. + * example: 'proj_V1StGXR8Z5jdHi6B' + * status: + * type: string + * enum: [open, closed] + * default: open + * description: Initial conversation status + * responses: + * '201': + * description: Conversation created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ConversationRecord' + * '400': + * description: Invalid request body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.post('/conversations', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { + projectId?: string; + status?: string; + }; + + let resolvedProjectPublicId = body.projectId; + if (!resolvedProjectPublicId) { + if (ctx.authUser.apiKeyProjectId) { + resolvedProjectPublicId = ctx.authUser.apiKeyProjectId; + } else { + ctx.status = 400; + ctx.body = { error: 'projectId is required' }; + return; + } + } + + const allowed = await ctx.authUser.isAllowed( + resolvedProjectPublicId, + 'conversations:CreateConversation' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ + where: { publicId: resolvedProjectPublicId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + const conversation = await createConversation({ + projectId: project.id, + status: body.status, + }); + + ctx.status = 201; + ctx.body = conversation; +}); + +/** + * @openapi + * /conversations/{id}: + * patch: + * tags: + * - Conversations + * summary: Update a conversation + * description: Updates the status of a conversation + * operationId: updateConversation + * parameters: + * - name: id + * in: path + * required: true + * description: Conversation ID + * schema: + * type: string + * example: 'conv_V1StGXR8Z5jdHi6B' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - status + * properties: + * status: + * type: string + * enum: [open, closed] + * description: New conversation status + * responses: + * '200': + * description: Conversation updated + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ConversationRecord' + * '400': + * description: Invalid request body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Conversation not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.patch('/conversations/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { status: string }; + + if (!body.status) { + ctx.status = 400; + ctx.body = { error: 'status is required' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + conversation.projectId!, + 'conversations:UpdateConversation' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const updated = await updateConversationStatus({ + id: ctx.params.id, + status: body.status, + }); + + ctx.body = updated; +}); + +/** + * @openapi + * /conversations/{id}: + * delete: + * tags: + * - Conversations + * summary: Delete a conversation + * description: Deletes a conversation by its public ID + * operationId: deleteConversation + * parameters: + * - name: id + * in: path + * required: true + * description: Conversation ID + * schema: + * type: string + * example: 'conv_V1StGXR8Z5jdHi6B' + * responses: + * '204': + * description: Conversation deleted + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Conversation not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.delete('/conversations/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + conversation.projectId!, + 'conversations:DeleteConversation' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + await deleteConversation({ id: ctx.params.id }); + ctx.status = 204; +}); + +/** + * @openapi + * /conversations/{id}/messages: + * get: + * tags: + * - Conversations + * summary: List conversation messages + * description: Returns all messages (documents) attached to a conversation, ordered by position + * operationId: listConversationMessages + * parameters: + * - name: id + * in: path + * required: true + * description: Conversation ID + * schema: + * type: string + * example: 'conv_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: List of messages + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ConversationMessageRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Conversation not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.get('/conversations/:id/messages', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + conversation.projectId!, + 'conversations:GetConversation' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const messages = await listConversationMessages({ + conversationId: ctx.params.id, + }); + + ctx.body = messages; +}); + +/** + * @openapi + * /conversations/{id}/messages: + * post: + * tags: + * - Conversations + * summary: Add a message to a conversation + * description: Creates a document from the message text and attaches it to the conversation at the given position. If position is omitted, it is appended at the end. + * operationId: addConversationMessage + * parameters: + * - name: id + * in: path + * required: true + * description: Conversation ID + * schema: + * type: string + * example: 'conv_V1StGXR8Z5jdHi6B' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - message + * - actorId + * properties: + * message: + * type: string + * description: Message text content to add to the conversation + * example: 'Hello, how can I help you?' + * actorId: + * type: string + * description: Actor public ID who is sending this message + * example: 'act_V1StGXR8Z5jdHi6B' + * position: + * type: integer + * description: Zero-based position. Defaults to MAX+1 (append). + * example: 0 + * responses: + * '201': + * description: Message added + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ConversationMessageRecord' + * '400': + * description: Invalid request body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Conversation or actor not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.post( + '/conversations/:id/messages', + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { + message: string; + actorId: string; + position?: number; + }; + + if (!body.message) { + ctx.status = 400; + ctx.body = { error: 'message is required' }; + return; + } + + if (!body.actorId) { + ctx.status = 400; + ctx.body = { error: 'actorId is required' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + conversation.projectId!, + 'conversations:UpdateConversation' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const message = await addConversationMessage({ + conversationId: ctx.params.id, + message: body.message, + actorId: body.actorId, + position: body.position, + }); + + if (!message) { + ctx.status = 404; + ctx.body = { error: 'Conversation or actor not found' }; + return; + } + + ctx.status = 201; + ctx.body = message; + } +); + +/** + * @openapi + * /conversations/{id}/messages/{documentId}: + * delete: + * tags: + * - Conversations + * summary: Remove a message from a conversation + * description: Removes a document from a conversation + * operationId: removeConversationMessage + * parameters: + * - name: id + * in: path + * required: true + * description: Conversation ID + * schema: + * type: string + * example: 'conv_V1StGXR8Z5jdHi6B' + * - name: documentId + * in: path + * required: true + * description: Document public ID + * schema: + * type: string + * example: 'doc_V1StGXR8Z5jdHi6B' + * responses: + * '204': + * description: Message removed + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Conversation or message not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.delete( + '/conversations/:id/messages/:documentId', + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + conversation.projectId!, + 'conversations:UpdateConversation' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const result = await removeConversationMessage({ + conversationId: ctx.params.id, + documentId: ctx.params.documentId, + }); + + if (!result) { + ctx.status = 404; + ctx.body = { error: 'Message not found' }; + return; + } + + ctx.status = 204; + } +); + +/** + * @openapi + * /conversations/{id}/actors: + * get: + * tags: + * - Conversations + * summary: List actors in a conversation + * description: Returns all distinct actors who have sent at least one message in the conversation + * operationId: listConversationActors + * parameters: + * - name: id + * in: path + * required: true + * description: Conversation ID + * schema: + * type: string + * example: 'conv_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: List of actors + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ConversationActorRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Conversation not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.get('/conversations/:id/actors', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + conversation.projectId!, + 'conversations:GetConversation' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const actors = await listConversationActors({ + conversationId: ctx.params.id, + }); + ctx.body = actors; +}); + +export { conversationsRouter }; diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index a26f541e..c476532e 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -2,6 +2,7 @@ import { Router } from '@ttoss/http-server'; import { actorsRouter } from './actors'; import { apiKeysRouter } from './apiKeys'; +import { conversationsRouter } from './conversations'; import { documentsRouter } from './documents'; import { filesRouter } from './files'; import { projectsRouter } from './projects'; @@ -11,6 +12,7 @@ const v1Router = new Router(); v1Router.use(actorsRouter.routes()); v1Router.use(apiKeysRouter.routes()); +v1Router.use(conversationsRouter.routes()); v1Router.use(documentsRouter.routes()); v1Router.use(filesRouter.routes()); v1Router.use(projectsRouter.routes()); diff --git a/packages/server/tests/unit/tests/conversations.test.ts b/packages/server/tests/unit/tests/conversations.test.ts new file mode 100644 index 00000000..01d64144 --- /dev/null +++ b/packages/server/tests/unit/tests/conversations.test.ts @@ -0,0 +1,506 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +jest.mock('src/lib/embedding', () => { + return { + getEmbedding: jest.fn().mockResolvedValue(Array(1024).fill(0.1)), + }; +}); + +describe('Conversations', () => { + let adminToken: string; + let userToken: string; + let userId: string; + let projectId: string; + let policyId: string; + let actorId: string; + + beforeAll(async () => { + const storageDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'soat-convo-test-') + ); + process.env.FILES_STORAGE_DIR = storageDir; + process.env.EMBEDDING_PROVIDER = 'ollama'; + process.env.EMBEDDING_MODEL = 'qwen3-embedding:0.6b'; + process.env.EMBEDDING_DIMENSIONS = '1024'; + + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + const createUserRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'convouser', password: 'convopass' }); + + userId = createUserRes.body.id; + userToken = await loginAs('convouser', 'convopass'); + + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Conversations Test Project' }); + projectId = projectRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: [ + 'actors:CreateActor', + 'actors:ListActors', + 'actors:GetActor', + 'documents:CreateDocument', + 'documents:GetDocument', + 'conversations:ListConversations', + 'conversations:GetConversation', + 'conversations:CreateConversation', + 'conversations:UpdateConversation', + 'conversations:DeleteConversation', + ], + }); + policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + + const actorRes = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'ConvoActor' }); + actorId = actorRes.body.id; + }); + + describe('POST /api/v1/conversations', () => { + test('authenticated user with permission can create a conversation', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + + expect(response.status).toBe(201); + expect(response.body.id).toMatch(/^conv_/); + expect(response.body.projectId).toBe(projectId); + expect(response.body.status).toBe('open'); + }); + + test('can create a conversation with closed status', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId, status: 'closed' }); + + expect(response.status).toBe(201); + expect(response.body.status).toBe('closed'); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .post('/api/v1/conversations') + .send({ projectId }); + + expect(response.status).toBe(401); + }); + + test('missing projectId returns 400 for JWT users', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({}); + + expect(response.status).toBe(400); + }); + }); + + describe('GET /api/v1/conversations', () => { + beforeAll(async () => { + await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + }); + + test('authenticated user with permission can list conversations', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/conversations?projectId=${projectId}` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThanOrEqual(2); + }); + + test('listing without projectId returns all accessible conversations', async () => { + const response = await authenticatedTestClient(userToken).get( + '/api/v1/conversations' + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('can filter by actorId', async () => { + const secondActorRes = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'FilterActor' }); + const secondActorId = secondActorRes.body.id; + + const convRes = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + const filteredConvId = convRes.body.id; + + await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${filteredConvId}/messages`) + .send({ message: 'Filter test', actorId: secondActorId }); + + const response = await authenticatedTestClient(userToken).get( + `/api/v1/conversations?actorId=${secondActorId}` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect( + response.body.some((c: { id: string }) => c.id === filteredConvId) + ).toBe(true); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get( + `/api/v1/conversations?projectId=${projectId}` + ); + + expect(response.status).toBe(401); + }); + }); + + describe('GET /api/v1/conversations/:id', () => { + let conversationId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + conversationId = res.body.id; + }); + + test('user with permission can get a conversation by ID', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/conversations/${conversationId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(conversationId); + expect(response.body.status).toBe('open'); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get( + `/api/v1/conversations/${conversationId}` + ); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent conversation', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/conversations/conv_nonexistent' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('PATCH /api/v1/conversations/:id', () => { + let conversationId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + conversationId = res.body.id; + }); + + test('user with permission can update conversation status', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/conversations/${conversationId}`) + .send({ status: 'closed' }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(conversationId); + expect(response.body.status).toBe('closed'); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .patch(`/api/v1/conversations/${conversationId}`) + .send({ status: 'open' }); + + expect(response.status).toBe(401); + }); + + test('missing status returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/conversations/${conversationId}`) + .send({}); + + expect(response.status).toBe(400); + }); + + test('returns 404 for non-existent conversation', async () => { + const response = await authenticatedTestClient(adminToken) + .patch('/api/v1/conversations/conv_nonexistent') + .send({ status: 'open' }); + + expect(response.status).toBe(404); + }); + }); + + describe('GET /api/v1/conversations/:id/messages', () => { + let conversationId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + conversationId = res.body.id; + }); + + test('user with permission can list messages (empty initially)', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/conversations/${conversationId}/messages` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBe(0); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get( + `/api/v1/conversations/${conversationId}/messages` + ); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent conversation', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/conversations/conv_nonexistent/messages' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('POST /api/v1/conversations/:id/messages', () => { + let conversationId: string; + let addedDocumentId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + conversationId = res.body.id; + }); + + test('user with permission can add a message to a conversation', async () => { + const response = await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({ message: 'Hello world', actorId }); + + expect(response.status).toBe(201); + expect(response.body.documentId).toMatch(/^doc_/); + expect(response.body.actorId).toBe(actorId); + expect(typeof response.body.position).toBe('number'); + addedDocumentId = response.body.documentId; + }); + + test('message appears in list after adding', async () => { + const listRes = await authenticatedTestClient(userToken).get( + `/api/v1/conversations/${conversationId}/messages` + ); + + expect(listRes.status).toBe(200); + expect( + listRes.body.some( + (m: { documentId: string }) => m.documentId === addedDocumentId + ) + ).toBe(true); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({ message: 'test' }); + + expect(response.status).toBe(401); + }); + + test('missing message returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({}); + + expect(response.status).toBe(400); + }); + + test('returns 404 for non-existent conversation', async () => { + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/conversations/conv_nonexistent/messages') + .send({ message: 'Hello world', actorId }); + + expect(response.status).toBe(404); + }); + }); + + describe('DELETE /api/v1/conversations/:id/messages/:documentId', () => { + let conversationId: string; + let secondDocumentId: string; + + beforeAll(async () => { + const convRes = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + conversationId = convRes.body.id; + + const msgRes = await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({ message: 'Remove me', actorId }); + secondDocumentId = msgRes.body.documentId; + }); + + test('user with permission can remove a message', async () => { + const deleteRes = await authenticatedTestClient(userToken).delete( + `/api/v1/conversations/${conversationId}/messages/${secondDocumentId}` + ); + + expect(deleteRes.status).toBe(204); + + const listRes = await authenticatedTestClient(userToken).get( + `/api/v1/conversations/${conversationId}/messages` + ); + expect( + listRes.body.some( + (m: { documentId: string }) => m.documentId === secondDocumentId + ) + ).toBe(false); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.delete( + `/api/v1/conversations/${conversationId}/messages/doc_nonexistent` + ); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent message', async () => { + const response = await authenticatedTestClient(adminToken).delete( + `/api/v1/conversations/${conversationId}/messages/doc_nonexistent` + ); + + expect(response.status).toBe(404); + }); + }); + + describe('DELETE /api/v1/conversations/:id', () => { + test('user with permission can delete a conversation', async () => { + const createRes = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + const conversationId = createRes.body.id; + + const deleteRes = await authenticatedTestClient(userToken).delete( + `/api/v1/conversations/${conversationId}` + ); + + expect(deleteRes.status).toBe(204); + + const getRes = await authenticatedTestClient(userToken).get( + `/api/v1/conversations/${conversationId}` + ); + expect(getRes.status).toBe(404); + }); + + test('unauthenticated request returns 401', async () => { + const createRes = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + + const response = await testClient.delete( + `/api/v1/conversations/${createRes.body.id}` + ); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent conversation', async () => { + const response = await authenticatedTestClient(adminToken).delete( + '/api/v1/conversations/conv_nonexistent' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('GET /api/v1/conversations/:id/actors', () => { + let conversationId: string; + let secondActorIdForActorsTest: string; + + beforeAll(async () => { + const convRes = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + conversationId = convRes.body.id; + + const actorRes = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'SecondActorForActors' }); + secondActorIdForActorsTest = actorRes.body.id; + + await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({ message: 'Message from actor 1', actorId }); + await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({ + message: 'Message from actor 2', + actorId: secondActorIdForActorsTest, + }); + await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({ message: 'Another from actor 1', actorId }); + }); + + test('returns distinct actors who sent messages', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/conversations/${conversationId}/actors` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBe(2); + const ids = response.body.map((a: { id: string }) => a.id); + expect(ids).toContain(actorId); + expect(ids).toContain(secondActorIdForActorsTest); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get( + `/api/v1/conversations/${conversationId}/actors` + ); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent conversation', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/conversations/conv_nonexistent/actors' + ); + + expect(response.status).toBe(404); + }); + }); +}); diff --git a/packages/website/docs/modules/conversations.md b/packages/website/docs/modules/conversations.md new file mode 100644 index 00000000..93550b71 --- /dev/null +++ b/packages/website/docs/modules/conversations.md @@ -0,0 +1,58 @@ +# Conversations Module + +The Conversations module represents a series of messages exchanged with an Actor within a project. Conversations group documents (messages) in an ordered sequence, tracking the dialogue between a system and an actor such as a WhatsApp contact. + +## Overview + +A Conversation belongs to a project and is associated with an Actor. It has a status (`open` or `closed`) and contains an ordered list of messages, where each message is a reference to a Document along with its position in the conversation. + +Conversations are identified by `publicId` prefixed with `conv_`. The internal database primary key is never returned. + +## Data Model + +### Conversation + +| Field | Type | Description | +| ----------- | ------ | --------------------------------------------------- | +| `id` | string | Public identifier prefixed with `conv_` | +| `projectId` | string | Public ID of the owning project | +| `actorId` | string | Public ID of the Actor this conversation belongs to | +| `status` | string | Conversation status: `open` or `closed` | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +### Conversation Message + +| Field | Type | Description | +| ------------ | ------- | ------------------------------------------------------ | +| `documentId` | string | Public ID of the Document attached as a message | +| `position` | integer | Zero-based position of the message in the conversation | + +## Key Concepts + +### Messages + +Messages are ordered references to Documents within a conversation. When adding a message, you can specify an explicit `position`. If omitted, the document is appended at the end (position = MAX + 1). Each document can appear at most once per conversation — adding the same document twice returns `409 Conflict`. + +### Status + +A conversation transitions between `open` and `closed`. Use `PATCH /conversations/:id` to update the status. New conversations default to `open`. + +### Actor Association + +Every conversation is linked to a single Actor. You can filter conversations by `actorId` using the `GET /conversations?actorId=` query parameter to retrieve all conversations for a specific contact. + +## Permissions + +Conversation operations are governed by per-project policies. Grant the following permissions: + +| Action | Permission string | +| -------------------------------- | ---------------------------------- | +| List conversations | `conversations:ListConversations` | +| Get conversation by ID | `conversations:GetConversation` | +| List conversation messages | `conversations:GetConversation` | +| Create conversation | `conversations:CreateConversation` | +| Update conversation status | `conversations:UpdateConversation` | +| Add message to conversation | `conversations:UpdateConversation` | +| Remove message from conversation | `conversations:UpdateConversation` | +| Delete conversation | `conversations:DeleteConversation` | diff --git a/packages/website/docs/modules/iam.md b/packages/website/docs/modules/iam.md index f57fcf15..761a908c 100644 --- a/packages/website/docs/modules/iam.md +++ b/packages/website/docs/modules/iam.md @@ -44,7 +44,7 @@ Create a key via `POST /api/v1/api-keys`. The full key value is returned **only Authorization: Bearer sk_ ``` -An API key's permissions are always limited to what its creator is allowed to do. A key can never grant more access than the user who created it. +An API key's effective permissions are the **intersection** of the key's own policy and the key owner's project membership policy. Both must allow an action for it to be permitted. A key can never grant more access than the owner's membership policy allows, and can be scoped to even less via the key's own policy. ## Roles @@ -85,12 +85,25 @@ A policy defines what actions a member (or API key) is allowed or denied within Permission strings follow the format `resource:Action`. The table below lists all available actions and the REST endpoint each one protects. -| Permission Action | REST Endpoint | What it controls | -| --------------------- | -------------------------- | ------------------------- | -| `files:GetFile` | `GET /api/v1/files/:id` | Retrieve a specific file | -| `files:CreateFile` | `POST /api/v1/files` | Upload a new file | -| `files:DeleteFile` | `DELETE /api/v1/files/:id` | Delete a file | -| `projects:GetProject` | `GET /api/v1/projects/:id` | View a project's policies | +| Permission Action | REST Endpoint | What it controls | +| ---------------------------------- | ---------------------------------- | --------------------------------------------------- | +| `files:GetFile` | `GET /api/v1/files/:id` | Retrieve file metadata | +| `files:CreateFile` | `POST /api/v1/files` | Create a new file record | +| `files:UploadFile` | `POST /api/v1/files/upload` | Upload file content | +| `files:DownloadFile` | `GET /api/v1/files/:id/download` | Download file content | +| `files:UpdateFileMetadata` | `PATCH /api/v1/files/:id/metadata` | Update file metadata | +| `files:DeleteFile` | `DELETE /api/v1/files/:id` | Delete a file | +| `projects:GetProject` | `GET /api/v1/projects/:id` | View a project's details | +| `documents:GetDocument` | `GET /api/v1/documents/:id` | Retrieve a document | +| `documents:CreateDocument` | `POST /api/v1/documents` | Create a document | +| `documents:DeleteDocument` | `DELETE /api/v1/documents/:id` | Delete a document | +| `actors:GetActor` | `GET /api/v1/actors/:id` | Retrieve an actor | +| `actors:CreateActor` | `POST /api/v1/actors` | Create an actor | +| `actors:DeleteActor` | `DELETE /api/v1/actors/:id` | Delete an actor | +| `conversations:GetConversation` | `GET /api/v1/conversations/:id` | Retrieve a conversation and its messages and actors | +| `conversations:CreateConversation` | `POST /api/v1/conversations` | Create a conversation | +| `conversations:UpdateConversation` | `PATCH /api/v1/conversations/:id` | Update a conversation, add or remove messages | +| `conversations:DeleteConversation` | `DELETE /api/v1/conversations/:id` | Delete a conversation | Use wildcards to grant broader access: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 538efe02..04669869 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,9 +115,6 @@ importers: packages/server: dependencies: - '@koa/multer': - specifier: ^4.0.0 - version: 4.0.0(koa@3.1.1)(multer@2.0.2) '@soat/postgresdb': specifier: workspace:* version: link:../postgresdb @@ -15551,9 +15548,9 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-config-turbo: 2.9.4(eslint@9.39.2(jiti@2.6.1))(turbo@2.9.4) - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-formatjs: 6.4.4(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jest: 29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2) eslint-plugin-jest-dom: 5.5.0(@testing-library/dom@10.4.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -18216,7 +18213,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) @@ -18227,18 +18224,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -18254,7 +18251,7 @@ snapshots: transitivePeerDependencies: - ts-jest - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -18265,7 +18262,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From 121591ac6773307f8df7d05373034d19b50c8334 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Sat, 11 Apr 2026 22:17:13 -0300 Subject: [PATCH 08/27] fix: bugs --- .github/instructions/website.instructions.md | 4 +- docs/prd-actors-conversations.md | 245 ----------------- packages/server/src/lib/actors.ts | 33 +++ packages/server/src/lib/conversations.ts | 55 +++- packages/server/src/lib/documents.ts | 31 ++- packages/server/src/lib/files.ts | 29 +- packages/server/src/mcp/tools/actors.ts | 45 +++- .../server/src/mcp/tools/conversations.ts | 59 +++- packages/server/src/mcp/tools/documents.ts | 36 ++- packages/server/src/mcp/tools/files.ts | 73 +++-- packages/server/src/rest/v1/actors.ts | 100 ++++++- packages/server/src/rest/v1/documents.ts | 6 + packages/server/src/rest/v1/files.ts | 253 +++++++++++++++++- .../server/tests/unit/tests/actors.test.ts | 48 ++++ .../tests/unit/tests/conversations.test.ts | 62 +++++ .../server/tests/unit/tests/documents.test.ts | 25 ++ .../server/tests/unit/tests/files.test.ts | 162 +++++++++++ packages/server/tests/unit/tests/mcp.test.ts | 4 + packages/website/docs/modules/actors.md | 13 +- .../website/docs/modules/conversations.md | 34 ++- packages/website/docs/modules/documents.md | 21 +- packages/website/docs/modules/files.md | 58 +++- packages/website/docs/modules/iam.md | 27 +- 23 files changed, 1053 insertions(+), 370 deletions(-) delete mode 100644 docs/prd-actors-conversations.md diff --git a/.github/instructions/website.instructions.md b/.github/instructions/website.instructions.md index 5b1d9b3c..1dc10d71 100644 --- a/.github/instructions/website.instructions.md +++ b/.github/instructions/website.instructions.md @@ -24,8 +24,8 @@ Agents working on the website package must adhere to the following guidelines to ## Module Documentation -- **Permission Actions**: `iam.md` is the single source of truth for all permission action strings. Do not duplicate permission tables in module docs. Instead, reference the relevant action inline when describing each endpoint — e.g., "Requires the `files:GetFile` permission action (see [IAM](../modules/iam.md#permission-actions))." -- **Keep in sync**: When a new permission action is added to the server, update the Permission Actions table in `iam.md` and add inline references in the corresponding module doc. +- **Permission Actions**: Each module doc owns its permission table in a `## Permissions` section. The table must have four columns: **Action**, **Permission**, **REST Endpoint**, and **MCP Tool**. `iam.md` explains the `resource:Action` format and wildcards but does **not** list individual actions — it links to each module's `## Permissions` section instead. +- **Keep in sync**: When a new permission action is added to the server, add a row to the relevant module doc's permissions table and, if it introduces a new module, add a link in `iam.md`. ## Technical Requirements diff --git a/docs/prd-actors-conversations.md b/docs/prd-actors-conversations.md deleted file mode 100644 index 23349a56..00000000 --- a/docs/prd-actors-conversations.md +++ /dev/null @@ -1,245 +0,0 @@ -# PRD: Actors & Conversations - -**Status**: In Development -**Branch**: `feat-files` -**Date**: April 10, 2026 - ---- - -## Overview - -This document describes the design and implementation of two new modules — **Actors** and **Conversations** — added to the Soat platform. These modules allow projects to model participants and their ordered interactions with documents stored in the system. - ---- - -## Problem Statement - -Projects that use Soat to manage documents often represent real-world conversations — support tickets, AI chat sessions, interview transcripts, onboarding flows — but the platform had no way to: - -1. Represent the **participant** (human, AI agent, service) who drives a conversation. -2. Group and **sequence documents** that belong to a single conversational exchange. -3. Track whether a conversation is **active or concluded**. - -Without these primitives, consumers must maintain that structure in their own databases, losing the ability to query, filter, and act on conversations server-side. - ---- - -## Goals - -- Introduce a first-class `Actor` entity scoped to a project. -- Introduce a first-class `Conversation` entity that ties together an actor, a project, and an ordered sequence of documents. -- Expose full CRUD over both entities via the REST API, MCP tools, and documentation. -- Fit naturally into the existing auth model (project policies, `isAllowed` permission checks). - ---- - -## Non-Goals - -- Real-time messaging or WebSocket streaming. -- Conversation history versioning or audit trails. -- Multi-actor conversations (a conversation has exactly one actor in this version). -- Document content modification through conversation endpoints. - ---- - -## Data Model - -### Actor - -Represents a participant — a human user, an AI agent, or any named entity that can own a conversation. - -| Field | Type | Notes | -| ----------- | ---------------- | --------------------------------------------- | -| `id` | `string` | Public ID, prefixed `act_` | -| `projectId` | `string` | Public ID of the owning project | -| `name` | `string` | Display name (required) | -| `type` | `string \| null` | Optional label, e.g. `human`, `ai`, `service` | -| `createdAt` | `string` | ISO 8601 | -| `updatedAt` | `string` | ISO 8601 | - -**Database table**: `actors` -**Cascade**: deleted when parent project is deleted (`onDelete: CASCADE`). - ---- - -### Conversation - -Represents a single conversational session, owned by one actor, containing an ordered list of documents. - -| Field | Type | Notes | -| ----------- | -------------------- | ------------------------------------------------- | -| `id` | `string` | Public ID, prefixed `conv_` | -| `projectId` | `string` | Public ID of the owning project | -| `actorId` | `string` | Public ID of the actor who owns this conversation | -| `status` | `"open" \| "closed"` | `open` by default | -| `createdAt` | `string` | ISO 8601 | -| `updatedAt` | `string` | ISO 8601 | - -**Database table**: `conversations` -**Cascade**: deleted when parent project or actor is deleted. - ---- - -### ConversationDocument (junction) - -Associates a document with a conversation at a specific position in the sequence. - -| Field | Type | Notes | -| ---------------- | -------- | ---------------------------------------------------- | -| `conversationId` | `number` | Internal FK to `conversations` | -| `documentId` | `number` | Internal FK to `documents` | -| `position` | `number` | Zero-based integer; auto-incremented if not provided | - -**Database table**: `conversation_documents` -**Unique constraint**: `(conversation_id, document_id)` — a document can appear only once per conversation. -**Ordering**: queries return entries sorted by `position ASC`. - ---- - -## Permission Model - -Both modules integrate with the existing project policy system. The following action strings are checked via `ctx.authUser.isAllowed(projectId, action)`: - -| Action | Endpoint | -| ---------------------------------- | -------------------------------------------- | -| `actors:ListActors` | `GET /actors` | -| `actors:GetActor` | `GET /actors/:id` | -| `actors:CreateActor` | `POST /actors` | -| `actors:DeleteActor` | `DELETE /actors/:id` | -| `conversations:ListConversations` | `GET /conversations` | -| `conversations:GetConversation` | `GET /conversations/:id` | -| `conversations:GetConversation` | `GET /conversations/:id/documents` | -| `conversations:CreateConversation` | `POST /conversations` | -| `conversations:UpdateConversation` | `PATCH /conversations/:id` | -| `conversations:UpdateConversation` | `POST /conversations/:id/documents` | -| `conversations:UpdateConversation` | `DELETE /conversations/:id/documents/:docId` | -| `conversations:DeleteConversation` | `DELETE /conversations/:id` | - ---- - -## REST API - -### Actors - -``` -GET /actors?projectId= List actors -GET /actors/:id Get actor -POST /actors Create actor -DELETE /actors/:id Delete actor -``` - -**POST /actors** body: - -```json -{ - "projectId": "proj_...", - "name": "Support Bot", - "type": "ai" -} -``` - ---- - -### Conversations - -``` -GET /conversations?projectId= List conversations -GET /conversations/:id Get conversation -POST /conversations Create conversation -PATCH /conversations/:id Update status -DELETE /conversations/:id Delete conversation -GET /conversations/:id/documents List documents in order -POST /conversations/:id/documents Add document to conversation -DELETE /conversations/:id/documents/:documentId Remove document -``` - -**POST /conversations** body: - -```json -{ - "projectId": "proj_...", - "actorId": "act_...", - "status": "open" -} -``` - -**PATCH /conversations/:id** body: - -```json -{ "status": "closed" } -``` - -**POST /conversations/:id/documents** body: - -```json -{ - "documentId": "doc_...", - "position": 2 -} -``` - -If `position` is omitted, the document is appended at `MAX(position) + 1` (or position `0` if the conversation is empty). - ---- - -## MCP Tools - -All operations are also exposed as MCP tools for AI agent consumption: - -| Tool name | Description | -| ------------------------------ | ------------------------------------------ | -| `list-actors` | List actors in a project | -| `get-actor` | Get actor by ID | -| `create-actor` | Create an actor | -| `delete-actor` | Delete an actor | -| `list-conversations` | List conversations in a project | -| `get-conversation` | Get conversation by ID | -| `create-conversation` | Create a conversation | -| `update-conversation-status` | Set status to `open` or `closed` | -| `delete-conversation` | Delete a conversation | -| `list-conversation-documents` | List documents in a conversation (ordered) | -| `add-conversation-document` | Add a document at a given position | -| `remove-conversation-document` | Remove a document from a conversation | - ---- - -## Implementation Architecture - -``` -packages/postgresdb/src/models/ - Actor.ts ← DB model, publicId: act_ - Conversation.ts ← DB model, publicId: conv_ - ConversationDocument.ts ← Junction table model - -packages/server/src/lib/ - actors.ts ← listActors, getActor, createActor, deleteActor - conversations.ts ← listConversations, getConversation, createConversation, - updateConversationStatus, deleteConversation, - listConversationDocuments, addDocumentToConversation, - removeDocumentFromConversation - -packages/server/src/rest/v1/ - actors.ts ← REST routes registered in index.ts - conversations.ts ← REST routes registered in index.ts - -packages/server/src/mcp/tools/ - actors.ts ← MCP tools registered in index.ts - conversations.ts ← MCP tools registered in index.ts -``` - ---- - -## Remaining Work - -- [ ] OpenAPI YAML component schemas (`ActorRecord`, `ConversationRecord`, `ConversationDocumentRecord`) in `src/rest/openapi/v1/` -- [ ] Module docs page at `packages/website/docs/modules/actors.md` and `conversations.md` -- [ ] Unit tests at `packages/server/tests/unit/tests/actors.test.ts` and `conversations.test.ts` -- [ ] Database migration / sync for `actors`, `conversations`, `conversation_documents` tables - ---- - -## Open Questions - -1. **Multi-actor conversations** — should a future version support multiple actors per conversation (e.g. a thread between a human and an AI)? -2. **Position gaps** — if a document is removed from the middle, positions are not re-indexed. Should a reorder endpoint be added? -3. **Conversation filters** — should `GET /conversations` support filtering by `status` or `actorId`? diff --git a/packages/server/src/lib/actors.ts b/packages/server/src/lib/actors.ts index bd9aabb8..1cfce958 100644 --- a/packages/server/src/lib/actors.ts +++ b/packages/server/src/lib/actors.ts @@ -87,3 +87,36 @@ export const deleteActor = async (args: { id: string }) => { return { id: args.id }; }; + +export const updateActor = async (args: { + id: string; + name?: string; + type?: string; + externalId?: string; +}) => { + const actor = await db.Actor.findOne({ where: { publicId: args.id } }); + + if (!actor) { + return null; + } + + const updates: Record = {}; + if (args.name !== undefined) { + updates.name = args.name; + } + if (args.type !== undefined) { + updates.type = args.type; + } + if (args.externalId !== undefined) { + updates.externalId = args.externalId; + } + + await actor.update(updates); + + const actorWithProject = await db.Actor.findOne({ + where: { id: actor.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + return mapActor(actorWithProject!); +}; diff --git a/packages/server/src/lib/conversations.ts b/packages/server/src/lib/conversations.ts index d6abcd17..b547a477 100644 --- a/packages/server/src/lib/conversations.ts +++ b/packages/server/src/lib/conversations.ts @@ -1,5 +1,7 @@ +import fs from 'node:fs'; + import { db } from '../db'; -import { createDocument } from './documents'; +import { createDocument, deleteDocument } from './documents'; const mapConversation = ( conversation: InstanceType<(typeof db)['Conversation']> & { @@ -17,14 +19,28 @@ const mapConversation = ( const mapMessage = ( message: InstanceType<(typeof db)['ConversationMessage']> & { - document?: InstanceType<(typeof db)['Document']>; + document?: InstanceType<(typeof db)['Document']> & { + file?: InstanceType<(typeof db)['File']>; + }; actor?: InstanceType<(typeof db)['Actor']>; } ) => { + let content: string | null = null; + if (message.document?.file?.storagePath) { + try { + if (fs.existsSync(message.document.file.storagePath)) { + content = fs.readFileSync(message.document.file.storagePath, 'utf-8'); + } + } catch { + // Ignore read errors + } + } + return { documentId: message.document?.publicId, actorId: message.actor?.publicId, position: message.position, + content, }; }; @@ -52,7 +68,9 @@ export const listConversations = async (args: { attributes: ['conversationId'], group: ['conversationId'], }); - const conversationIds = messages.map((m) => m.conversationId); + const conversationIds = messages.map((m) => { + return m.conversationId; + }); where.id = conversationIds; } @@ -144,7 +162,11 @@ export const listConversationMessages = async (args: { const messages = await db.ConversationMessage.findAll({ where: { conversationId: conversation.id }, include: [ - { model: db.Document, as: 'document' }, + { + model: db.Document, + as: 'document', + include: [{ model: db.File, as: 'file' }], + }, { model: db.Actor, as: 'actor' }, ], order: [['position', 'ASC']], @@ -247,6 +269,11 @@ export const removeConversationMessage = async (args: { await message.destroy(); + // Also delete the associated document to avoid orphans (Bug #3) + if (document.publicId) { + await deleteDocument({ id: document.publicId }); + } + return { conversationId: args.conversationId, documentId: args.documentId }; }; @@ -281,13 +308,15 @@ export const listConversationActors = async (args: { } } - return actors.map((actor) => ({ - id: actor.publicId, - projectId: actor.project?.publicId, - name: actor.name, - type: actor.type ?? undefined, - externalId: actor.externalId ?? undefined, - createdAt: actor.createdAt, - updatedAt: actor.updatedAt, - })); + return actors.map((actor) => { + return { + id: actor.publicId, + projectId: actor.project?.publicId, + name: actor.name, + type: actor.type ?? undefined, + externalId: actor.externalId ?? undefined, + createdAt: actor.createdAt, + updatedAt: actor.updatedAt, + }; + }); }; diff --git a/packages/server/src/lib/documents.ts b/packages/server/src/lib/documents.ts index cc785877..b9f7fd71 100644 --- a/packages/server/src/lib/documents.ts +++ b/packages/server/src/lib/documents.ts @@ -152,6 +152,7 @@ export const searchDocuments = async (args: { projectIds?: number[]; query: string; limit?: number; + threshold?: number; }) => { if (args.projectIds !== undefined && args.projectIds.length === 0) { return []; @@ -165,6 +166,14 @@ export const searchDocuments = async (args: { args.projectIds !== undefined ? { projectId: args.projectIds } : undefined; const documents = await db.Document.findAll({ + attributes: { + include: [ + [ + db.Document.sequelize!.literal(`embedding <=> '${embeddingLiteral}'`), + 'distance', + ], + ], + }, include: [ { model: db.File, @@ -179,5 +188,25 @@ export const searchDocuments = async (args: { limit, }); - return documents.map(mapDocument); + return documents + .map((doc) => { + const distance = parseFloat( + (doc.getDataValue('distance') as string) ?? '1' + ); + const score = 1 - distance; + const mapped = mapDocument(doc); + + let content: string | null = null; + if (doc.file?.storagePath && fs.existsSync(doc.file.storagePath)) { + content = fs.readFileSync(doc.file.storagePath, 'utf-8'); + } + + return { ...mapped, content, score }; + }) + .filter((doc) => { + if (args.threshold !== undefined) { + return doc.score >= args.threshold; + } + return true; + }); }; diff --git a/packages/server/src/lib/files.ts b/packages/server/src/lib/files.ts index 7c2ab42d..483875cf 100644 --- a/packages/server/src/lib/files.ts +++ b/packages/server/src/lib/files.ts @@ -25,8 +25,20 @@ const mapFile = (file: InstanceType<(typeof db)['File']>) => { }; }; -export const listFiles = async () => { - const allFiles = await db.File.findAll(); +export const listFiles = async (args: { projectIds?: number[] }) => { + if (args.projectIds !== undefined && args.projectIds.length === 0) { + return []; + } + + const where: Record = {}; + + if (args.projectIds !== undefined) { + where.projectId = args.projectIds; + } + + const allFiles = await db.File.findAll({ + where: Object.keys(where).length > 0 ? where : undefined, + }); return allFiles.map(mapFile); }; @@ -103,7 +115,8 @@ export const downloadFile = async (args: { id: string }) => { export const updateFileMetadata = async (args: { id: string; - metadata: string; + metadata?: string; + filename?: string; }) => { const file = await db.File.findOne({ where: { publicId: args.id } }); @@ -111,7 +124,15 @@ export const updateFileMetadata = async (args: { return null; } - await file.update({ metadata: args.metadata }); + const updates: Record = {}; + if (args.metadata !== undefined) { + updates.metadata = args.metadata; + } + if (args.filename !== undefined) { + updates.filename = args.filename; + } + + await file.update(updates); return mapFile(file); }; diff --git a/packages/server/src/mcp/tools/actors.ts b/packages/server/src/mcp/tools/actors.ts index 996a778e..c6a0d749 100644 --- a/packages/server/src/mcp/tools/actors.ts +++ b/packages/server/src/mcp/tools/actors.ts @@ -81,8 +81,49 @@ const registerTools = (server: McpServer) => { }, }, async ({ id }) => { - await apiCall('DELETE', `/actors/${id}`); - return { content: [{ type: 'text', text: 'Actor deleted' }] }; + try { + await apiCall('DELETE', `/actors/${id}`); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ id, deleted: true }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + id, + deleted: false, + error: String(error), + }), + }, + ], + }; + } + } + ); + + server.registerTool( + 'update-actor', + { + description: 'Update an actor by ID', + inputSchema: { + id: z.string().describe('Actor ID'), + name: z.string().optional().describe('New name'), + type: z.string().optional().describe('New type'), + externalId: z.string().optional().describe('New external ID'), + }, + }, + async ({ id, name, type, externalId }) => { + const data = await apiCall('PATCH', `/actors/${id}`, { + body: { name, type, externalId }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } ); }; diff --git a/packages/server/src/mcp/tools/conversations.ts b/packages/server/src/mcp/tools/conversations.ts index cfc43053..4a5e4601 100644 --- a/packages/server/src/mcp/tools/conversations.ts +++ b/packages/server/src/mcp/tools/conversations.ts @@ -93,8 +93,30 @@ const registerTools = (server: McpServer) => { }, }, async ({ id }) => { - await apiCall('DELETE', `/conversations/${id}`); - return { content: [{ type: 'text', text: 'Conversation deleted' }] }; + try { + await apiCall('DELETE', `/conversations/${id}`); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ id, deleted: true }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + id, + deleted: false, + error: String(error), + }), + }, + ], + }; + } } ); @@ -163,10 +185,35 @@ const registerTools = (server: McpServer) => { }, }, async ({ id, documentId }) => { - await apiCall('DELETE', `/conversations/${id}/messages/${documentId}`); - return { - content: [{ type: 'text', text: 'Message removed from conversation' }], - }; + try { + await apiCall('DELETE', `/conversations/${id}/messages/${documentId}`); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + conversationId: id, + documentId, + deleted: true, + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + conversationId: id, + documentId, + deleted: false, + error: String(error), + }), + }, + ], + }; + } } ); }; diff --git a/packages/server/src/mcp/tools/documents.ts b/packages/server/src/mcp/tools/documents.ts index 09e72c14..7d3e79ff 100644 --- a/packages/server/src/mcp/tools/documents.ts +++ b/packages/server/src/mcp/tools/documents.ts @@ -65,8 +65,30 @@ const registerTools = (server: McpServer) => { }, }, async ({ id }) => { - await apiCall('DELETE', `/documents/${id}`); - return { content: [{ type: 'text', text: 'Document deleted' }] }; + try { + await apiCall('DELETE', `/documents/${id}`); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ id, deleted: true }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + id, + deleted: false, + error: String(error), + }), + }, + ], + }; + } } ); @@ -82,11 +104,17 @@ const registerTools = (server: McpServer) => { .number() .optional() .describe('Maximum number of results (default: 10)'), + threshold: z + .number() + .optional() + .describe( + 'Minimum similarity score (0-1). Only results with score >= threshold are returned.' + ), }, }, - async ({ projectId, query, limit }) => { + async ({ projectId, query, limit, threshold }) => { const data = await apiCall('POST', '/documents/search', { - body: { projectId, query, limit }, + body: { projectId, query, limit, threshold }, }); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } diff --git a/packages/server/src/mcp/tools/files.ts b/packages/server/src/mcp/tools/files.ts index e9dd3a5b..22146f43 100644 --- a/packages/server/src/mcp/tools/files.ts +++ b/packages/server/src/mcp/tools/files.ts @@ -5,11 +5,14 @@ const registerTools = (server: McpServer) => { server.registerTool( 'list-files', { - description: 'List all files', - inputSchema: {}, + description: 'List files. Optionally filter by projectId.', + inputSchema: { + projectId: z.string().optional().describe('Project ID to filter by'), + }, }, - async () => { - const data = await apiCall('GET', '/files'); + async ({ projectId }) => { + const qs = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ''; + const data = await apiCall('GET', `/files${qs}`); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } ); @@ -41,20 +44,30 @@ const registerTools = (server: McpServer) => { .string() .optional() .describe('MIME content type, e.g. text/plain'), + mimeType: z + .string() + .optional() + .describe('Alias for contentType (MIME type, e.g. text/plain)'), metadata: z .string() .optional() .describe('Additional metadata as a JSON string'), }, }, - async ({ projectId, content, filename, contentType, metadata }) => { - const buffer = Buffer.from(content, 'base64'); - const data = await apiCall('POST', '/files/upload', { + async ({ + projectId, + content, + filename, + contentType, + mimeType, + metadata, + }) => { + const data = await apiCall('POST', '/files/upload/base64', { body: { projectId, - fileBuffer: buffer.toString('base64'), + content, filename, - contentType, + contentType: contentType || mimeType, metadata, }, }); @@ -71,7 +84,7 @@ const registerTools = (server: McpServer) => { }, }, async ({ id }) => { - const data = await apiCall('GET', `/files/${id}/download`); + const data = await apiCall('GET', `/files/${id}/download/base64`); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } ); @@ -79,15 +92,19 @@ const registerTools = (server: McpServer) => { server.registerTool( 'update-file-metadata', { - description: 'Update the metadata of a file', + description: 'Update the metadata and/or filename of a file', inputSchema: { id: z.string().describe('File ID'), - metadata: z.string().describe('New metadata as a JSON string'), + metadata: z + .string() + .optional() + .describe('New metadata as a JSON string'), + filename: z.string().optional().describe('New filename'), }, }, - async ({ id, metadata }) => { + async ({ id, metadata, filename }) => { const data = await apiCall('PATCH', `/files/${id}/metadata`, { - body: { metadata }, + body: { metadata, filename }, }); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } @@ -123,10 +140,30 @@ const registerTools = (server: McpServer) => { }, }, async ({ id }) => { - await apiCall('DELETE', `/files/${id}`); - return { - content: [{ type: 'text', text: `File ${id} deleted successfully` }], - }; + try { + await apiCall('DELETE', `/files/${id}`); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ id, deleted: true }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + id, + deleted: false, + error: String(error), + }), + }, + ], + }; + } } ); }; diff --git a/packages/server/src/rest/v1/actors.ts b/packages/server/src/rest/v1/actors.ts index f0a68fdc..39fd6dc2 100644 --- a/packages/server/src/rest/v1/actors.ts +++ b/packages/server/src/rest/v1/actors.ts @@ -1,7 +1,13 @@ import { Router } from '@ttoss/http-server'; import type { Context } from 'src/Context'; import { db } from 'src/db'; -import { createActor, deleteActor, getActor, listActors } from 'src/lib/actors'; +import { + createActor, + deleteActor, + getActor, + listActors, + updateActor, +} from 'src/lib/actors'; const actorsRouter = new Router(); @@ -347,4 +353,96 @@ actorsRouter.delete('/actors/:id', async (ctx: Context) => { ctx.status = 204; }); +/** + * @openapi + * /actors/{id}: + * patch: + * tags: + * - Actors + * summary: Update an actor + * description: Updates an actor's name, type, or externalId + * operationId: updateActor + * parameters: + * - name: id + * in: path + * required: true + * description: Actor ID + * schema: + * type: string + * example: 'act_V1StGXR8Z5jdHi6B' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * example: 'Updated Actor' + * type: + * type: string + * example: 'assistant' + * externalId: + * type: string + * example: '+15551234567' + * responses: + * '200': + * description: Actor updated + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ActorRecord' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: Actor not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +actorsRouter.patch('/actors/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const actor = await getActor({ id: ctx.params.id }); + + if (!actor) { + ctx.status = 404; + ctx.body = { error: 'Actor not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + actor.projectId!, + 'actors:UpdateActor' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const body = ctx.request.body as { + name?: string; + type?: string; + externalId?: string; + }; + + const updated = await updateActor({ + id: ctx.params.id, + name: body.name, + type: body.type, + externalId: body.externalId, + }); + + ctx.body = updated; +}); + export { actorsRouter }; diff --git a/packages/server/src/rest/v1/documents.ts b/packages/server/src/rest/v1/documents.ts index 1d94b0b4..db49ab4f 100644 --- a/packages/server/src/rest/v1/documents.ts +++ b/packages/server/src/rest/v1/documents.ts @@ -361,6 +361,10 @@ documentsRouter.delete('/documents/:id', async (ctx: Context) => { * limit: * type: integer * example: 5 + * threshold: + * type: number + * description: Minimum similarity score (0-1). Only results with score >= threshold are returned. + * example: 0.7 * responses: * '200': * description: Search results @@ -400,6 +404,7 @@ documentsRouter.post('/documents/search', async (ctx: Context) => { projectId?: string; query: string; limit?: number; + threshold?: number; }; if (!body.query) { @@ -423,6 +428,7 @@ documentsRouter.post('/documents/search', async (ctx: Context) => { projectIds, query: body.query, limit: body.limit, + threshold: body.threshold, }); ctx.body = results; diff --git a/packages/server/src/rest/v1/files.ts b/packages/server/src/rest/v1/files.ts index d9dec782..1f444fe3 100644 --- a/packages/server/src/rest/v1/files.ts +++ b/packages/server/src/rest/v1/files.ts @@ -22,9 +22,17 @@ const filesRouter = new Router(); * get: * tags: * - Files - * summary: List all files - * description: Returns a list of all stored files + * summary: List files + * description: Returns a list of files. Requires authentication. Optionally filter by projectId. * operationId: listFiles + * parameters: + * - name: projectId + * in: query + * required: false + * description: Filter files by project ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' * responses: * '200': * description: List of files returned successfully @@ -34,15 +42,26 @@ const filesRouter = new Router(); * type: array * items: * $ref: '#/components/schemas/FileRecord' - * '500': - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ErrorResponse' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' */ filesRouter.get('/files', async (ctx: Context) => { - ctx.body = await listFiles(); + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const projectPublicId = (ctx.query as Record).projectId; + + const projectIds = await ctx.authUser.resolveProjectIds({ + projectPublicId, + action: 'files:GetFile', + }); + + ctx.body = await listFiles({ projectIds: projectIds ?? undefined }); }); /** @@ -390,6 +409,118 @@ filesRouter.post( } ); +/** + * @openapi + * /files/upload/base64: + * post: + * tags: + * - Files + * summary: Upload a file via JSON (base64-encoded) + * description: Uploads a file using a JSON body with base64-encoded content. Designed for programmatic/MCP usage. + * operationId: uploadFileBase64 + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - projectId + * - content + * properties: + * projectId: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * content: + * type: string + * description: Base64-encoded file content + * filename: + * type: string + * example: 'document.txt' + * contentType: + * type: string + * example: 'text/plain' + * metadata: + * type: string + * example: '{"author":"John"}' + * responses: + * '201': + * description: File uploaded successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FileRecord' + * '400': + * description: Missing content or invalid project + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ +filesRouter.post('/files/upload/base64', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { + projectId: string; + content: string; + filename?: string; + contentType?: string; + metadata?: string; + }; + + if (!body.content) { + ctx.status = 400; + ctx.body = { error: 'content is required (base64-encoded)' }; + return; + } + + if (!body.projectId) { + ctx.status = 400; + ctx.body = { error: 'projectId is required' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + body.projectId, + 'files:UploadFile' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ + where: { publicId: body.projectId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + const fileBuffer = Buffer.from(body.content, 'base64'); + + const record = await uploadFile({ + projectId: project.id, + fileBuffer, + filename: body.filename, + contentType: body.contentType, + metadata: body.metadata, + }); + + ctx.status = 201; + ctx.body = record; +}); + /** * @openapi * /files/{id}/download: @@ -469,6 +600,98 @@ filesRouter.get('/files/:id/download', async (ctx: Context) => { ctx.body = result.stream; }); +/** + * @openapi + * /files/{id}/download/base64: + * get: + * tags: + * - Files + * summary: Download a file as base64 + * description: Returns JSON with base64-encoded file content. Designed for programmatic/MCP usage. + * operationId: downloadFileBase64 + * parameters: + * - name: id + * in: path + * required: true + * description: File ID + * schema: + * type: string + * example: 'fil_abc123' + * responses: + * '200': + * description: Base64-encoded file content + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * description: Base64-encoded file content + * filename: + * type: string + * contentType: + * type: string + * size: + * type: number + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: File not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.get('/files/:id/download/base64', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const fileRecord = await getFile({ id: ctx.params.id }); + + if (!fileRecord) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + fileRecord.projectId!, + 'files:DownloadFile' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const result = await downloadFile({ id: ctx.params.id }); + + if (!result) { + ctx.status = 404; + ctx.body = { error: 'File not found on disk' }; + return; + } + + const chunks: Buffer[] = []; + for await (const chunk of result.stream) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + const buffer = Buffer.concat(chunks); + + ctx.body = { + content: buffer.toString('base64'), + filename: result.filename, + contentType: result.contentType, + size: result.size, + }; +}); + /** * @openapi * /files/{id}/metadata: @@ -476,7 +699,7 @@ filesRouter.get('/files/:id/download', async (ctx: Context) => { * tags: * - Files * summary: Update file metadata - * description: Updates the metadata field of a file + * description: Updates the metadata and/or filename of a file * operationId: updateFileMetadata * parameters: * - name: id @@ -492,15 +715,16 @@ filesRouter.get('/files/:id/download', async (ctx: Context) => { * application/json: * schema: * type: object - * required: - * - metadata * properties: * metadata: * type: string * example: '{"author":"Jane","tags":["report"]}' + * filename: + * type: string + * example: 'renamed-file.txt' * responses: * '200': - * description: Metadata updated successfully + * description: File updated successfully * content: * application/json: * schema: @@ -541,10 +765,11 @@ filesRouter.patch('/files/:id/metadata', async (ctx: Context) => { return; } - const body = ctx.request.body as { metadata: string }; + const body = ctx.request.body as { metadata?: string; filename?: string }; const updated = await updateFileMetadata({ id: ctx.params.id, metadata: body.metadata, + filename: body.filename, }); ctx.body = updated; diff --git a/packages/server/tests/unit/tests/actors.test.ts b/packages/server/tests/unit/tests/actors.test.ts index fb2db280..a4fcd783 100644 --- a/packages/server/tests/unit/tests/actors.test.ts +++ b/packages/server/tests/unit/tests/actors.test.ts @@ -34,6 +34,7 @@ describe('Actors', () => { 'actors:GetActor', 'actors:CreateActor', 'actors:DeleteActor', + 'actors:UpdateActor', ], }); policyId = policyRes.body.id; @@ -242,4 +243,51 @@ describe('Actors', () => { expect(response.status).toBe(404); }); }); + + describe('PATCH /api/v1/actors/:id', () => { + let actorId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'UpdateMe', type: 'customer' }); + actorId = res.body.id; + }); + + test('user with permission can update an actor name', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/actors/${actorId}`) + .send({ name: 'UpdatedName' }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(actorId); + expect(response.body.name).toBe('UpdatedName'); + }); + + test('user can update type and externalId', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/actors/${actorId}`) + .send({ type: 'assistant', externalId: '+15551112222' }); + + expect(response.status).toBe(200); + expect(response.body.type).toBe('assistant'); + expect(response.body.externalId).toBe('+15551112222'); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .patch(`/api/v1/actors/${actorId}`) + .send({ name: 'Hacked' }); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent actor', async () => { + const response = await authenticatedTestClient(adminToken) + .patch('/api/v1/actors/act_nonexistent') + .send({ name: 'Ghost' }); + + expect(response.status).toBe(404); + }); + }); }); diff --git a/packages/server/tests/unit/tests/conversations.test.ts b/packages/server/tests/unit/tests/conversations.test.ts index 01d64144..bb910cc4 100644 --- a/packages/server/tests/unit/tests/conversations.test.ts +++ b/packages/server/tests/unit/tests/conversations.test.ts @@ -503,4 +503,66 @@ describe('Conversations', () => { expect(response.status).toBe(404); }); }); + + describe('Message content field', () => { + let conversationId: string; + + beforeAll(async () => { + const convRes = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + conversationId = convRes.body.id; + + await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({ message: 'Content check message', actorId }); + }); + + test('listed messages include content field', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/conversations/${conversationId}/messages` + ); + + expect(response.status).toBe(200); + expect(response.body.length).toBeGreaterThanOrEqual(1); + expect(response.body[0].content).toBe('Content check message'); + }); + }); + + describe('Message removal cleans up document', () => { + let conversationId: string; + let documentId: string; + + beforeAll(async () => { + const convRes = await authenticatedTestClient(userToken) + .post('/api/v1/conversations') + .send({ projectId }); + conversationId = convRes.body.id; + + const msgRes = await authenticatedTestClient(userToken) + .post(`/api/v1/conversations/${conversationId}/messages`) + .send({ message: 'Orphan test message', actorId }); + documentId = msgRes.body.documentId; + }); + + test('removing a message also deletes the underlying document', async () => { + // Verify document exists before removal + const docBefore = await authenticatedTestClient(userToken).get( + `/api/v1/documents/${documentId}` + ); + expect(docBefore.status).toBe(200); + + // Remove the message + const deleteRes = await authenticatedTestClient(userToken).delete( + `/api/v1/conversations/${conversationId}/messages/${documentId}` + ); + expect(deleteRes.status).toBe(204); + + // Verify document is also gone + const docAfter = await authenticatedTestClient(userToken).get( + `/api/v1/documents/${documentId}` + ); + expect(docAfter.status).toBe(404); + }); + }); }); diff --git a/packages/server/tests/unit/tests/documents.test.ts b/packages/server/tests/unit/tests/documents.test.ts index c4dede30..97c93eae 100644 --- a/packages/server/tests/unit/tests/documents.test.ts +++ b/packages/server/tests/unit/tests/documents.test.ts @@ -230,6 +230,31 @@ describe('Documents', () => { expect(response.status).toBe(400); }); + + test('search results include score and content fields', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents/search') + .send({ projectId, query: 'capital of France' }); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + if (response.body.length > 0) { + expect(typeof response.body[0].score).toBe('number'); + expect(typeof response.body[0].content).toBe('string'); + } + }); + + test('search with threshold filters low-score results', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents/search') + .send({ projectId, query: 'capital of France', threshold: 0.99 }); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + for (const doc of response.body) { + expect(doc.score).toBeGreaterThanOrEqual(0.99); + } + }); }); describe('DELETE /api/v1/documents/:id', () => { diff --git a/packages/server/tests/unit/tests/files.test.ts b/packages/server/tests/unit/tests/files.test.ts index 001ad0c9..ab30283f 100644 --- a/packages/server/tests/unit/tests/files.test.ts +++ b/packages/server/tests/unit/tests/files.test.ts @@ -283,4 +283,166 @@ describe('Files', () => { expect(response.status).toBe(404); }); }); + + describe('GET /api/v1/files/:id/download/base64', () => { + let fileId: string; + const originalContent = 'Base64 download test content!'; + + beforeAll(async () => { + const fileContent = Buffer.from(originalContent); + const res = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { + filename: 'base64dl.txt', + contentType: 'text/plain', + }) + .field('projectId', projectId); + fileId = res.body.id; + }); + + test('user with permission can download a file as base64', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files/${fileId}/download/base64` + ); + + expect(response.status).toBe(200); + expect(response.body.content).toBe( + Buffer.from(originalContent).toString('base64') + ); + expect(response.body.filename).toBe('base64dl.txt'); + expect(response.body.contentType).toBe('text/plain'); + expect(response.body.size).toBe(Buffer.from(originalContent).length); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get( + `/api/v1/files/${fileId}/download/base64` + ); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent file', async () => { + const response = await authenticatedTestClient(adminToken).get( + '/api/v1/files/nonexistent-file-id/download/base64' + ); + + expect(response.status).toBe(404); + }); + }); + + describe('POST /api/v1/files/upload/base64', () => { + test('user with permission can upload a file via base64', async () => { + const content = Buffer.from('Hello base64 upload!').toString('base64'); + + const response = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload/base64') + .send({ + projectId, + content, + filename: 'base64upload.txt', + contentType: 'text/plain', + }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.filename).toBe('base64upload.txt'); + expect(response.body.contentType).toBe('text/plain'); + }); + + test('unauthenticated request returns 401', async () => { + const content = Buffer.from('data').toString('base64'); + + const response = await testClient + .post('/api/v1/files/upload/base64') + .send({ projectId, content, filename: 'test.txt' }); + + expect(response.status).toBe(401); + }); + }); + + describe('PATCH /api/v1/files/:id/metadata - filename update', () => { + let fileId: string; + + beforeAll(async () => { + const fileContent = Buffer.from('Filename update target'); + const res = await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', fileContent, { + filename: 'original-name.txt', + contentType: 'text/plain', + }) + .field('projectId', projectId); + fileId = res.body.id; + }); + + test('user can update filename only', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/files/${fileId}/metadata`) + .send({ filename: 'renamed-file.txt' }); + + expect(response.status).toBe(200); + expect(response.body.filename).toBe('renamed-file.txt'); + }); + + test('user can update both filename and metadata', async () => { + const newMetadata = JSON.stringify({ version: 3 }); + + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/files/${fileId}/metadata`) + .send({ filename: 'both-updated.txt', metadata: newMetadata }); + + expect(response.status).toBe(200); + expect(response.body.filename).toBe('both-updated.txt'); + expect(response.body.metadata).toBe(newMetadata); + }); + }); + + describe('GET /api/v1/files - projectId filter', () => { + let secondProjectId: string; + + beforeAll(async () => { + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Files Filter Project' }); + secondProjectId = projectRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${secondProjectId}/policies`) + .send({ permissions: ['files:UploadFile', 'files:GetFile'] }); + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${secondProjectId}/members`) + .send({ userId, policyId: policyRes.body.id }); + + await authenticatedTestClient(userToken) + .post('/api/v1/files/upload') + .attach('file', Buffer.from('proj2 file'), { + filename: 'proj2.txt', + contentType: 'text/plain', + }) + .field('projectId', secondProjectId); + }); + + test('listing with projectId returns only files in that project', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files?projectId=${secondProjectId}` + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBe(1); + expect(response.body[0].filename).toBe('proj2.txt'); + }); + + test('listing without projectId returns files across projects', async () => { + const response = + await authenticatedTestClient(userToken).get('/api/v1/files'); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + // Should include files from both projects + expect(response.body.length).toBeGreaterThanOrEqual(2); + }); + }); }); diff --git a/packages/server/tests/unit/tests/mcp.test.ts b/packages/server/tests/unit/tests/mcp.test.ts index a5570772..d3cdf358 100644 --- a/packages/server/tests/unit/tests/mcp.test.ts +++ b/packages/server/tests/unit/tests/mcp.test.ts @@ -17,5 +17,9 @@ describe('MCP tools/list', () => { expect(names).toContain('get-file'); expect(names).toContain('create-file'); expect(names).toContain('delete-file'); + expect(names).toContain('update-actor'); + expect(names).toContain('upload-file'); + expect(names).toContain('download-file'); + expect(names).toContain('update-file-metadata'); }); }); diff --git a/packages/website/docs/modules/actors.md b/packages/website/docs/modules/actors.md index e2f0b9c7..55288808 100644 --- a/packages/website/docs/modules/actors.md +++ b/packages/website/docs/modules/actors.md @@ -36,9 +36,10 @@ A `null` / absent `externalId` is never considered a duplicate — PostgreSQL's Actor operations are governed by per-project policies. Grant the following permissions: -| Action | Permission string | -| --------------- | -------------------- | -| List actors | `actors:ListActors` | -| Get actor by ID | `actors:GetActor` | -| Create actor | `actors:CreateActor` | -| Delete actor | `actors:DeleteActor` | +| Action | Permission | REST Endpoint | MCP Tool | +| --------------- | -------------------- | --------------------------- | -------------- | +| List actors | `actors:ListActors` | `GET /api/v1/actors` | `list-actors` | +| Get actor by ID | `actors:GetActor` | `GET /api/v1/actors/:id` | `get-actor` | +| Create actor | `actors:CreateActor` | `POST /api/v1/actors` | `create-actor` | +| Update actor | `actors:UpdateActor` | `PATCH /api/v1/actors/:id` | `update-actor` | +| Delete actor | `actors:DeleteActor` | `DELETE /api/v1/actors/:id` | `delete-actor` | diff --git a/packages/website/docs/modules/conversations.md b/packages/website/docs/modules/conversations.md index 93550b71..b67015fc 100644 --- a/packages/website/docs/modules/conversations.md +++ b/packages/website/docs/modules/conversations.md @@ -23,10 +23,11 @@ Conversations are identified by `publicId` prefixed with `conv_`. The internal d ### Conversation Message -| Field | Type | Description | -| ------------ | ------- | ------------------------------------------------------ | -| `documentId` | string | Public ID of the Document attached as a message | -| `position` | integer | Zero-based position of the message in the conversation | +| Field | Type | Description | +| ------------ | ------- | -------------------------------------------------------------------- | +| `documentId` | string | Public ID of the Document attached as a message | +| `position` | integer | Zero-based position of the message in the conversation | +| `content` | string | Full text content of the message (read from the underlying document) | ## Key Concepts @@ -34,6 +35,10 @@ Conversations are identified by `publicId` prefixed with `conv_`. The internal d Messages are ordered references to Documents within a conversation. When adding a message, you can specify an explicit `position`. If omitted, the document is appended at the end (position = MAX + 1). Each document can appear at most once per conversation — adding the same document twice returns `409 Conflict`. +When listing messages, each entry includes the full text `content` of the underlying document. + +Removing a message from a conversation also deletes its underlying Document and the associated File on disk, preventing orphaned records. + ### Status A conversation transitions between `open` and `closed`. Use `PATCH /conversations/:id` to update the status. New conversations default to `open`. @@ -46,13 +51,14 @@ Every conversation is linked to a single Actor. You can filter conversations by Conversation operations are governed by per-project policies. Grant the following permissions: -| Action | Permission string | -| -------------------------------- | ---------------------------------- | -| List conversations | `conversations:ListConversations` | -| Get conversation by ID | `conversations:GetConversation` | -| List conversation messages | `conversations:GetConversation` | -| Create conversation | `conversations:CreateConversation` | -| Update conversation status | `conversations:UpdateConversation` | -| Add message to conversation | `conversations:UpdateConversation` | -| Remove message from conversation | `conversations:UpdateConversation` | -| Delete conversation | `conversations:DeleteConversation` | +| Action | Permission | REST Endpoint | MCP Tool | +| -------------------------------- | ---------------------------------- | ------------------------------------------- | ----------------------------- | +| List conversations | `conversations:ListConversations` | `GET /api/v1/conversations` | `list-conversations` | +| Get conversation by ID | `conversations:GetConversation` | `GET /api/v1/conversations/:id` | `get-conversation` | +| List conversation messages | `conversations:GetConversation` | `GET /api/v1/conversations/:id/messages` | `list-conversation-messages` | +| List conversation actors | `conversations:GetConversation` | `GET /api/v1/conversations/:id/actors` | `list-conversation-actors` | +| Create conversation | `conversations:CreateConversation` | `POST /api/v1/conversations` | `create-conversation` | +| Update conversation status | `conversations:UpdateConversation` | `PATCH /api/v1/conversations/:id` | `update-conversation` | +| Add message to conversation | `conversations:UpdateConversation` | `POST /api/v1/conversations/:id/messages` | `add-conversation-message` | +| Remove message from conversation | `conversations:UpdateConversation` | `DELETE /api/v1/conversations/:id/messages` | `remove-conversation-message` | +| Delete conversation | `conversations:DeleteConversation` | `DELETE /api/v1/conversations/:id` | `delete-conversation` | diff --git a/packages/website/docs/modules/documents.md b/packages/website/docs/modules/documents.md index c0bd7c98..7e89c158 100644 --- a/packages/website/docs/modules/documents.md +++ b/packages/website/docs/modules/documents.md @@ -56,13 +56,13 @@ The `embedding` column (pgvector `vector(N)`) is stored in the database but neve Document operations are governed by per-project policies. Grant the following permissions: -| Action | Permission string | -| ----------------- | --------------------------- | -| List documents | `documents:ListDocuments` | -| Get a document | `documents:GetDocument` | -| Create a document | `documents:CreateDocument` | -| Delete a document | `documents:DeleteDocument` | -| Semantic search | `documents:SearchDocuments` | +| Action | Permission | REST Endpoint | MCP Tool | +| ----------------- | --------------------------- | ------------------------------- | ------------------ | +| List documents | `documents:ListDocuments` | `GET /api/v1/documents` | `list-documents` | +| Get a document | `documents:GetDocument` | `GET /api/v1/documents/:id` | `get-document` | +| Create a document | `documents:CreateDocument` | `POST /api/v1/documents` | `create-document` | +| Delete a document | `documents:DeleteDocument` | `DELETE /api/v1/documents/:id` | `delete-document` | +| Semantic search | `documents:SearchDocuments` | `POST /api/v1/documents/search` | `search-documents` | ## Operations @@ -121,11 +121,14 @@ Content-Type: application/json { "projectId": "proj_xxx", "query": "What is the capital of France?", - "limit": 5 + "limit": 5, + "threshold": 0.7 } ``` -The `limit` field defaults to `10` when omitted. +The `limit` field defaults to `10` when omitted. The optional `threshold` field (between 0 and 1) filters out results whose similarity score falls below the given value. + +Each result in the response includes a `score` field (1 − cosine distance) and a `content` field with the full document text. ## Project ID Resolution diff --git a/packages/website/docs/modules/files.md b/packages/website/docs/modules/files.md index a4aaaca6..345c356e 100644 --- a/packages/website/docs/modules/files.md +++ b/packages/website/docs/modules/files.md @@ -46,14 +46,15 @@ volumes: File operations are governed by per-project policies. Grant the following permissions to allow a user to perform each action: -| Action | Permission string | -| ----------------------------- | -------------------------- | -| Upload a file | `files:UploadFile` | -| Get file metadata | `files:GetFile` | -| Download file content | `files:DownloadFile` | -| Update metadata | `files:UpdateFileMetadata` | -| Create a metadata-only record | `files:CreateFile` | -| Delete a file | `files:DeleteFile` | +| Action | Permission | REST Endpoint | MCP Tool | +| ----------------------------- | -------------------------- | ---------------------------------- | ---------------------- | +| List files | `files:ListFiles` | `GET /api/v1/files` | `list-files` | +| Get file metadata | `files:GetFile` | `GET /api/v1/files/:id` | `get-file` | +| Create a metadata-only record | `files:CreateFile` | `POST /api/v1/files` | `create-file` | +| Upload a file | `files:UploadFile` | `POST /api/v1/files/upload` | `upload-file` | +| Download file content | `files:DownloadFile` | `GET /api/v1/files/:id/download` | `download-file` | +| Update metadata | `files:UpdateFileMetadata` | `PATCH /api/v1/files/:id/metadata` | `update-file-metadata` | +| Delete a file | `files:DeleteFile` | `DELETE /api/v1/files/:id` | `delete-file` | ## Operations @@ -74,14 +75,51 @@ Optional fields: `metadata` (JSON string). ### Update metadata -`PATCH /api/v1/files/{id}/metadata` — Replaces the `metadata` field on an existing file record. +`PATCH /api/v1/files/{id}/metadata` — Updates the `metadata` and/or `filename` fields on an existing file record. Both fields are optional; supply at least one. Request body: ```json -{ "metadata": "{\"author\":\"Alice\",\"tags\":[\"report\"]}" } +{ + "metadata": "{\"author\":\"Alice\",\"tags\":[\"report\"]}", + "filename": "new-name.pdf" +} ``` ### Delete a file `DELETE /api/v1/files/{id}` — Removes the file from disk and deletes the database record. Returns `204 No Content`. + +### Upload a file (Base64) + +`POST /api/v1/files/upload/base64` — JSON-based upload that accepts the file content as a Base64-encoded string. Useful when multipart form-data is not practical (e.g., from MCP tools). + +Request body: + +```json +{ + "projectId": "proj_xxx", + "content": "", + "filename": "report.pdf", + "contentType": "application/pdf" +} +``` + +### Download a file (Base64) + +`GET /api/v1/files/{id}/download/base64` — Returns the file content as a Base64-encoded JSON response instead of raw bytes. Useful for MCP tool integrations. + +Response body: + +```json +{ + "content": "", + "filename": "report.pdf", + "contentType": "application/pdf", + "size": 12345 +} +``` + +### List files with project filter + +`GET /api/v1/files` accepts an optional `projectId` query parameter to filter files by project. When omitted, the server resolves accessible projects based on the caller's identity (same resolution rules as the Documents module). diff --git a/packages/website/docs/modules/iam.md b/packages/website/docs/modules/iam.md index 761a908c..44482677 100644 --- a/packages/website/docs/modules/iam.md +++ b/packages/website/docs/modules/iam.md @@ -83,27 +83,12 @@ A policy defines what actions a member (or API key) is allowed or denied within ### Permission Actions -Permission strings follow the format `resource:Action`. The table below lists all available actions and the REST endpoint each one protects. - -| Permission Action | REST Endpoint | What it controls | -| ---------------------------------- | ---------------------------------- | --------------------------------------------------- | -| `files:GetFile` | `GET /api/v1/files/:id` | Retrieve file metadata | -| `files:CreateFile` | `POST /api/v1/files` | Create a new file record | -| `files:UploadFile` | `POST /api/v1/files/upload` | Upload file content | -| `files:DownloadFile` | `GET /api/v1/files/:id/download` | Download file content | -| `files:UpdateFileMetadata` | `PATCH /api/v1/files/:id/metadata` | Update file metadata | -| `files:DeleteFile` | `DELETE /api/v1/files/:id` | Delete a file | -| `projects:GetProject` | `GET /api/v1/projects/:id` | View a project's details | -| `documents:GetDocument` | `GET /api/v1/documents/:id` | Retrieve a document | -| `documents:CreateDocument` | `POST /api/v1/documents` | Create a document | -| `documents:DeleteDocument` | `DELETE /api/v1/documents/:id` | Delete a document | -| `actors:GetActor` | `GET /api/v1/actors/:id` | Retrieve an actor | -| `actors:CreateActor` | `POST /api/v1/actors` | Create an actor | -| `actors:DeleteActor` | `DELETE /api/v1/actors/:id` | Delete an actor | -| `conversations:GetConversation` | `GET /api/v1/conversations/:id` | Retrieve a conversation and its messages and actors | -| `conversations:CreateConversation` | `POST /api/v1/conversations` | Create a conversation | -| `conversations:UpdateConversation` | `PATCH /api/v1/conversations/:id` | Update a conversation, add or remove messages | -| `conversations:DeleteConversation` | `DELETE /api/v1/conversations/:id` | Delete a conversation | +Permission strings follow the format `resource:Action`. Each module documents its own permission actions — see the individual module pages for the full tables: + +- [Files](../modules/files.md#permissions) +- [Actors](../modules/actors.md#permissions) +- [Documents](../modules/documents.md#permissions) +- [Conversations](../modules/conversations.md#permissions) Use wildcards to grant broader access: From 629d507fd11a285762f4f1df1ec154883da2a19a Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Sat, 11 Apr 2026 22:59:58 -0300 Subject: [PATCH 09/27] fix: bugs --- .../page-2026-04-12T01-26-58-080Z.yml | 0 .../page-2026-04-12T01-27-02-826Z.png | Bin 0 -> 61007 bytes .../page-2026-04-12T01-28-10-941Z.yml | 0 packages/server/src/lib/conversations.ts | 6 +- packages/server/src/mcp/tools/actors.ts | 18 ++++- .../server/src/mcp/tools/conversations.ts | 18 ++++- packages/server/src/mcp/tools/documents.ts | 18 ++++- .../server/src/rest/openapi/v1/actors.yaml | 12 ++-- .../src/rest/openapi/v1/conversations.yaml | 26 ++++---- .../server/src/rest/openapi/v1/documents.yaml | 12 ++-- .../server/src/rest/openapi/v1/files.yaml | 6 +- packages/server/src/rest/v1/actors.ts | 8 +-- packages/server/src/rest/v1/apiKeys.ts | 6 +- packages/server/src/rest/v1/conversations.ts | 14 ++-- packages/server/src/rest/v1/documents.ts | 6 +- packages/server/src/rest/v1/files.ts | 6 ++ .../tests/unit/tests/conversations.test.ts | 1 + .../server/tests/unit/tests/files.test.ts | 13 ++++ packages/server/tests/unit/tests/mcp.test.ts | 63 +++++++++++++++++- packages/website/docs/modules/actors.md | 2 +- .../website/docs/modules/conversations.md | 2 +- packages/website/docs/modules/documents.md | 2 +- packages/website/docs/modules/files.md | 26 ++++---- packages/website/docusaurus.config.ts | 1 + packages/website/sidebars.ts | 16 +---- 25 files changed, 199 insertions(+), 83 deletions(-) create mode 100644 .playwright-mcp/page-2026-04-12T01-26-58-080Z.yml create mode 100644 .playwright-mcp/page-2026-04-12T01-27-02-826Z.png create mode 100644 .playwright-mcp/page-2026-04-12T01-28-10-941Z.yml diff --git a/.playwright-mcp/page-2026-04-12T01-26-58-080Z.yml b/.playwright-mcp/page-2026-04-12T01-26-58-080Z.yml new file mode 100644 index 00000000..e69de29b diff --git a/.playwright-mcp/page-2026-04-12T01-27-02-826Z.png b/.playwright-mcp/page-2026-04-12T01-27-02-826Z.png new file mode 100644 index 0000000000000000000000000000000000000000..de57909eaef54acf20c3ac6f0b335e53d1397a9a GIT binary patch literal 61007 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 { }, }, async ({ id }) => { - const data = await apiCall('GET', `/actors/${id}`); - return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + try { + const data = await apiCall('GET', `/actors/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: 'not_found', + message: String(error), + }), + }, + ], + }; + } } ); diff --git a/packages/server/src/mcp/tools/conversations.ts b/packages/server/src/mcp/tools/conversations.ts index 4a5e4601..5954d80f 100644 --- a/packages/server/src/mcp/tools/conversations.ts +++ b/packages/server/src/mcp/tools/conversations.ts @@ -34,8 +34,22 @@ const registerTools = (server: McpServer) => { }, }, async ({ id }) => { - const data = await apiCall('GET', `/conversations/${id}`); - return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + try { + const data = await apiCall('GET', `/conversations/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: 'not_found', + message: String(error), + }), + }, + ], + }; + } } ); diff --git a/packages/server/src/mcp/tools/documents.ts b/packages/server/src/mcp/tools/documents.ts index 7d3e79ff..4b645652 100644 --- a/packages/server/src/mcp/tools/documents.ts +++ b/packages/server/src/mcp/tools/documents.ts @@ -27,8 +27,22 @@ const registerTools = (server: McpServer) => { }, }, async ({ id }) => { - const data = await apiCall('GET', `/documents/${id}`); - return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + try { + const data = await apiCall('GET', `/documents/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: 'not_found', + message: String(error), + }), + }, + ], + }; + } } ); diff --git a/packages/server/src/rest/openapi/v1/actors.yaml b/packages/server/src/rest/openapi/v1/actors.yaml index e5c672fb..26a4d44d 100644 --- a/packages/server/src/rest/openapi/v1/actors.yaml +++ b/packages/server/src/rest/openapi/v1/actors.yaml @@ -21,7 +21,7 @@ paths: - name: projectId in: query required: false - description: Project public ID (optional) + description: Project ID (optional) schema: type: string example: 'proj_V1StGXR8Z5jdHi6B' @@ -70,7 +70,7 @@ paths: properties: projectId: type: string - description: Project public ID. Required for JWT auth; omit when using an API key. + description: Project ID. Required for JWT auth; omit when using an API key. example: 'proj_V1StGXR8Z5jdHi6B' name: type: string @@ -113,7 +113,7 @@ paths: tags: - Actors summary: Get an actor by ID - description: Returns an actor by its public ID + description: Returns an actor by its ID operationId: getActor parameters: - name: id @@ -152,7 +152,7 @@ paths: tags: - Actors summary: Delete an actor - description: Deletes an actor by its public ID + description: Deletes an actor by its ID operationId: deleteActor parameters: - name: id @@ -190,11 +190,11 @@ components: properties: id: type: string - description: Actor public ID + description: Actor ID example: 'act_V1StGXR8Z5jdHi6B' projectId: type: string - description: Project public ID + description: Project ID example: 'proj_V1StGXR8Z5jdHi6B' name: type: string diff --git a/packages/server/src/rest/openapi/v1/conversations.yaml b/packages/server/src/rest/openapi/v1/conversations.yaml index 5a9023f8..ef11c801 100644 --- a/packages/server/src/rest/openapi/v1/conversations.yaml +++ b/packages/server/src/rest/openapi/v1/conversations.yaml @@ -21,14 +21,14 @@ paths: - name: projectId in: query required: false - description: Project public ID (optional) + description: Project ID (optional) schema: type: string example: 'proj_V1StGXR8Z5jdHi6B' - name: actorId in: query required: false - description: Filter by actor public ID + description: Filter by actor ID schema: type: string example: 'act_V1StGXR8Z5jdHi6B' @@ -68,7 +68,7 @@ paths: properties: projectId: type: string - description: Project public ID. Required for JWT auth; omit when using an API key. + description: Project ID. Required for JWT auth; omit when using an API key. example: 'proj_V1StGXR8Z5jdHi6B' status: type: string @@ -105,7 +105,7 @@ paths: tags: - Conversations summary: Get a conversation by ID - description: Returns a conversation by its public ID + description: Returns a conversation by its ID operationId: getConversation parameters: - name: id @@ -202,7 +202,7 @@ paths: tags: - Conversations summary: Delete a conversation - description: Deletes a conversation by its public ID + description: Deletes a conversation by its ID operationId: deleteConversation parameters: - name: id @@ -305,7 +305,7 @@ paths: example: 'Hello, how can I help you?' actorId: type: string - description: Actor public ID who is sending this message + description: Actor ID who is sending this message example: 'act_V1StGXR8Z5jdHi6B' position: type: integer @@ -402,7 +402,7 @@ paths: - name: documentId in: path required: true - description: Document public ID + description: Document ID schema: type: string example: 'doc_V1StGXR8Z5jdHi6B' @@ -434,11 +434,11 @@ components: properties: id: type: string - description: Conversation public ID + description: Conversation ID example: 'conv_V1StGXR8Z5jdHi6B' projectId: type: string - description: Project public ID + description: Project ID example: 'proj_V1StGXR8Z5jdHi6B' status: type: string @@ -458,11 +458,11 @@ components: properties: documentId: type: string - description: Document public ID + description: Document ID example: 'doc_V1StGXR8Z5jdHi6B' actorId: type: string - description: Actor public ID who sent this message + description: Actor ID who sent this message example: 'act_V1StGXR8Z5jdHi6B' position: type: integer @@ -473,11 +473,11 @@ components: properties: id: type: string - description: Actor public ID + description: Actor ID example: 'act_V1StGXR8Z5jdHi6B' projectId: type: string - description: Project public ID + description: Project ID example: 'proj_V1StGXR8Z5jdHi6B' name: type: string diff --git a/packages/server/src/rest/openapi/v1/documents.yaml b/packages/server/src/rest/openapi/v1/documents.yaml index 2dd1287d..bb46630c 100644 --- a/packages/server/src/rest/openapi/v1/documents.yaml +++ b/packages/server/src/rest/openapi/v1/documents.yaml @@ -21,7 +21,7 @@ paths: - name: projectId in: query required: false - description: Project public ID (optional) + description: Project ID (optional) schema: type: string example: 'proj_V1StGXR8Z5jdHi6B' @@ -63,7 +63,7 @@ paths: properties: projectId: type: string - description: Project public ID. Required for JWT auth; omit when using an API key. + description: Project ID. Required for JWT auth; omit when using an API key. example: 'proj_V1StGXR8Z5jdHi6B' content: type: string @@ -189,7 +189,7 @@ paths: properties: projectId: type: string - description: Project public ID (optional). Omit to search across all accessible projects. + description: Project ID (optional). Omit to search across all accessible projects. example: 'proj_V1StGXR8Z5jdHi6B' query: type: string @@ -231,15 +231,15 @@ components: properties: id: type: string - description: Document public ID + description: Document ID example: 'doc_V1StGXR8Z5jdHi6B' fileId: type: string - description: Underlying file public ID + description: Underlying file ID example: 'file_V1StGXR8Z5jdHi6B' projectId: type: string - description: Project public ID + description: Project ID example: 'proj_V1StGXR8Z5jdHi6B' filename: type: string diff --git a/packages/server/src/rest/openapi/v1/files.yaml b/packages/server/src/rest/openapi/v1/files.yaml index 2bcbde3d..4e700b8b 100644 --- a/packages/server/src/rest/openapi/v1/files.yaml +++ b/packages/server/src/rest/openapi/v1/files.yaml @@ -249,13 +249,15 @@ paths: application/json: schema: type: object - required: - - metadata properties: metadata: type: string description: New metadata as a JSON string example: '{"author":"Jane","tags":["report"]}' + filename: + type: string + description: New filename for the file + example: 'renamed-file.txt' responses: '200': description: Metadata updated successfully diff --git a/packages/server/src/rest/v1/actors.ts b/packages/server/src/rest/v1/actors.ts index 39fd6dc2..349d597f 100644 --- a/packages/server/src/rest/v1/actors.ts +++ b/packages/server/src/rest/v1/actors.ts @@ -24,7 +24,7 @@ const actorsRouter = new Router(); * - name: projectId * in: query * required: false - * description: Project public ID (optional) + * description: Project ID (optional) * schema: * type: string * example: 'proj_V1StGXR8Z5jdHi6B' @@ -88,7 +88,7 @@ actorsRouter.get('/actors', async (ctx: Context) => { * tags: * - Actors * summary: Get an actor by ID - * description: Returns an actor by its public ID + * description: Returns an actor by its ID * operationId: getActor * parameters: * - name: id @@ -172,7 +172,7 @@ actorsRouter.get('/actors/:id', async (ctx: Context) => { * properties: * projectId: * type: string - * description: Project public ID. Required for JWT auth; omit when using an API key. + * description: Project ID. Required for JWT auth; omit when using an API key. * example: 'proj_V1StGXR8Z5jdHi6B' * name: * type: string @@ -292,7 +292,7 @@ actorsRouter.post('/actors', async (ctx: Context) => { * tags: * - Actors * summary: Delete an actor - * description: Deletes an actor by its public ID + * description: Deletes an actor by its ID * operationId: deleteActor * parameters: * - name: id diff --git a/packages/server/src/rest/v1/apiKeys.ts b/packages/server/src/rest/v1/apiKeys.ts index ff150894..d3320ef5 100644 --- a/packages/server/src/rest/v1/apiKeys.ts +++ b/packages/server/src/rest/v1/apiKeys.ts @@ -27,10 +27,10 @@ const apiKeysRouter = new Router(); * properties: * projectId: * type: string - * description: Project public ID + * description: Project ID * policyId: * type: string - * description: Policy public ID + * description: Policy ID * name: * type: string * description: API key name @@ -230,7 +230,7 @@ apiKeysRouter.get('/api-keys/:id', async (ctx: Context) => { * properties: * policyId: * type: string - * description: New policy public ID + * description: New policy ID * responses: * '200': * description: API key updated successfully diff --git a/packages/server/src/rest/v1/conversations.ts b/packages/server/src/rest/v1/conversations.ts index 9ac1031e..9774a93f 100644 --- a/packages/server/src/rest/v1/conversations.ts +++ b/packages/server/src/rest/v1/conversations.ts @@ -28,14 +28,14 @@ const conversationsRouter = new Router(); * - name: projectId * in: query * required: false - * description: Project public ID (optional) + * description: Project ID (optional) * schema: * type: string * example: 'proj_V1StGXR8Z5jdHi6B' * - name: actorId * in: query * required: false - * description: Filter by actor public ID + * description: Filter by actor ID * schema: * type: string * example: 'act_V1StGXR8Z5jdHi6B' @@ -92,7 +92,7 @@ conversationsRouter.get('/conversations', async (ctx: Context) => { * tags: * - Conversations * summary: Get a conversation by ID - * description: Returns a conversation by its public ID + * description: Returns a conversation by its ID * operationId: getConversation * parameters: * - name: id @@ -174,7 +174,7 @@ conversationsRouter.get('/conversations/:id', async (ctx: Context) => { * properties: * projectId: * type: string - * description: Project public ID. Required for JWT auth; omit when using an API key. + * description: Project ID. Required for JWT auth; omit when using an API key. * example: 'proj_V1StGXR8Z5jdHi6B' * status: * type: string @@ -368,7 +368,7 @@ conversationsRouter.patch('/conversations/:id', async (ctx: Context) => { * tags: * - Conversations * summary: Delete a conversation - * description: Deletes a conversation by its public ID + * description: Deletes a conversation by its ID * operationId: deleteConversation * parameters: * - name: id @@ -539,7 +539,7 @@ conversationsRouter.get('/conversations/:id/messages', async (ctx: Context) => { * example: 'Hello, how can I help you?' * actorId: * type: string - * description: Actor public ID who is sending this message + * description: Actor ID who is sending this message * example: 'act_V1StGXR8Z5jdHi6B' * position: * type: integer @@ -660,7 +660,7 @@ conversationsRouter.post( * - name: documentId * in: path * required: true - * description: Document public ID + * description: Document ID * schema: * type: string * example: 'doc_V1StGXR8Z5jdHi6B' diff --git a/packages/server/src/rest/v1/documents.ts b/packages/server/src/rest/v1/documents.ts index db49ab4f..b27aeb7f 100644 --- a/packages/server/src/rest/v1/documents.ts +++ b/packages/server/src/rest/v1/documents.ts @@ -24,7 +24,7 @@ const documentsRouter = new Router(); * - name: projectId * in: query * required: false - * description: Project public ID (optional) + * description: Project ID (optional) * schema: * type: string * example: 'proj_V1StGXR8Z5jdHi6B' @@ -164,7 +164,7 @@ documentsRouter.get('/documents/:id', async (ctx: Context) => { * properties: * projectId: * type: string - * description: Project public ID. Required for JWT auth; omit when using an API key. + * description: Project ID. Required for JWT auth; omit when using an API key. * example: 'proj_V1StGXR8Z5jdHi6B' * content: * type: string @@ -353,7 +353,7 @@ documentsRouter.delete('/documents/:id', async (ctx: Context) => { * properties: * projectId: * type: string - * description: Project public ID (optional). Omit to search across all accessible projects. + * description: Project ID (optional). Omit to search across all accessible projects. * example: 'proj_V1StGXR8Z5jdHi6B' * query: * type: string diff --git a/packages/server/src/rest/v1/files.ts b/packages/server/src/rest/v1/files.ts index 1f444fe3..9ad7d642 100644 --- a/packages/server/src/rest/v1/files.ts +++ b/packages/server/src/rest/v1/files.ts @@ -61,6 +61,12 @@ filesRouter.get('/files', async (ctx: Context) => { action: 'files:GetFile', }); + if (projectIds === null) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + ctx.body = await listFiles({ projectIds: projectIds ?? undefined }); }); diff --git a/packages/server/tests/unit/tests/conversations.test.ts b/packages/server/tests/unit/tests/conversations.test.ts index bb910cc4..2dad1eec 100644 --- a/packages/server/tests/unit/tests/conversations.test.ts +++ b/packages/server/tests/unit/tests/conversations.test.ts @@ -314,6 +314,7 @@ describe('Conversations', () => { expect(response.body.documentId).toMatch(/^doc_/); expect(response.body.actorId).toBe(actorId); expect(typeof response.body.position).toBe('number'); + expect(response.body.content).toBe('Hello world'); addedDocumentId = response.body.documentId; }); diff --git a/packages/server/tests/unit/tests/files.test.ts b/packages/server/tests/unit/tests/files.test.ts index ab30283f..c348401e 100644 --- a/packages/server/tests/unit/tests/files.test.ts +++ b/packages/server/tests/unit/tests/files.test.ts @@ -444,5 +444,18 @@ describe('Files', () => { // Should include files from both projects expect(response.body.length).toBeGreaterThanOrEqual(2); }); + + test('returns 403 when requesting files for a project the user cannot access', async () => { + const forbiddenProjectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Forbidden Files Project' }); + const forbiddenProjectId = forbiddenProjectRes.body.id; + + const response = await authenticatedTestClient(userToken).get( + `/api/v1/files?projectId=${forbiddenProjectId}` + ); + + expect(response.status).toBe(403); + }); }); }); diff --git a/packages/server/tests/unit/tests/mcp.test.ts b/packages/server/tests/unit/tests/mcp.test.ts index d3cdf358..8ec2ea1c 100644 --- a/packages/server/tests/unit/tests/mcp.test.ts +++ b/packages/server/tests/unit/tests/mcp.test.ts @@ -1,4 +1,4 @@ -import { testClient } from '../testClient'; +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; describe('MCP tools/list', () => { test('registers the expected tools', async () => { @@ -23,3 +23,64 @@ describe('MCP tools/list', () => { expect(names).toContain('update-file-metadata'); }); }); + +describe('MCP get-* tools with nonexistent ids', () => { + let adminToken: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'mcpadmin', password: 'mcppass' }); + adminToken = await loginAs('mcpadmin', 'mcppass'); + }); + + const mcpCall = ( + token: string, + toolName: string, + args: Record + ) => { + return authenticatedTestClient(token) + .post('/mcp') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json, text/event-stream') + .send({ + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { name: toolName, arguments: args }, + }); + }; + + test('get-document with nonexistent id returns structured JSON error', async () => { + const res = await mcpCall(adminToken, 'get-document', { + id: 'doc_nonexistent_xyz', + }); + expect(res.status).toBe(200); + const text = res.body.result?.content?.[0]?.text; + expect(text).toBeDefined(); + const parsed = JSON.parse(text); + expect(parsed.error).toBe('not_found'); + }); + + test('get-actor with nonexistent id returns structured JSON error', async () => { + const res = await mcpCall(adminToken, 'get-actor', { + id: 'actor_nonexistent_xyz', + }); + expect(res.status).toBe(200); + const text = res.body.result?.content?.[0]?.text; + expect(text).toBeDefined(); + const parsed = JSON.parse(text); + expect(parsed.error).toBe('not_found'); + }); + + test('get-conversation with nonexistent id returns structured JSON error', async () => { + const res = await mcpCall(adminToken, 'get-conversation', { + id: 'conv_nonexistent_xyz', + }); + expect(res.status).toBe(200); + const text = res.body.result?.content?.[0]?.text; + expect(text).toBeDefined(); + const parsed = JSON.parse(text); + expect(parsed.error).toBe('not_found'); + }); +}); diff --git a/packages/website/docs/modules/actors.md b/packages/website/docs/modules/actors.md index 55288808..c83398f0 100644 --- a/packages/website/docs/modules/actors.md +++ b/packages/website/docs/modules/actors.md @@ -6,7 +6,7 @@ The Actors module represents entities (people, bots, or other participants) that An Actor belongs to a project and has a display name, an optional type, and an optional `externalId`. The `externalId` is unique within a project and is designed for correlating actors with external systems — for example, mapping a WhatsApp phone number to a known contact. -Actors are identified by `publicId` prefixed with `act_`. The internal database primary key is never returned. +Actors are identified by an `id` prefixed with `act_`. The internal database primary key is never returned. ## Data Model diff --git a/packages/website/docs/modules/conversations.md b/packages/website/docs/modules/conversations.md index b67015fc..2f2ed54b 100644 --- a/packages/website/docs/modules/conversations.md +++ b/packages/website/docs/modules/conversations.md @@ -6,7 +6,7 @@ The Conversations module represents a series of messages exchanged with an Actor A Conversation belongs to a project and is associated with an Actor. It has a status (`open` or `closed`) and contains an ordered list of messages, where each message is a reference to a Document along with its position in the conversation. -Conversations are identified by `publicId` prefixed with `conv_`. The internal database primary key is never returned. +Conversations are identified by an `id` prefixed with `conv_`. The internal database primary key is never returned. ## Data Model diff --git a/packages/website/docs/modules/documents.md b/packages/website/docs/modules/documents.md index 7e89c158..7ced4230 100644 --- a/packages/website/docs/modules/documents.md +++ b/packages/website/docs/modules/documents.md @@ -6,7 +6,7 @@ The Documents module stores plain-text documents along with an embedding vector A Document IS a File — it always uses `.txt` format and is associated with a project. When a document is created, its text content is passed to a configured embedding provider (currently Ollama only), and the resulting vector is stored alongside the text. This allows cosine-similarity search at query time without an external vector database. -Documents are identified by `publicId` prefixed with `doc_`. The internal database primary key is never returned. +Documents are identified by an `id` prefixed with `doc_`. The internal database primary key is never returned. ## Configuration diff --git a/packages/website/docs/modules/files.md b/packages/website/docs/modules/files.md index 345c356e..e0e4eb33 100644 --- a/packages/website/docs/modules/files.md +++ b/packages/website/docs/modules/files.md @@ -4,7 +4,7 @@ The Files module provides file upload, download, metadata management, and deleti ## Overview -Files are associated with a project and stored at `{FILES_STORAGE_DIR}/{publicId}{ext}` on the server's local filesystem. Every file record exposes a `publicId` as its `id` — the internal database primary key is never returned. +Files are associated with a project and stored at `{FILES_STORAGE_DIR}/{id}{ext}` on the server's local filesystem. Every file record exposes an `id` — the internal database primary key is never returned. ## Configuration @@ -29,18 +29,18 @@ volumes: ## Data Model -| Field | Type | Description | -| ------------- | ------------------------ | ------------------------------------------------------------------ | -| `id` | string | Public identifier (exposed as `id`, maps to `publicId` internally) | -| `filename` | string | Original filename | -| `contentType` | string | MIME type | -| `size` | number | File size in bytes | -| `storageType` | `local` \| `s3` \| `gcs` | Storage backend (currently `local`) | -| `storagePath` | string | Absolute path on disk | -| `metadata` | string | Arbitrary JSON string for custom metadata | -| `projectId` | string | ID of the owning project | -| `createdAt` | string | ISO 8601 creation timestamp | -| `updatedAt` | string | ISO 8601 last-updated timestamp | +| Field | Type | Description | +| ------------- | ------------------------ | ----------------------------------------- | +| `id` | string | Public identifier | +| `filename` | string | Original filename | +| `contentType` | string | MIME type | +| `size` | number | File size in bytes | +| `storageType` | `local` \| `s3` \| `gcs` | Storage backend (currently `local`) | +| `storagePath` | string | Absolute path on disk | +| `metadata` | string | Arbitrary JSON string for custom metadata | +| `projectId` | string | ID of the owning project | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | ## Permissions diff --git a/packages/website/docusaurus.config.ts b/packages/website/docusaurus.config.ts index 5a905c33..918a5b4a 100644 --- a/packages/website/docusaurus.config.ts +++ b/packages/website/docusaurus.config.ts @@ -22,6 +22,7 @@ const buildOpenApiConfig = () => { outputDir: `docs/api/${name}`, sidebarOptions: { groupPathsBy: 'tag' }, hideSendButton: false, + showInfoPage: false, }, ]; }) diff --git a/packages/website/sidebars.ts b/packages/website/sidebars.ts index 5bf5c1db..d743ae6d 100644 --- a/packages/website/sidebars.ts +++ b/packages/website/sidebars.ts @@ -1,8 +1,5 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; -import filesSidebar from './docs/api/files/sidebar'; -import usersSidebar from './docs/api/users/sidebar'; - // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) /** @@ -28,18 +25,7 @@ const sidebars: SidebarsConfig = { ], // API sidebar - combines the generated OpenAPI sidebars - apiSidebar: [ - { - type: 'category', - label: 'Files API', - items: filesSidebar, - }, - { - type: 'category', - label: 'Users API', - items: usersSidebar, - }, - ], + apiSidebar: [{ type: 'autogenerated', dirName: 'api' }], // But you can create a sidebar manually /* From 53ec49dec6197114f55b396f14d61eba3ceefc74 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Sat, 11 Apr 2026 23:00:54 -0300 Subject: [PATCH 10/27] fix: remove unused files --- .gitignore | 3 ++- .../page-2026-04-12T01-26-58-080Z.yml | 0 .../page-2026-04-12T01-27-02-826Z.png | Bin 61007 -> 0 bytes .../page-2026-04-12T01-28-10-941Z.yml | 0 packages/website/docs/modules/actors.md | 2 +- packages/website/docs/modules/conversations.md | 6 +++--- packages/website/docs/modules/documents.md | 4 ++-- 7 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .playwright-mcp/page-2026-04-12T01-26-58-080Z.yml delete mode 100644 .playwright-mcp/page-2026-04-12T01-27-02-826Z.png delete mode 100644 .playwright-mcp/page-2026-04-12T01-28-10-941Z.yml diff --git a/.gitignore b/.gitignore index 52d2824e..f5aa195e 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,5 @@ github-app/ **/i18n/compiled/ **/i18n/missing/ **/i18n/unused/ -tsup.config.bundled*.mjs \ No newline at end of file +tsup.config.bundled*.mjs +.playwright-mcp/ \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-12T01-26-58-080Z.yml b/.playwright-mcp/page-2026-04-12T01-26-58-080Z.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/.playwright-mcp/page-2026-04-12T01-27-02-826Z.png b/.playwright-mcp/page-2026-04-12T01-27-02-826Z.png deleted file mode 100644 index de57909eaef54acf20c3ac6f0b335e53d1397a9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61007 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 Date: Sun, 12 Apr 2026 07:35:47 -0300 Subject: [PATCH 11/27] fix: smoke tests working --- docker-compose.dev.yml | 9 +- docker-compose.test.yml | 21 +- docs/prd/chats-agents.md | 203 ++++++++++++++++++ package.json | 3 +- packages/postgresdb/src/models/Document.ts | 18 ++ packages/server/src/lib/actors.ts | 25 ++- packages/server/src/lib/agents.ts | 12 ++ packages/server/src/lib/conversations.ts | 26 ++- packages/server/src/lib/documents.ts | 98 ++++++++- packages/server/src/lib/files.ts | 17 +- packages/server/src/mcp/tools/actors.ts | 24 ++- .../server/src/mcp/tools/conversations.ts | 28 ++- packages/server/src/mcp/tools/documents.ts | 75 ++++++- packages/server/src/mcp/tools/files.ts | 16 +- packages/server/src/rest/v1/actors.ts | 60 +++++- packages/server/src/rest/v1/agents.ts | 93 ++++++++ packages/server/src/rest/v1/conversations.ts | 119 ++++++++-- packages/server/src/rest/v1/documents.ts | 167 +++++++++++++- packages/server/src/rest/v1/files.ts | 41 +++- packages/server/src/rest/v1/index.ts | 2 + .../server/tests/unit/tests/actors.test.ts | 124 ++++++++++- .../tests/unit/tests/conversations.test.ts | 22 +- .../server/tests/unit/tests/documents.test.ts | 168 ++++++++++++++- .../server/tests/unit/tests/files.test.ts | 10 +- pnpm-lock.yaml | 16 +- tests/smoke-test.sh | 46 +++- 26 files changed, 1337 insertions(+), 106 deletions(-) create mode 100644 docs/prd/chats-agents.md create mode 100644 packages/server/src/lib/agents.ts create mode 100644 packages/server/src/rest/v1/agents.ts diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 0bbc8781..9d20d38b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,7 +1,7 @@ services: database: image: pgvector/pgvector:0.8.1-pg18-trixie - container_name: soat-database-dev + container_name: soat-database-dev-docker environment: POSTGRES_DB: ${DATABASE_NAME:-soat_dev} POSTGRES_USER: ${DATABASE_USER:-soat_user} @@ -20,13 +20,13 @@ services: ollama: image: ollama/ollama:latest - container_name: soat-ollama-dev + container_name: soat-ollama-dev-docker volumes: - ollama_cache:/root/.ollama entrypoint: - /bin/sh - -c - - 'ollama serve & sleep 5 && ollama pull qwen3-embedding:0.6b && wait' + - 'ollama serve & sleep 5 && ollama pull qwen3-embedding:0.6b && ollama pull qwen2.5:0.5b && wait' healthcheck: test: ['CMD-SHELL', 'ollama list | grep qwen3-embedding'] interval: 10s @@ -36,7 +36,7 @@ services: server: build: . - container_name: soat-server-dev + container_name: soat-server-dev-docker depends_on: database: condition: service_healthy @@ -55,6 +55,7 @@ services: EMBEDDING_PROVIDER: ollama EMBEDDING_MODEL: qwen3-embedding:0.6b EMBEDDING_DIMENSIONS: '1024' + AGENT_MODEL: qwen2.5:0.5b volumes: - files_data:/data/files diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 0318d6c2..8afc7c92 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -5,6 +5,8 @@ services: POSTGRES_DB: soat_test POSTGRES_USER: soat_user POSTGRES_PASSWORD: soat_password + tmpfs: + - /var/lib/postgresql healthcheck: test: ['CMD-SHELL', 'pg_isready -U soat_user -d soat_test'] interval: 5s @@ -14,13 +16,17 @@ services: ollama: image: ollama/ollama:latest volumes: - - ollama_cache:/root/.ollama + - ${HOME}/.ollama:/root/.ollama entrypoint: - /bin/sh - -c - - 'ollama serve & sleep 5 && ollama pull qwen3-embedding:0.6b && wait' + - 'ollama serve & sleep 5 && (ollama list | grep -q qwen3-embedding:0.6b || ollama pull qwen3-embedding:0.6b) && (ollama list | grep -q qwen2.5:0.5b || ollama pull qwen2.5:0.5b) && wait' healthcheck: - test: ['CMD-SHELL', 'ollama list | grep qwen3-embedding'] + test: + [ + 'CMD-SHELL', + 'ollama list | grep qwen3-embedding && ollama list | grep qwen2.5', + ] interval: 10s timeout: 30s retries: 30 @@ -34,6 +40,7 @@ services: ollama: condition: service_healthy environment: + PORT: 50477 DATABASE_HOST: database DATABASE_PORT: '5432' DATABASE_NAME: soat_test @@ -44,13 +51,14 @@ services: EMBEDDING_PROVIDER: ollama EMBEDDING_MODEL: qwen3-embedding:0.6b EMBEDDING_DIMENSIONS: '1024' + AGENT_MODEL: qwen2.5:0.5b healthcheck: test: - CMD - node - '-e' - >- - require('http').get('http://localhost:5047/health', + require('http').get('http://localhost:50477/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); } ).on('error', () => process.exit(1)) interval: 5s @@ -66,8 +74,5 @@ services: volumes: - ./tests/smoke-test.sh:/smoke-test.sh:ro environment: - SERVER_URL: 'http://server:5047' + SERVER_URL: 'http://server:50477' command: sh -c "apk add --no-cache curl jq && sh /smoke-test.sh" - -volumes: - ollama_cache: diff --git a/docs/prd/chats-agents.md b/docs/prd/chats-agents.md new file mode 100644 index 00000000..e5ac1ca3 --- /dev/null +++ b/docs/prd/chats-agents.md @@ -0,0 +1,203 @@ +# PRD: Chats and Agents Modules + +## Context + +Soat acts as an **LLM Gateway**: applications call Soat instead of calling LLM providers directly. Soat adds auth, routing, logging, and cost tracking as middleware. Applications swap only `baseURL` and `apiKey` — no other client-side changes required. + +Two distinct interaction patterns emerge from this role, each deserving its own module: + +| | Chats | Agents | +| --------- | ------------------------ | ----------------------------- | +| Pattern | Request → Response | Loop until goal is reached | +| Tools | No | Yes | +| Latency | Short | Variable (seconds to minutes) | +| API shape | OpenAI-compatible | Soat-native | +| State | Per-conversation history | Per-run scratchpad | + +--- + +## Module: Chats + +### Overview + +The Chats module exposes an **OpenAI Chat Completions-compatible API**. Any client already using the OpenAI SDK can point its `baseURL` at Soat and work without code changes. + +Conversations are persistent: a chat has an ID, and messages accumulate in it across multiple calls. + +### Data Model + +The existing `Conversation` and `ConversationMessage` models map directly: + +- `Conversation` — the chat session; scoped to a `Project`; has a `status` (`open` | `closed`) +- `ConversationMessage` — a single turn; references a `Document` (message content stored as a file), an `Actor` (sender), and a `position` (ordering within the conversation) +- `Actor` — the identity sending a message; scoped to a `Project`; carries an `externalId` for the calling application to use its own user IDs + +### API + +``` +POST /v1/chats Create a new chat +GET /v1/chats List chats (scoped to project) +GET /v1/chats/{chatId} Get chat metadata +DELETE /v1/chats/{chatId} Delete a chat and all messages + +POST /v1/chats/{chatId}/completions Send a message and get a completion +GET /v1/chats/{chatId}/messages List messages in a chat +``` + +#### `POST /v1/chats/{chatId}/completions` + +Request body mirrors OpenAI's `POST /v1/chat/completions`: + +```json +{ + "model": "llama3.2", + "messages": [{ "role": "user", "content": "What files do I have?" }], + "stream": false +} +``` + +- `model` — optional; falls back to `CHAT_MODEL` env var +- `messages` — the new turn(s) to append; prior history is loaded from the DB automatically +- `stream` — `false` returns JSON; `true` returns SSE (`text/event-stream`) + +Response (non-streaming) mirrors OpenAI's response shape: + +```json +{ + "id": "conv_V1StGXR8Z5jdHi6B", + "object": "chat.completion", + "model": "llama3.2", + "choices": [ + { + "index": 0, + "message": { "role": "assistant", "content": "You have 3 files: ..." }, + "finish_reason": "stop" + } + ] +} +``` + +Streaming response: SSE chunks with `data: {"choices":[{"delta":{"content":"..."}}]}` matching the OpenAI streaming format, terminated with `data: [DONE]`. + +### Behaviour + +- Each call to `/completions` **appends** the incoming messages and the model's reply to the conversation's message history. +- The full prior history is passed to the LLM on each call. +- History trimming (context window management) is handled transparently by Soat. +- No tool execution. If an application needs tools, it uses the Agents module. + +### Public ID prefix + +`chat_` — introduced alongside this module. The existing `conv_` prefix on `Conversation` maps to chats. + +--- + +## Module: Agents + +### Overview + +The Agents module executes **multi-step, tool-using workflows**. The caller provides a goal; the agent decides which tools to call and in what order until the goal is met or a step limit is reached. + +This is the ReAct loop pattern: Reason → Act → Observe → repeat. + +There is no OpenAI-compatible wrapper here — the API surface is Soat-native because no equivalent standard exists for agentic loops. + +### Data Model + +Agents do not require a persistent DB entity for most use cases (a run is ephemeral). Future iterations may persist run history, but v1 is stateless per run. + +If persistence is needed in the future: + +- `AgentRun` — one execution; has a `status` (`running` | `completed` | `failed`), a `goal`, and a reference to the project +- `AgentStep` — one ReAct iteration within a run; stores the model's reasoning, the tool called, and the tool result + +### API + +``` +POST /v1/agents/run Run an agent to completion (blocking, JSON response) +POST /v1/agents/run/stream Run an agent with streaming output (SSE) +``` + +#### `POST /v1/agents/run/stream` + +Already implemented. Request: + +```json +{ + "model": "qwen2.5:0.5b", + "prompt": "Summarise all documents in project proj_XYZ" +} +``` + +Response: SSE chunks: + +``` +data: {"text":"I'll look at the documents..."} +data: {"text":" Here is the summary:"} +data: {"event":"done"} +``` + +#### `POST /v1/agents/run` (future) + +Same request body; returns when the agent finishes: + +```json +{ + "runId": "run_V1StGXR8Z5jdHi6B", + "status": "completed", + "result": "...", + "steps": 4 +} +``` + +### Tools available to agents + +Tools are registered from the MCP tool definitions. Current tool surface: + +| Tool | Description | +| -------------------- | ----------------------- | +| `list-files` | List files in a project | +| `list-documents` | List documents | +| `list-conversations` | List conversations | +| `list-projects` | List projects | +| `list-actors` | List actors | + +Future tool additions (e.g. `create-document`, `search-documents`) extend the registry without changing the agent API. + +### Stop conditions (v1) + +- Default: maximum 20 steps +- Agent emits a `done` step with no tool call + +--- + +## Implementation Notes + +### SDK dependency + +Current state: `ollama@^0.6.3` (native). No tool loop. + +To implement the Agents module properly (tool execution, multi-step), either: + +1. **Stay native**: implement a manual ReAct loop with `ollama.chat()` + tool dispatch +2. **Migrate to Vercel AI SDK**: install `ai` + `@ai-sdk/ollama`; use `ToolLoopAgent` for modes 3–8 and `streamText` for the Chats module + +Option 2 is preferred: `ToolLoopAgent` handles stop conditions, context trimming, and dynamic tool selection out of the box. `streamText` produces an OpenAI-compatible streaming format needed by the Chats module. + +### Module file locations (following codebase conventions) + +``` +packages/server/src/lib/chats.ts +packages/server/src/rest/v1/chats.ts +packages/server/src/mcp/tools/chats.ts +packages/website/docs/modules/chats.md + +packages/server/src/lib/agents.ts (exists, extend) +packages/server/src/rest/v1/agents.ts (exists, extend) +packages/server/src/mcp/tools/agents.ts (new) +packages/website/docs/modules/agents.md (new) +``` + +### Migration from `conversations` to `chats` + +The existing `Conversation` / `ConversationMessage` models are reused as-is. The Chats REST module is a new API layer on top of the same DB tables. The existing `conversations` REST routes remain until deprecated. diff --git a/package.json b/package.json index 1c518959..26fbffdf 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ }, "scripts": { "prepare": "husky", - "smoke-test": "docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from smoke-test", + "smoke-test": "docker compose -f docker-compose.test.yml up --build --renew-anon-volumes --remove-orphans --abort-on-container-exit --exit-code-from smoke-test", + "postsmoke-test": "docker compose -f docker-compose.test.yml down --volumes", "dev": "docker compose -f docker-compose.dev.yml up --build", "syncpack:fix": "syncpack fix", "syncpack:lint": "syncpack lint" diff --git a/packages/postgresdb/src/models/Document.ts b/packages/postgresdb/src/models/Document.ts index 06213938..2e61f5ca 100644 --- a/packages/postgresdb/src/models/Document.ts +++ b/packages/postgresdb/src/models/Document.ts @@ -39,6 +39,24 @@ export class Document extends Model { }) declare file: File; + @Column({ + type: DataType.STRING, + allowNull: true, + }) + declare title: string | null; + + @Column({ + type: DataType.TEXT, + allowNull: true, + }) + declare metadata: string | null; + + @Column({ + type: DataType.ARRAY(DataType.TEXT), + allowNull: true, + }) + declare tags: string[] | null; + @Column({ type: DataType.VECTOR(1024), allowNull: true, diff --git a/packages/server/src/lib/actors.ts b/packages/server/src/lib/actors.ts index 1cfce958..6fa47f83 100644 --- a/packages/server/src/lib/actors.ts +++ b/packages/server/src/lib/actors.ts @@ -1,3 +1,5 @@ +import { Op } from '@ttoss/postgresdb'; + import { db } from '../db'; const mapActor = ( @@ -19,9 +21,16 @@ const mapActor = ( export const listActors = async (args: { projectIds?: number[]; externalId?: string; + name?: string; + type?: string; + limit?: number; + offset?: number; }) => { + const limit = args.limit ?? 50; + const offset = args.offset ?? 0; + if (args.projectIds !== undefined && args.projectIds.length === 0) { - return []; + return { data: [], total: 0, limit, offset }; } const where: Record = {}; @@ -34,12 +43,22 @@ export const listActors = async (args: { where.externalId = args.externalId; } - const actors = await db.Actor.findAll({ + if (args.name !== undefined) { + where.name = { [Op.iLike]: `%${args.name}%` }; + } + + if (args.type !== undefined) { + where.type = args.type; + } + + const { count, rows } = await db.Actor.findAndCountAll({ where: Object.keys(where).length > 0 ? where : undefined, include: [{ model: db.Project, as: 'project' }], + limit, + offset, }); - return actors.map(mapActor); + return { data: rows.map(mapActor), total: count, limit, offset }; }; export const getActor = async (args: { id: string }) => { diff --git a/packages/server/src/lib/agents.ts b/packages/server/src/lib/agents.ts new file mode 100644 index 00000000..7f8fc607 --- /dev/null +++ b/packages/server/src/lib/agents.ts @@ -0,0 +1,12 @@ +import { Ollama } from 'ollama'; + +export const streamAgent = async (args: { model: string; prompt: string }) => { + const host = process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434'; + const ollama = new Ollama({ host }); + + return ollama.chat({ + model: args.model, + messages: [{ role: 'user', content: args.prompt }], + stream: true, + }); +}; diff --git a/packages/server/src/lib/conversations.ts b/packages/server/src/lib/conversations.ts index 6949e117..6ed709d9 100644 --- a/packages/server/src/lib/conversations.ts +++ b/packages/server/src/lib/conversations.ts @@ -47,9 +47,14 @@ const mapMessage = ( export const listConversations = async (args: { projectIds?: number[]; actorId?: string; + limit?: number; + offset?: number; }) => { + const limit = args.limit ?? 50; + const offset = args.offset ?? 0; + if (args.projectIds !== undefined && args.projectIds.length === 0) { - return []; + return { data: [], total: 0, limit, offset }; } const where: Record = {}; @@ -61,7 +66,7 @@ export const listConversations = async (args: { if (args.actorId !== undefined) { const actor = await db.Actor.findOne({ where: { publicId: args.actorId } }); if (!actor) { - return []; + return { data: [], total: 0, limit, offset }; } const messages = await db.ConversationMessage.findAll({ where: { actorId: actor.id }, @@ -74,12 +79,14 @@ export const listConversations = async (args: { where.id = conversationIds; } - const conversations = await db.Conversation.findAll({ + const { count, rows } = await db.Conversation.findAndCountAll({ where: Object.keys(where).length > 0 ? where : undefined, include: [{ model: db.Project, as: 'project' }], + limit, + offset, }); - return conversations.map(mapConversation); + return { data: rows.map(mapConversation), total: count, limit, offset }; }; export const getConversation = async (args: { id: string }) => { @@ -150,7 +157,12 @@ export const deleteConversation = async (args: { id: string }) => { export const listConversationMessages = async (args: { conversationId: string; + limit?: number; + offset?: number; }) => { + const limit = args.limit ?? 50; + const offset = args.offset ?? 0; + const conversation = await db.Conversation.findOne({ where: { publicId: args.conversationId }, }); @@ -159,7 +171,7 @@ export const listConversationMessages = async (args: { return null; } - const messages = await db.ConversationMessage.findAll({ + const { count, rows } = await db.ConversationMessage.findAndCountAll({ where: { conversationId: conversation.id }, include: [ { @@ -170,9 +182,11 @@ export const listConversationMessages = async (args: { { model: db.Actor, as: 'actor' }, ], order: [['position', 'ASC']], + limit, + offset, }); - return messages.map(mapMessage); + return { data: rows.map(mapMessage), total: count, limit, offset }; }; export const addConversationMessage = async (args: { diff --git a/packages/server/src/lib/documents.ts b/packages/server/src/lib/documents.ts index b9f7fd71..8f5d4cf1 100644 --- a/packages/server/src/lib/documents.ts +++ b/packages/server/src/lib/documents.ts @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { Op } from '@ttoss/postgresdb'; + import { db } from '../db'; import { getEmbedding } from './embedding'; @@ -25,20 +27,39 @@ const mapDocument = ( projectId: doc.file?.project?.publicId, filename: doc.file?.filename, size: doc.file?.size, + title: doc.title ?? undefined, + metadata: doc.metadata + ? (() => { + try { + return JSON.parse(doc.metadata!); + } catch { + return doc.metadata; + } + })() + : undefined, + tags: doc.tags ?? undefined, createdAt: doc.createdAt, updatedAt: doc.updatedAt, }; }; -export const listDocuments = async (args: { projectIds?: number[] }) => { +export const listDocuments = async (args: { + projectIds?: number[]; + limit?: number; + offset?: number; +}) => { + const limit = args.limit ?? 50; + const offset = args.offset ?? 0; + if (args.projectIds !== undefined && args.projectIds.length === 0) { - return []; + return { data: [], total: 0, limit, offset }; } const fileWhere = args.projectIds !== undefined ? { projectId: args.projectIds } : undefined; - const documents = await db.Document.findAll({ + const { count, rows } = await db.Document.findAndCountAll({ + distinct: true, include: [ { model: db.File, @@ -47,8 +68,10 @@ export const listDocuments = async (args: { projectIds?: number[] }) => { include: [{ model: db.Project, as: 'project' }], }, ], + limit, + offset, }); - return documents.map(mapDocument); + return { data: rows.map(mapDocument), total: count, limit, offset }; }; export const getDocument = async (args: { id: string }) => { @@ -81,6 +104,9 @@ export const createDocument = async (args: { projectId: number; content: string; filename?: string; + title?: string; + metadata?: Record; + tags?: string[]; }) => { const storageDir = getStorageDir(); fs.mkdirSync(storageDir, { recursive: true }); @@ -106,6 +132,9 @@ export const createDocument = async (args: { const doc = await db.Document.create({ fileId: file.id, embedding, + title: args.title ?? null, + metadata: args.metadata ? JSON.stringify(args.metadata) : null, + tags: args.tags ?? null, }); const created = await db.Document.findOne({ @@ -148,11 +177,66 @@ export const deleteDocument = async (args: { id: string }) => { return true; }; +export const updateDocument = async (args: { + id: string; + content?: string; + title?: string; + metadata?: Record; + tags?: string[]; +}) => { + const doc = await db.Document.findOne({ + where: { publicId: args.id }, + include: [ + { + model: db.File, + as: 'file', + include: [{ model: db.Project, as: 'project' }], + }, + ], + }); + + if (!doc) { + return null; + } + + if (args.content !== undefined && doc.file?.storagePath) { + fs.writeFileSync(doc.file.storagePath, args.content, 'utf-8'); + await doc.file.update({ + size: Buffer.byteLength(args.content, 'utf-8'), + }); + const embedding = await getEmbedding({ text: args.content }); + await doc.update({ embedding }); + } + + const updates: Record = {}; + if (args.title !== undefined) updates.title = args.title; + if (args.metadata !== undefined) + updates.metadata = JSON.stringify(args.metadata); + if (args.tags !== undefined) updates.tags = args.tags; + if (Object.keys(updates).length > 0) { + await doc.update(updates); + } + + const refreshed = await db.Document.findOne({ + where: { id: doc.id }, + include: [ + { + model: db.File, + as: 'file', + include: [{ model: db.Project, as: 'project' }], + }, + ], + }); + + return mapDocument(refreshed!); +}; + export const searchDocuments = async (args: { projectIds?: number[]; query: string; limit?: number; threshold?: number; + tags?: string[]; }) => { if (args.projectIds !== undefined && args.projectIds.length === 0) { return []; @@ -165,7 +249,13 @@ export const searchDocuments = async (args: { const fileWhere = args.projectIds !== undefined ? { projectId: args.projectIds } : undefined; + const docWhere: Record = + args.tags && args.tags.length > 0 + ? { tags: { [Op.overlap]: args.tags } } + : {}; + const documents = await db.Document.findAll({ + where: docWhere, attributes: { include: [ [ diff --git a/packages/server/src/lib/files.ts b/packages/server/src/lib/files.ts index 483875cf..2590a5ef 100644 --- a/packages/server/src/lib/files.ts +++ b/packages/server/src/lib/files.ts @@ -25,9 +25,16 @@ const mapFile = (file: InstanceType<(typeof db)['File']>) => { }; }; -export const listFiles = async (args: { projectIds?: number[] }) => { +export const listFiles = async (args: { + projectIds?: number[]; + limit?: number; + offset?: number; +}) => { + const limit = args.limit ?? 50; + const offset = args.offset ?? 0; + if (args.projectIds !== undefined && args.projectIds.length === 0) { - return []; + return { data: [], total: 0, limit, offset }; } const where: Record = {}; @@ -36,10 +43,12 @@ export const listFiles = async (args: { projectIds?: number[] }) => { where.projectId = args.projectIds; } - const allFiles = await db.File.findAll({ + const { count, rows } = await db.File.findAndCountAll({ where: Object.keys(where).length > 0 ? where : undefined, + limit, + offset, }); - return allFiles.map(mapFile); + return { data: rows.map(mapFile), total: count, limit, offset }; }; export const getFile = async (args: { id: string }) => { diff --git a/packages/server/src/mcp/tools/actors.ts b/packages/server/src/mcp/tools/actors.ts index 7471b64c..b2bde50d 100644 --- a/packages/server/src/mcp/tools/actors.ts +++ b/packages/server/src/mcp/tools/actors.ts @@ -6,19 +6,39 @@ const registerTools = (server: McpServer) => { 'list-actors', { description: - 'List actors. If projectId is omitted, returns all actors accessible to the caller. Optionally filter by externalId (e.g. WhatsApp phone number).', + 'List actors. If projectId is omitted, returns all actors accessible to the caller. Optionally filter by externalId, name (partial match), or type (exact match).', inputSchema: { projectId: z.string().optional().describe('Project ID (optional)'), externalId: z .string() .optional() .describe('External ID to filter by (e.g. WhatsApp phone number)'), + name: z + .string() + .optional() + .describe('Partial, case-insensitive name filter'), + type: z + .string() + .optional() + .describe('Exact type filter (e.g. customer, agent)'), + limit: z + .number() + .optional() + .describe('Maximum number of results to return (default 50)'), + offset: z + .number() + .optional() + .describe('Number of results to skip (default 0)'), }, }, - async ({ projectId, externalId }) => { + async ({ projectId, externalId, name, type, limit, offset }) => { const params = new URLSearchParams(); if (projectId) params.set('projectId', projectId); if (externalId) params.set('externalId', externalId); + if (name) params.set('name', name); + if (type) params.set('type', type); + if (limit !== undefined) params.set('limit', String(limit)); + if (offset !== undefined) params.set('offset', String(offset)); const qs = params.toString() ? `?${params.toString()}` : ''; const data = await apiCall('GET', `/actors${qs}`); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; diff --git a/packages/server/src/mcp/tools/conversations.ts b/packages/server/src/mcp/tools/conversations.ts index 5954d80f..a69260af 100644 --- a/packages/server/src/mcp/tools/conversations.ts +++ b/packages/server/src/mcp/tools/conversations.ts @@ -13,12 +13,22 @@ const registerTools = (server: McpServer) => { .string() .optional() .describe('Actor ID to filter conversations by'), + limit: z + .number() + .optional() + .describe('Maximum number of results to return (default 50)'), + offset: z + .number() + .optional() + .describe('Number of results to skip (default 0)'), }, }, - async ({ projectId, actorId }) => { + async ({ projectId, actorId, limit, offset }) => { const params = new URLSearchParams(); if (projectId) params.set('projectId', projectId); if (actorId) params.set('actorId', actorId); + if (limit !== undefined) params.set('limit', String(limit)); + if (offset !== undefined) params.set('offset', String(offset)); const qs = params.toString() ? `?${params.toString()}` : ''; const data = await apiCall('GET', `/conversations${qs}`); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; @@ -141,10 +151,22 @@ const registerTools = (server: McpServer) => { 'List all messages (documents) in a conversation, ordered by position', inputSchema: { id: z.string().describe('Conversation ID'), + limit: z + .number() + .optional() + .describe('Maximum number of results to return (default 50)'), + offset: z + .number() + .optional() + .describe('Number of results to skip (default 0)'), }, }, - async ({ id }) => { - const data = await apiCall('GET', `/conversations/${id}/messages`); + async ({ id, limit, offset }) => { + const params = new URLSearchParams(); + if (limit !== undefined) params.set('limit', String(limit)); + if (offset !== undefined) params.set('offset', String(offset)); + const qs = params.toString() ? `?${params.toString()}` : ''; + const data = await apiCall('GET', `/conversations/${id}/messages${qs}`); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } ); diff --git a/packages/server/src/mcp/tools/documents.ts b/packages/server/src/mcp/tools/documents.ts index 4b645652..ec31fa1a 100644 --- a/packages/server/src/mcp/tools/documents.ts +++ b/packages/server/src/mcp/tools/documents.ts @@ -9,10 +9,22 @@ const registerTools = (server: McpServer) => { 'List documents. If projectId is omitted, returns all documents accessible to the caller.', inputSchema: { projectId: z.string().optional().describe('Project ID (optional)'), + limit: z + .number() + .optional() + .describe('Maximum number of results to return (default 50)'), + offset: z + .number() + .optional() + .describe('Number of results to skip (default 0)'), }, }, - async ({ projectId }) => { - const qs = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ''; + async ({ projectId, limit, offset }) => { + const params = new URLSearchParams(); + if (projectId) params.set('projectId', projectId); + if (limit !== undefined) params.set('limit', String(limit)); + if (offset !== undefined) params.set('offset', String(offset)); + const qs = params.toString() ? `?${params.toString()}` : ''; const data = await apiCall('GET', `/documents${qs}`); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } @@ -60,11 +72,17 @@ const registerTools = (server: McpServer) => { ), content: z.string().describe('Text content of the document'), filename: z.string().optional().describe('Optional filename'), + title: z.string().optional().describe('Optional document title'), + metadata: z + .record(z.unknown()) + .optional() + .describe('Arbitrary key-value metadata'), + tags: z.array(z.string()).optional().describe('Optional list of tags'), }, }, - async ({ projectId, content, filename }) => { + async ({ projectId, content, filename, title, metadata, tags }) => { const data = await apiCall('POST', '/documents', { - body: { projectId, content, filename }, + body: { projectId, content, filename, title, metadata, tags }, }); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } @@ -124,15 +142,60 @@ const registerTools = (server: McpServer) => { .describe( 'Minimum similarity score (0-1). Only results with score >= threshold are returned.' ), + tags: z + .array(z.string()) + .optional() + .describe('Filter to documents with any of these tags'), }, }, - async ({ projectId, query, limit, threshold }) => { + async ({ projectId, query, limit, threshold, tags }) => { const data = await apiCall('POST', '/documents/search', { - body: { projectId, query, limit, threshold }, + body: { projectId, query, limit, threshold, tags }, }); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } ); + + server.registerTool( + 'update-document', + { + description: + 'Update a document by ID. Can update content (re-embeds), title, metadata, or tags.', + inputSchema: { + id: z.string().describe('Document ID'), + content: z + .string() + .optional() + .describe('New text content (re-computes the embedding)'), + title: z.string().optional().describe('New title'), + metadata: z + .record(z.unknown()) + .optional() + .describe('New metadata object'), + tags: z.array(z.string()).optional().describe('New list of tags'), + }, + }, + async ({ id, content, title, metadata, tags }) => { + try { + const data = await apiCall('PATCH', `/documents/${id}`, { + body: { content, title, metadata, tags }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: 'not_found', + message: String(error), + }), + }, + ], + }; + } + } + ); }; export { registerTools }; diff --git a/packages/server/src/mcp/tools/files.ts b/packages/server/src/mcp/tools/files.ts index 22146f43..555b56f2 100644 --- a/packages/server/src/mcp/tools/files.ts +++ b/packages/server/src/mcp/tools/files.ts @@ -8,10 +8,22 @@ const registerTools = (server: McpServer) => { description: 'List files. Optionally filter by projectId.', inputSchema: { projectId: z.string().optional().describe('Project ID to filter by'), + limit: z + .number() + .optional() + .describe('Maximum number of results to return (default 50)'), + offset: z + .number() + .optional() + .describe('Number of results to skip (default 0)'), }, }, - async ({ projectId }) => { - const qs = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ''; + async ({ projectId, limit, offset }) => { + const params = new URLSearchParams(); + if (projectId) params.set('projectId', projectId); + if (limit !== undefined) params.set('limit', String(limit)); + if (offset !== undefined) params.set('offset', String(offset)); + const qs = params.toString() ? `?${params.toString()}` : ''; const data = await apiCall('GET', `/files${qs}`); return { content: [{ type: 'text', text: JSON.stringify(data) }] }; } diff --git a/packages/server/src/rest/v1/actors.ts b/packages/server/src/rest/v1/actors.ts index 349d597f..222e48c8 100644 --- a/packages/server/src/rest/v1/actors.ts +++ b/packages/server/src/rest/v1/actors.ts @@ -35,15 +35,52 @@ const actorsRouter = new Router(); * schema: * type: string * example: '+15551234567' + * - name: name + * in: query + * required: false + * description: Partial, case-insensitive name filter + * schema: + * type: string + * example: 'alice' + * - name: type + * in: query + * required: false + * description: Exact type filter (e.g. customer, agent) + * schema: + * type: string + * example: 'customer' + * - name: limit + * in: query + * required: false + * description: Maximum number of results to return (default 50) + * schema: + * type: integer + * example: 50 + * - name: offset + * in: query + * required: false + * description: Number of results to skip (default 0) + * schema: + * type: integer + * example: 0 * responses: * '200': * description: List of actors * content: * application/json: * schema: - * type: array - * items: - * $ref: '#/components/schemas/ActorRecord' + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/ActorRecord' + * total: + * type: integer + * limit: + * type: integer + * offset: + * type: integer * '401': * description: Unauthorized * content: @@ -66,6 +103,14 @@ actorsRouter.get('/actors', async (ctx: Context) => { const projectPublicId = ctx.query.projectId as string | undefined; const externalId = ctx.query.externalId as string | undefined; + const name = ctx.query.name as string | undefined; + const type = ctx.query.type as string | undefined; + const limit = ctx.query.limit + ? parseInt(ctx.query.limit as string, 10) + : undefined; + const offset = ctx.query.offset + ? parseInt(ctx.query.offset as string, 10) + : undefined; const projectIds = await ctx.authUser.resolveProjectIds({ projectPublicId, @@ -78,7 +123,14 @@ actorsRouter.get('/actors', async (ctx: Context) => { return; } - ctx.body = await listActors({ projectIds, externalId }); + ctx.body = await listActors({ + projectIds, + externalId, + name, + type, + limit, + offset, + }); }); /** diff --git a/packages/server/src/rest/v1/agents.ts b/packages/server/src/rest/v1/agents.ts new file mode 100644 index 00000000..f392da82 --- /dev/null +++ b/packages/server/src/rest/v1/agents.ts @@ -0,0 +1,93 @@ +import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { streamAgent } from 'src/lib/agents'; + +const agentsRouter = new Router(); + +/** + * @openapi + * /agents/run/stream: + * post: + * tags: + * - Agents + * summary: Stream an agent response via SSE + * description: Streams a text response from the agent using Server-Sent Events. Requires authentication. + * operationId: runAgentStream + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - prompt + * properties: + * model: + * type: string + * description: Model name to use. Defaults to AGENT_MODEL env var. + * example: qwen2.5:0.5b + * prompt: + * type: string + * description: The user prompt to send to the agent. + * example: Hello, who are you? + * responses: + * '200': + * description: SSE stream of text chunks + * content: + * text/event-stream: + * schema: + * type: string + * '400': + * description: Missing required fields + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * '401': + * $ref: '#/components/responses/Unauthorized' + */ +agentsRouter.post('/agents/run/stream', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { model?: string; prompt?: string }; + const model = body.model ?? process.env.AGENT_MODEL; + + if (!model || !body.prompt) { + ctx.status = 400; + ctx.body = { + error: + 'prompt is required, and model must be provided or set via AGENT_MODEL', + }; + return; + } + + ctx.respond = false; + ctx.res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }); + + try { + const stream = await streamAgent({ model, prompt: body.prompt }); + for await (const chunk of stream) { + const text = chunk.message.content; + if (text) { + ctx.res.write(`data: ${JSON.stringify({ text })}\n\n`); + } + } + ctx.res.write('event: done\ndata: {}\n\n'); + } catch (error) { + ctx.res.write( + `event: error\ndata: ${JSON.stringify({ error: String(error) })}\n\n` + ); + } finally { + ctx.res.end(); + } +}); + +export { agentsRouter }; diff --git a/packages/server/src/rest/v1/conversations.ts b/packages/server/src/rest/v1/conversations.ts index 9774a93f..97adc29e 100644 --- a/packages/server/src/rest/v1/conversations.ts +++ b/packages/server/src/rest/v1/conversations.ts @@ -39,27 +39,75 @@ const conversationsRouter = new Router(); * schema: * type: string * example: 'act_V1StGXR8Z5jdHi6B' + * - name: limit + * in: query + * required: false + * description: Maximum number of results to return (default 50) + * schema: + * type: integer + * example: 50 + * - name: offset + * in: query + * required: false + * description: Number of results to skip (default 0) + * schema: + * type: integer + * example: 0 * responses: * '200': * description: List of conversations * content: * application/json: * schema: - * type: array - * items: - * $ref: '#/components/schemas/ConversationRecord' - * '401': - * description: Unauthorized - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ErrorResponse' - * '403': - * description: Forbidden + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/ConversationRecord' + * total: + * type: integer + * limit: + * type: integer + * offset: + * type: integer + * required: false + * description: Number of results to skip (default 0) + * schema: + * type: integer + * example: 0 + * responses: + * '200': + * description: List of conversations * content: * application/json: * schema: - * $ref: '#/components/schemas/ErrorResponse' + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/ConversationRecord' + * total: + * type: integer + * limit: + * type: integer + * offset: + const limit = ctx.query.limit ? parseInt(ctx.query.limit as string, 10) : undefined; + const offset = ctx.query.offset ? parseInt(ctx.query.offset as string, 10) : undefined; + + const projectIds = await ctx.authUser.resolveProjectIds({ + projectPublicId, + action: 'conversations:ListConversations', + }); + + if (projectIds === null) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await listConversations({ projectIds, actorId, limit, offset' */ conversationsRouter.get('/conversations', async (ctx: Context) => { if (!ctx.authUser) { @@ -70,6 +118,12 @@ conversationsRouter.get('/conversations', async (ctx: Context) => { const projectPublicId = ctx.query.projectId as string | undefined; const actorId = ctx.query.actorId as string | undefined; + const limit = ctx.query.limit + ? parseInt(ctx.query.limit as string, 10) + : undefined; + const offset = ctx.query.offset + ? parseInt(ctx.query.offset as string, 10) + : undefined; const projectIds = await ctx.authUser.resolveProjectIds({ projectPublicId, @@ -82,7 +136,7 @@ conversationsRouter.get('/conversations', async (ctx: Context) => { return; } - ctx.body = await listConversations({ projectIds, actorId }); + ctx.body = await listConversations({ projectIds, actorId, limit, offset }); }); /** @@ -426,6 +480,7 @@ conversationsRouter.delete('/conversations/:id', async (ctx: Context) => { } await deleteConversation({ id: ctx.params.id }); + ctx.status = 204; }); @@ -446,15 +501,38 @@ conversationsRouter.delete('/conversations/:id', async (ctx: Context) => { * schema: * type: string * example: 'conv_V1StGXR8Z5jdHi6B' + * - name: limit + * in: query + * required: false + * description: Maximum number of results to return (default 50) + * schema: + * type: integer + * example: 50 + * - name: offset + * in: query + * required: false + * description: Number of results to skip (default 0) + * schema: + * type: integer + * example: 0 * responses: * '200': * description: List of messages * content: * application/json: * schema: - * type: array - * items: - * $ref: '#/components/schemas/ConversationMessageRecord' + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/ConversationMessageRecord' + * total: + * type: integer + * limit: + * type: integer + * offset: + * type: integer * '401': * description: Unauthorized * content: @@ -499,8 +577,17 @@ conversationsRouter.get('/conversations/:id/messages', async (ctx: Context) => { return; } + const limit = ctx.query.limit + ? parseInt(ctx.query.limit as string, 10) + : undefined; + const offset = ctx.query.offset + ? parseInt(ctx.query.offset as string, 10) + : undefined; + const messages = await listConversationMessages({ conversationId: ctx.params.id, + limit, + offset, }); ctx.body = messages; diff --git a/packages/server/src/rest/v1/documents.ts b/packages/server/src/rest/v1/documents.ts index b27aeb7f..0ebcd515 100644 --- a/packages/server/src/rest/v1/documents.ts +++ b/packages/server/src/rest/v1/documents.ts @@ -7,6 +7,7 @@ import { getDocument, listDocuments, searchDocuments, + updateDocument, } from 'src/lib/documents'; const documentsRouter = new Router(); @@ -28,15 +29,38 @@ const documentsRouter = new Router(); * schema: * type: string * example: 'proj_V1StGXR8Z5jdHi6B' + * - name: limit + * in: query + * required: false + * description: Maximum number of results to return (default 50) + * schema: + * type: integer + * example: 50 + * - name: offset + * in: query + * required: false + * description: Number of results to skip (default 0) + * schema: + * type: integer + * example: 0 * responses: * '200': * description: List of documents * content: * application/json: * schema: - * type: array - * items: - * $ref: '#/components/schemas/DocumentRecord' + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/DocumentRecord' + * total: + * type: integer + * limit: + * type: integer + * offset: + * type: integer * '401': * description: Unauthorized * content: @@ -58,6 +82,12 @@ documentsRouter.get('/documents', async (ctx: Context) => { } const projectPublicId = ctx.query.projectId as string | undefined; + const limit = ctx.query.limit + ? parseInt(ctx.query.limit as string, 10) + : undefined; + const offset = ctx.query.offset + ? parseInt(ctx.query.offset as string, 10) + : undefined; const projectIds = await ctx.authUser!.resolveProjectIds({ projectPublicId, @@ -70,7 +100,7 @@ documentsRouter.get('/documents', async (ctx: Context) => { return; } - ctx.body = await listDocuments({ projectIds }); + ctx.body = await listDocuments({ projectIds, limit, offset }); }); /** @@ -172,6 +202,17 @@ documentsRouter.get('/documents/:id', async (ctx: Context) => { * filename: * type: string * example: 'my-doc.txt' + * title: + * type: string + * example: 'My Document' + * metadata: + * type: object + * description: Arbitrary key-value metadata + * tags: + * type: array + * items: + * type: string + * example: ['tag1', 'tag2'] * responses: * '201': * description: Document created @@ -209,6 +250,9 @@ documentsRouter.post('/documents', async (ctx: Context) => { projectId?: string; content: string; filename?: string; + title?: string; + metadata?: Record; + tags?: string[]; }; if (!body.content) { @@ -252,6 +296,9 @@ documentsRouter.post('/documents', async (ctx: Context) => { projectId: project.id, content: body.content, filename: body.filename, + title: body.title, + metadata: body.metadata, + tags: body.tags, }); ctx.status = 201; @@ -333,6 +380,110 @@ documentsRouter.delete('/documents/:id', async (ctx: Context) => { ctx.status = 204; }); +/** + * @openapi + * /documents/{id}: + * patch: + * tags: + * - Documents + * summary: Update a document + * description: Update a document's content, title, metadata, or tags. Updating content re-computes the embedding vector. + * operationId: updateDocument + * parameters: + * - name: id + * in: path + * required: true + * description: Document ID + * schema: + * type: string + * example: 'doc_V1StGXR8Z5jdHi6B' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * description: New text content (re-embeds the document) + * title: + * type: string + * metadata: + * type: object + * tags: + * type: array + * items: + * type: string + * responses: + * '200': + * description: Document updated + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DocumentRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Document not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.patch('/documents/:id', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const doc = await getDocument({ id: ctx.params.id }); + + if (!doc) { + ctx.status = 404; + ctx.body = { error: 'Document not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed( + doc.projectId!, + 'documents:UpdateDocument' + ); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const body = ctx.request.body as { + content?: string; + title?: string; + metadata?: Record; + tags?: string[]; + }; + + const updated = await updateDocument({ + id: ctx.params.id, + content: body.content, + title: body.title, + metadata: body.metadata, + tags: body.tags, + }); + + ctx.body = updated; +}); + /** * @openapi * /documents/search: @@ -365,6 +516,12 @@ documentsRouter.delete('/documents/:id', async (ctx: Context) => { * type: number * description: Minimum similarity score (0-1). Only results with score >= threshold are returned. * example: 0.7 + * tags: + * type: array + * items: + * type: string + * description: Filter to documents with any of these tags. + * example: ['tag1', 'tag2'] * responses: * '200': * description: Search results @@ -405,6 +562,7 @@ documentsRouter.post('/documents/search', async (ctx: Context) => { query: string; limit?: number; threshold?: number; + tags?: string[]; }; if (!body.query) { @@ -429,6 +587,7 @@ documentsRouter.post('/documents/search', async (ctx: Context) => { query: body.query, limit: body.limit, threshold: body.threshold, + tags: body.tags, }); ctx.body = results; diff --git a/packages/server/src/rest/v1/files.ts b/packages/server/src/rest/v1/files.ts index 9ad7d642..ec1adc9b 100644 --- a/packages/server/src/rest/v1/files.ts +++ b/packages/server/src/rest/v1/files.ts @@ -33,15 +33,38 @@ const filesRouter = new Router(); * schema: * type: string * example: 'proj_V1StGXR8Z5jdHi6B' + * - name: limit + * in: query + * required: false + * description: Maximum number of results to return (default 50) + * schema: + * type: integer + * example: 50 + * - name: offset + * in: query + * required: false + * description: Number of results to skip (default 0) + * schema: + * type: integer + * example: 0 * responses: * '200': * description: List of files returned successfully * content: * application/json: * schema: - * type: array - * items: - * $ref: '#/components/schemas/FileRecord' + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/FileRecord' + * total: + * type: integer + * limit: + * type: integer + * offset: + * type: integer * '401': * $ref: '#/components/responses/Unauthorized' * '403': @@ -55,6 +78,12 @@ filesRouter.get('/files', async (ctx: Context) => { } const projectPublicId = (ctx.query as Record).projectId; + const limit = ctx.query.limit + ? parseInt(ctx.query.limit as string, 10) + : undefined; + const offset = ctx.query.offset + ? parseInt(ctx.query.offset as string, 10) + : undefined; const projectIds = await ctx.authUser.resolveProjectIds({ projectPublicId, @@ -67,7 +96,11 @@ filesRouter.get('/files', async (ctx: Context) => { return; } - ctx.body = await listFiles({ projectIds: projectIds ?? undefined }); + ctx.body = await listFiles({ + projectIds: projectIds ?? undefined, + limit, + offset, + }); }); /** diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index c476532e..15eb01a6 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -1,5 +1,6 @@ import { Router } from '@ttoss/http-server'; +import { agentsRouter } from './agents'; import { actorsRouter } from './actors'; import { apiKeysRouter } from './apiKeys'; import { conversationsRouter } from './conversations'; @@ -10,6 +11,7 @@ import { usersRouter } from './users'; const v1Router = new Router(); +v1Router.use(agentsRouter.routes()); v1Router.use(actorsRouter.routes()); v1Router.use(apiKeysRouter.routes()); v1Router.use(conversationsRouter.routes()); diff --git a/packages/server/tests/unit/tests/actors.test.ts b/packages/server/tests/unit/tests/actors.test.ts index a4fcd783..c6a61a92 100644 --- a/packages/server/tests/unit/tests/actors.test.ts +++ b/packages/server/tests/unit/tests/actors.test.ts @@ -127,8 +127,9 @@ describe('Actors', () => { ); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body.length).toBeGreaterThanOrEqual(2); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBeGreaterThanOrEqual(2); + expect(response.body.total).toBeGreaterThanOrEqual(2); }); test('listing without projectId returns all accessible actors', async () => { @@ -136,7 +137,7 @@ describe('Actors', () => { await authenticatedTestClient(userToken).get('/api/v1/actors'); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); }); test('can filter by externalId', async () => { @@ -151,9 +152,9 @@ describe('Actors', () => { ); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); expect( - response.body.some((a: { externalId: string }) => { + response.body.data.some((a: { externalId: string }) => { return a.externalId === '+15558887777'; }) ).toBe(true); @@ -290,4 +291,117 @@ describe('Actors', () => { expect(response.status).toBe(404); }); }); + + describe('PATCH /api/v1/actors/:id', () => { + let actorId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'UpdateMe', type: 'customer' }); + actorId = res.body.id; + }); + + test('user with permission can update an actor name', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/actors/${actorId}`) + .send({ name: 'UpdatedName' }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(actorId); + expect(response.body.name).toBe('UpdatedName'); + }); + + test('user can update type and externalId', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/actors/${actorId}`) + .send({ type: 'assistant', externalId: '+15559998888' }); + + expect(response.status).toBe(200); + expect(response.body.type).toBe('assistant'); + expect(response.body.externalId).toBe('+15559998888'); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .patch(`/api/v1/actors/${actorId}`) + .send({ name: 'Hacked' }); + + expect(response.status).toBe(401); + }); + + test('returns 404 for non-existent actor', async () => { + const response = await authenticatedTestClient(adminToken) + .patch('/api/v1/actors/act_nonexistent') + .send({ name: 'Ghost' }); + + expect(response.status).toBe(404); + }); + }); + + describe('GET /api/v1/actors with name and type filters (FEAT-9)', () => { + beforeAll(async () => { + await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'NameFilterAgent', type: 'agent' }); + await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'NameFilterCustomer', type: 'customer' }); + await authenticatedTestClient(userToken) + .post('/api/v1/actors') + .send({ projectId, name: 'Unrelated', type: 'agent' }); + }); + + test('filtering by name (partial, case-insensitive) returns matching actors', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/actors?projectId=${projectId}&name=namefilter` + ); + + expect(response.status).toBe(200); + const names = response.body.data.map((a: { name: string }) => { + return a.name; + }); + expect(names).toContain('NameFilterAgent'); + expect(names).toContain('NameFilterCustomer'); + expect(names).not.toContain('Unrelated'); + }); + + test('filtering by type returns only matching actors', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/actors?projectId=${projectId}&type=customer` + ); + + expect(response.status).toBe(200); + const types = response.body.data.map((a: { type: string }) => { + return a.type; + }); + expect( + types.every((t: string) => { + return t === 'customer'; + }) + ).toBe(true); + }); + + test('filtering by name and type combined', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/actors?projectId=${projectId}&name=namefilter&type=agent` + ); + + expect(response.status).toBe(200); + const names = response.body.data.map((a: { name: string }) => { + return a.name; + }); + expect(names).toContain('NameFilterAgent'); + expect(names).not.toContain('NameFilterCustomer'); + }); + + test('non-matching name filter returns empty array', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/actors?projectId=${projectId}&name=xyznonexistentxyz` + ); + + expect(response.status).toBe(200); + expect(response.body.data).toEqual([]); + }); + }); }); diff --git a/packages/server/tests/unit/tests/conversations.test.ts b/packages/server/tests/unit/tests/conversations.test.ts index 2dad1eec..5768d85d 100644 --- a/packages/server/tests/unit/tests/conversations.test.ts +++ b/packages/server/tests/unit/tests/conversations.test.ts @@ -127,8 +127,8 @@ describe('Conversations', () => { ); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body.length).toBeGreaterThanOrEqual(2); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBeGreaterThanOrEqual(2); }); test('listing without projectId returns all accessible conversations', async () => { @@ -137,7 +137,7 @@ describe('Conversations', () => { ); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); }); test('can filter by actorId', async () => { @@ -160,9 +160,9 @@ describe('Conversations', () => { ); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); expect( - response.body.some((c: { id: string }) => c.id === filteredConvId) + response.body.data.some((c: { id: string }) => c.id === filteredConvId) ).toBe(true); }); @@ -273,8 +273,8 @@ describe('Conversations', () => { ); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body.length).toBe(0); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBe(0); }); test('unauthenticated request returns 401', async () => { @@ -325,7 +325,7 @@ describe('Conversations', () => { expect(listRes.status).toBe(200); expect( - listRes.body.some( + listRes.body.data.some( (m: { documentId: string }) => m.documentId === addedDocumentId ) ).toBe(true); @@ -383,7 +383,7 @@ describe('Conversations', () => { `/api/v1/conversations/${conversationId}/messages` ); expect( - listRes.body.some( + listRes.body.data.some( (m: { documentId: string }) => m.documentId === secondDocumentId ) ).toBe(false); @@ -525,8 +525,8 @@ describe('Conversations', () => { ); expect(response.status).toBe(200); - expect(response.body.length).toBeGreaterThanOrEqual(1); - expect(response.body[0].content).toBe('Content check message'); + expect(response.body.data.length).toBeGreaterThanOrEqual(1); + expect(response.body.data[0].content).toBe('Content check message'); }); }); diff --git a/packages/server/tests/unit/tests/documents.test.ts b/packages/server/tests/unit/tests/documents.test.ts index 97c93eae..46478440 100644 --- a/packages/server/tests/unit/tests/documents.test.ts +++ b/packages/server/tests/unit/tests/documents.test.ts @@ -56,6 +56,7 @@ describe('Documents', () => { 'documents:CreateDocument', 'documents:DeleteDocument', 'documents:SearchDocuments', + 'documents:UpdateDocument', ], }); policyId = policyRes.body.id; @@ -120,7 +121,7 @@ describe('Documents', () => { ); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); }); test('unauthenticated request cannot list documents', async () => { @@ -136,7 +137,7 @@ describe('Documents', () => { await authenticatedTestClient(userToken).get('/api/v1/documents'); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); }); }); @@ -305,6 +306,165 @@ describe('Documents', () => { }); }); + describe('POST /api/v1/documents with title, metadata, tags (FEAT-13)', () => { + test('creates a document with title, metadata, and tags', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ + projectId, + content: 'Tagged document content.', + filename: 'tagged.txt', + title: 'My Title', + metadata: { source: 'test' }, + tags: ['alpha', 'beta'], + }); + + expect(response.status).toBe(201); + expect(response.body.title).toBe('My Title'); + expect(response.body.metadata).toEqual({ source: 'test' }); + expect(response.body.tags).toEqual( + expect.arrayContaining(['alpha', 'beta']) + ); + }); + }); + + describe('PATCH /api/v1/documents/:id (FEAT-2)', () => { + let documentId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ + projectId, + content: 'Original content.', + filename: 'patchme.txt', + title: 'Original Title', + tags: ['initial'], + }); + documentId = res.body.id; + }); + + test('updates title only', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/documents/${documentId}`) + .send({ title: 'Updated Title' }); + + expect(response.status).toBe(200); + expect(response.body.title).toBe('Updated Title'); + }); + + test('updates content and re-embeds', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/documents/${documentId}`) + .send({ content: 'Updated content for re-embedding.' }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(documentId); + }); + + test('updates tags', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/documents/${documentId}`) + .send({ tags: ['new-tag', 'another'] }); + + expect(response.status).toBe(200); + expect(response.body.tags).toEqual( + expect.arrayContaining(['new-tag', 'another']) + ); + }); + + test('updates metadata', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/documents/${documentId}`) + .send({ metadata: { updated: true } }); + + expect(response.status).toBe(200); + expect(response.body.metadata).toEqual({ updated: true }); + }); + + test('returns 404 for non-existent document', async () => { + const response = await authenticatedTestClient(adminToken) + .patch('/api/v1/documents/doc_nonexistent') + .send({ title: 'Ghost' }); + + expect(response.status).toBe(404); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .patch(`/api/v1/documents/${documentId}`) + .send({ title: 'No Auth' }); + + expect(response.status).toBe(401); + }); + + test('user without UpdateDocument permission returns 403', async () => { + // Create a second user with no UpdateDocument permission + const createRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'noupdate', password: 'nopass' }); + const noUpdateUserId = createRes.body.id; + const noUpdateToken = await loginAs('noupdate', 'nopass'); + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: ['documents:GetDocument'], + }); + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId: noUpdateUserId, policyId: policyRes.body.id }); + + const response = await authenticatedTestClient(noUpdateToken) + .patch(`/api/v1/documents/${documentId}`) + .send({ title: 'Forbidden' }); + + expect(response.status).toBe(403); + }); + }); + + describe('POST /api/v1/documents/search with tags (FEAT-13)', () => { + let taggedDocId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(userToken) + .post('/api/v1/documents') + .send({ + projectId, + content: 'Tag-filtered search document.', + filename: 'tag-search.txt', + tags: ['unique-search-tag'], + }); + taggedDocId = res.body.id; + }); + + test('search by matching tag returns tagged document', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents/search') + .send({ + projectId, + query: 'tag-filtered', + tags: ['unique-search-tag'], + }); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + const ids = response.body.map((d: { id: string }) => d.id); + expect(ids).toContain(taggedDocId); + }); + + test('search by non-matching tag returns empty results', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/documents/search') + .send({ projectId, query: 'tag-filtered', tags: ['no-such-tag-xyz'] }); + + expect(response.status).toBe(200); + const ids = response.body.map((d: { id: string }) => d.id); + expect(ids).not.toContain(taggedDocId); + }); + }); + describe('API key access', () => { let apiKey: string; @@ -338,8 +498,8 @@ describe('Documents', () => { .set('Authorization', `Bearer ${apiKey}`); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body.length).toBeGreaterThan(0); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBeGreaterThan(0); }); test('API key can search documents without providing projectId', async () => { diff --git a/packages/server/tests/unit/tests/files.test.ts b/packages/server/tests/unit/tests/files.test.ts index c348401e..bc735746 100644 --- a/packages/server/tests/unit/tests/files.test.ts +++ b/packages/server/tests/unit/tests/files.test.ts @@ -430,9 +430,9 @@ describe('Files', () => { ); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body.length).toBe(1); - expect(response.body[0].filename).toBe('proj2.txt'); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBe(1); + expect(response.body.data[0].filename).toBe('proj2.txt'); }); test('listing without projectId returns files across projects', async () => { @@ -440,9 +440,9 @@ describe('Files', () => { await authenticatedTestClient(userToken).get('/api/v1/files'); expect(response.status).toBe(200); - expect(Array.isArray(response.body)).toBe(true); + expect(Array.isArray(response.body.data)).toBe(true); // Should include files from both projects - expect(response.body.length).toBeGreaterThanOrEqual(2); + expect(response.body.data.length).toBeGreaterThanOrEqual(2); }); test('returns 403 when requesting files for a project the user cannot access', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04669869..c0332e92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15548,9 +15548,9 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-config-turbo: 2.9.4(eslint@9.39.2(jiti@2.6.1))(turbo@2.9.4) - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-formatjs: 6.4.4(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jest: 29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2) eslint-plugin-jest-dom: 5.5.0(@testing-library/dom@10.4.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -18213,7 +18213,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) @@ -18224,18 +18224,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -18251,7 +18251,7 @@ snapshots: transitivePeerDependencies: - ts-jest - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -18262,7 +18262,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/tests/smoke-test.sh b/tests/smoke-test.sh index 8f884aef..3b044145 100755 --- a/tests/smoke-test.sh +++ b/tests/smoke-test.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -BASE_URL="${SERVER_URL:-http://localhost:5047}/api/v1" +BASE_URL="${SERVER_URL:-http://localhost:50477}/api/v1" echo "=== Smoke test started ===" @@ -22,7 +22,7 @@ LOGIN_RESP=$(curl -sf -X POST "$BASE_URL/users/login" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"Admin1234!"}') TOKEN=$(echo "$LOGIN_RESP" | jq -r '.token') -echo "Token: ${TOKEN:0:20}..." +echo "Token: $(echo "$TOKEN" | cut -c1-20)..." # 3. Create a project echo "--- Creating project ---" @@ -143,5 +143,47 @@ if [ "$DELETE_DOC2" != "204" ]; then fi echo "Documents deleted." +# 14. Agent SSE stream — 401 without auth +echo "--- Agent SSE stream: 401 without auth ---" +AGENT_UNAUTH=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/agents/run/stream" \ + -H "Content-Type: application/json" \ + -d '{"prompt":"hello"}') +if [ "$AGENT_UNAUTH" != "401" ]; then + echo "ERROR: Expected 401, got $AGENT_UNAUTH" >&2 + exit 1 +fi +echo "401 without auth: OK" + +# 15. Agent SSE stream — 400 without prompt +echo "--- Agent SSE stream: 400 without prompt ---" +AGENT_NOPROMPT=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/agents/run/stream" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{}') +if [ "$AGENT_NOPROMPT" != "400" ]; then + echo "ERROR: Expected 400, got $AGENT_NOPROMPT" >&2 + exit 1 +fi +echo "400 without prompt: OK" + +# 16. Agent SSE stream — valid request +echo "--- Agent SSE stream: valid request ---" +AGENT_STATUS=$(curl -s -o /tmp/agent_sse.txt -w "%{http_code}" -X POST "$BASE_URL/agents/run/stream" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"prompt":"tell me a joke"}') +if [ "$AGENT_STATUS" != "200" ]; then + echo "ERROR: Agent stream returned $AGENT_STATUS, expected 200" >&2 + exit 1 +fi +if ! grep -q "event: done" /tmp/agent_sse.txt; then + echo "ERROR: Agent stream missing 'event: done'" >&2 + cat /tmp/agent_sse.txt >&2 + exit 1 +fi +echo "Agent SSE stream OK." +echo "--- Agent SSE stream output ---" +cat /tmp/agent_sse.txt + echo "" echo "=== All smoke tests passed! ===" From 5aa2d883506df5a0cdb7303b2fa8aec8105bf2f1 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Sun, 12 Apr 2026 16:24:52 -0300 Subject: [PATCH 12/27] chore: before iam changes --- .github/instructions/server.instructions.md | 115 +++- .github/instructions/tests.instructions.md | 114 ++++ docker-compose.dev.yml | 4 +- docs/prd/chats-agents.md | 264 ++++++-- docs/prd/iam-implementation.md | 607 +++++++++++++++++ docs/prd/iam.md | 617 ++++++++++++++++++ packages/server/src/lib/projects.ts | 11 +- packages/server/src/server.ts | 3 +- .../server/tests/unit/tests/projects.test.ts | 53 ++ 9 files changed, 1705 insertions(+), 83 deletions(-) create mode 100644 .github/instructions/tests.instructions.md create mode 100644 docs/prd/iam-implementation.md create mode 100644 docs/prd/iam.md diff --git a/.github/instructions/server.instructions.md b/.github/instructions/server.instructions.md index d34b50dd..969fdebb 100644 --- a/.github/instructions/server.instructions.md +++ b/.github/instructions/server.instructions.md @@ -140,51 +140,110 @@ pnpm build This uses `tsup` to compile the TypeScript code. -## Manual Testing +## Testing -### REST API Endpoints +### Running Tests -To test REST API endpoints during development: +Run all server tests from the repo root: -1. **Start the development server:** +```bash +pnpm --filter @soat/server test +``` + +Run tests for a specific file using `--testPathPatterns` (plural): + +```bash +pnpm --filter @soat/server test --testPathPatterns=users.test.ts +``` + +Do **not** use `npx jest` directly or `--testPathPattern` (singular). + +### Test File Location and Naming + +- Server unit tests live in `packages/server/tests/unit/tests/` +- Test file name must match the module: `.test.ts` (e.g., `projects.test.ts`) +- Every public lib function and every REST route must have at least one test - ```bash - cd packages/server - pnpm dev - ``` +### Test Infrastructure -2. **Make requests using curl to `0.0.0.0:5047`:** +Tests are integration tests that run against `app.callback()` via supertest. A real PostgreSQL instance is spun up via testcontainers, configured in `setupTestsAfterEnv.ts`. No mocking of the database layer is needed. - **Important:** Always use `0.0.0.0` instead of `localhost` when making curl requests to avoid connection issues. +#### Helpers (from `tests/unit/testClient.ts`) - Example endpoints: +- `testClient` — unauthenticated supertest client +- `authenticatedTestClient(token)` — returns a client that sets `Authorization: Bearer ` on every request +- `loginAs(username, password)` — bootstrap helper that logs in and returns the token string - ```bash - # List files - curl -X GET http://0.0.0.0:5047/api/v1/files +For API key authentication, pass the raw `SDK_`-prefixed key directly to `authenticatedTestClient`. - # Upload a file - curl -X POST http://0.0.0.0:5047/api/v1/files/upload \ - -H "Content-Type: application/json" \ - -d '{"content":"Hello World!","options":{"contentType":"text/plain","metadata":{"filename":"test.txt"}}}' +### Writing Unit Tests - # Get file by ID - curl -X GET http://0.0.0.0:5047/api/v1/files/{file-id} +Group tests by HTTP method and route path using nested `describe` blocks: - # Delete file - curl -X DELETE http://0.0.0.0:5047/api/v1/files/{file-id} - ``` +```ts +describe('MyModule', () => { + let adminToken: string; + let userToken: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'alice', password: 'alicepass' }); + + userToken = await loginAs('alice', 'alicepass'); + }); + + describe('GET /api/v1/resource', () => { + test('authenticated user can list resources', async () => { + const response = + await authenticatedTestClient(userToken).get('/api/v1/resource'); + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get('/api/v1/resource'); + expect(response.status).toBe(401); + }); + }); +}); +``` + +Every module must cover: -## Unit Testing +- Happy path for each route (correct status code and response shape) +- `401` for unauthenticated requests +- `403` for requests by users without required permission +- Edge cases specific to the business logic (e.g., API key scoping, missing resources returning `404`) -Unit tests are located in the #file:../../packages/server/tests/unit/ folder. To run the unit tests for the server package, use the following command from the root directory: +Always assert the shape of the response body, not just the status code: + +```ts +expect(response.body.id).toBeDefined(); +expect(response.body.name).toBe('expected name'); +expect(response.body.password).toBeUndefined(); // sensitive fields must be absent +``` + +Internal database IDs must never appear in responses — assert `id` maps to `publicId`. + +### Manual Testing (curl) + +You can run the server in dev mode and test endpoints manually with curl: ```bash -pnpm --filter @soat/server test +pnpm run -w dev ``` -To run tests for a specific file, use the `--testPathPatterns` flag: +When the dev server is running, use `0.0.0.0` (not `localhost`) for curl requests: ```bash -pnpm --filter @soat/server test --testPathPatterns=users.test.ts +curl -X GET http://0.0.0.0:5047/api/v1/files +curl -X GET http://0.0.0.0:5047/api/v1/files/{file-id} +curl -X DELETE http://0.0.0.0:5047/api/v1/files/{file-id} ``` diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 00000000..8c1422cb --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -0,0 +1,114 @@ +--- +applyTo: '**' +description: Instructions for writing, running, and maintaining unit tests across the codebase. +--- + +# Test Instructions + +## Running Tests + +Run all tests for a package from the repo root: + +```bash +pnpm --filter @soat/server test +``` + +Run tests for a specific file using `--testPathPatterns` (plural): + +```bash +pnpm --filter @soat/server test --testPathPatterns=users.test.ts +``` + +Do **not** use `npx jest` directly or `--testPathPattern` (singular). + +## Test File Location and Naming + +- Server unit tests live in `packages/server/tests/unit/tests/` +- Test file name must match the module: `.test.ts` (e.g., `projects.test.ts`) +- Every public lib function and every REST route must have at least one test + +## Test Infrastructure + +Tests are integration tests that run against `app.callback()` via supertest. A real PostgreSQL instance is spun up via testcontainers, configured in `setupTestsAfterEnv.ts`. No mocking of the database layer is needed. + +### Helpers (from `tests/unit/testClient.ts`) + +- `testClient` — unauthenticated supertest client +- `authenticatedTestClient(token)` — returns a client that sets `Authorization: Bearer ` on every request +- `loginAs(username, password)` — bootstrap helper that logs in and returns the token string + +For API key authentication, pass the raw `SDK_`-prefixed key directly to `authenticatedTestClient`. + +## Writing Unit Tests + +### Structure + +Group tests by HTTP method and route path using nested `describe` blocks: + +```ts +describe('MyModule', () => { + let adminToken: string; + let userToken: string; + + beforeAll(async () => { + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'admin', password: 'supersecret' }); + + adminToken = await loginAs('admin', 'supersecret'); + + await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'alice', password: 'alicepass' }); + + userToken = await loginAs('alice', 'alicepass'); + }); + + describe('GET /api/v1/resource', () => { + test('authenticated user can list resources', async () => { + const response = + await authenticatedTestClient(userToken).get('/api/v1/resource'); + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get('/api/v1/resource'); + expect(response.status).toBe(401); + }); + }); +}); +``` + +### Coverage Requirements + +Every module must cover: + +- Happy path for each route (correct status code and response shape) +- `401` for unauthenticated requests +- `403` for requests by users without required permission +- Edge cases specific to the business logic (e.g., API key scoping, missing resources returning `404`) + +### Response Shape Assertions + +Always assert the shape of the response body, not just the status code: + +```ts +expect(response.body.id).toBeDefined(); +expect(response.body.name).toBe('expected name'); +expect(response.body.password).toBeUndefined(); // sensitive fields must be absent +``` + +Internal database IDs must never appear in responses — assert `id` maps to `publicId`. + +## Smoke Test + +The smoke test (`tests/smoke-test.sh`) is an end-to-end shell script that runs against a live server. It requires `curl` and `jq`. + +### Running + +```bash +pnpm run -w smoke-test +``` + +The script uses `set -e` and exits with a non-zero code on the first failure, printing which step failed. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9d20d38b..22f0fd86 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -21,12 +21,14 @@ services: ollama: image: ollama/ollama:latest container_name: soat-ollama-dev-docker + logging: + driver: 'none' volumes: - ollama_cache:/root/.ollama entrypoint: - /bin/sh - -c - - 'ollama serve & sleep 5 && ollama pull qwen3-embedding:0.6b && ollama pull qwen2.5:0.5b && wait' + - 'ollama serve > /dev/null 2>&1 & sleep 5 && ollama pull qwen3-embedding:0.6b > /dev/null 2>&1 && ollama pull qwen2.5:0.5b > /dev/null 2>&1 && wait' healthcheck: test: ['CMD-SHELL', 'ollama list | grep qwen3-embedding'] interval: 10s diff --git a/docs/prd/chats-agents.md b/docs/prd/chats-agents.md index e5ac1ca3..f8d036fa 100644 --- a/docs/prd/chats-agents.md +++ b/docs/prd/chats-agents.md @@ -1,72 +1,165 @@ -# PRD: Chats and Agents Modules +# PRD: Secrets, AI Providers, Chats, and Agents ## Context Soat acts as an **LLM Gateway**: applications call Soat instead of calling LLM providers directly. Soat adds auth, routing, logging, and cost tracking as middleware. Applications swap only `baseURL` and `apiKey` — no other client-side changes required. -Two distinct interaction patterns emerge from this role, each deserving its own module: +Four modules work together to deliver this: -| | Chats | Agents | -| --------- | ------------------------ | ----------------------------- | -| Pattern | Request → Response | Loop until goal is reached | -| Tools | No | Yes | -| Latency | Short | Variable (seconds to minutes) | -| API shape | OpenAI-compatible | Soat-native | -| State | Per-conversation history | Per-run scratchpad | +| Module | Role | +| ------------ | -------------------------------------------------- | +| Secrets | Store credentials (API keys, OAuth tokens, etc.) | +| AI Providers | Configure a provider + model using a secret | +| Chats | OpenAI-compatible request → response conversations | +| Agents | Multi-step tool-using workflows (ReAct loop) | + +Dependency chain: **Secrets ← AI Providers ← Chats / Agents** --- -## Module: Chats +## Module: Secrets ### Overview -The Chats module exposes an **OpenAI Chat Completions-compatible API**. Any client already using the OpenAI SDK can point its `baseURL` at Soat and work without code changes. - -Conversations are persistent: a chat has an ID, and messages accumulate in it across multiple calls. +The Secrets module stores sensitive credentials scoped to a project. A secret can hold any kind of credential: an AI provider API key, Google service account JSON, OAuth refresh tokens, or arbitrary key-value pairs. Secrets are encrypted at rest and their values are never returned in API responses after creation. ### Data Model -The existing `Conversation` and `ConversationMessage` models map directly: +- `Secret` — a named credential; scoped to a `Project` + - `publicId` — `sec_` prefix + - `projectId` — FK to `Project` + - `name` — human-readable label (e.g., "OpenAI Production Key") + - `value` — encrypted JSON string; can hold any structure (a plain string for API keys, a JSON object for Google credentials, etc.) + - `createdAt`, `updatedAt` + +### API + +``` +POST /v1/secrets Create a secret +GET /v1/secrets List secrets (scoped to project; values redacted) +GET /v1/secrets/{secretId} Get secret metadata (value redacted) +PATCH /v1/secrets/{secretId} Update a secret (name, type, or value) +DELETE /v1/secrets/{secretId} Delete a secret +``` + +### Behaviour -- `Conversation` — the chat session; scoped to a `Project`; has a `status` (`open` | `closed`) -- `ConversationMessage` — a single turn; references a `Document` (message content stored as a file), an `Actor` (sender), and a `position` (ordering within the conversation) -- `Actor` — the identity sending a message; scoped to a `Project`; carries an `externalId` for the calling application to use its own user IDs +- **Value is write-only**: `POST` and `PATCH` accept a `value` field; `GET` never returns it. List/Get responses include a `hasValue: true` flag instead. +- **Encryption**: values are encrypted with AES-256-GCM using a server-managed key (`SECRETS_ENCRYPTION_KEY` env var) before being stored in the database. +- **Cascade**: deleting a secret that is referenced by an AI provider returns `409 Conflict` unless `force=true` is passed (which also deletes dependent AI providers). + +### Public ID prefix + +`sec_` + +--- + +## Module: AI Providers + +### Overview + +The AI Providers module configures a connection to an AI model provider. Each AI provider references a secret (for credentials) and specifies a provider type and default model. A project can have multiple AI providers — e.g., two separate OpenAI providers with different API keys, plus an Anthropic provider. + +Chats and Agents reference an AI provider instead of specifying model/credentials directly. This decouples credential management from LLM usage. + +### Data Model + +- `AiProvider` — a configured provider instance; scoped to a `Project` + - `publicId` — `aip_` prefix + - `projectId` — FK to `Project` + - `secretId` — FK to `Secret` (the credential to authenticate with the provider) + - `name` — human-readable label (e.g., "Claude Production", "OpenAI Internal") + - `provider` — provider slug matching the Vercel AI SDK gateway format: `openai` | `anthropic` | `google` | `xai` | `groq` | `ollama` | `azure` | `bedrock` | `custom` + - `defaultModel` — default model ID (e.g., `gpt-4o`, `claude-sonnet-4`, `llama3.2`); can be overridden per-request + - `baseUrl` — optional; custom API base URL (for self-hosted or proxy endpoints) + - `config` — optional JSON; provider-specific settings (e.g., `{ "organization": "org-xxx" }` for OpenAI, `{ "region": "us-east-1" }` for Bedrock) + - `createdAt`, `updatedAt` ### API ``` -POST /v1/chats Create a new chat -GET /v1/chats List chats (scoped to project) -GET /v1/chats/{chatId} Get chat metadata -DELETE /v1/chats/{chatId} Delete a chat and all messages +POST /v1/ai-providers Create an AI provider +GET /v1/ai-providers List AI providers (scoped to project) +GET /v1/ai-providers/{aiProviderId} Get AI provider details +PATCH /v1/ai-providers/{aiProviderId} Update an AI provider +DELETE /v1/ai-providers/{aiProviderId} Delete an AI provider +``` + +### Behaviour + +- **Secret resolution**: when Chats or Agents use an AI provider, the server decrypts the referenced secret to build the provider client. The decrypted value is never exposed to the caller. +- **Vercel AI SDK integration**: at runtime, each AI provider maps to a Vercel AI SDK provider instance. For example, an `openai` provider creates an `@ai-sdk/openai` instance configured with the decrypted API key and optional base URL. +- **Multiple providers per project**: a project may have several AI providers of the same type (e.g., two OpenAI providers for different teams/budgets) or different types. +- **Cascade**: deleting an AI provider that is referenced as a default by chats returns `409 Conflict` unless `force=true`. + +### Public ID prefix + +`aip_` + +### Provider → AI SDK Mapping + +| `provider` value | AI SDK package | Auth from secret | +| ---------------- | ------------------------- | --------------------- | +| `openai` | `@ai-sdk/openai` | `apiKey` | +| `anthropic` | `@ai-sdk/anthropic` | `apiKey` | +| `google` | `@ai-sdk/google` | `apiKey` | +| `xai` | `@ai-sdk/xai` | `apiKey` | +| `groq` | `@ai-sdk/groq` | `apiKey` | +| `azure` | `@ai-sdk/azure` | `apiKey` + `baseUrl` | +| `bedrock` | `@ai-sdk/amazon-bedrock` | `google_credentials` | +| `ollama` | `ollama-ai-provider` | none (local) | +| `gateway` | `ai` (built-in gateway) | `apiKey` (AI Gateway) | +| `custom` | OpenAI-compatible wrapper | `apiKey` + `baseUrl` | + +--- + +## Module: Chats + +### Overview + +The Chats module exposes an **OpenAI Chat Completions-compatible API** for **stateless, single-call completions**. Any client already using the OpenAI SDK can point its `baseURL` at Soat and work without code changes. + +Chats are **not** persistent — every request is self-contained. The caller sends the full message array each time. For persistent conversation history, use the **Conversations** module instead. An application can use Conversations without Chats, and vice versa. + +### Data Model + +Chats are stateless — no new DB entity is required. Each request is processed and returned without storing messages. + +### API -POST /v1/chats/{chatId}/completions Send a message and get a completion -GET /v1/chats/{chatId}/messages List messages in a chat +``` +POST /v1/chats/completions Send messages and get a completion ``` -#### `POST /v1/chats/{chatId}/completions` +#### `POST /v1/chats/completions` Request body mirrors OpenAI's `POST /v1/chat/completions`: ```json { - "model": "llama3.2", + "aiProviderId": "aip_V1StGXR8Z5jdHi6B", + "model": "gpt-4o", "messages": [{ "role": "user", "content": "What files do I have?" }], "stream": false } ``` -- `model` — optional; falls back to `CHAT_MODEL` env var -- `messages` — the new turn(s) to append; prior history is loaded from the DB automatically +- `aiProviderId` — optional; specifies which AI provider to use for this request +- `model` — optional; overrides the AI provider's `defaultModel` +- `messages` — the full message array (system, user, assistant turns); the caller manages history - `stream` — `false` returns JSON; `true` returns SSE (`text/event-stream`) +**Resolution order for provider/model:** + +1. `aiProviderId` on the request body +2. Fallback: `CHAT_MODEL` env var with default Ollama (backward compat) + Response (non-streaming) mirrors OpenAI's response shape: ```json { - "id": "conv_V1StGXR8Z5jdHi6B", "object": "chat.completion", - "model": "llama3.2", + "model": "gpt-4o", "choices": [ { "index": 0, @@ -81,14 +174,22 @@ Streaming response: SSE chunks with `data: {"choices":[{"delta":{"content":"..." ### Behaviour -- Each call to `/completions` **appends** the incoming messages and the model's reply to the conversation's message history. -- The full prior history is passed to the LLM on each call. -- History trimming (context window management) is handled transparently by Soat. +- Each call is **stateless** — Soat does not store or accumulate messages. The caller is responsible for managing conversation history and sending the full message array on every request. - No tool execution. If an application needs tools, it uses the Agents module. +- The AI provider determines which SDK client and credentials are used. The server decrypts the secret, instantiates the appropriate Vercel AI SDK provider, and calls `generateText` or `streamText`. -### Public ID prefix +### Relationship with Conversations + +Chats and Conversations are **independent modules**: + +| Aspect | Chats | Conversations | +| ----------- | -------------------------------- | ---------------------------------- | +| State | Stateless (single API call) | Persistent (messages stored in DB) | +| History | Caller manages message array | Soat manages message history | +| Use case | Direct LLM proxy / quick prompts | Multi-turn sessions with recall | +| Data stored | None | ConversationMessage per turn | -`chat_` — introduced alongside this module. The existing `conv_` prefix on `Conversation` maps to chats. +An application can use either or both modules depending on its needs. --- @@ -120,15 +221,19 @@ POST /v1/agents/run/stream Run an agent with streaming output (SSE) #### `POST /v1/agents/run/stream` -Already implemented. Request: +Already implemented (to be updated). Request: ```json { - "model": "qwen2.5:0.5b", + "aiProviderId": "aip_V1StGXR8Z5jdHi6B", + "model": "gpt-4o", "prompt": "Summarise all documents in project proj_XYZ" } ``` +- `aiProviderId` — required (or fallback to `AGENT_MODEL` env var + Ollama for backward compat) +- `model` — optional; overrides the AI provider's `defaultModel` + Response: SSE chunks: ``` @@ -177,27 +282,84 @@ Future tool additions (e.g. `create-document`, `search-documents`) extend the re Current state: `ollama@^0.6.3` (native). No tool loop. -To implement the Agents module properly (tool execution, multi-step), either: +Migrate to **Vercel AI SDK**: install `ai` + provider-specific packages (`@ai-sdk/openai`, `@ai-sdk/anthropic`, etc.). This enables: + +- **Chats**: `generateText` / `streamText` for OpenAI-compatible completions +- **Agents**: `generateText` with `maxSteps` for the ReAct tool loop +- **AI Providers**: each `AiProvider` record maps to a Vercel AI SDK provider instance at runtime + +### Provider instantiation (runtime) + +```ts +// Pseudocode: resolving an AI provider to a Vercel AI SDK model +const resolveModel = async (aiProviderId: string) => { + const aiProvider = await db.AiProvider.findOne({ + where: { publicId: aiProviderId }, + }); + const secret = await db.Secret.findOne({ + where: { id: aiProvider.secretId }, + }); + const decryptedValue = decrypt(secret.value); + + switch (aiProvider.provider) { + case 'openai': + return createOpenAI({ + apiKey: decryptedValue, + baseURL: aiProvider.baseUrl, + })(aiProvider.defaultModel); + case 'anthropic': + return createAnthropic({ apiKey: decryptedValue })( + aiProvider.defaultModel + ); + case 'ollama': + return ollama(aiProvider.defaultModel); + // ... other providers + } +}; +``` -1. **Stay native**: implement a manual ReAct loop with `ollama.chat()` + tool dispatch -2. **Migrate to Vercel AI SDK**: install `ai` + `@ai-sdk/ollama`; use `ToolLoopAgent` for modes 3–8 and `streamText` for the Chats module +### Encryption for Secrets -Option 2 is preferred: `ToolLoopAgent` handles stop conditions, context trimming, and dynamic tool selection out of the box. `streamText` produces an OpenAI-compatible streaming format needed by the Chats module. +- Algorithm: AES-256-GCM +- Key: `SECRETS_ENCRYPTION_KEY` environment variable (32 bytes, hex or base64 encoded) +- Each secret gets a random IV stored alongside the ciphertext +- Storage format: `iv:ciphertext:authTag` (all base64) ### Module file locations (following codebase conventions) ``` -packages/server/src/lib/chats.ts -packages/server/src/rest/v1/chats.ts -packages/server/src/mcp/tools/chats.ts -packages/website/docs/modules/chats.md - -packages/server/src/lib/agents.ts (exists, extend) -packages/server/src/rest/v1/agents.ts (exists, extend) -packages/server/src/mcp/tools/agents.ts (new) -packages/website/docs/modules/agents.md (new) +packages/postgresdb/src/models/Secret.ts (new) +packages/postgresdb/src/models/AiProvider.ts (new) + +packages/server/src/lib/secrets.ts (new) +packages/server/src/lib/aiProviders.ts (new) +packages/server/src/lib/chats.ts (new) +packages/server/src/lib/agents.ts (exists, extend) + +packages/server/src/rest/v1/secrets.ts (new) +packages/server/src/rest/v1/aiProviders.ts (new) +packages/server/src/rest/v1/chats.ts (new) +packages/server/src/rest/v1/agents.ts (exists, extend) + +packages/server/src/mcp/tools/secrets.ts (new) +packages/server/src/mcp/tools/aiProviders.ts (new) +packages/server/src/mcp/tools/chats.ts (new) +packages/server/src/mcp/tools/agents.ts (new) + +packages/website/docs/modules/secrets.md (new) +packages/website/docs/modules/ai-providers.md (new) +packages/website/docs/modules/chats.md (new) +packages/website/docs/modules/agents.md (new) + +packages/server/tests/unit/tests/secrets.test.ts (new) +packages/server/tests/unit/tests/aiProviders.test.ts (new) +packages/server/tests/unit/tests/chats.test.ts (new) +packages/server/tests/unit/tests/agents.test.ts (new) ``` -### Migration from `conversations` to `chats` +### Implementation order -The existing `Conversation` / `ConversationMessage` models are reused as-is. The Chats REST module is a new API layer on top of the same DB tables. The existing `conversations` REST routes remain until deprecated. +1. **Secrets** — no dependencies; foundation for everything else +2. **AI Providers** — depends on Secrets +3. **Chats** — depends on AI Providers; stateless, no new DB models needed +4. **Agents** — depends on AI Providers; extends existing agent streaming diff --git a/docs/prd/iam-implementation.md b/docs/prd/iam-implementation.md new file mode 100644 index 00000000..315120d9 --- /dev/null +++ b/docs/prd/iam-implementation.md @@ -0,0 +1,607 @@ +# IAM Implementation Plan + +Step-by-step implementation of the IAM system defined in [iam.md](./iam.md). Each step is self-contained, testable, and builds on the previous one. + +--- + +## Step 1: Define TypeScript Types and Validation + +**Goal**: Create the core type definitions and a validation function for the new policy document format. + +**Files to create/modify**: + +- Create `packages/server/src/lib/iam.ts` + +**What to do**: + +1. Define `PolicyDocument`, `Statement`, `Condition`, and `SRN` types: + + ```ts + type Effect = 'Allow' | 'Deny'; + + type ConditionOperator = 'StringEquals' | 'StringNotEquals' | 'StringLike'; + + type Condition = { + [operator in ConditionOperator]?: Record; + }; + + type Statement = { + effect: Effect; + action: string[]; + resource?: string[]; + condition?: Condition; + }; + + type PolicyDocument = { + statement: Statement[]; + }; + ``` + +2. Write `validatePolicyDocument(doc: unknown): { valid: boolean; errors: string[] }`: + - Each statement must have `effect` ∈ `{"Allow", "Deny"}` + - `action` must be a non-empty `string[]` where each entry matches `module:Operation` or `*` or `module:*` + - `resource`, if present, must be a non-empty `string[]` of valid SRN patterns + - `condition` operators must be from `{StringEquals, StringNotEquals, StringLike}` + - Condition keys must start with `soat:` + +3. Write `buildSrn(args: { projectPublicId: string; resourceType: string; resourceId: string }): string` that returns `soat:::`. + +**Tests**: Add `packages/server/tests/unit/tests/iam.test.ts` with: + +- Valid document passes validation +- Invalid `effect` fails +- Empty `action` array fails +- Invalid SRN format fails +- Invalid condition operator fails +- `buildSrn` produces correct SRN strings + +**Run**: + +```bash +pnpm --filter @soat/server test --testPathPatterns=iam.test.ts +``` + +--- + +## Step 2: Implement the Policy Evaluation Engine + +**Goal**: Build the core `evaluate` function that takes policies, an action, a resource SRN, and context — then returns Allow or Deny. + +**Files to modify**: + +- `packages/server/src/lib/iam.ts` (extend from Step 1) + +**What to do**: + +1. Write `matchesPattern(args: { pattern: string; value: string }): boolean`: + - `*` matches everything + - `module:*` matches all actions in `module:` + - `soat:proj_ABC:document:*` matches all documents in `proj_ABC` + - Exact match for non-wildcard patterns + +2. Write `evaluateCondition(args: { condition: Condition; context: Record }): boolean`: + - `StringEquals`: every key-value in the operator block must match exactly in context + - `StringNotEquals`: every key-value must NOT match + - `StringLike`: glob pattern match (`*` = any chars, `?` = single char) + - All operator blocks must pass (AND logic) + +3. Write `statementMatches(args: { statement: Statement; action: string; resource: string; context: Record }): boolean`: + - At least one `action` pattern matches the requested action + - At least one `resource` pattern matches the target SRN (or resource is `["*"]` / omitted) + - All conditions pass (or no conditions) + +4. Write `evaluatePolicies(args: { policies: PolicyDocument[]; action: string; resource?: string; context?: Record }): boolean`: + - Default `resource` to `*` if omitted + - Default `context` to `{}` if omitted + - Loop through all policies and all statements + - If any matching statement has `effect: "Deny"` → return `false` (explicit deny, short-circuit) + - If any matching statement has `effect: "Allow"` → mark allowed + - Return the allowed flag (default `false`) + +**Tests**: Extend `iam.test.ts`: + +- Action-only policy (no resource/condition) allows matching action +- Action-only policy denies non-matching action +- Wildcard `*` allows everything +- `module:*` matches all actions in module +- Resource-specific policy allows matching SRN +- Resource-specific policy denies different SRN +- Resource wildcard `soat:proj_ABC:document:*` matches specific doc +- Explicit deny overrides allow +- Multiple policies: allow from one + deny from another = deny +- Multiple policies: allow from one + no match from another = allow +- Condition `StringEquals` matches +- Condition `StringEquals` does not match +- Condition `StringNotEquals` works +- Condition `StringLike` with glob patterns +- No matching statements → default deny +- Omitted `resource` defaults to `["*"]` + +**Run**: + +```bash +pnpm --filter @soat/server test --testPathPatterns=iam.test.ts +``` + +--- + +## Step 3: Update the Database Models + +**Goal**: Replace `ProjectPolicy` flat permission columns with IAM document support and add `tags` to taggable models. + +**Files to modify**: + +- `packages/postgresdb/src/models/ProjectPolicy.ts` +- `packages/postgresdb/src/models/UserProject.ts` +- `packages/postgresdb/src/models/Document.ts` +- `packages/postgresdb/src/models/File.ts` +- `packages/postgresdb/src/models/Actor.ts` +- `packages/postgresdb/src/models/Conversation.ts` + +**What to do**: + +### 3a. Replace `ProjectPolicy` Columns + +Remove the existing `permissions` and `notPermissions` columns. Replace with: + +```ts +@Column({ type: DataType.STRING(255), allowNull: true }) +declare name: string | null; + +@Column({ type: DataType.TEXT, allowNull: true }) +declare description: string | null; + +@Column({ type: DataType.JSONB, allowNull: false }) +declare document: object; // PolicyDocument JSON +``` + +The `document` column stores a validated `PolicyDocument` and is now required for all policies. + +### 3b. Update `UserProject` for Multiple Policies + +Replace the single `policyId` FK with an array: + +```ts +@Column({ + type: DataType.ARRAY(DataType.INTEGER), + allowNull: false, + defaultValue: [], +}) +declare policyIds: number[]; +``` + +Remove the `@ForeignKey` and `@BelongsTo` for the single policy relationship. + +> **Note**: Since the project is not shipped yet, drop and recreate the dev DB after these schema changes. + +### 3c. Add `tags` to Taggable Models + +Add to `Document`, `File`, `Actor`, and `Conversation`: + +```ts +@Column({ + type: DataType.JSONB, + allowNull: true, + defaultValue: {}, +}) +declare tags: Record | null; +``` + +> **Note on Document model**: The existing `tags` column on Document is `DataType.ARRAY(DataType.TEXT)`. This changes to `DataType.JSONB` (key-value instead of array). Drop and recreate the dev DB to apply this type change. + +### 3d. Rebuild + +```bash +pnpm --filter @soat/postgresdb build +``` + +**Tests**: Run existing tests to verify nothing breaks: + +```bash +pnpm --filter @soat/server test +``` + +--- + +## Step 4: Update the Permission Functions (`permissions.ts`) + +**Goal**: Replace the permission evaluation to use the new IAM engine. + +**Files to modify**: + +- `packages/server/src/lib/permissions.ts` + +**What to do**: + +1. Import `evaluatePolicies`, `validatePolicyDocument`, and `PolicyDocument` from `./iam`. + +2. Replace `policyAllows` to use the new format: + + ```ts + export const policyAllows = (args: { + policy: { document: object }; + action: string; + resource?: string; + context?: Record; + }): boolean => { + return evaluatePolicies({ + policies: [args.policy.document as PolicyDocument], + action: args.action, + resource: args.resource, + context: args.context, + }); + }; + ``` + +3. Update `createJwtIsAllowed` and `createApiKeyIsAllowed`: + - Load **all** policies from the `policyIds` array on UserProject (instead of a single `policyId`) + - Extract the `document` from each policy + - Call `evaluatePolicies` with the full array + +**Tests**: Extend `iam.test.ts`: + +- `policyAllows` works with IAM document +- Multiple policies evaluated together + +**Run**: + +```bash +pnpm --filter @soat/server test --testPathPatterns=iam.test.ts +``` + +--- + +## Step 5: Update `isAllowed` Signature and Auth Middleware + +**Goal**: Extend `isAllowed` to accept optional `resource` and `context` parameters. Update the auth middleware to load multiple policies and pass tags as context. + +**Files to modify**: + +- `packages/server/src/Context.ts` +- `packages/server/src/middleware/auth.ts` + +**What to do**: + +### 5a. Update `Context.ts` + +Change the `isAllowed` signature on `AuthUser`: + +```ts +isAllowed: (args: { + projectPublicId: string; + action: string; + resource?: string; + context?: Record; +}) => Promise; +``` + +### 5b. Update `auth.ts` + +**`resolveJwt`**: + +Update `createJwtIsAllowed` call: + +1. Load all policies for the user's membership by looking up `policyIds` array from `UserProject`, then fetching all matching `ProjectPolicy` rows. +2. Pass the array of policy documents to the evaluation. +3. Support the new `resource` and `context` args. + +**`resolveApiKey`**: + +Same approach — load the API key's policy AND the user's project policies, evaluate with intersection logic: + +- Action must be allowed by user policies AND API key policy. +- Resource and context are checked in both. + +### 5c. Update All `isAllowed` Call Sites + +Since the signature changes from `(projectPublicId, action)` to `({ projectPublicId, action, resource?, context? })`, every call site in the REST handlers must be updated. Search for `isAllowed(` across `packages/server/src/rest/` and update to the object form: + +```ts +// Before +await ctx.authUser.isAllowed(projectId, 'documents:GetDocument'); + +// After +await ctx.authUser.isAllowed({ + projectPublicId: projectId, + action: 'documents:GetDocument', +}); +``` + +At this point, no call sites need to pass `resource` or `context` yet — those will be added per-module in Step 7. + +**Tests**: Run full test suite to verify everything works: + +```bash +pnpm --filter @soat/server test +``` + +--- + +## Step 6: Update Policy REST Endpoints + +**Goal**: Update the policy CRUD endpoints to accept the new IAM document format. + +**Files to modify**: + +- `packages/server/src/lib/projects.ts` +- `packages/server/src/rest/v1/projects.ts` +- `packages/server/src/rest/openapi/v1/projects.yaml` (if it exists) + +**What to do**: + +### 6a. Update `createProjectPolicy` in `lib/projects.ts` + +Accept either legacy or new format: + +```ts +export const createProjectPolicy = async (args: { + projectId: string; + name?: string; + description?: string; + document: PolicyDocument; +}) => { ... }; +``` + +- Validate the document with `validatePolicyDocument`. Return 400 on invalid. + +### 6b. Update `POST /projects/:projectId/policies` + +Accept the new request body shape: + +```json +{ + "name": "Document Readers", + "description": "Read-only access to documents", + "document": { "statement": [...] } +} +``` + +### 6c. Add `PUT /projects/:projectId/policies/:policyId` + +Full replacement of a policy document. Validate the new document before saving. + +### 6d. Add `DELETE /projects/:projectId/policies/:policyId` + +Delete a policy. Must check that no UserProject references it before deleting (or cascade). + +### 6e. Update Member Endpoints + +Update `POST /projects/:projectId/members` to accept `policyIds: string[]` (array of policy public IDs) instead of a single `policyId`. + +Add `PUT /projects/:projectId/members/:userId/policies` to update a member's attached policies. + +Add `GET /projects/:projectId/members/:userId/policies` to list a member's attached policies. + +### 6f. Update Response Shapes + +Policy responses should include `name`, `description`, and `document` when present: + +```json +{ + "id": "pol_...", + "name": "Document Readers", + "description": "Read-only access to documents", + "document": { "statement": [...] }, + "projectId": "proj_...", + "createdAt": "...", + "updatedAt": "..." +} +``` + +**Tests**: Add to `projects.test.ts`: + +- Create policy with IAM document format +- Invalid document returns 400 +- Update policy with PUT +- Delete policy +- Add member with multiple `policyIds` +- Update member policies +- Get member policies +- Validation: invalid `effect`, empty `action` array + +**Run**: + +```bash +pnpm --filter @soat/server test --testPathPatterns=projects.test.ts +``` + +--- + +## Step 7: Add Resource-Level and Tag-Based Checks to Modules + +**Goal**: Pass `resource` SRN and `context` (tags) to `isAllowed` in modules that benefit from fine-grained access control. + +**Files to modify**: + +- `packages/server/src/lib/documents.ts` +- `packages/server/src/lib/files.ts` +- `packages/server/src/lib/actors.ts` +- `packages/server/src/lib/conversations.ts` +- `packages/server/src/rest/v1/documents.ts` +- `packages/server/src/rest/v1/files.ts` +- `packages/server/src/rest/v1/actors.ts` +- `packages/server/src/rest/v1/conversations.ts` + +**What to do**: + +### 7a. Add SRN Construction to Lib Functions + +In each module's lib layer, construct the SRN after loading the resource: + +```ts +// documents.ts +const srn = buildSrn({ + projectPublicId: project.publicId, + resourceType: 'document', + resourceId: document.publicId, +}); +``` + +### 7b. Add Tag Context Loading + +When a resource is loaded and it has `tags`, build the condition context: + +```ts +const context: Record = {}; +if (resource.tags) { + for (const [key, value] of Object.entries(resource.tags)) { + context[`soat:ResourceTag/${key}`] = value; + } +} +context['soat:ResourceType'] = 'document'; +``` + +### 7c. Update `isAllowed` Calls in Routes + +For single-resource operations (`GET /:id`, `PUT /:id`, `DELETE /:id`): + +1. Load the resource from DB +2. Build SRN and context +3. Call `isAllowed({ projectPublicId, action, resource: srn, context })` + +For list operations (`GET /`), the policy check stays at the action level (no specific resource). Resources are filtered post-query if needed, or the action-level check is sufficient for v1. + +### 7d. Add Tag CRUD Sub-Endpoints + +For each taggable module, add: + +``` +PUT /v1/documents/:id/tags — replace all tags +PATCH /v1/documents/:id/tags — merge tags +GET /v1/documents/:id/tags — get tags +``` + +The same pattern for `files`, `actors`, `conversations`. + +**Tests**: Extend each module's test file: + +- Resource-level allow: policy allows `doc_XYZ`, request for `doc_XYZ` passes +- Resource-level deny: policy allows `documents:*` but denies specific `doc_XYZ`, request for `doc_XYZ` blocked +- Tag condition: policy with `StringEquals { "soat:ResourceTag/env": "prod" }`, resource tagged `env=prod` passes, `env=dev` fails +- Tag CRUD: create/update/get tags on a resource + +**Run**: + +```bash +pnpm --filter @soat/server test +``` + +--- + +## Step 8: Update MCP Tools + +**Goal**: Expose policy management and tag operations via MCP tools. + +**Files to modify**: + +- `packages/server/src/mcp/tools/projects.ts` +- `packages/server/src/mcp/tools/index.ts` + +**What to do**: + +### 8a. Add Policy Management Tools + +``` +create-policy POST /projects/{projectId}/policies +list-policies GET /projects/{projectId}/policies +get-policy GET /projects/{projectId}/policies/{id} +update-policy PUT /projects/{projectId}/policies/{id} +delete-policy DELETE /projects/{projectId}/policies/{id} +``` + +### 8b. Add Member Policy Tools + +``` +set-member-policies PUT /projects/{projectId}/members/{userId}/policies +get-member-policies GET /projects/{projectId}/members/{userId}/policies +``` + +### 8c. Add Tag Tools (per module) + +For each taggable module, add tools: + +``` +set-document-tags PUT /documents/{id}/tags +get-document-tags GET /documents/{id}/tags +``` + +(Same for files, actors, conversations.) + +### 8d. Register in `index.ts` + +Import and call `registerTools` for the new/updated tool files. + +**Tests**: Manual verification via MCP inspector or integration tests. + +--- + +## Step 9: Documentation + +- `packages/website/docs/modules/iam.md` + +**What to do**: + +1. Write the module doc covering: + - Overview: what the IAM system does + - Key concepts: policy documents, statements, SRNs, conditions, tags + - Policy evaluation rules (default deny, explicit deny wins) + - How to create and attach policies + - Tag-based ABAC examples + +2. Update `packages/website/sidebars.ts` if needed to include the IAM doc. + +--- + +## Step 10: End-to-End Smoke Test + +**Goal**: Verify the full IAM flow works end-to-end. + +**Files to modify**: + +- `tests/smoke-test.sh` + +**What to do**: + +Add IAM scenarios to the smoke test: + +1. Bootstrap admin + create project +2. Create an IAM policy document (resource-level allow for a specific document) +3. Create a user, add to project with the policy +4. Upload a document, tag it +5. Verify user CAN access the allowed document +6. Verify user CANNOT access a different document +7. Create a deny policy for a specific resource +8. Verify deny overrides allow + +**Run**: + +```bash +pnpm run -w smoke-test +``` + +--- + +## Implementation Order Summary + +| Step | Description | Depends On | Risk | +| ---- | ---------------------------------- | ---------- | ------ | +| 1 | Types and validation | — | Low | +| 2 | Policy evaluation engine | 1 | Low | +| 3 | Database model changes | — | Medium | +| 4 | Update permission functions | 1, 2, 3 | Medium | +| 5 | Update isAllowed + auth middleware | 4 | High | +| 6 | Update policy REST endpoints | 3, 4 | Medium | +| 7 | Resource-level + tag checks | 5, 6 | Medium | +| 8 | MCP tools | 6, 7 | Low | +| 9 | Documentation | All | Low | +| 10 | End-to-end smoke test | All | Low | + +Steps 1–2 and 3 can be done in parallel. Steps 4–5 are the critical path. Steps 8–10 can be done in any order after their dependencies. + +### Estimated Breakdown by Area + +- **Policy engine** (Steps 1–2): Pure functions, no side effects, highly testable in isolation +- **Database** (Step 3): Drop and recreate dev DB after schema changes +- **Auth integration** (Steps 4–5): Highest risk — touches every authenticated request +- **REST API** (Steps 6–7): Incremental — add new endpoints, update existing ones +- **MCP + Docs + Smoke test** (Steps 8–10): Low risk, mostly additive diff --git a/docs/prd/iam.md b/docs/prd/iam.md new file mode 100644 index 00000000..841721e6 --- /dev/null +++ b/docs/prd/iam.md @@ -0,0 +1,617 @@ +# PRD: Identity and Access Management (IAM) + +## Context + +Soat's current authorization system uses flat permission lists (`permissions` / `notPermissions`) on project policies. These lists support only **action-level** checks — e.g., allowing `files:GetFile` or denying `documents:*`. This is insufficient for real-world multi-tenant scenarios: + +| Limitation | Example | +| ---------------------------------- | ----------------------------------------------------------------- | +| No resource-level targeting | Cannot grant access to a specific document (`doc_XYZ`) only | +| No tag/attribute-based conditions | Cannot allow agents tagged `"internal"` while denying `"public"` | +| No explicit deny on specific items | Cannot deny a user access to one secret while allowing all others | +| Single policy per membership | A user gets exactly one policy per project — no composition | + +This PRD introduces an **AWS IAM-inspired policy engine** that replaces the current flat-list model with structured policy statements supporting `Effect`, `Action`, `Resource`, and `Condition`. + +### Goals + +1. **Resource-level permissions** — grant or deny access to individual resources (a specific document, file, secret, etc.) +2. **Condition-based access control** — support tag-based conditions (e.g., only agents with tag `internal`) +3. **Explicit deny** — deny always wins over allow, enabling blocklists +4. **Policy composition** — attach multiple policies to a user/API key; the engine merges them +5. **Backward compatibility** — existing `permissions`/`notPermissions` arrays continue to work during migration + +### Non-goals + +- Federated identity (SSO, SAML, OIDC) — out of scope for v1 +- Cross-account/cross-project trust policies +- Policy versioning or rollback +- Real-time policy simulation API (future iteration) + +--- + +## Concepts + +### Policy Document + +A **policy document** is a JSON object containing one or more **statements**. Each statement describes a permission rule. This is the core primitive — everything else (attaching to users, API keys, projects) is about where the document lives. + +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["documents:GetDocument", "documents:ListDocuments"], + "resource": ["soat:proj_ABC:document:doc_XYZ"] + }, + { + "effect": "Deny", + "action": ["secrets:*"], + "resource": ["soat:proj_ABC:secret:sec_PROD_KEY"] + } + ] +} +``` + +### Statement + +A single permission rule within a policy document. + +| Field | Type | Required | Description | +| ----------- | ---------- | -------- | ------------------------------------------------------- | +| `effect` | `string` | Yes | `"Allow"` or `"Deny"` | +| `action` | `string[]` | Yes | Actions this statement applies to (supports wildcards) | +| `resource` | `string[]` | No | SRNs this statement applies to (default: `["*"]`) | +| `condition` | `object` | No | Conditions that must be true for the statement to apply | + +### Soat Resource Name (SRN) + +Every addressable entity in Soat has a canonical identifier called a **Soat Resource Name (SRN)**. Format: + +``` +soat::: +``` + +Examples: + +``` +soat:proj_ABC:document:doc_XYZ # specific document +soat:proj_ABC:document:* # all documents in project +soat:proj_ABC:secret:sec_PROD # specific secret +soat:proj_ABC:file:* # all files in project +soat:proj_ABC:actor:act_123 # specific actor +soat:proj_ABC:conversation:conv_789 # specific conversation +soat:proj_ABC:ai-provider:aip_456 # specific AI provider +soat:*:*:* # everything (admin-level) +``` + +Wildcards (`*`) are supported at any segment. The resource type maps directly to module names. + +**Resource types:** + +| Resource type | Public ID prefix | Module | +| -------------- | ---------------- | ------------- | +| `document` | `doc_` | Documents | +| `file` | `file_` | Files | +| `secret` | `sec_` | Secrets | +| `actor` | `act_` | Actors | +| `conversation` | `conv_` | Conversations | +| `ai-provider` | `aip_` | AI Providers | +| `project` | `proj_` | Projects | +| `policy` | `pol_` | Policies | +| `api-key` | `key_` | API Keys | + +### Actions + +Actions follow the existing `module:Operation` pattern: + +``` +documents:GetDocument +documents:ListDocuments +documents:CreateDocument +documents:UpdateDocument +documents:DeleteDocument +documents:SearchDocuments +files:GetFile +files:CreateFile +files:DeleteFile +files:UploadFile +files:DownloadFile +files:UpdateFileMetadata +secrets:GetSecret +secrets:ListSecrets +secrets:CreateSecret +secrets:UpdateSecret +secrets:DeleteSecret +actors:ListActors +actors:GetActor +actors:CreateActor +actors:UpdateActor +actors:DeleteActor +conversations:ListConversations +conversations:GetConversation +conversations:CreateConversation +conversations:UpdateConversation +conversations:DeleteConversation +projects:GetProject +chats:CreateCompletion +agents:RunAgent +``` + +Wildcards: `documents:*` matches all document actions. `*` matches everything. + +### Conditions + +Conditions add attribute-based constraints. Each condition key is a **condition operator**, and its value is an object mapping **condition keys** to expected values. + +```json +{ + "condition": { + "StringEquals": { + "soat:ResourceTag/environment": "production" + }, + "StringLike": { + "soat:ResourceTag/team": "engineering-*" + } + } +} +``` + +**Condition operators (v1):** + +| Operator | Description | +| ----------------- | ----------------------------- | +| `StringEquals` | Exact string match | +| `StringNotEquals` | Negated exact match | +| `StringLike` | Glob pattern match (`*`, `?`) | + +**Condition keys (v1):** + +| Key | Source | Description | +| ------------------------ | ------------- | --------------------------------------- | +| `soat:ResourceTag/` | Resource tags | Tag value on the target resource | +| `soat:ResourceType` | Request | The type of the resource being accessed | + +> **Future condition keys**: `soat:CurrentTime`, `soat:SourceIp`, `soat:RequestedModel`, `soat:TokenCost`, etc. + +### Tags + +Tags are key-value pairs attached to resources. They enable attribute-based access control (ABAC) via conditions. Any taggable resource (documents, files, actors, secrets, AI providers, conversations) can have tags. + +```json +{ + "tags": { + "environment": "production", + "team": "engineering", + "sensitivity": "high" + } +} +``` + +Tags are stored as a JSONB column on each taggable model. + +--- + +## Policy Evaluation Logic + +Policy evaluation follows **AWS IAM semantics**: + +1. **Default deny** — if no statement matches, access is denied +2. **Explicit deny wins** — if any policy statement explicitly denies, access is denied regardless of allows +3. **Allow** — if at least one statement allows and no statement denies, access is granted + +``` +function evaluate(policies, action, resource, context): + decision = DENY (default) + + for each policy in policies: + for each statement in policy.statements: + if statement matches (action, resource, context): + if statement.effect == "Deny": + return DENY (explicit deny, short-circuit) + if statement.effect == "Allow": + decision = ALLOW + + return decision +``` + +### Statement matching + +A statement matches a request when **all** of the following are true: + +1. At least one pattern in `action` matches the requested action +2. At least one pattern in `resource` matches the target SRN (or `resource` is omitted / `["*"]`) +3. All `condition` blocks evaluate to true (or `condition` is omitted) + +### Pattern matching + +- `*` matches everything +- `module:*` matches all actions in a module +- `soat:proj_ABC:document:*` matches all documents in project `proj_ABC` +- Wildcards only at segment boundaries (no partial wildcards like `doc_X*`) + +--- + +## Data Model + +### `PolicyDocument` (new model) + +Replaces the current `ProjectPolicy` model. A policy document contains the full IAM policy JSON and is scoped to a project. + +- `publicId` — `pol_` prefix (reuses existing prefix) +- `projectId` — FK to `Project` +- `name` — human-readable label (e.g., "Read-only Documents", "Full Access") +- `description` — optional description of what this policy grants +- `document` — JSONB; the policy document (version + statement array) +- `createdAt`, `updatedAt` + +```sql +CREATE TABLE policy_documents ( + id SERIAL PRIMARY KEY, + public_id VARCHAR(32) UNIQUE NOT NULL, + project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + document JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### `UserProject` (updated) + +The existing `UserProject` model gains support for **multiple policies**. Instead of a single `policyId` FK, it references a join table or an array of policy IDs. + +**Option chosen: array column** (simpler than a join table for v1): + +- `policyIds` — `INTEGER[]`; replaces the single `policyId` column + +During evaluation, all attached policies are loaded and merged using the evaluation logic above. + +### Tags on existing models + +Add a `tags` JSONB column to all taggable models: + +- `Document`: `tags JSONB DEFAULT '{}'` +- `File`: `tags JSONB DEFAULT '{}'` +- `Actor`: `tags JSONB DEFAULT '{}'` +- `Secret` (when added): `tags JSONB DEFAULT '{}'` +- `AiProvider` (when added): `tags JSONB DEFAULT '{}'` +- `Conversation`: `tags JSONB DEFAULT '{}'` + +--- + +## API + +### Policy Documents + +``` +POST /v1/projects/{projectId}/policies Create a policy document +GET /v1/projects/{projectId}/policies List policy documents +GET /v1/projects/{projectId}/policies/{id} Get a policy document +PUT /v1/projects/{projectId}/policies/{id} Update a policy document +DELETE /v1/projects/{projectId}/policies/{id} Delete a policy document +``` + +#### `POST /v1/projects/{projectId}/policies` + +```json +{ + "name": "Document Readers", + "description": "Read-only access to all documents", + "document": { + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": [ + "documents:GetDocument", + "documents:ListDocuments", + "documents:SearchDocuments" + ], + "resource": ["soat:proj_ABC:document:*"] + } + ] + } +} +``` + +Response (`201`): + +```json +{ + "id": "pol_V1StGXR8Z5jdHi6B", + "name": "Document Readers", + "description": "Read-only access to all documents", + "document": { "..." }, + "projectId": "proj_ABC", + "createdAt": "2025-01-15T10:00:00Z", + "updatedAt": "2025-01-15T10:00:00Z" +} +``` + +#### `PUT /v1/projects/{projectId}/policies/{id}` + +Full replacement of the policy document. Partial updates are not supported — send the complete document. + +### Project Members (updated) + +``` +POST /v1/projects/{projectId}/members Add user with policies +PUT /v1/projects/{projectId}/members/{userId}/policies Set policies for a member +GET /v1/projects/{projectId}/members/{userId}/policies Get policies for a member +``` + +#### `POST /v1/projects/{projectId}/members` + +```json +{ + "userId": "usr_V1StGXR8Z5jdHi6B", + "policyIds": ["pol_AAAA", "pol_BBBB"] +} +``` + +#### `PUT /v1/projects/{projectId}/members/{userId}/policies` + +```json +{ + "policyIds": ["pol_AAAA", "pol_BBBB", "pol_CCCC"] +} +``` + +### Tags + +Tags are managed via the existing resource endpoints. Each `POST`, `PATCH`, and `PUT` for taggable resources accepts an optional `tags` field: + +```json +{ + "name": "Q4 Report", + "tags": { + "department": "finance", + "sensitivity": "confidential" + } +} +``` + +Tags can also be managed with dedicated sub-endpoints: + +``` +PUT /v1/documents/{id}/tags Replace all tags on a document +PATCH /v1/documents/{id}/tags Merge tags (add/update keys, keep others) +GET /v1/documents/{id}/tags Get tags for a document +``` + +The same pattern applies to all taggable resources (`files`, `actors`, `secrets`, `ai-providers`, `conversations`). + +--- + +## Examples + +### 1. Grant read access to a specific document + +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["documents:GetDocument"], + "resource": ["soat:proj_ABC:document:doc_REPORT"] + } + ] +} +``` + +### 2. Allow all file operations except delete + +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["files:*"], + "resource": ["soat:proj_ABC:file:*"] + }, + { + "effect": "Deny", + "action": ["files:DeleteFile"], + "resource": ["soat:proj_ABC:file:*"] + } + ] +} +``` + +### 3. Allow agents tagged "internal" only + +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["agents:RunAgent"], + "resource": ["soat:proj_ABC:actor:*"], + "condition": { + "StringEquals": { + "soat:ResourceTag/visibility": "internal" + } + } + } + ] +} +``` + +### 4. Deny access to a specific secret + +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["secrets:*"], + "resource": ["soat:proj_ABC:secret:*"] + }, + { + "effect": "Deny", + "action": ["secrets:GetSecret"], + "resource": ["soat:proj_ABC:secret:sec_PROD_KEY"] + } + ] +} +``` + +### 5. Full admin policy (equivalent to current `*` permission) + +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["*"], + "resource": ["*"] + } + ] +} +``` + +### 6. Read-only across all modules + +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": [ + "documents:GetDocument", + "documents:ListDocuments", + "documents:SearchDocuments", + "files:GetFile", + "files:DownloadFile", + "secrets:ListSecrets", + "secrets:GetSecret", + "actors:ListActors", + "actors:GetActor", + "conversations:ListConversations", + "conversations:GetConversation", + "projects:GetProject" + ], + "resource": ["*"] + } + ] +} +``` + +--- + +## Migration from Current System + +The current `ProjectPolicy` model stores flat arrays: + +```json +{ + "permissions": ["files:*", "documents:GetDocument"], + "notPermissions": ["secrets:DeleteSecret"] +} +``` + +### Migration strategy + +1. **Dual-mode evaluation**: the policy engine checks if a `PolicyDocument` has a `document` field (new format) or `permissions`/`notPermissions` (legacy format). Both are supported simultaneously. + +2. **Automatic conversion**: an admin endpoint or CLI command converts legacy policies to the new format: + + ```json + { + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["files:*", "documents:GetDocument"], + "resource": ["*"] + }, + { + "effect": "Deny", + "action": ["secrets:DeleteSecret"], + "resource": ["*"] + } + ] + } + ``` + +3. **Deprecation**: after migration, the `permissions` and `notPermissions` columns are removed. + +### `isAllowed` signature change + +Current: + +```typescript +isAllowed(projectPublicId: string, action: string): Promise +``` + +New: + +```typescript +isAllowed(args: { + projectPublicId: string; + action: string; + resource?: string; // SRN of the target resource + context?: Record; // condition context (e.g., tags) +}): Promise +``` + +The new signature is backward-compatible: if `resource` and `context` are omitted, it behaves like the current system (action-only check with `resource = "*"`). + +--- + +## Implementation Notes + +### File locations + +| Component | Path | +| -------------------- | ---------------------------------------------------- | +| PolicyDocument model | `packages/postgresdb/src/models/PolicyDocument.ts` | +| Policy engine | `packages/server/src/lib/permissions.ts` (extend) | +| REST routes | `packages/server/src/rest/v1/projects.ts` (update) | +| MCP tools | `packages/server/src/mcp/tools/projects.ts` (update) | +| Auth middleware | `packages/server/src/middleware/auth.ts` (update) | +| Tests | `packages/server/tests/unit/tests/projects.test.ts` | +| Docs | `packages/website/docs/modules/iam.md` | + +### Policy document validation + +The server must validate policy documents on create/update: + +- `version` must be a recognized version string +- Each statement must have `effect` ∈ `{"Allow", "Deny"}` +- `action` must be a non-empty array of strings matching `module:Operation` or wildcard patterns +- `resource`, if present, must be valid SRN patterns +- `condition` operators must be from the supported set +- Condition keys must be from the recognized set + +Invalid documents are rejected with `400 Bad Request` and a descriptive error. + +### Performance considerations + +- **Policy caching**: resolved policies for a user+project pair should be cached in memory for the duration of a request (already the case in the auth middleware) +- **Tag loading**: when conditions reference `soat:ResourceTag/*`, tags must be loaded for the target resource. This adds one query per authorization check that uses tag conditions. Use eager loading where possible. +- **Index on tags**: add a GIN index on the `tags` JSONB column for efficient queries + +### SRN construction + +Each module's lib layer constructs the SRN from the resource being accessed: + +```typescript +// Example in documents.ts +const srn = `soat:${projectPublicId}:document:${document.publicId}`; +``` + +The auth middleware passes this SRN to the policy engine. + +### Public ID prefix + +Reuses `pol_` for policy documents (same concept, upgraded model). diff --git a/packages/server/src/lib/projects.ts b/packages/server/src/lib/projects.ts index fd24314c..2bae5065 100644 --- a/packages/server/src/lib/projects.ts +++ b/packages/server/src/lib/projects.ts @@ -16,6 +16,13 @@ export const listProjects = async (args: { authUser: AuthUser }) => { return projects.map(mapProject); } + if (args.authUser.apiKeyProjectId) { + const project = await db.Project.findOne({ + where: { publicId: args.authUser.apiKeyProjectId }, + }); + return project ? [mapProject(project)] : []; + } + const userProjects = await db.UserProject.findAll({ where: { userId: args.authUser.id }, include: [{ model: db.Project }], @@ -90,7 +97,9 @@ export const listProjectPolicies = async (args: { projectId: string }) => { where: { projectId: project.id }, }); - return policies.map((policy) => mapPolicy(policy, project.publicId)); + return policies.map((policy) => { + return mapPolicy(policy, project.publicId); + }); }; export const createProjectPolicy = async (args: { diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 3623a343..d4f7bdbb 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,4 +1,3 @@ -/* eslint-disable turbo/no-undeclared-env-vars */ import 'dotenv/config'; import { app } from './app'; @@ -12,7 +11,7 @@ const SOAT_PORT = process.env.PORT || 5047; const startServer = async () => { try { const database = await initializeDatabase(app); - await database.sequelize.sync(); + await database.sequelize.sync({ alter: true }); // Sync models with the database } catch (error) { // eslint-disable-next-line no-console console.error('Failed to connect to database:', error); diff --git a/packages/server/tests/unit/tests/projects.test.ts b/packages/server/tests/unit/tests/projects.test.ts index f96ed4b3..d855320b 100644 --- a/packages/server/tests/unit/tests/projects.test.ts +++ b/packages/server/tests/unit/tests/projects.test.ts @@ -96,6 +96,59 @@ describe('Projects', () => { ).toBe(true); expect(response.body.length).toBe(1); }); + describe('api key only sees its scoped project', () => { + let projectAId: string; + let rawApiKey: string; + + beforeAll(async () => { + const projARes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'API Key Project A' }); + projectAId = projARes.body.id; + + const projBRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'API Key Project B' }); + const projectBId = projBRes.body.id; + + const policyARes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectAId}/policies`) + .send({ permissions: ['projects:GetProject'] }); + const policyAId = policyARes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectAId}/members`) + .send({ userId, policyId: policyAId }); + + const policyBRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectBId}/policies`) + .send({ permissions: ['projects:GetProject'] }); + const policyBId = policyBRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectBId}/members`) + .send({ userId, policyId: policyBId }); + + const apiKeyRes = await authenticatedTestClient(userToken) + .post('/api/v1/api-keys') + .send({ + projectId: projectAId, + policyId: policyAId, + name: 'Scoped Key', + }); + rawApiKey = apiKeyRes.body.key; + }); + + test('api key user only sees the scoped project', async () => { + const response = + await authenticatedTestClient(rawApiKey).get('/api/v1/projects'); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBe(1); + expect(response.body[0].id).toBe(projectAId); + }); + }); }); describe('GET /api/v1/projects/:id', () => { From 2b12e2a6f23e5d1ac2c34603db1cb3df37b0b092 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Sun, 12 Apr 2026 21:34:04 -0300 Subject: [PATCH 13/27] feat: new iam --- docs/prd/iam-implementation.md | 607 ----------------- docs/prd/iam.md | 617 ------------------ packages/postgresdb/src/models/Actor.ts | 7 + .../postgresdb/src/models/Conversation.ts | 7 + packages/postgresdb/src/models/Document.ts | 4 +- packages/postgresdb/src/models/File.ts | 7 + .../src/models/{ApiKey.ts => ProjectKey.ts} | 12 +- .../postgresdb/src/models/ProjectPolicy.ts | 18 +- packages/postgresdb/src/models/UserProject.ts | 18 +- packages/postgresdb/src/models/index.ts | 2 +- packages/postgresdb/src/utils/publicId.ts | 6 +- packages/server/src/Context.ts | 11 +- packages/server/src/lib/actors.ts | 35 + packages/server/src/lib/apiKeys.ts | 75 --- packages/server/src/lib/conversations.ts | 47 +- packages/server/src/lib/documents.ts | 63 +- packages/server/src/lib/files.ts | 30 + packages/server/src/lib/iam.ts | 256 ++++++++ packages/server/src/lib/permissions.ts | 107 +-- packages/server/src/lib/projectKeys.ts | 78 +++ packages/server/src/lib/projects.ts | 239 ++++++- packages/server/src/mcp/tools/actors.ts | 4 +- .../server/src/mcp/tools/conversations.ts | 4 +- packages/server/src/mcp/tools/documents.ts | 4 +- packages/server/src/middleware/auth.ts | 62 +- .../server/src/rest/openapi/v1/actors.yaml | 6 +- .../src/rest/openapi/v1/conversations.yaml | 6 +- .../server/src/rest/openapi/v1/documents.yaml | 6 +- packages/server/src/rest/v1/actors.ts | 317 ++++++++- packages/server/src/rest/v1/conversations.ts | 435 ++++++++++-- packages/server/src/rest/v1/documents.ts | 383 ++++++++++- packages/server/src/rest/v1/files.ts | 371 ++++++++++- packages/server/src/rest/v1/index.ts | 4 +- .../rest/v1/{apiKeys.ts => projectKeys.ts} | 98 +-- packages/server/src/rest/v1/projects.ts | 532 +++++++++++++-- .../server/tests/unit/tests/documents.test.ts | 30 +- packages/server/tests/unit/tests/iam.test.ts | 436 +++++++++++++ .../tests/unit/tests/permissionsFlow.test.ts | 114 ++-- .../{apiKeys.test.ts => projectKeys.test.ts} | 72 +- .../server/tests/unit/tests/projects.test.ts | 36 +- packages/website/docs/modules/documents.md | 25 +- packages/website/docs/modules/files.md | 2 +- packages/website/docs/modules/iam.md | 403 ++++++++++-- packages/website/docs/modules/projects.md | 281 ++++++++ 44 files changed, 4012 insertions(+), 1865 deletions(-) delete mode 100644 docs/prd/iam-implementation.md delete mode 100644 docs/prd/iam.md rename packages/postgresdb/src/models/{ApiKey.ts => ProjectKey.ts} (87%) delete mode 100644 packages/server/src/lib/apiKeys.ts create mode 100644 packages/server/src/lib/iam.ts create mode 100644 packages/server/src/lib/projectKeys.ts rename packages/server/src/rest/v1/{apiKeys.ts => projectKeys.ts} (74%) create mode 100644 packages/server/tests/unit/tests/iam.test.ts rename packages/server/tests/unit/tests/{apiKeys.test.ts => projectKeys.test.ts} (79%) create mode 100644 packages/website/docs/modules/projects.md diff --git a/docs/prd/iam-implementation.md b/docs/prd/iam-implementation.md deleted file mode 100644 index 315120d9..00000000 --- a/docs/prd/iam-implementation.md +++ /dev/null @@ -1,607 +0,0 @@ -# IAM Implementation Plan - -Step-by-step implementation of the IAM system defined in [iam.md](./iam.md). Each step is self-contained, testable, and builds on the previous one. - ---- - -## Step 1: Define TypeScript Types and Validation - -**Goal**: Create the core type definitions and a validation function for the new policy document format. - -**Files to create/modify**: - -- Create `packages/server/src/lib/iam.ts` - -**What to do**: - -1. Define `PolicyDocument`, `Statement`, `Condition`, and `SRN` types: - - ```ts - type Effect = 'Allow' | 'Deny'; - - type ConditionOperator = 'StringEquals' | 'StringNotEquals' | 'StringLike'; - - type Condition = { - [operator in ConditionOperator]?: Record; - }; - - type Statement = { - effect: Effect; - action: string[]; - resource?: string[]; - condition?: Condition; - }; - - type PolicyDocument = { - statement: Statement[]; - }; - ``` - -2. Write `validatePolicyDocument(doc: unknown): { valid: boolean; errors: string[] }`: - - Each statement must have `effect` ∈ `{"Allow", "Deny"}` - - `action` must be a non-empty `string[]` where each entry matches `module:Operation` or `*` or `module:*` - - `resource`, if present, must be a non-empty `string[]` of valid SRN patterns - - `condition` operators must be from `{StringEquals, StringNotEquals, StringLike}` - - Condition keys must start with `soat:` - -3. Write `buildSrn(args: { projectPublicId: string; resourceType: string; resourceId: string }): string` that returns `soat:::`. - -**Tests**: Add `packages/server/tests/unit/tests/iam.test.ts` with: - -- Valid document passes validation -- Invalid `effect` fails -- Empty `action` array fails -- Invalid SRN format fails -- Invalid condition operator fails -- `buildSrn` produces correct SRN strings - -**Run**: - -```bash -pnpm --filter @soat/server test --testPathPatterns=iam.test.ts -``` - ---- - -## Step 2: Implement the Policy Evaluation Engine - -**Goal**: Build the core `evaluate` function that takes policies, an action, a resource SRN, and context — then returns Allow or Deny. - -**Files to modify**: - -- `packages/server/src/lib/iam.ts` (extend from Step 1) - -**What to do**: - -1. Write `matchesPattern(args: { pattern: string; value: string }): boolean`: - - `*` matches everything - - `module:*` matches all actions in `module:` - - `soat:proj_ABC:document:*` matches all documents in `proj_ABC` - - Exact match for non-wildcard patterns - -2. Write `evaluateCondition(args: { condition: Condition; context: Record }): boolean`: - - `StringEquals`: every key-value in the operator block must match exactly in context - - `StringNotEquals`: every key-value must NOT match - - `StringLike`: glob pattern match (`*` = any chars, `?` = single char) - - All operator blocks must pass (AND logic) - -3. Write `statementMatches(args: { statement: Statement; action: string; resource: string; context: Record }): boolean`: - - At least one `action` pattern matches the requested action - - At least one `resource` pattern matches the target SRN (or resource is `["*"]` / omitted) - - All conditions pass (or no conditions) - -4. Write `evaluatePolicies(args: { policies: PolicyDocument[]; action: string; resource?: string; context?: Record }): boolean`: - - Default `resource` to `*` if omitted - - Default `context` to `{}` if omitted - - Loop through all policies and all statements - - If any matching statement has `effect: "Deny"` → return `false` (explicit deny, short-circuit) - - If any matching statement has `effect: "Allow"` → mark allowed - - Return the allowed flag (default `false`) - -**Tests**: Extend `iam.test.ts`: - -- Action-only policy (no resource/condition) allows matching action -- Action-only policy denies non-matching action -- Wildcard `*` allows everything -- `module:*` matches all actions in module -- Resource-specific policy allows matching SRN -- Resource-specific policy denies different SRN -- Resource wildcard `soat:proj_ABC:document:*` matches specific doc -- Explicit deny overrides allow -- Multiple policies: allow from one + deny from another = deny -- Multiple policies: allow from one + no match from another = allow -- Condition `StringEquals` matches -- Condition `StringEquals` does not match -- Condition `StringNotEquals` works -- Condition `StringLike` with glob patterns -- No matching statements → default deny -- Omitted `resource` defaults to `["*"]` - -**Run**: - -```bash -pnpm --filter @soat/server test --testPathPatterns=iam.test.ts -``` - ---- - -## Step 3: Update the Database Models - -**Goal**: Replace `ProjectPolicy` flat permission columns with IAM document support and add `tags` to taggable models. - -**Files to modify**: - -- `packages/postgresdb/src/models/ProjectPolicy.ts` -- `packages/postgresdb/src/models/UserProject.ts` -- `packages/postgresdb/src/models/Document.ts` -- `packages/postgresdb/src/models/File.ts` -- `packages/postgresdb/src/models/Actor.ts` -- `packages/postgresdb/src/models/Conversation.ts` - -**What to do**: - -### 3a. Replace `ProjectPolicy` Columns - -Remove the existing `permissions` and `notPermissions` columns. Replace with: - -```ts -@Column({ type: DataType.STRING(255), allowNull: true }) -declare name: string | null; - -@Column({ type: DataType.TEXT, allowNull: true }) -declare description: string | null; - -@Column({ type: DataType.JSONB, allowNull: false }) -declare document: object; // PolicyDocument JSON -``` - -The `document` column stores a validated `PolicyDocument` and is now required for all policies. - -### 3b. Update `UserProject` for Multiple Policies - -Replace the single `policyId` FK with an array: - -```ts -@Column({ - type: DataType.ARRAY(DataType.INTEGER), - allowNull: false, - defaultValue: [], -}) -declare policyIds: number[]; -``` - -Remove the `@ForeignKey` and `@BelongsTo` for the single policy relationship. - -> **Note**: Since the project is not shipped yet, drop and recreate the dev DB after these schema changes. - -### 3c. Add `tags` to Taggable Models - -Add to `Document`, `File`, `Actor`, and `Conversation`: - -```ts -@Column({ - type: DataType.JSONB, - allowNull: true, - defaultValue: {}, -}) -declare tags: Record | null; -``` - -> **Note on Document model**: The existing `tags` column on Document is `DataType.ARRAY(DataType.TEXT)`. This changes to `DataType.JSONB` (key-value instead of array). Drop and recreate the dev DB to apply this type change. - -### 3d. Rebuild - -```bash -pnpm --filter @soat/postgresdb build -``` - -**Tests**: Run existing tests to verify nothing breaks: - -```bash -pnpm --filter @soat/server test -``` - ---- - -## Step 4: Update the Permission Functions (`permissions.ts`) - -**Goal**: Replace the permission evaluation to use the new IAM engine. - -**Files to modify**: - -- `packages/server/src/lib/permissions.ts` - -**What to do**: - -1. Import `evaluatePolicies`, `validatePolicyDocument`, and `PolicyDocument` from `./iam`. - -2. Replace `policyAllows` to use the new format: - - ```ts - export const policyAllows = (args: { - policy: { document: object }; - action: string; - resource?: string; - context?: Record; - }): boolean => { - return evaluatePolicies({ - policies: [args.policy.document as PolicyDocument], - action: args.action, - resource: args.resource, - context: args.context, - }); - }; - ``` - -3. Update `createJwtIsAllowed` and `createApiKeyIsAllowed`: - - Load **all** policies from the `policyIds` array on UserProject (instead of a single `policyId`) - - Extract the `document` from each policy - - Call `evaluatePolicies` with the full array - -**Tests**: Extend `iam.test.ts`: - -- `policyAllows` works with IAM document -- Multiple policies evaluated together - -**Run**: - -```bash -pnpm --filter @soat/server test --testPathPatterns=iam.test.ts -``` - ---- - -## Step 5: Update `isAllowed` Signature and Auth Middleware - -**Goal**: Extend `isAllowed` to accept optional `resource` and `context` parameters. Update the auth middleware to load multiple policies and pass tags as context. - -**Files to modify**: - -- `packages/server/src/Context.ts` -- `packages/server/src/middleware/auth.ts` - -**What to do**: - -### 5a. Update `Context.ts` - -Change the `isAllowed` signature on `AuthUser`: - -```ts -isAllowed: (args: { - projectPublicId: string; - action: string; - resource?: string; - context?: Record; -}) => Promise; -``` - -### 5b. Update `auth.ts` - -**`resolveJwt`**: - -Update `createJwtIsAllowed` call: - -1. Load all policies for the user's membership by looking up `policyIds` array from `UserProject`, then fetching all matching `ProjectPolicy` rows. -2. Pass the array of policy documents to the evaluation. -3. Support the new `resource` and `context` args. - -**`resolveApiKey`**: - -Same approach — load the API key's policy AND the user's project policies, evaluate with intersection logic: - -- Action must be allowed by user policies AND API key policy. -- Resource and context are checked in both. - -### 5c. Update All `isAllowed` Call Sites - -Since the signature changes from `(projectPublicId, action)` to `({ projectPublicId, action, resource?, context? })`, every call site in the REST handlers must be updated. Search for `isAllowed(` across `packages/server/src/rest/` and update to the object form: - -```ts -// Before -await ctx.authUser.isAllowed(projectId, 'documents:GetDocument'); - -// After -await ctx.authUser.isAllowed({ - projectPublicId: projectId, - action: 'documents:GetDocument', -}); -``` - -At this point, no call sites need to pass `resource` or `context` yet — those will be added per-module in Step 7. - -**Tests**: Run full test suite to verify everything works: - -```bash -pnpm --filter @soat/server test -``` - ---- - -## Step 6: Update Policy REST Endpoints - -**Goal**: Update the policy CRUD endpoints to accept the new IAM document format. - -**Files to modify**: - -- `packages/server/src/lib/projects.ts` -- `packages/server/src/rest/v1/projects.ts` -- `packages/server/src/rest/openapi/v1/projects.yaml` (if it exists) - -**What to do**: - -### 6a. Update `createProjectPolicy` in `lib/projects.ts` - -Accept either legacy or new format: - -```ts -export const createProjectPolicy = async (args: { - projectId: string; - name?: string; - description?: string; - document: PolicyDocument; -}) => { ... }; -``` - -- Validate the document with `validatePolicyDocument`. Return 400 on invalid. - -### 6b. Update `POST /projects/:projectId/policies` - -Accept the new request body shape: - -```json -{ - "name": "Document Readers", - "description": "Read-only access to documents", - "document": { "statement": [...] } -} -``` - -### 6c. Add `PUT /projects/:projectId/policies/:policyId` - -Full replacement of a policy document. Validate the new document before saving. - -### 6d. Add `DELETE /projects/:projectId/policies/:policyId` - -Delete a policy. Must check that no UserProject references it before deleting (or cascade). - -### 6e. Update Member Endpoints - -Update `POST /projects/:projectId/members` to accept `policyIds: string[]` (array of policy public IDs) instead of a single `policyId`. - -Add `PUT /projects/:projectId/members/:userId/policies` to update a member's attached policies. - -Add `GET /projects/:projectId/members/:userId/policies` to list a member's attached policies. - -### 6f. Update Response Shapes - -Policy responses should include `name`, `description`, and `document` when present: - -```json -{ - "id": "pol_...", - "name": "Document Readers", - "description": "Read-only access to documents", - "document": { "statement": [...] }, - "projectId": "proj_...", - "createdAt": "...", - "updatedAt": "..." -} -``` - -**Tests**: Add to `projects.test.ts`: - -- Create policy with IAM document format -- Invalid document returns 400 -- Update policy with PUT -- Delete policy -- Add member with multiple `policyIds` -- Update member policies -- Get member policies -- Validation: invalid `effect`, empty `action` array - -**Run**: - -```bash -pnpm --filter @soat/server test --testPathPatterns=projects.test.ts -``` - ---- - -## Step 7: Add Resource-Level and Tag-Based Checks to Modules - -**Goal**: Pass `resource` SRN and `context` (tags) to `isAllowed` in modules that benefit from fine-grained access control. - -**Files to modify**: - -- `packages/server/src/lib/documents.ts` -- `packages/server/src/lib/files.ts` -- `packages/server/src/lib/actors.ts` -- `packages/server/src/lib/conversations.ts` -- `packages/server/src/rest/v1/documents.ts` -- `packages/server/src/rest/v1/files.ts` -- `packages/server/src/rest/v1/actors.ts` -- `packages/server/src/rest/v1/conversations.ts` - -**What to do**: - -### 7a. Add SRN Construction to Lib Functions - -In each module's lib layer, construct the SRN after loading the resource: - -```ts -// documents.ts -const srn = buildSrn({ - projectPublicId: project.publicId, - resourceType: 'document', - resourceId: document.publicId, -}); -``` - -### 7b. Add Tag Context Loading - -When a resource is loaded and it has `tags`, build the condition context: - -```ts -const context: Record = {}; -if (resource.tags) { - for (const [key, value] of Object.entries(resource.tags)) { - context[`soat:ResourceTag/${key}`] = value; - } -} -context['soat:ResourceType'] = 'document'; -``` - -### 7c. Update `isAllowed` Calls in Routes - -For single-resource operations (`GET /:id`, `PUT /:id`, `DELETE /:id`): - -1. Load the resource from DB -2. Build SRN and context -3. Call `isAllowed({ projectPublicId, action, resource: srn, context })` - -For list operations (`GET /`), the policy check stays at the action level (no specific resource). Resources are filtered post-query if needed, or the action-level check is sufficient for v1. - -### 7d. Add Tag CRUD Sub-Endpoints - -For each taggable module, add: - -``` -PUT /v1/documents/:id/tags — replace all tags -PATCH /v1/documents/:id/tags — merge tags -GET /v1/documents/:id/tags — get tags -``` - -The same pattern for `files`, `actors`, `conversations`. - -**Tests**: Extend each module's test file: - -- Resource-level allow: policy allows `doc_XYZ`, request for `doc_XYZ` passes -- Resource-level deny: policy allows `documents:*` but denies specific `doc_XYZ`, request for `doc_XYZ` blocked -- Tag condition: policy with `StringEquals { "soat:ResourceTag/env": "prod" }`, resource tagged `env=prod` passes, `env=dev` fails -- Tag CRUD: create/update/get tags on a resource - -**Run**: - -```bash -pnpm --filter @soat/server test -``` - ---- - -## Step 8: Update MCP Tools - -**Goal**: Expose policy management and tag operations via MCP tools. - -**Files to modify**: - -- `packages/server/src/mcp/tools/projects.ts` -- `packages/server/src/mcp/tools/index.ts` - -**What to do**: - -### 8a. Add Policy Management Tools - -``` -create-policy POST /projects/{projectId}/policies -list-policies GET /projects/{projectId}/policies -get-policy GET /projects/{projectId}/policies/{id} -update-policy PUT /projects/{projectId}/policies/{id} -delete-policy DELETE /projects/{projectId}/policies/{id} -``` - -### 8b. Add Member Policy Tools - -``` -set-member-policies PUT /projects/{projectId}/members/{userId}/policies -get-member-policies GET /projects/{projectId}/members/{userId}/policies -``` - -### 8c. Add Tag Tools (per module) - -For each taggable module, add tools: - -``` -set-document-tags PUT /documents/{id}/tags -get-document-tags GET /documents/{id}/tags -``` - -(Same for files, actors, conversations.) - -### 8d. Register in `index.ts` - -Import and call `registerTools` for the new/updated tool files. - -**Tests**: Manual verification via MCP inspector or integration tests. - ---- - -## Step 9: Documentation - -- `packages/website/docs/modules/iam.md` - -**What to do**: - -1. Write the module doc covering: - - Overview: what the IAM system does - - Key concepts: policy documents, statements, SRNs, conditions, tags - - Policy evaluation rules (default deny, explicit deny wins) - - How to create and attach policies - - Tag-based ABAC examples - -2. Update `packages/website/sidebars.ts` if needed to include the IAM doc. - ---- - -## Step 10: End-to-End Smoke Test - -**Goal**: Verify the full IAM flow works end-to-end. - -**Files to modify**: - -- `tests/smoke-test.sh` - -**What to do**: - -Add IAM scenarios to the smoke test: - -1. Bootstrap admin + create project -2. Create an IAM policy document (resource-level allow for a specific document) -3. Create a user, add to project with the policy -4. Upload a document, tag it -5. Verify user CAN access the allowed document -6. Verify user CANNOT access a different document -7. Create a deny policy for a specific resource -8. Verify deny overrides allow - -**Run**: - -```bash -pnpm run -w smoke-test -``` - ---- - -## Implementation Order Summary - -| Step | Description | Depends On | Risk | -| ---- | ---------------------------------- | ---------- | ------ | -| 1 | Types and validation | — | Low | -| 2 | Policy evaluation engine | 1 | Low | -| 3 | Database model changes | — | Medium | -| 4 | Update permission functions | 1, 2, 3 | Medium | -| 5 | Update isAllowed + auth middleware | 4 | High | -| 6 | Update policy REST endpoints | 3, 4 | Medium | -| 7 | Resource-level + tag checks | 5, 6 | Medium | -| 8 | MCP tools | 6, 7 | Low | -| 9 | Documentation | All | Low | -| 10 | End-to-end smoke test | All | Low | - -Steps 1–2 and 3 can be done in parallel. Steps 4–5 are the critical path. Steps 8–10 can be done in any order after their dependencies. - -### Estimated Breakdown by Area - -- **Policy engine** (Steps 1–2): Pure functions, no side effects, highly testable in isolation -- **Database** (Step 3): Drop and recreate dev DB after schema changes -- **Auth integration** (Steps 4–5): Highest risk — touches every authenticated request -- **REST API** (Steps 6–7): Incremental — add new endpoints, update existing ones -- **MCP + Docs + Smoke test** (Steps 8–10): Low risk, mostly additive diff --git a/docs/prd/iam.md b/docs/prd/iam.md deleted file mode 100644 index 841721e6..00000000 --- a/docs/prd/iam.md +++ /dev/null @@ -1,617 +0,0 @@ -# PRD: Identity and Access Management (IAM) - -## Context - -Soat's current authorization system uses flat permission lists (`permissions` / `notPermissions`) on project policies. These lists support only **action-level** checks — e.g., allowing `files:GetFile` or denying `documents:*`. This is insufficient for real-world multi-tenant scenarios: - -| Limitation | Example | -| ---------------------------------- | ----------------------------------------------------------------- | -| No resource-level targeting | Cannot grant access to a specific document (`doc_XYZ`) only | -| No tag/attribute-based conditions | Cannot allow agents tagged `"internal"` while denying `"public"` | -| No explicit deny on specific items | Cannot deny a user access to one secret while allowing all others | -| Single policy per membership | A user gets exactly one policy per project — no composition | - -This PRD introduces an **AWS IAM-inspired policy engine** that replaces the current flat-list model with structured policy statements supporting `Effect`, `Action`, `Resource`, and `Condition`. - -### Goals - -1. **Resource-level permissions** — grant or deny access to individual resources (a specific document, file, secret, etc.) -2. **Condition-based access control** — support tag-based conditions (e.g., only agents with tag `internal`) -3. **Explicit deny** — deny always wins over allow, enabling blocklists -4. **Policy composition** — attach multiple policies to a user/API key; the engine merges them -5. **Backward compatibility** — existing `permissions`/`notPermissions` arrays continue to work during migration - -### Non-goals - -- Federated identity (SSO, SAML, OIDC) — out of scope for v1 -- Cross-account/cross-project trust policies -- Policy versioning or rollback -- Real-time policy simulation API (future iteration) - ---- - -## Concepts - -### Policy Document - -A **policy document** is a JSON object containing one or more **statements**. Each statement describes a permission rule. This is the core primitive — everything else (attaching to users, API keys, projects) is about where the document lives. - -```json -{ - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": ["documents:GetDocument", "documents:ListDocuments"], - "resource": ["soat:proj_ABC:document:doc_XYZ"] - }, - { - "effect": "Deny", - "action": ["secrets:*"], - "resource": ["soat:proj_ABC:secret:sec_PROD_KEY"] - } - ] -} -``` - -### Statement - -A single permission rule within a policy document. - -| Field | Type | Required | Description | -| ----------- | ---------- | -------- | ------------------------------------------------------- | -| `effect` | `string` | Yes | `"Allow"` or `"Deny"` | -| `action` | `string[]` | Yes | Actions this statement applies to (supports wildcards) | -| `resource` | `string[]` | No | SRNs this statement applies to (default: `["*"]`) | -| `condition` | `object` | No | Conditions that must be true for the statement to apply | - -### Soat Resource Name (SRN) - -Every addressable entity in Soat has a canonical identifier called a **Soat Resource Name (SRN)**. Format: - -``` -soat::: -``` - -Examples: - -``` -soat:proj_ABC:document:doc_XYZ # specific document -soat:proj_ABC:document:* # all documents in project -soat:proj_ABC:secret:sec_PROD # specific secret -soat:proj_ABC:file:* # all files in project -soat:proj_ABC:actor:act_123 # specific actor -soat:proj_ABC:conversation:conv_789 # specific conversation -soat:proj_ABC:ai-provider:aip_456 # specific AI provider -soat:*:*:* # everything (admin-level) -``` - -Wildcards (`*`) are supported at any segment. The resource type maps directly to module names. - -**Resource types:** - -| Resource type | Public ID prefix | Module | -| -------------- | ---------------- | ------------- | -| `document` | `doc_` | Documents | -| `file` | `file_` | Files | -| `secret` | `sec_` | Secrets | -| `actor` | `act_` | Actors | -| `conversation` | `conv_` | Conversations | -| `ai-provider` | `aip_` | AI Providers | -| `project` | `proj_` | Projects | -| `policy` | `pol_` | Policies | -| `api-key` | `key_` | API Keys | - -### Actions - -Actions follow the existing `module:Operation` pattern: - -``` -documents:GetDocument -documents:ListDocuments -documents:CreateDocument -documents:UpdateDocument -documents:DeleteDocument -documents:SearchDocuments -files:GetFile -files:CreateFile -files:DeleteFile -files:UploadFile -files:DownloadFile -files:UpdateFileMetadata -secrets:GetSecret -secrets:ListSecrets -secrets:CreateSecret -secrets:UpdateSecret -secrets:DeleteSecret -actors:ListActors -actors:GetActor -actors:CreateActor -actors:UpdateActor -actors:DeleteActor -conversations:ListConversations -conversations:GetConversation -conversations:CreateConversation -conversations:UpdateConversation -conversations:DeleteConversation -projects:GetProject -chats:CreateCompletion -agents:RunAgent -``` - -Wildcards: `documents:*` matches all document actions. `*` matches everything. - -### Conditions - -Conditions add attribute-based constraints. Each condition key is a **condition operator**, and its value is an object mapping **condition keys** to expected values. - -```json -{ - "condition": { - "StringEquals": { - "soat:ResourceTag/environment": "production" - }, - "StringLike": { - "soat:ResourceTag/team": "engineering-*" - } - } -} -``` - -**Condition operators (v1):** - -| Operator | Description | -| ----------------- | ----------------------------- | -| `StringEquals` | Exact string match | -| `StringNotEquals` | Negated exact match | -| `StringLike` | Glob pattern match (`*`, `?`) | - -**Condition keys (v1):** - -| Key | Source | Description | -| ------------------------ | ------------- | --------------------------------------- | -| `soat:ResourceTag/` | Resource tags | Tag value on the target resource | -| `soat:ResourceType` | Request | The type of the resource being accessed | - -> **Future condition keys**: `soat:CurrentTime`, `soat:SourceIp`, `soat:RequestedModel`, `soat:TokenCost`, etc. - -### Tags - -Tags are key-value pairs attached to resources. They enable attribute-based access control (ABAC) via conditions. Any taggable resource (documents, files, actors, secrets, AI providers, conversations) can have tags. - -```json -{ - "tags": { - "environment": "production", - "team": "engineering", - "sensitivity": "high" - } -} -``` - -Tags are stored as a JSONB column on each taggable model. - ---- - -## Policy Evaluation Logic - -Policy evaluation follows **AWS IAM semantics**: - -1. **Default deny** — if no statement matches, access is denied -2. **Explicit deny wins** — if any policy statement explicitly denies, access is denied regardless of allows -3. **Allow** — if at least one statement allows and no statement denies, access is granted - -``` -function evaluate(policies, action, resource, context): - decision = DENY (default) - - for each policy in policies: - for each statement in policy.statements: - if statement matches (action, resource, context): - if statement.effect == "Deny": - return DENY (explicit deny, short-circuit) - if statement.effect == "Allow": - decision = ALLOW - - return decision -``` - -### Statement matching - -A statement matches a request when **all** of the following are true: - -1. At least one pattern in `action` matches the requested action -2. At least one pattern in `resource` matches the target SRN (or `resource` is omitted / `["*"]`) -3. All `condition` blocks evaluate to true (or `condition` is omitted) - -### Pattern matching - -- `*` matches everything -- `module:*` matches all actions in a module -- `soat:proj_ABC:document:*` matches all documents in project `proj_ABC` -- Wildcards only at segment boundaries (no partial wildcards like `doc_X*`) - ---- - -## Data Model - -### `PolicyDocument` (new model) - -Replaces the current `ProjectPolicy` model. A policy document contains the full IAM policy JSON and is scoped to a project. - -- `publicId` — `pol_` prefix (reuses existing prefix) -- `projectId` — FK to `Project` -- `name` — human-readable label (e.g., "Read-only Documents", "Full Access") -- `description` — optional description of what this policy grants -- `document` — JSONB; the policy document (version + statement array) -- `createdAt`, `updatedAt` - -```sql -CREATE TABLE policy_documents ( - id SERIAL PRIMARY KEY, - public_id VARCHAR(32) UNIQUE NOT NULL, - project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - name VARCHAR(255) NOT NULL, - description TEXT, - document JSONB NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); -``` - -### `UserProject` (updated) - -The existing `UserProject` model gains support for **multiple policies**. Instead of a single `policyId` FK, it references a join table or an array of policy IDs. - -**Option chosen: array column** (simpler than a join table for v1): - -- `policyIds` — `INTEGER[]`; replaces the single `policyId` column - -During evaluation, all attached policies are loaded and merged using the evaluation logic above. - -### Tags on existing models - -Add a `tags` JSONB column to all taggable models: - -- `Document`: `tags JSONB DEFAULT '{}'` -- `File`: `tags JSONB DEFAULT '{}'` -- `Actor`: `tags JSONB DEFAULT '{}'` -- `Secret` (when added): `tags JSONB DEFAULT '{}'` -- `AiProvider` (when added): `tags JSONB DEFAULT '{}'` -- `Conversation`: `tags JSONB DEFAULT '{}'` - ---- - -## API - -### Policy Documents - -``` -POST /v1/projects/{projectId}/policies Create a policy document -GET /v1/projects/{projectId}/policies List policy documents -GET /v1/projects/{projectId}/policies/{id} Get a policy document -PUT /v1/projects/{projectId}/policies/{id} Update a policy document -DELETE /v1/projects/{projectId}/policies/{id} Delete a policy document -``` - -#### `POST /v1/projects/{projectId}/policies` - -```json -{ - "name": "Document Readers", - "description": "Read-only access to all documents", - "document": { - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": [ - "documents:GetDocument", - "documents:ListDocuments", - "documents:SearchDocuments" - ], - "resource": ["soat:proj_ABC:document:*"] - } - ] - } -} -``` - -Response (`201`): - -```json -{ - "id": "pol_V1StGXR8Z5jdHi6B", - "name": "Document Readers", - "description": "Read-only access to all documents", - "document": { "..." }, - "projectId": "proj_ABC", - "createdAt": "2025-01-15T10:00:00Z", - "updatedAt": "2025-01-15T10:00:00Z" -} -``` - -#### `PUT /v1/projects/{projectId}/policies/{id}` - -Full replacement of the policy document. Partial updates are not supported — send the complete document. - -### Project Members (updated) - -``` -POST /v1/projects/{projectId}/members Add user with policies -PUT /v1/projects/{projectId}/members/{userId}/policies Set policies for a member -GET /v1/projects/{projectId}/members/{userId}/policies Get policies for a member -``` - -#### `POST /v1/projects/{projectId}/members` - -```json -{ - "userId": "usr_V1StGXR8Z5jdHi6B", - "policyIds": ["pol_AAAA", "pol_BBBB"] -} -``` - -#### `PUT /v1/projects/{projectId}/members/{userId}/policies` - -```json -{ - "policyIds": ["pol_AAAA", "pol_BBBB", "pol_CCCC"] -} -``` - -### Tags - -Tags are managed via the existing resource endpoints. Each `POST`, `PATCH`, and `PUT` for taggable resources accepts an optional `tags` field: - -```json -{ - "name": "Q4 Report", - "tags": { - "department": "finance", - "sensitivity": "confidential" - } -} -``` - -Tags can also be managed with dedicated sub-endpoints: - -``` -PUT /v1/documents/{id}/tags Replace all tags on a document -PATCH /v1/documents/{id}/tags Merge tags (add/update keys, keep others) -GET /v1/documents/{id}/tags Get tags for a document -``` - -The same pattern applies to all taggable resources (`files`, `actors`, `secrets`, `ai-providers`, `conversations`). - ---- - -## Examples - -### 1. Grant read access to a specific document - -```json -{ - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": ["documents:GetDocument"], - "resource": ["soat:proj_ABC:document:doc_REPORT"] - } - ] -} -``` - -### 2. Allow all file operations except delete - -```json -{ - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": ["files:*"], - "resource": ["soat:proj_ABC:file:*"] - }, - { - "effect": "Deny", - "action": ["files:DeleteFile"], - "resource": ["soat:proj_ABC:file:*"] - } - ] -} -``` - -### 3. Allow agents tagged "internal" only - -```json -{ - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": ["agents:RunAgent"], - "resource": ["soat:proj_ABC:actor:*"], - "condition": { - "StringEquals": { - "soat:ResourceTag/visibility": "internal" - } - } - } - ] -} -``` - -### 4. Deny access to a specific secret - -```json -{ - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": ["secrets:*"], - "resource": ["soat:proj_ABC:secret:*"] - }, - { - "effect": "Deny", - "action": ["secrets:GetSecret"], - "resource": ["soat:proj_ABC:secret:sec_PROD_KEY"] - } - ] -} -``` - -### 5. Full admin policy (equivalent to current `*` permission) - -```json -{ - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": ["*"], - "resource": ["*"] - } - ] -} -``` - -### 6. Read-only across all modules - -```json -{ - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": [ - "documents:GetDocument", - "documents:ListDocuments", - "documents:SearchDocuments", - "files:GetFile", - "files:DownloadFile", - "secrets:ListSecrets", - "secrets:GetSecret", - "actors:ListActors", - "actors:GetActor", - "conversations:ListConversations", - "conversations:GetConversation", - "projects:GetProject" - ], - "resource": ["*"] - } - ] -} -``` - ---- - -## Migration from Current System - -The current `ProjectPolicy` model stores flat arrays: - -```json -{ - "permissions": ["files:*", "documents:GetDocument"], - "notPermissions": ["secrets:DeleteSecret"] -} -``` - -### Migration strategy - -1. **Dual-mode evaluation**: the policy engine checks if a `PolicyDocument` has a `document` field (new format) or `permissions`/`notPermissions` (legacy format). Both are supported simultaneously. - -2. **Automatic conversion**: an admin endpoint or CLI command converts legacy policies to the new format: - - ```json - { - "version": "2025-01-01", - "statement": [ - { - "effect": "Allow", - "action": ["files:*", "documents:GetDocument"], - "resource": ["*"] - }, - { - "effect": "Deny", - "action": ["secrets:DeleteSecret"], - "resource": ["*"] - } - ] - } - ``` - -3. **Deprecation**: after migration, the `permissions` and `notPermissions` columns are removed. - -### `isAllowed` signature change - -Current: - -```typescript -isAllowed(projectPublicId: string, action: string): Promise -``` - -New: - -```typescript -isAllowed(args: { - projectPublicId: string; - action: string; - resource?: string; // SRN of the target resource - context?: Record; // condition context (e.g., tags) -}): Promise -``` - -The new signature is backward-compatible: if `resource` and `context` are omitted, it behaves like the current system (action-only check with `resource = "*"`). - ---- - -## Implementation Notes - -### File locations - -| Component | Path | -| -------------------- | ---------------------------------------------------- | -| PolicyDocument model | `packages/postgresdb/src/models/PolicyDocument.ts` | -| Policy engine | `packages/server/src/lib/permissions.ts` (extend) | -| REST routes | `packages/server/src/rest/v1/projects.ts` (update) | -| MCP tools | `packages/server/src/mcp/tools/projects.ts` (update) | -| Auth middleware | `packages/server/src/middleware/auth.ts` (update) | -| Tests | `packages/server/tests/unit/tests/projects.test.ts` | -| Docs | `packages/website/docs/modules/iam.md` | - -### Policy document validation - -The server must validate policy documents on create/update: - -- `version` must be a recognized version string -- Each statement must have `effect` ∈ `{"Allow", "Deny"}` -- `action` must be a non-empty array of strings matching `module:Operation` or wildcard patterns -- `resource`, if present, must be valid SRN patterns -- `condition` operators must be from the supported set -- Condition keys must be from the recognized set - -Invalid documents are rejected with `400 Bad Request` and a descriptive error. - -### Performance considerations - -- **Policy caching**: resolved policies for a user+project pair should be cached in memory for the duration of a request (already the case in the auth middleware) -- **Tag loading**: when conditions reference `soat:ResourceTag/*`, tags must be loaded for the target resource. This adds one query per authorization check that uses tag conditions. Use eager loading where possible. -- **Index on tags**: add a GIN index on the `tags` JSONB column for efficient queries - -### SRN construction - -Each module's lib layer constructs the SRN from the resource being accessed: - -```typescript -// Example in documents.ts -const srn = `soat:${projectPublicId}:document:${document.publicId}`; -``` - -The auth middleware passes this SRN to the policy engine. - -### Public ID prefix - -Reuses `pol_` for policy documents (same concept, upgraded model). diff --git a/packages/postgresdb/src/models/Actor.ts b/packages/postgresdb/src/models/Actor.ts index 07d796e9..88bac1f0 100644 --- a/packages/postgresdb/src/models/Actor.ts +++ b/packages/postgresdb/src/models/Actor.ts @@ -54,6 +54,13 @@ export class Actor extends Model { @Column({ type: DataType.STRING }) declare externalId?: string; + @Column({ + type: DataType.JSONB, + allowNull: true, + defaultValue: {}, + }) + declare tags: Record | null; + @Column({ type: DataType.DATE }) declare createdAt: Date; diff --git a/packages/postgresdb/src/models/Conversation.ts b/packages/postgresdb/src/models/Conversation.ts index 8b2f3315..a28d1b35 100644 --- a/packages/postgresdb/src/models/Conversation.ts +++ b/packages/postgresdb/src/models/Conversation.ts @@ -48,6 +48,13 @@ export class Conversation extends Model { }) declare status: string; + @Column({ + type: DataType.JSONB, + allowNull: true, + defaultValue: {}, + }) + declare tags: Record | null; + @HasMany(() => { return ConversationMessage; }) diff --git a/packages/postgresdb/src/models/Document.ts b/packages/postgresdb/src/models/Document.ts index 2e61f5ca..b07bcc25 100644 --- a/packages/postgresdb/src/models/Document.ts +++ b/packages/postgresdb/src/models/Document.ts @@ -52,10 +52,10 @@ export class Document extends Model { declare metadata: string | null; @Column({ - type: DataType.ARRAY(DataType.TEXT), + type: DataType.JSONB, allowNull: true, }) - declare tags: string[] | null; + declare tags: Record | null; @Column({ type: DataType.VECTOR(1024), diff --git a/packages/postgresdb/src/models/File.ts b/packages/postgresdb/src/models/File.ts index 24c6c8fa..1f9f816b 100644 --- a/packages/postgresdb/src/models/File.ts +++ b/packages/postgresdb/src/models/File.ts @@ -57,6 +57,13 @@ export class File extends Model { @Column({ type: DataType.TEXT }) declare metadata?: string; // JSON string + @Column({ + type: DataType.JSONB, + allowNull: true, + defaultValue: {}, + }) + declare tags: Record | null; + @Column({ type: DataType.DATE }) declare createdAt: Date; diff --git a/packages/postgresdb/src/models/ApiKey.ts b/packages/postgresdb/src/models/ProjectKey.ts similarity index 87% rename from packages/postgresdb/src/models/ApiKey.ts rename to packages/postgresdb/src/models/ProjectKey.ts index 53c752e0..0f9fe3a4 100644 --- a/packages/postgresdb/src/models/ApiKey.ts +++ b/packages/postgresdb/src/models/ProjectKey.ts @@ -13,16 +13,16 @@ import { ProjectPolicy } from './ProjectPolicy'; import { User } from './User'; @Table({ - tableName: 'api_keys', + tableName: 'project_keys', hooks: { - beforeValidate: (instance: ApiKey) => { + beforeValidate: (instance: ProjectKey) => { if (!instance.publicId) { - instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.apiKey); + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.projectKey); } }, }, }) -export class ApiKey extends Model { +export class ProjectKey extends Model { @Column({ type: DataType.STRING(32), unique: true, @@ -76,7 +76,7 @@ export class ApiKey extends Model { declare name: string; /** - * First 8 characters of the raw API key. Stored in plaintext to allow + * First 8 characters of the raw project key. Stored in plaintext to allow * fast DB lookup before running bcrypt.compare against keyHash. */ @Column({ @@ -86,7 +86,7 @@ export class ApiKey extends Model { declare keyPrefix: string; /** - * Bcrypt hash of the raw API key value (soat_sk_). + * Bcrypt hash of the raw project key value (pk_). * The raw key is shown once at creation and never stored in plaintext. */ @Column({ diff --git a/packages/postgresdb/src/models/ProjectPolicy.ts b/packages/postgresdb/src/models/ProjectPolicy.ts index 86d2a3b5..43ce4b12 100644 --- a/packages/postgresdb/src/models/ProjectPolicy.ts +++ b/packages/postgresdb/src/models/ProjectPolicy.ts @@ -43,18 +43,22 @@ export class ProjectPolicy extends Model { declare project: Project; @Column({ - type: DataType.ARRAY(DataType.TEXT), - allowNull: false, - defaultValue: [], + type: DataType.STRING, + allowNull: true, + }) + declare name: string | null; + + @Column({ + type: DataType.TEXT, + allowNull: true, }) - declare permissions: string[]; + declare description: string | null; @Column({ - type: DataType.ARRAY(DataType.TEXT), + type: DataType.JSONB, allowNull: false, - defaultValue: [], }) - declare notPermissions: string[]; + declare document: object; @Column({ type: DataType.DATE }) declare createdAt: Date; diff --git a/packages/postgresdb/src/models/UserProject.ts b/packages/postgresdb/src/models/UserProject.ts index 07e45d3d..91d55ffc 100644 --- a/packages/postgresdb/src/models/UserProject.ts +++ b/packages/postgresdb/src/models/UserProject.ts @@ -8,7 +8,6 @@ import { } from '@ttoss/postgresdb'; import { Project } from './Project'; -import { ProjectPolicy } from './ProjectPolicy'; import { User } from './User'; @Table({ @@ -41,19 +40,12 @@ export class UserProject extends Model { ) declare project: Project; - @ForeignKey(() => { - return ProjectPolicy; + @Column({ + type: DataType.ARRAY(DataType.INTEGER), + allowNull: false, + defaultValue: [], }) - @Column({ type: DataType.INTEGER, allowNull: false }) - declare policyId: number; - - @BelongsTo( - () => { - return ProjectPolicy; - }, - { onDelete: 'CASCADE' } - ) - declare policy: ProjectPolicy; + declare policyIds: number[]; @Column({ type: DataType.DATE }) declare createdAt: Date; diff --git a/packages/postgresdb/src/models/index.ts b/packages/postgresdb/src/models/index.ts index 37ae33f3..8d011128 100644 --- a/packages/postgresdb/src/models/index.ts +++ b/packages/postgresdb/src/models/index.ts @@ -1,5 +1,5 @@ export { Actor } from './Actor'; -export { ApiKey } from './ApiKey'; +export { ProjectKey } from './ProjectKey'; export { Conversation } from './Conversation'; export { ConversationMessage } from './ConversationMessage'; export { Document } from './Document'; diff --git a/packages/postgresdb/src/utils/publicId.ts b/packages/postgresdb/src/utils/publicId.ts index cd24a359..e1bc212a 100644 --- a/packages/postgresdb/src/utils/publicId.ts +++ b/packages/postgresdb/src/utils/publicId.ts @@ -9,17 +9,17 @@ export const PUBLIC_ID_PREFIXES = { user: 'usr_', project: 'proj_', policy: 'pol_', - apiKey: 'key_', + projectKey: 'key_', document: 'doc_', actor: 'act_', conversation: 'conv_', } as const; /** - * Prefix for raw API key values (shown once at creation, then hashed). + * Prefix for raw project key values (shown once at creation, then hashed). * Format: sk_{random} — distinguishable from JWTs (which start with 'eyJ'). */ -export const API_KEY_RAW_PREFIX = 'sk_'; +export const PROJECT_KEY_RAW_PREFIX = 'sk_'; export type PublicIdPrefix = (typeof PUBLIC_ID_PREFIXES)[keyof typeof PUBLIC_ID_PREFIXES]; diff --git a/packages/server/src/Context.ts b/packages/server/src/Context.ts index 696725ee..0e980b61 100644 --- a/packages/server/src/Context.ts +++ b/packages/server/src/Context.ts @@ -5,11 +5,16 @@ export type AuthUser = { publicId: string; username: string; role: 'admin' | 'user'; - isAllowed: (projectPublicId: string, action: string) => Promise; + isAllowed: (args: { + projectPublicId: string; + action: string; + resource?: string; + context?: Record; + }) => Promise; /** * Resolves the internal project IDs the caller may access for the given action. * - Explicit projectPublicId: verifies permission and returns [id], or null if forbidden/not found. - * - API key (no explicit id): infers from the key's scoped project. + * - project key (no explicit id): infers from the key's scoped project. * - JWT admin (no explicit id): returns undefined (no filter — all projects). * - JWT user (no explicit id): enumerates all projects the user has access to. */ @@ -17,7 +22,7 @@ export type AuthUser = { projectPublicId?: string; action: string; }) => Promise; - apiKeyProjectId?: string; + projectKeyProjectId?: string; }; export type Context = { diff --git a/packages/server/src/lib/actors.ts b/packages/server/src/lib/actors.ts index 6fa47f83..0d8079e6 100644 --- a/packages/server/src/lib/actors.ts +++ b/packages/server/src/lib/actors.ts @@ -13,6 +13,7 @@ const mapActor = ( name: actor.name, type: actor.type ?? undefined, externalId: actor.externalId ?? undefined, + tags: actor.tags ?? undefined, createdAt: actor.createdAt, updatedAt: actor.updatedAt, }; @@ -139,3 +140,37 @@ export const updateActor = async (args: { return mapActor(actorWithProject!); }; + +export const getActorTags = async (args: { id: string }) => { + const actor = await db.Actor.findOne({ where: { publicId: args.id } }); + + if (!actor) { + return null; + } + + return actor.tags ?? {}; +}; + +export const updateActorTags = async (args: { + id: string; + tags: Record; + merge?: boolean; +}) => { + const actor = await db.Actor.findOne({ where: { publicId: args.id } }); + + if (!actor) { + return null; + } + + const newTags = args.merge + ? { ...(actor.tags ?? {}), ...args.tags } + : args.tags; + await actor.update({ tags: newTags }); + + const actorWithProject = await db.Actor.findOne({ + where: { id: actor.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + return mapActor(actorWithProject!); +}; diff --git a/packages/server/src/lib/apiKeys.ts b/packages/server/src/lib/apiKeys.ts deleted file mode 100644 index bc513962..00000000 --- a/packages/server/src/lib/apiKeys.ts +++ /dev/null @@ -1,75 +0,0 @@ -import crypto from 'node:crypto'; - -import { API_KEY_RAW_PREFIX } from '@soat/postgresdb'; -import bcrypt from 'bcryptjs'; - -import { db } from '../db'; - -const mapApiKey = (apiKey: InstanceType<(typeof db)['ApiKey']>) => { - return { - id: apiKey.publicId, - name: apiKey.name, - keyPrefix: apiKey.keyPrefix, - createdAt: apiKey.createdAt, - updatedAt: apiKey.updatedAt, - }; -}; - -export const createApiKey = async (args: { - userId: number; - projectId: number; - policyId: number; - name: string; -}) => { - const random = crypto.randomBytes(32).toString('hex'); - const key = `${API_KEY_RAW_PREFIX}${random}`; - const keyPrefix = key.slice(0, 8); - const keyHash = await bcrypt.hash(key, 10); - - const apiKey = await db.ApiKey.create({ - ...args, - keyPrefix, - keyHash, - }); - - return { - ...mapApiKey(apiKey), - key, // Return the full key only once at creation - }; -}; - -export const getApiKey = async (args: { id: string }) => { - const apiKey = await db.ApiKey.findOne({ - where: { publicId: args.id }, - include: [ - { model: db.User, as: 'user' }, - { model: db.Project, as: 'project' }, - { model: db.ProjectPolicy, as: 'policy' }, - ], - }); - - if (!apiKey) { - return null; - } - - return { - ...mapApiKey(apiKey), - userId: apiKey.user?.publicId, - projectId: apiKey.project?.publicId, - policyId: apiKey.policy?.publicId, - }; -}; - -export const updateApiKey = async (args: { id: string; policyId: number }) => { - const apiKey = await db.ApiKey.findOne({ - where: { publicId: args.id }, - }); - - if (!apiKey) { - return null; - } - - await apiKey.update({ policyId: args.policyId }); - - return getApiKey({ id: args.id }); -}; diff --git a/packages/server/src/lib/conversations.ts b/packages/server/src/lib/conversations.ts index 6ed709d9..f5459b57 100644 --- a/packages/server/src/lib/conversations.ts +++ b/packages/server/src/lib/conversations.ts @@ -12,6 +12,7 @@ const mapConversation = ( id: conversation.publicId, projectId: conversation.project?.publicId, status: conversation.status, + tags: conversation.tags ?? undefined, createdAt: conversation.createdAt, updatedAt: conversation.updatedAt, }; @@ -73,9 +74,11 @@ export const listConversations = async (args: { attributes: ['conversationId'], group: ['conversationId'], }); - const conversationIds = messages.map((m) => { - return m.conversationId; - }); + const conversationIds = messages.map( + (m: InstanceType<(typeof db)['ConversationMessage']>) => { + return m.conversationId; + } + ); where.id = conversationIds; } @@ -155,6 +158,44 @@ export const deleteConversation = async (args: { id: string }) => { return { id: args.id }; }; +export const getConversationTags = async (args: { id: string }) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.id }, + }); + + if (!conversation) { + return null; + } + + return conversation.tags ?? {}; +}; + +export const updateConversationTags = async (args: { + id: string; + tags: Record; + merge?: boolean; +}) => { + const conversation = await db.Conversation.findOne({ + where: { publicId: args.id }, + }); + + if (!conversation) { + return null; + } + + const newTags = args.merge + ? { ...(conversation.tags ?? {}), ...args.tags } + : args.tags; + await conversation.update({ tags: newTags }); + + const updated = await db.Conversation.findOne({ + where: { publicId: args.id }, + include: [{ model: db.Project, as: 'project' }], + }); + + return mapConversation(updated!); +}; + export const listConversationMessages = async (args: { conversationId: string; limit?: number; diff --git a/packages/server/src/lib/documents.ts b/packages/server/src/lib/documents.ts index 8f5d4cf1..1cb38a17 100644 --- a/packages/server/src/lib/documents.ts +++ b/packages/server/src/lib/documents.ts @@ -106,7 +106,7 @@ export const createDocument = async (args: { filename?: string; title?: string; metadata?: Record; - tags?: string[]; + tags?: Record; }) => { const storageDir = getStorageDir(); fs.mkdirSync(storageDir, { recursive: true }); @@ -182,7 +182,7 @@ export const updateDocument = async (args: { content?: string; title?: string; metadata?: Record; - tags?: string[]; + tags?: Record; }) => { const doc = await db.Document.findOne({ where: { publicId: args.id }, @@ -236,7 +236,7 @@ export const searchDocuments = async (args: { query: string; limit?: number; threshold?: number; - tags?: string[]; + tags?: Record; }) => { if (args.projectIds !== undefined && args.projectIds.length === 0) { return []; @@ -250,8 +250,8 @@ export const searchDocuments = async (args: { args.projectIds !== undefined ? { projectId: args.projectIds } : undefined; const docWhere: Record = - args.tags && args.tags.length > 0 - ? { tags: { [Op.overlap]: args.tags } } + args.tags && Object.keys(args.tags).length > 0 + ? { tags: { [Op.contains]: args.tags } } : {}; const documents = await db.Document.findAll({ @@ -279,7 +279,7 @@ export const searchDocuments = async (args: { }); return documents - .map((doc) => { + .map((doc: InstanceType<(typeof db)['Document']>) => { const distance = parseFloat( (doc.getDataValue('distance') as string) ?? '1' ); @@ -293,10 +293,59 @@ export const searchDocuments = async (args: { return { ...mapped, content, score }; }) - .filter((doc) => { + .filter((doc: { score: number }) => { if (args.threshold !== undefined) { return doc.score >= args.threshold; } return true; }); }; + +export const getDocumentTags = async (args: { id: string }) => { + const doc = await db.Document.findOne({ where: { publicId: args.id } }); + + if (!doc) { + return null; + } + + return doc.tags ?? {}; +}; + +export const updateDocumentTags = async (args: { + id: string; + tags: Record; + merge?: boolean; +}) => { + const doc = await db.Document.findOne({ + where: { publicId: args.id }, + include: [ + { + model: db.File, + as: 'file', + include: [{ model: db.Project, as: 'project' }], + }, + ], + }); + + if (!doc) { + return null; + } + + const newTags = args.merge + ? { ...(doc.tags ?? {}), ...args.tags } + : args.tags; + await doc.update({ tags: newTags }); + + const refreshed = await db.Document.findOne({ + where: { id: doc.id }, + include: [ + { + model: db.File, + as: 'file', + include: [{ model: db.Project, as: 'project' }], + }, + ], + }); + + return mapDocument(refreshed!); +}; diff --git a/packages/server/src/lib/files.ts b/packages/server/src/lib/files.ts index 2590a5ef..4c9cb8bd 100644 --- a/packages/server/src/lib/files.ts +++ b/packages/server/src/lib/files.ts @@ -20,6 +20,7 @@ const mapFile = (file: InstanceType<(typeof db)['File']>) => { storageType: file.storageType, storagePath: file.storagePath, metadata: file.metadata, + tags: file.tags ?? undefined, createdAt: file.createdAt, updatedAt: file.updatedAt, }; @@ -176,3 +177,32 @@ export const deleteFile = async (args: { id: string }) => { await file.destroy(); return true; }; + +export const getFileTags = async (args: { id: string }) => { + const file = await db.File.findOne({ where: { publicId: args.id } }); + + if (!file) { + return null; + } + + return file.tags ?? {}; +}; + +export const updateFileTags = async (args: { + id: string; + tags: Record; + merge?: boolean; +}) => { + const file = await db.File.findOne({ where: { publicId: args.id } }); + + if (!file) { + return null; + } + + const newTags = args.merge + ? { ...(file.tags ?? {}), ...args.tags } + : args.tags; + await file.update({ tags: newTags }); + + return { ...mapFile(file), tags: newTags }; +}; diff --git a/packages/server/src/lib/iam.ts b/packages/server/src/lib/iam.ts new file mode 100644 index 00000000..e85a3d0b --- /dev/null +++ b/packages/server/src/lib/iam.ts @@ -0,0 +1,256 @@ +export type Effect = 'Allow' | 'Deny'; + +export type ConditionOperator = + | 'StringEquals' + | 'StringNotEquals' + | 'StringLike'; + +export type Condition = { + [operator in ConditionOperator]?: Record; +}; + +export type Statement = { + effect: Effect; + action: string[]; + resource?: string[]; + condition?: Condition; +}; + +export type PolicyDocument = { + statement: Statement[]; +}; + +const VALID_EFFECTS: Effect[] = ['Allow', 'Deny']; +const VALID_OPERATORS: ConditionOperator[] = [ + 'StringEquals', + 'StringNotEquals', + 'StringLike', +]; + +const isValidAction = (action: string): boolean => { + if (action === '*') return true; + if (/^[a-zA-Z0-9_-]+:\*$/.test(action)) return true; + if (/^[a-zA-Z0-9_-]+:[a-zA-Z0-9_-]+$/.test(action)) return true; + return false; +}; + +const isValidSrnPattern = (srn: string): boolean => { + if (srn === '*') return true; + // soat::: + return /^soat:[^:]+:[^:]+:[^:]+$/.test(srn); +}; + +export const validatePolicyDocument = ( + doc: unknown +): { valid: boolean; errors: string[] } => { + const errors: string[] = []; + + if (!doc || typeof doc !== 'object' || Array.isArray(doc)) { + errors.push('Policy document must be an object'); + return { valid: false, errors }; + } + + const d = doc as Record; + + if (!Array.isArray(d.statement)) { + errors.push('Policy document must have a "statement" array'); + return { valid: false, errors }; + } + + for (let i = 0; i < d.statement.length; i++) { + const stmt = d.statement[i] as Record; + const prefix = `statement[${i}]`; + + if (!stmt || typeof stmt !== 'object' || Array.isArray(stmt)) { + errors.push(`${prefix}: must be an object`); + continue; + } + + // Validate effect + if (!VALID_EFFECTS.includes(stmt.effect as Effect)) { + errors.push( + `${prefix}.effect: must be one of ${VALID_EFFECTS.join(', ')}` + ); + } + + // Validate action + if (!Array.isArray(stmt.action) || stmt.action.length === 0) { + errors.push(`${prefix}.action: must be a non-empty array`); + } else { + for (const act of stmt.action) { + if (typeof act !== 'string' || !isValidAction(act)) { + errors.push( + `${prefix}.action: "${act}" is invalid — must be *, module:*, or module:Operation` + ); + } + } + } + + // Validate resource (optional) + if (stmt.resource !== undefined) { + if (!Array.isArray(stmt.resource) || stmt.resource.length === 0) { + errors.push( + `${prefix}.resource: must be a non-empty array when present` + ); + } else { + for (const res of stmt.resource) { + if (typeof res !== 'string' || !isValidSrnPattern(res)) { + errors.push( + `${prefix}.resource: "${res}" is invalid — must be * or soat:::` + ); + } + } + } + } + + // Validate condition (optional) + if (stmt.condition !== undefined) { + if ( + !stmt.condition || + typeof stmt.condition !== 'object' || + Array.isArray(stmt.condition) + ) { + errors.push(`${prefix}.condition: must be an object`); + } else { + const cond = stmt.condition as Record; + for (const op of Object.keys(cond)) { + if (!VALID_OPERATORS.includes(op as ConditionOperator)) { + errors.push( + `${prefix}.condition: "${op}" is not a valid operator — must be one of ${VALID_OPERATORS.join(', ')}` + ); + } else { + const block = cond[op] as Record; + if (!block || typeof block !== 'object' || Array.isArray(block)) { + errors.push(`${prefix}.condition.${op}: must be an object`); + } else { + for (const key of Object.keys(block)) { + if (!key.startsWith('soat:')) { + errors.push( + `${prefix}.condition.${op}: key "${key}" must start with "soat:"` + ); + } + } + } + } + } + } + } + } + + return { valid: errors.length === 0, errors }; +}; + +export const buildSrn = (args: { + projectPublicId: string; + resourceType: string; + resourceId: string; +}): string => { + return `soat:${args.projectPublicId}:${args.resourceType}:${args.resourceId}`; +}; + +export const matchesPattern = (args: { + pattern: string; + value: string; +}): boolean => { + const { pattern, value } = args; + + if (pattern === '*') return true; + if (pattern === value) return true; + + // module:* matches module:Anything + if (pattern.endsWith(':*')) { + const prefix = pattern.slice(0, -1); // e.g. "files:" + return value.startsWith(prefix); + } + + // Glob: * = any chars, ? = single char + const regexStr = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars + .replace(/\*/g, '.*') + .replace(/\?/g, '.'); + return new RegExp(`^${regexStr}$`).test(value); +}; + +export const evaluateCondition = (args: { + condition: Condition; + context: Record; +}): boolean => { + const { condition, context } = args; + + for (const op of Object.keys(condition) as ConditionOperator[]) { + const block = condition[op]; + if (!block) continue; + + for (const [key, expected] of Object.entries(block)) { + const actual = context[key]; + + if (op === 'StringEquals') { + if (actual !== expected) return false; + } else if (op === 'StringNotEquals') { + if (actual === expected) return false; + } else if (op === 'StringLike') { + if (!matchesPattern({ pattern: expected, value: actual ?? '' })) + return false; + } + } + } + + return true; +}; + +export const statementMatches = (args: { + statement: Statement; + action: string; + resource: string; + context: Record; +}): boolean => { + const { statement, action, resource, context } = args; + + // Check action + const actionMatch = statement.action.some((pattern) => { + return matchesPattern({ pattern, value: action }); + }); + if (!actionMatch) return false; + + // Check resource (omitted resource defaults to ["*"]) + const resources = statement.resource ?? ['*']; + const resourceMatch = resources.some((pattern) => { + return matchesPattern({ pattern, value: resource }); + }); + if (!resourceMatch) return false; + + // Check condition + if (statement.condition) { + if (!evaluateCondition({ condition: statement.condition, context })) + return false; + } + + return true; +}; + +export const evaluatePolicies = (args: { + policies: PolicyDocument[]; + action: string; + resource?: string; + context?: Record; +}): boolean => { + const resource = args.resource ?? '*'; + const context = args.context ?? {}; + + let allowed = false; + + for (const policy of args.policies) { + for (const statement of policy.statement) { + if ( + statementMatches({ statement, action: args.action, resource, context }) + ) { + if (statement.effect === 'Deny') { + return false; // Explicit deny, short-circuit + } + allowed = true; + } + } + } + + return allowed; +}; diff --git a/packages/server/src/lib/permissions.ts b/packages/server/src/lib/permissions.ts index 0b668094..29173934 100644 --- a/packages/server/src/lib/permissions.ts +++ b/packages/server/src/lib/permissions.ts @@ -1,49 +1,46 @@ import type { DB } from '../db'; +import { evaluatePolicies, type PolicyDocument } from './iam'; -export type Policy = { - permissions: string[]; - notPermissions: string[]; -}; - -const matchesPattern = (pattern: string, action: string): boolean => { - if (pattern === '*') return true; - if (pattern === action) return true; - - if (pattern.endsWith(':*')) { - const prefix = pattern.slice(0, -1); // e.g. "files:" - return action.startsWith(prefix); - } +export const createProjectKeyIsAllowed = (args: { + projectPublicId: string; + userPolicyIds: number[]; + projectKeyPolicyId: number; + db: DB; +}) => { + return async (reqArgs: { + projectPublicId: string; + action: string; + resource?: string; + context?: Record; + }): Promise => { + if (reqArgs.projectPublicId !== args.projectPublicId) return false; - return false; -}; + const [userPolicies, projectKeyPolicy] = await Promise.all([ + args.userPolicyIds.length > 0 + ? args.db.ProjectPolicy.findAll({ where: { id: args.userPolicyIds } }) + : Promise.resolve([]), + args.db.ProjectPolicy.findOne({ where: { id: args.projectKeyPolicyId } }), + ]); -export const policyAllows = (policy: Policy, action: string): boolean => { - for (const pattern of policy.notPermissions) { - if (matchesPattern(pattern, action)) return false; - } + if (!projectKeyPolicy) return false; - for (const pattern of policy.permissions) { - if (matchesPattern(pattern, action)) return true; - } + const userAllowed = evaluatePolicies({ + policies: userPolicies.map((p) => { + return p.document as PolicyDocument; + }), + action: reqArgs.action, + resource: reqArgs.resource, + context: reqArgs.context, + }); - return false; -}; + if (!userAllowed) return false; -export const createApiKeyIsAllowed = (args: { - projectPublicId: string; - userPolicy: Policy; - apiKeyPolicy: Policy; -}) => { - return async ( - reqProjectPublicId: string, - action: string - ): Promise => { - if (reqProjectPublicId !== args.projectPublicId) return false; - // Intersection: action must be allowed by both user policy and API key policy - return ( - policyAllows(args.userPolicy, action) && - policyAllows(args.apiKeyPolicy, action) - ); + return evaluatePolicies({ + policies: [projectKeyPolicy.document as PolicyDocument], + action: reqArgs.action, + resource: reqArgs.resource, + context: reqArgs.context, + }); }; }; @@ -52,25 +49,33 @@ export const createJwtIsAllowed = (args: { userId: number; db: DB; }) => { - return async (projectPublicId: string, action: string): Promise => { + return async (reqArgs: { + projectPublicId: string; + action: string; + resource?: string; + context?: Record; + }): Promise => { if (args.role === 'admin') return true; const project = await args.db.Project.findOne({ - where: { publicId: projectPublicId }, + where: { publicId: reqArgs.projectPublicId }, }); if (!project) return false; const membership = await args.db.UserProject.findOne({ where: { userId: args.userId, projectId: project.id as number }, - include: [{ model: args.db.ProjectPolicy }], }); if (!membership) return false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const policy = (membership as any).policy; - return policyAllows( - { - permissions: policy.permissions as string[], - notPermissions: policy.notPermissions as string[], - }, - action - ); + const policyIds = membership.policyIds as number[]; + if (policyIds.length === 0) return false; + const policies = await args.db.ProjectPolicy.findAll({ + where: { id: policyIds }, + }); + return evaluatePolicies({ + policies: policies.map((p) => { + return p.document as PolicyDocument; + }), + action: reqArgs.action, + resource: reqArgs.resource, + context: reqArgs.context, + }); }; }; diff --git a/packages/server/src/lib/projectKeys.ts b/packages/server/src/lib/projectKeys.ts new file mode 100644 index 00000000..40c12048 --- /dev/null +++ b/packages/server/src/lib/projectKeys.ts @@ -0,0 +1,78 @@ +import crypto from 'node:crypto'; + +import { PROJECT_KEY_RAW_PREFIX } from '@soat/postgresdb'; +import bcrypt from 'bcryptjs'; + +import { db } from '../db'; + +const mapProjectKey = (projectKey: InstanceType<(typeof db)['ProjectKey']>) => { + return { + id: projectKey.publicId, + name: projectKey.name, + keyPrefix: projectKey.keyPrefix, + createdAt: projectKey.createdAt, + updatedAt: projectKey.updatedAt, + }; +}; + +export const createProjectKey = async (args: { + userId: number; + projectId: number; + policyId: number; + name: string; +}) => { + const random = crypto.randomBytes(32).toString('hex'); + const key = `${PROJECT_KEY_RAW_PREFIX}${random}`; + const keyPrefix = key.slice(0, 8); + const keyHash = await bcrypt.hash(key, 10); + + const projectKey = await db.ProjectKey.create({ + ...args, + keyPrefix, + keyHash, + }); + + return { + ...mapProjectKey(projectKey), + key, // Return the full key only once at creation + }; +}; + +export const getProjectKey = async (args: { id: string }) => { + const projectKey = await db.ProjectKey.findOne({ + where: { publicId: args.id }, + include: [ + { model: db.User, as: 'user' }, + { model: db.Project, as: 'project' }, + { model: db.ProjectPolicy, as: 'policy' }, + ], + }); + + if (!projectKey) { + return null; + } + + return { + ...mapProjectKey(projectKey), + userId: projectKey.user?.publicId, + projectId: projectKey.project?.publicId, + policyId: projectKey.policy?.publicId, + }; +}; + +export const updateProjectKey = async (args: { + id: string; + policyId: number; +}) => { + const projectKey = await db.ProjectKey.findOne({ + where: { publicId: args.id }, + }); + + if (!projectKey) { + return null; + } + + await projectKey.update({ policyId: args.policyId }); + + return getProjectKey({ id: args.id }); +}; diff --git a/packages/server/src/lib/projects.ts b/packages/server/src/lib/projects.ts index 2bae5065..721da4e9 100644 --- a/packages/server/src/lib/projects.ts +++ b/packages/server/src/lib/projects.ts @@ -1,5 +1,7 @@ import type { AuthUser } from '../Context'; import { db } from '../db'; +import type { PolicyDocument } from './iam'; +import { validatePolicyDocument } from './iam'; const mapProject = (project: InstanceType<(typeof db)['Project']>) => { return { @@ -16,9 +18,9 @@ export const listProjects = async (args: { authUser: AuthUser }) => { return projects.map(mapProject); } - if (args.authUser.apiKeyProjectId) { + if (args.authUser.projectKeyProjectId) { const project = await db.Project.findOne({ - where: { publicId: args.authUser.apiKeyProjectId }, + where: { publicId: args.authUser.projectKeyProjectId }, }); return project ? [mapProject(project)] : []; } @@ -28,7 +30,7 @@ export const listProjects = async (args: { authUser: AuthUser }) => { include: [{ model: db.Project }], }); - return userProjects.map((up) => { + return userProjects.map((up: InstanceType<(typeof db)['UserProject']>) => { return mapProject(up.project as InstanceType<(typeof db)['Project']>); }); }; @@ -75,10 +77,29 @@ const mapPolicy = ( policy: InstanceType<(typeof db)['ProjectPolicy']>, projectPublicId: string ) => { + const doc = policy.document as PolicyDocument | undefined; + const permissions = + doc?.statement + ?.filter((s) => { + return s.effect === 'Allow'; + }) + .flatMap((s) => { + return s.action; + }) ?? []; + const notPermissions = + doc?.statement + ?.filter((s) => { + return s.effect === 'Deny'; + }) + .flatMap((s) => { + return s.action; + }) ?? []; return { id: policy.publicId, - permissions: policy.permissions, - notPermissions: policy.notPermissions, + name: policy.name, + description: policy.description, + permissions, + notPermissions, projectId: projectPublicId, createdAt: policy.createdAt, updatedAt: policy.updatedAt, @@ -97,28 +118,121 @@ export const listProjectPolicies = async (args: { projectId: string }) => { where: { projectId: project.id }, }); - return policies.map((policy) => { + return policies.map((policy: InstanceType<(typeof db)['ProjectPolicy']>) => { return mapPolicy(policy, project.publicId); }); }; export const createProjectPolicy = async (args: { projectId: string; - permissions: string[]; - notPermissions?: string[]; -}) => { + name?: string; + description?: string; + document: PolicyDocument; +}): Promise< + | ReturnType + | 'not_found' + | { invalid: true; errors: string[] } +> => { + const validation = validatePolicyDocument(args.document); + if (!validation.valid) { + return { invalid: true, errors: validation.errors }; + } + const project = await db.Project.findOne({ where: { publicId: args.projectId }, }); if (!project) { - return null; + return 'not_found'; } const policy = await db.ProjectPolicy.create({ projectId: project.id, - permissions: args.permissions, - notPermissions: args.notPermissions || [], + name: args.name ?? null, + description: args.description ?? null, + document: args.document as object, + }); + + return mapPolicy(policy, project.publicId); +}; + +export const updateProjectPolicy = async (args: { + projectId: string; + policyId: string; + name?: string; + description?: string; + document: PolicyDocument; +}): Promise< + | ReturnType + | 'not_found' + | { invalid: true; errors: string[] } +> => { + const validation = validatePolicyDocument(args.document); + if (!validation.valid) { + return { invalid: true, errors: validation.errors }; + } + + const project = await db.Project.findOne({ + where: { publicId: args.projectId }, + }); + if (!project) { + return 'not_found'; + } + + const policy = await db.ProjectPolicy.findOne({ + where: { publicId: args.policyId, projectId: project.id }, + }); + if (!policy) { + return 'not_found'; + } + + await policy.update({ + name: args.name ?? policy.name, + description: args.description ?? policy.description, + document: args.document as object, + }); + + return mapPolicy(policy, project.publicId); +}; + +export const deleteProjectPolicy = async (args: { + projectId: string; + policyId: string; +}): Promise<'not_found' | true> => { + const project = await db.Project.findOne({ + where: { publicId: args.projectId }, + }); + if (!project) { + return 'not_found'; + } + + const policy = await db.ProjectPolicy.findOne({ + where: { publicId: args.policyId, projectId: project.id }, + }); + if (!policy) { + return 'not_found'; + } + + await policy.destroy(); + return true; +}; + +export const getProjectPolicy = async (args: { + projectId: string; + policyId: string; +}) => { + const project = await db.Project.findOne({ + where: { publicId: args.projectId }, + }); + if (!project) { + return null; + } + + const policy = await db.ProjectPolicy.findOne({ + where: { publicId: args.policyId, projectId: project.id }, }); + if (!policy) { + return null; + } return mapPolicy(policy, project.publicId); }; @@ -126,7 +240,7 @@ export const createProjectPolicy = async (args: { export const addUserToProject = async (args: { projectId: string; userId: string; - policyId: string; + policyIds?: string[]; }) => { const project = await db.Project.findOne({ where: { publicId: args.projectId }, @@ -140,11 +254,19 @@ export const addUserToProject = async (args: { return null; } - const policy = await db.ProjectPolicy.findOne({ - where: { publicId: args.policyId }, - }); - if (!policy) { - return null; + let resolvedPolicyIds: number[] = []; + if (args.policyIds && args.policyIds.length > 0) { + const policies = await db.ProjectPolicy.findAll({ + where: { publicId: args.policyIds, projectId: project.id }, + }); + if (policies.length !== args.policyIds.length) { + return null; + } + resolvedPolicyIds = policies.map( + (p: InstanceType<(typeof db)['ProjectPolicy']>) => { + return p.id as number; + } + ); } // Check if membership already exists @@ -153,17 +275,90 @@ export const addUserToProject = async (args: { }); if (existing) { - // Update existing membership - await existing.update({ policyId: policy.id }); + await existing.update({ policyIds: resolvedPolicyIds }); return true; } - // Create new membership await db.UserProject.create({ userId: user.id, projectId: project.id, - policyId: policy.id, + policyIds: resolvedPolicyIds, + }); + + return true; +}; + +export const updateUserProjectPolicies = async (args: { + projectId: string; + userId: string; + policyIds: string[]; +}) => { + const project = await db.Project.findOne({ + where: { publicId: args.projectId }, + }); + if (!project) { + return 'not_found' as const; + } + + const user = await db.User.findOne({ where: { publicId: args.userId } }); + if (!user) { + return 'not_found' as const; + } + + const membership = await db.UserProject.findOne({ + where: { userId: user.id, projectId: project.id }, + }); + if (!membership) { + return 'not_found' as const; + } + + const policies = await db.ProjectPolicy.findAll({ + where: { publicId: args.policyIds, projectId: project.id }, }); + if (policies.length !== args.policyIds.length) { + return 'not_found' as const; + } + await membership.update({ + policyIds: policies.map((p: InstanceType<(typeof db)['ProjectPolicy']>) => { + return p.id as number; + }), + }); return true; }; + +export const getUserProjectPolicies = async (args: { + projectId: string; + userId: string; +}) => { + const project = await db.Project.findOne({ + where: { publicId: args.projectId }, + }); + if (!project) { + return null; + } + + const user = await db.User.findOne({ where: { publicId: args.userId } }); + if (!user) { + return null; + } + + const membership = await db.UserProject.findOne({ + where: { userId: user.id, projectId: project.id }, + }); + if (!membership) { + return null; + } + + if (!membership.policyIds || membership.policyIds.length === 0) { + return []; + } + + const policies = await db.ProjectPolicy.findAll({ + where: { id: membership.policyIds, projectId: project.id }, + }); + + return policies.map((p: InstanceType<(typeof db)['ProjectPolicy']>) => { + return mapPolicy(p, project.publicId); + }); +}; diff --git a/packages/server/src/mcp/tools/actors.ts b/packages/server/src/mcp/tools/actors.ts index b2bde50d..d08dc223 100644 --- a/packages/server/src/mcp/tools/actors.ts +++ b/packages/server/src/mcp/tools/actors.ts @@ -77,13 +77,13 @@ const registerTools = (server: McpServer) => { 'create-actor', { description: - 'Create a new actor. API keys infer the project automatically; JWT callers must supply projectId.', + 'Create a new actor. project keys infer the project automatically; JWT callers must supply projectId.', inputSchema: { projectId: z .string() .optional() .describe( - 'Project ID (required for JWT auth, optional for API keys)' + 'Project ID (required for JWT auth, optional for project keys)' ), name: z.string().describe('Actor name'), type: z diff --git a/packages/server/src/mcp/tools/conversations.ts b/packages/server/src/mcp/tools/conversations.ts index a69260af..3f72e486 100644 --- a/packages/server/src/mcp/tools/conversations.ts +++ b/packages/server/src/mcp/tools/conversations.ts @@ -67,13 +67,13 @@ const registerTools = (server: McpServer) => { 'create-conversation', { description: - 'Create a new conversation. API keys infer the project automatically; JWT callers must supply projectId.', + 'Create a new conversation. project keys infer the project automatically; JWT callers must supply projectId.', inputSchema: { projectId: z .string() .optional() .describe( - 'Project ID (required for JWT auth, optional for API keys)' + 'Project ID (required for JWT auth, optional for project keys)' ), status: z .string() diff --git a/packages/server/src/mcp/tools/documents.ts b/packages/server/src/mcp/tools/documents.ts index ec31fa1a..8ac2d640 100644 --- a/packages/server/src/mcp/tools/documents.ts +++ b/packages/server/src/mcp/tools/documents.ts @@ -62,13 +62,13 @@ const registerTools = (server: McpServer) => { 'create-document', { description: - 'Create a new text document with an embedding vector for semantic search. API keys infer the project automatically; JWT callers must supply projectId.', + 'Create a new text document with an embedding vector for semantic search. project keys infer the project automatically; JWT callers must supply projectId.', inputSchema: { projectId: z .string() .optional() .describe( - 'Project ID (required for JWT auth, optional for API keys)' + 'Project ID (required for JWT auth, optional for project keys)' ), content: z.string().describe('Text content of the document'), filename: z.string().optional().describe('Optional filename'), diff --git a/packages/server/src/middleware/auth.ts b/packages/server/src/middleware/auth.ts index fb068d08..747d77c5 100644 --- a/packages/server/src/middleware/auth.ts +++ b/packages/server/src/middleware/auth.ts @@ -1,9 +1,12 @@ -import { API_KEY_RAW_PREFIX } from '@soat/postgresdb'; +import { PROJECT_KEY_RAW_PREFIX } from '@soat/postgresdb'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import type { Context } from '../Context'; -import { createApiKeyIsAllowed, createJwtIsAllowed } from '../lib/permissions'; +import { + createProjectKeyIsAllowed, + createJwtIsAllowed, +} from '../lib/permissions'; export const JWT_SECRET = process.env.JWT_SECRET ?? 'dev-secret'; @@ -23,16 +26,12 @@ export const signUserToken = (payload: { publicId: string; role: string }) => { type Next = () => Promise; -const resolveApiKey = async (ctx: Context, rawKey: string) => { +const resolveProjectKey = async (ctx: Context, rawKey: string) => { const keyPrefix = rawKey.substring(0, 8); - const candidates = await ctx.db.ApiKey.findAll({ + const candidates = await ctx.db.ProjectKey.findAll({ where: { keyPrefix }, - include: [ - { model: ctx.db.Project }, - { model: ctx.db.ProjectPolicy }, - { model: ctx.db.User }, - ], + include: [{ model: ctx.db.Project }, { model: ctx.db.User }], }); for (const row of candidates) { @@ -40,35 +39,24 @@ const resolveApiKey = async (ctx: Context, rawKey: string) => { if (match) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const projectPublicId = (row as any).project?.publicId as string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const policyData = (row as any).policy; - const keyPolicy = { - permissions: policyData.permissions as string[], - notPermissions: policyData.notPermissions as string[], - }; + const projectKeyPolicyId = row.policyId as number; // eslint-disable-next-line @typescript-eslint/no-explicit-any const keyUser = (row as any).user; - // Get user's project policy + // Get user's project memberships const project = await ctx.db.Project.findOne({ where: { publicId: projectPublicId }, }); const membership = await ctx.db.UserProject.findOne({ where: { userId: keyUser.id, projectId: project?.id }, - include: [{ model: ctx.db.ProjectPolicy }], }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const userPolicyData = (membership as any)?.policy; - const userPolicy = userPolicyData - ? { - permissions: userPolicyData.permissions as string[], - notPermissions: userPolicyData.notPermissions as string[], - } - : { permissions: [], notPermissions: [] }; + const userPolicyIds = ((membership as any)?.policyIds as number[]) ?? []; - const apiKeyIsAllowed = createApiKeyIsAllowed({ + const projectKeyIsAllowed = createProjectKeyIsAllowed({ projectPublicId, - userPolicy, - apiKeyPolicy: keyPolicy, + userPolicyIds, + projectKeyPolicyId, + db: ctx.db, }); ctx.authUser = { @@ -76,11 +64,14 @@ const resolveApiKey = async (ctx: Context, rawKey: string) => { publicId: keyUser.publicId as string, username: keyUser.username as string, role: keyUser.role as 'admin' | 'user', - apiKeyProjectId: projectPublicId, - isAllowed: apiKeyIsAllowed, + projectKeyProjectId: projectPublicId, + isAllowed: projectKeyIsAllowed, resolveProjectIds: async ({ projectPublicId: reqId, action }) => { const targetId = reqId ?? projectPublicId; - const allowed = await apiKeyIsAllowed(targetId, action); + const allowed = await projectKeyIsAllowed({ + projectPublicId: targetId, + action, + }); if (!allowed) return null; const proj = await ctx.db.Project.findOne({ where: { publicId: targetId }, @@ -124,7 +115,7 @@ const resolveJwt = async (ctx: Context, token: string) => { isAllowed: jwtIsAllowed, resolveProjectIds: async ({ projectPublicId, action }) => { if (projectPublicId) { - const allowed = await jwtIsAllowed(projectPublicId, action); + const allowed = await jwtIsAllowed({ projectPublicId, action }); if (!allowed) return null; const proj = await ctx.db.Project.findOne({ where: { publicId: projectPublicId }, @@ -145,7 +136,10 @@ const resolveJwt = async (ctx: Context, token: string) => { } ).project; if (!proj) continue; - const allowed = await jwtIsAllowed(proj.publicId as string, action); + const allowed = await jwtIsAllowed({ + projectPublicId: proj.publicId as string, + action, + }); if (allowed) accessible.push(proj.id as number); } return accessible; @@ -159,8 +153,8 @@ export const authMiddleware = async (ctx: Context, next: Next) => { if (authHeader?.startsWith('Bearer ')) { const token = authHeader.slice(7); - if (token.startsWith(API_KEY_RAW_PREFIX)) { - await resolveApiKey(ctx, token); + if (token.startsWith(PROJECT_KEY_RAW_PREFIX)) { + await resolveProjectKey(ctx, token); } else { await resolveJwt(ctx, token); } diff --git a/packages/server/src/rest/openapi/v1/actors.yaml b/packages/server/src/rest/openapi/v1/actors.yaml index 26a4d44d..63616c5c 100644 --- a/packages/server/src/rest/openapi/v1/actors.yaml +++ b/packages/server/src/rest/openapi/v1/actors.yaml @@ -15,7 +15,7 @@ paths: tags: - Actors summary: List actors - description: Returns all actors the caller has access to. If projectId is provided, returns only actors in that project. API keys are scoped to a single project automatically. JWT users without projectId receive actors across all their accessible projects. + description: Returns all actors the caller has access to. If projectId is provided, returns only actors in that project. project keys are scoped to a single project automatically. JWT users without projectId receive actors across all their accessible projects. operationId: listActors parameters: - name: projectId @@ -57,7 +57,7 @@ paths: tags: - Actors summary: Create an actor - description: Creates a new actor. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + description: Creates a new actor. project keys automatically infer the project from the key's scope; JWT callers must supply projectId. operationId: createActor requestBody: required: true @@ -70,7 +70,7 @@ paths: properties: projectId: type: string - description: Project ID. Required for JWT auth; omit when using an API key. + description: Project ID. Required for JWT auth; omit when using an project key. example: 'proj_V1StGXR8Z5jdHi6B' name: type: string diff --git a/packages/server/src/rest/openapi/v1/conversations.yaml b/packages/server/src/rest/openapi/v1/conversations.yaml index ef11c801..17c726a9 100644 --- a/packages/server/src/rest/openapi/v1/conversations.yaml +++ b/packages/server/src/rest/openapi/v1/conversations.yaml @@ -15,7 +15,7 @@ paths: tags: - Conversations summary: List conversations - description: Returns all conversations the caller has access to. If projectId is provided, returns only conversations in that project. API keys are scoped to a single project automatically. + description: Returns all conversations the caller has access to. If projectId is provided, returns only conversations in that project. project keys are scoped to a single project automatically. operationId: listConversations parameters: - name: projectId @@ -57,7 +57,7 @@ paths: tags: - Conversations summary: Create a conversation - description: Creates a new conversation. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + description: Creates a new conversation. project keys automatically infer the project from the key's scope; JWT callers must supply projectId. operationId: createConversation requestBody: required: true @@ -68,7 +68,7 @@ paths: properties: projectId: type: string - description: Project ID. Required for JWT auth; omit when using an API key. + description: Project ID. Required for JWT auth; omit when using an project key. example: 'proj_V1StGXR8Z5jdHi6B' status: type: string diff --git a/packages/server/src/rest/openapi/v1/documents.yaml b/packages/server/src/rest/openapi/v1/documents.yaml index bb46630c..32fa8c2b 100644 --- a/packages/server/src/rest/openapi/v1/documents.yaml +++ b/packages/server/src/rest/openapi/v1/documents.yaml @@ -15,7 +15,7 @@ paths: tags: - Documents summary: List documents - description: Returns all documents the caller has access to. If projectId is provided, returns only documents in that project. API keys are scoped to a single project automatically. JWT users without projectId receive documents across all their accessible projects. + description: Returns all documents the caller has access to. If projectId is provided, returns only documents in that project. project keys are scoped to a single project automatically. JWT users without projectId receive documents across all their accessible projects. operationId: listDocuments parameters: - name: projectId @@ -50,7 +50,7 @@ paths: tags: - Documents summary: Create a document - description: Creates a new text document and generates an embedding vector for semantic search. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + description: Creates a new text document and generates an embedding vector for semantic search. project keys automatically infer the project from the key's scope; JWT callers must supply projectId. operationId: createDocument requestBody: required: true @@ -63,7 +63,7 @@ paths: properties: projectId: type: string - description: Project ID. Required for JWT auth; omit when using an API key. + description: Project ID. Required for JWT auth; omit when using an project key. example: 'proj_V1StGXR8Z5jdHi6B' content: type: string diff --git a/packages/server/src/rest/v1/actors.ts b/packages/server/src/rest/v1/actors.ts index 222e48c8..c3faadd2 100644 --- a/packages/server/src/rest/v1/actors.ts +++ b/packages/server/src/rest/v1/actors.ts @@ -5,9 +5,12 @@ import { createActor, deleteActor, getActor, + getActorTags, listActors, updateActor, + updateActorTags, } from 'src/lib/actors'; +import { buildSrn } from 'src/lib/iam'; const actorsRouter = new Router(); @@ -191,10 +194,23 @@ actorsRouter.get('/actors/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - actor.projectId!, - 'actors:GetActor' - ); + const srnGet = buildSrn({ + projectPublicId: actor.projectId!, + resourceType: 'actor', + resourceId: actor.id, + }); + const contextGet: Record = { 'soat:ResourceType': 'actor' }; + if (actor.tags) { + for (const [k, v] of Object.entries(actor.tags)) { + contextGet[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: actor.projectId!, + action: 'actors:GetActor', + resource: srnGet, + context: contextGet, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -285,8 +301,8 @@ actorsRouter.post('/actors', async (ctx: Context) => { let resolvedProjectPublicId = body.projectId; if (!resolvedProjectPublicId) { - if (ctx.authUser.apiKeyProjectId) { - resolvedProjectPublicId = ctx.authUser.apiKeyProjectId; + if (ctx.authUser.projectKeyProjectId) { + resolvedProjectPublicId = ctx.authUser.projectKeyProjectId; } else { ctx.status = 400; ctx.body = { error: 'projectId is required' }; @@ -294,10 +310,10 @@ actorsRouter.post('/actors', async (ctx: Context) => { } } - const allowed = await ctx.authUser.isAllowed( - resolvedProjectPublicId, - 'actors:CreateActor' - ); + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: resolvedProjectPublicId, + action: 'actors:CreateActor', + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -391,10 +407,23 @@ actorsRouter.delete('/actors/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - actor.projectId!, - 'actors:DeleteActor' - ); + const srnDel = buildSrn({ + projectPublicId: actor.projectId!, + resourceType: 'actor', + resourceId: actor.id, + }); + const contextDel: Record = { 'soat:ResourceType': 'actor' }; + if (actor.tags) { + for (const [k, v] of Object.entries(actor.tags)) { + contextDel[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: actor.projectId!, + action: 'actors:DeleteActor', + resource: srnDel, + context: contextDel, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -471,10 +500,23 @@ actorsRouter.patch('/actors/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - actor.projectId!, - 'actors:UpdateActor' - ); + const srnUpd = buildSrn({ + projectPublicId: actor.projectId!, + resourceType: 'actor', + resourceId: actor.id, + }); + const contextUpd: Record = { 'soat:ResourceType': 'actor' }; + if (actor.tags) { + for (const [k, v] of Object.entries(actor.tags)) { + contextUpd[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: actor.projectId!, + action: 'actors:UpdateActor', + resource: srnUpd, + context: contextUpd, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -497,4 +539,243 @@ actorsRouter.patch('/actors/:id', async (ctx: Context) => { ctx.body = updated; }); +/** + * @openapi + * /actors/{id}/tags: + * get: + * tags: + * - Actors + * summary: Get actor tags + * operationId: getActorTagsRoute + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * responses: + * '200': + * description: Actor tags + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: Actor not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +actorsRouter.get('/actors/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const actor = await getActor({ id: ctx.params.id }); + + if (!actor) { + ctx.status = 404; + ctx.body = { error: 'Actor not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: actor.projectId!, + resourceType: 'actor', + resourceId: actor.id, + }); + const context: Record = { 'soat:ResourceType': 'actor' }; + if (actor.tags) { + for (const [k, v] of Object.entries(actor.tags)) { + context[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: actor.projectId!, + action: 'actors:GetActor', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await getActorTags({ id: ctx.params.id }); +}); + +/** + * @openapi + * /actors/{id}/tags: + * put: + * tags: + * - Actors + * summary: Replace actor tags + * operationId: putActorTags + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * responses: + * '200': + * description: Tags replaced + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ActorRecord' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: Actor not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +actorsRouter.put('/actors/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const actor = await getActor({ id: ctx.params.id }); + + if (!actor) { + ctx.status = 404; + ctx.body = { error: 'Actor not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: actor.projectId!, + resourceType: 'actor', + resourceId: actor.id, + }); + const context: Record = { 'soat:ResourceType': 'actor' }; + if (actor.tags) { + for (const [k, v] of Object.entries(actor.tags)) { + context[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: actor.projectId!, + action: 'actors:UpdateActor', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const tags = ctx.request.body as Record; + ctx.body = await updateActorTags({ id: ctx.params.id, tags, merge: false }); +}); + +/** + * @openapi + * /actors/{id}/tags: + * patch: + * tags: + * - Actors + * summary: Merge actor tags + * operationId: patchActorTags + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * responses: + * '200': + * description: Tags merged + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ActorRecord' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: Actor not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +actorsRouter.patch('/actors/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const actor = await getActor({ id: ctx.params.id }); + + if (!actor) { + ctx.status = 404; + ctx.body = { error: 'Actor not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: actor.projectId!, + resourceType: 'actor', + resourceId: actor.id, + }); + const context: Record = { 'soat:ResourceType': 'actor' }; + if (actor.tags) { + for (const [k, v] of Object.entries(actor.tags)) { + context[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: actor.projectId!, + action: 'actors:UpdateActor', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const tags = ctx.request.body as Record; + ctx.body = await updateActorTags({ id: ctx.params.id, tags, merge: true }); +}); + export { actorsRouter }; diff --git a/packages/server/src/rest/v1/conversations.ts b/packages/server/src/rest/v1/conversations.ts index 97adc29e..faa8278a 100644 --- a/packages/server/src/rest/v1/conversations.ts +++ b/packages/server/src/rest/v1/conversations.ts @@ -6,12 +6,15 @@ import { createConversation, deleteConversation, getConversation, + getConversationTags, listConversationActors, listConversationMessages, listConversations, removeConversationMessage, updateConversationStatus, + updateConversationTags, } from 'src/lib/conversations'; +import { buildSrn } from 'src/lib/iam'; const conversationsRouter = new Router(); @@ -22,7 +25,7 @@ const conversationsRouter = new Router(); * tags: * - Conversations * summary: List conversations - * description: Returns all conversations the caller has access to. If projectId is provided, returns only conversations in that project. API keys are scoped to a single project automatically. + * description: Returns all conversations the caller has access to. If projectId is provided, returns only conversations in that project. project keys are scoped to a single project automatically. * operationId: listConversations * parameters: * - name: projectId @@ -197,10 +200,25 @@ conversationsRouter.get('/conversations/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - conversation.projectId!, - 'conversations:GetConversation' - ); + const srnGet = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const contextGet: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + contextGet[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:GetConversation', + resource: srnGet, + context: contextGet, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -217,7 +235,7 @@ conversationsRouter.get('/conversations/:id', async (ctx: Context) => { * tags: * - Conversations * summary: Create a conversation - * description: Creates a new conversation. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + * description: Creates a new conversation. project keys automatically infer the project from the key's scope; JWT callers must supply projectId. * operationId: createConversation * requestBody: * required: true @@ -228,7 +246,7 @@ conversationsRouter.get('/conversations/:id', async (ctx: Context) => { * properties: * projectId: * type: string - * description: Project ID. Required for JWT auth; omit when using an API key. + * description: Project ID. Required for JWT auth; omit when using an project key. * example: 'proj_V1StGXR8Z5jdHi6B' * status: * type: string @@ -275,8 +293,8 @@ conversationsRouter.post('/conversations', async (ctx: Context) => { let resolvedProjectPublicId = body.projectId; if (!resolvedProjectPublicId) { - if (ctx.authUser.apiKeyProjectId) { - resolvedProjectPublicId = ctx.authUser.apiKeyProjectId; + if (ctx.authUser.projectKeyProjectId) { + resolvedProjectPublicId = ctx.authUser.projectKeyProjectId; } else { ctx.status = 400; ctx.body = { error: 'projectId is required' }; @@ -284,10 +302,10 @@ conversationsRouter.post('/conversations', async (ctx: Context) => { } } - const allowed = await ctx.authUser.isAllowed( - resolvedProjectPublicId, - 'conversations:CreateConversation' - ); + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: resolvedProjectPublicId, + action: 'conversations:CreateConversation', + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -397,10 +415,25 @@ conversationsRouter.patch('/conversations/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - conversation.projectId!, - 'conversations:UpdateConversation' - ); + const srnUpd = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const contextUpd: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + contextUpd[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:UpdateConversation', + resource: srnUpd, + context: contextUpd, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -469,10 +502,25 @@ conversationsRouter.delete('/conversations/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - conversation.projectId!, - 'conversations:DeleteConversation' - ); + const srnDel = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const contextDel: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + contextDel[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:DeleteConversation', + resource: srnDel, + context: contextDel, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -567,10 +615,25 @@ conversationsRouter.get('/conversations/:id/messages', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - conversation.projectId!, - 'conversations:GetConversation' - ); + const srnMsgs = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const contextMsgs: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + contextMsgs[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:GetConversation', + resource: srnMsgs, + context: contextMsgs, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -699,10 +762,25 @@ conversationsRouter.post( return; } - const allowed = await ctx.authUser.isAllowed( - conversation.projectId!, - 'conversations:UpdateConversation' - ); + const srnAddMsg = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const contextAddMsg: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + contextAddMsg[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:UpdateConversation', + resource: srnAddMsg, + context: contextAddMsg, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -790,10 +868,25 @@ conversationsRouter.delete( return; } - const allowed = await ctx.authUser.isAllowed( - conversation.projectId!, - 'conversations:UpdateConversation' - ); + const srnRmMsg = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const contextRmMsg: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + contextRmMsg[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:UpdateConversation', + resource: srnRmMsg, + context: contextRmMsg, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -875,10 +968,25 @@ conversationsRouter.get('/conversations/:id/actors', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - conversation.projectId!, - 'conversations:GetConversation' - ); + const srnActors = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const contextActors: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + contextActors[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:GetConversation', + resource: srnActors, + context: contextActors, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -891,4 +999,257 @@ conversationsRouter.get('/conversations/:id/actors', async (ctx: Context) => { ctx.body = actors; }); +/** + * @openapi + * /conversations/{id}/tags: + * get: + * tags: + * - Conversations + * summary: Get conversation tags + * operationId: getConversationTagsRoute + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * responses: + * '200': + * description: Conversation tags + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: Conversation not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.get('/conversations/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const context: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + context[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:GetConversation', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await getConversationTags({ id: ctx.params.id }); +}); + +/** + * @openapi + * /conversations/{id}/tags: + * put: + * tags: + * - Conversations + * summary: Replace conversation tags + * operationId: putConversationTags + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * responses: + * '200': + * description: Tags replaced + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ConversationRecord' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: Conversation not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.put('/conversations/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const context: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + context[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:UpdateConversation', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const tags = ctx.request.body as Record; + ctx.body = await updateConversationTags({ + id: ctx.params.id, + tags, + merge: false, + }); +}); + +/** + * @openapi + * /conversations/{id}/tags: + * patch: + * tags: + * - Conversations + * summary: Merge conversation tags + * operationId: patchConversationTags + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * responses: + * '200': + * description: Tags merged + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ConversationRecord' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: Conversation not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +conversationsRouter.patch('/conversations/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const conversation = await getConversation({ id: ctx.params.id }); + + if (!conversation) { + ctx.status = 404; + ctx.body = { error: 'Conversation not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: conversation.projectId!, + resourceType: 'conversation', + resourceId: conversation.id, + }); + const context: Record = { + 'soat:ResourceType': 'conversation', + }; + if (conversation.tags) { + for (const [k, v] of Object.entries(conversation.tags)) { + context[`soat:ResourceTag/${k}`] = v as string; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: conversation.projectId!, + action: 'conversations:UpdateConversation', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const tags = ctx.request.body as Record; + ctx.body = await updateConversationTags({ + id: ctx.params.id, + tags, + merge: true, + }); +}); + export { conversationsRouter }; diff --git a/packages/server/src/rest/v1/documents.ts b/packages/server/src/rest/v1/documents.ts index 0ebcd515..f43b0f08 100644 --- a/packages/server/src/rest/v1/documents.ts +++ b/packages/server/src/rest/v1/documents.ts @@ -5,10 +5,13 @@ import { createDocument, deleteDocument, getDocument, + getDocumentTags, listDocuments, searchDocuments, updateDocument, + updateDocumentTags, } from 'src/lib/documents'; +import { buildSrn } from 'src/lib/iam'; const documentsRouter = new Router(); @@ -19,7 +22,7 @@ const documentsRouter = new Router(); * tags: * - Documents * summary: List documents - * description: Returns all documents the caller has access to. If projectId is provided, returns only documents in that project. API keys are scoped to a single project automatically. JWT users without projectId receive documents across all their accessible projects. + * description: Returns all documents the caller has access to. If projectId is provided, returns only documents in that project. project keys are scoped to a single project automatically. JWT users without projectId receive documents across all their accessible projects. * operationId: listDocuments * parameters: * - name: projectId @@ -161,10 +164,23 @@ documentsRouter.get('/documents/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - doc.projectId!, - 'documents:GetDocument' - ); + const srn = buildSrn({ + projectPublicId: doc.projectId!, + resourceType: 'document', + resourceId: doc.id, + }); + const context: Record = { 'soat:ResourceType': 'document' }; + if (doc.tags) { + for (const [k, v] of Object.entries(doc.tags)) { + context[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: doc.projectId!, + action: 'documents:GetDocument', + resource: srn, + context, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -181,7 +197,7 @@ documentsRouter.get('/documents/:id', async (ctx: Context) => { * tags: * - Documents * summary: Create a document - * description: Creates a new text document with an embedding vector. API keys automatically infer the project from the key's scope; JWT callers must supply projectId. + * description: Creates a new text document with an embedding vector. project keys automatically infer the project from the key's scope; JWT callers must supply projectId. * operationId: createDocument * requestBody: * required: true @@ -194,7 +210,7 @@ documentsRouter.get('/documents/:id', async (ctx: Context) => { * properties: * projectId: * type: string - * description: Project ID. Required for JWT auth; omit when using an API key. + * description: Project ID. Required for JWT auth; omit when using an project key. * example: 'proj_V1StGXR8Z5jdHi6B' * content: * type: string @@ -209,10 +225,11 @@ documentsRouter.get('/documents/:id', async (ctx: Context) => { * type: object * description: Arbitrary key-value metadata * tags: - * type: array - * items: + * type: object + * additionalProperties: * type: string - * example: ['tag1', 'tag2'] + * description: Key-value tags + * example: { env: 'production' } * responses: * '201': * description: Document created @@ -252,7 +269,7 @@ documentsRouter.post('/documents', async (ctx: Context) => { filename?: string; title?: string; metadata?: Record; - tags?: string[]; + tags?: Record; }; if (!body.content) { @@ -261,11 +278,11 @@ documentsRouter.post('/documents', async (ctx: Context) => { return; } - // Resolve projectId: use explicit value, infer from API key, or error for JWT + // Resolve projectId: use explicit value, infer from project key, or error for JWT let resolvedProjectPublicId = body.projectId; if (!resolvedProjectPublicId) { - if (ctx.authUser.apiKeyProjectId) { - resolvedProjectPublicId = ctx.authUser.apiKeyProjectId; + if (ctx.authUser.projectKeyProjectId) { + resolvedProjectPublicId = ctx.authUser.projectKeyProjectId; } else { ctx.status = 400; ctx.body = { error: 'projectId is required' }; @@ -273,10 +290,10 @@ documentsRouter.post('/documents', async (ctx: Context) => { } } - const allowed = await ctx.authUser.isAllowed( - resolvedProjectPublicId, - 'documents:CreateDocument' - ); + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: resolvedProjectPublicId, + action: 'documents:CreateDocument', + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -359,10 +376,25 @@ documentsRouter.delete('/documents/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - doc.projectId!, - 'documents:DeleteDocument' - ); + const srnDel = buildSrn({ + projectPublicId: doc.projectId!, + resourceType: 'document', + resourceId: doc.id, + }); + const contextDel: Record = { + 'soat:ResourceType': 'document', + }; + if (doc.tags) { + for (const [k, v] of Object.entries(doc.tags)) { + contextDel[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: doc.projectId!, + action: 'documents:DeleteDocument', + resource: srnDel, + context: contextDel, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -412,9 +444,10 @@ documentsRouter.delete('/documents/:id', async (ctx: Context) => { * metadata: * type: object * tags: - * type: array - * items: + * type: object + * additionalProperties: * type: string + * description: Key-value tags * responses: * '200': * description: Document updated @@ -456,10 +489,25 @@ documentsRouter.patch('/documents/:id', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - doc.projectId!, - 'documents:UpdateDocument' - ); + const srnUpd = buildSrn({ + projectPublicId: doc.projectId!, + resourceType: 'document', + resourceId: doc.id, + }); + const contextUpd: Record = { + 'soat:ResourceType': 'document', + }; + if (doc.tags) { + for (const [k, v] of Object.entries(doc.tags)) { + contextUpd[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: doc.projectId!, + action: 'documents:UpdateDocument', + resource: srnUpd, + context: contextUpd, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -470,7 +518,7 @@ documentsRouter.patch('/documents/:id', async (ctx: Context) => { content?: string; title?: string; metadata?: Record; - tags?: string[]; + tags?: Record; }; const updated = await updateDocument({ @@ -517,11 +565,11 @@ documentsRouter.patch('/documents/:id', async (ctx: Context) => { * description: Minimum similarity score (0-1). Only results with score >= threshold are returned. * example: 0.7 * tags: - * type: array - * items: + * type: object + * additionalProperties: * type: string - * description: Filter to documents with any of these tags. - * example: ['tag1', 'tag2'] + * description: Filter to documents matching all these key-value tags. + * example: { env: 'production' } * responses: * '200': * description: Search results @@ -562,7 +610,7 @@ documentsRouter.post('/documents/search', async (ctx: Context) => { query: string; limit?: number; threshold?: number; - tags?: string[]; + tags?: Record; }; if (!body.query) { @@ -593,4 +641,271 @@ documentsRouter.post('/documents/search', async (ctx: Context) => { ctx.body = results; }); +/** + * @openapi + * /documents/{id}/tags: + * get: + * tags: + * - Documents + * summary: Get document tags + * operationId: getDocumentTagsRoute + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * responses: + * '200': + * description: Document tags + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Document not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.get('/documents/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const doc = await getDocument({ id: ctx.params.id }); + + if (!doc) { + ctx.status = 404; + ctx.body = { error: 'Document not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: doc.projectId!, + resourceType: 'document', + resourceId: doc.id, + }); + const context: Record = { 'soat:ResourceType': 'document' }; + if (doc.tags) { + for (const [k, v] of Object.entries(doc.tags)) { + context[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: doc.projectId!, + action: 'documents:GetDocument', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await getDocumentTags({ id: ctx.params.id }); +}); + +/** + * @openapi + * /documents/{id}/tags: + * put: + * tags: + * - Documents + * summary: Replace document tags + * operationId: putDocumentTags + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * responses: + * '200': + * description: Tags replaced + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DocumentRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Document not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.put('/documents/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const doc = await getDocument({ id: ctx.params.id }); + + if (!doc) { + ctx.status = 404; + ctx.body = { error: 'Document not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: doc.projectId!, + resourceType: 'document', + resourceId: doc.id, + }); + const context: Record = { 'soat:ResourceType': 'document' }; + if (doc.tags) { + for (const [k, v] of Object.entries(doc.tags)) { + context[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: doc.projectId!, + action: 'documents:UpdateDocument', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const tags = ctx.request.body as Record; + ctx.body = await updateDocumentTags({ + id: ctx.params.id, + tags, + merge: false, + }); +}); + +/** + * @openapi + * /documents/{id}/tags: + * patch: + * tags: + * - Documents + * summary: Merge document tags + * operationId: patchDocumentTags + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * responses: + * '200': + * description: Tags merged + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DocumentRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Document not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +documentsRouter.patch('/documents/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const doc = await getDocument({ id: ctx.params.id }); + + if (!doc) { + ctx.status = 404; + ctx.body = { error: 'Document not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: doc.projectId!, + resourceType: 'document', + resourceId: doc.id, + }); + const context: Record = { 'soat:ResourceType': 'document' }; + if (doc.tags) { + for (const [k, v] of Object.entries(doc.tags)) { + context[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: doc.projectId!, + action: 'documents:UpdateDocument', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const tags = ctx.request.body as Record; + ctx.body = await updateDocumentTags({ id: ctx.params.id, tags, merge: true }); +}); + export { documentsRouter }; diff --git a/packages/server/src/rest/v1/files.ts b/packages/server/src/rest/v1/files.ts index ec1adc9b..0316646f 100644 --- a/packages/server/src/rest/v1/files.ts +++ b/packages/server/src/rest/v1/files.ts @@ -7,10 +7,13 @@ import { deleteFile, downloadFile, getFile, + getFileTags, listFiles, updateFileMetadata, + updateFileTags, uploadFile, } from 'src/lib/files'; +import { buildSrn } from 'src/lib/iam'; const upload = multer({ storage: multer.memoryStorage() }); @@ -156,10 +159,23 @@ filesRouter.get('/files/:id', async (ctx: Context) => { } // Check if user is allowed to read files in this project - const allowed = await ctx.authUser.isAllowed( - file.projectId!, - 'files:GetFile' - ); + const srn = buildSrn({ + projectPublicId: file.projectId!, + resourceType: 'file', + resourceId: file.id, + }); + const context: Record = { 'soat:ResourceType': 'file' }; + if (file.tags) { + for (const [k, v] of Object.entries(file.tags)) { + context[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: file.projectId!, + action: 'files:GetFile', + resource: srn, + context, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -243,10 +259,10 @@ filesRouter.post('/files', async (ctx: Context) => { }; // Check if user is allowed to create files in this project - const allowed = await ctx.authUser.isAllowed( - body.projectId, - 'files:CreateFile' - ); + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: body.projectId, + action: 'files:CreateFile', + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -324,10 +340,23 @@ filesRouter.delete('/files/:id', async (ctx: Context) => { } // Check if user is allowed to delete files in this project - const allowed = await ctx.authUser.isAllowed( - file.project!.publicId, - 'files:DeleteFile' - ); + const srnDel = buildSrn({ + projectPublicId: file.project!.publicId, + resourceType: 'file', + resourceId: file.publicId, + }); + const contextDel: Record = { 'soat:ResourceType': 'file' }; + if (file.tags) { + for (const [k, v] of Object.entries(file.tags as Record)) { + contextDel[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: file.project!.publicId, + action: 'files:DeleteFile', + resource: srnDel, + context: contextDel, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -416,10 +445,10 @@ filesRouter.post( return; } - const allowed = await ctx.authUser.isAllowed( - body.projectId, - 'files:UploadFile' - ); + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: body.projectId, + action: 'files:UploadFile', + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -527,10 +556,10 @@ filesRouter.post('/files/upload/base64', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - body.projectId, - 'files:UploadFile' - ); + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: body.projectId, + action: 'files:UploadFile', + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -611,10 +640,23 @@ filesRouter.get('/files/:id/download', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - fileRecord.projectId!, - 'files:DownloadFile' - ); + const srnDl = buildSrn({ + projectPublicId: fileRecord.projectId!, + resourceType: 'file', + resourceId: fileRecord.id, + }); + const contextDl: Record = { 'soat:ResourceType': 'file' }; + if (fileRecord.tags) { + for (const [k, v] of Object.entries(fileRecord.tags)) { + contextDl[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: fileRecord.projectId!, + action: 'files:DownloadFile', + resource: srnDl, + context: contextDl, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -699,10 +741,23 @@ filesRouter.get('/files/:id/download/base64', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - fileRecord.projectId!, - 'files:DownloadFile' - ); + const srnDlB64 = buildSrn({ + projectPublicId: fileRecord.projectId!, + resourceType: 'file', + resourceId: fileRecord.id, + }); + const contextDlB64: Record = { 'soat:ResourceType': 'file' }; + if (fileRecord.tags) { + for (const [k, v] of Object.entries(fileRecord.tags)) { + contextDlB64[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: fileRecord.projectId!, + action: 'files:DownloadFile', + resource: srnDlB64, + context: contextDlB64, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -794,10 +849,23 @@ filesRouter.patch('/files/:id/metadata', async (ctx: Context) => { return; } - const allowed = await ctx.authUser.isAllowed( - fileRecord.projectId!, - 'files:UpdateFileMetadata' - ); + const srnMeta = buildSrn({ + projectPublicId: fileRecord.projectId!, + resourceType: 'file', + resourceId: fileRecord.id, + }); + const contextMeta: Record = { 'soat:ResourceType': 'file' }; + if (fileRecord.tags) { + for (const [k, v] of Object.entries(fileRecord.tags)) { + contextMeta[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: fileRecord.projectId!, + action: 'files:UpdateFileMetadata', + resource: srnMeta, + context: contextMeta, + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -814,4 +882,243 @@ filesRouter.patch('/files/:id/metadata', async (ctx: Context) => { ctx.body = updated; }); +/** + * @openapi + * /files/{id}/tags: + * get: + * tags: + * - Files + * summary: Get file tags + * operationId: getFileTagsRoute + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * responses: + * '200': + * description: File tags + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: File not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.get('/files/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const file = await getFile({ id: ctx.params.id }); + + if (!file) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: file.projectId!, + resourceType: 'file', + resourceId: file.id, + }); + const context: Record = { 'soat:ResourceType': 'file' }; + if (file.tags) { + for (const [k, v] of Object.entries(file.tags)) { + context[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: file.projectId!, + action: 'files:GetFile', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await getFileTags({ id: ctx.params.id }); +}); + +/** + * @openapi + * /files/{id}/tags: + * put: + * tags: + * - Files + * summary: Replace file tags + * operationId: putFileTags + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * responses: + * '200': + * description: Tags replaced + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FileRecord' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: File not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.put('/files/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const file = await getFile({ id: ctx.params.id }); + + if (!file) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: file.projectId!, + resourceType: 'file', + resourceId: file.id, + }); + const context: Record = { 'soat:ResourceType': 'file' }; + if (file.tags) { + for (const [k, v] of Object.entries(file.tags)) { + context[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: file.projectId!, + action: 'files:UpdateFileMetadata', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const tags = ctx.request.body as Record; + ctx.body = await updateFileTags({ id: ctx.params.id, tags, merge: false }); +}); + +/** + * @openapi + * /files/{id}/tags: + * patch: + * tags: + * - Files + * summary: Merge file tags + * operationId: patchFileTags + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: string + * responses: + * '200': + * description: Tags merged + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FileRecord' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * description: File not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +filesRouter.patch('/files/:id/tags', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const file = await getFile({ id: ctx.params.id }); + + if (!file) { + ctx.status = 404; + ctx.body = { error: 'File not found' }; + return; + } + + const srn = buildSrn({ + projectPublicId: file.projectId!, + resourceType: 'file', + resourceId: file.id, + }); + const context: Record = { 'soat:ResourceType': 'file' }; + if (file.tags) { + for (const [k, v] of Object.entries(file.tags)) { + context[`soat:ResourceTag/${k}`] = v; + } + } + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: file.projectId!, + action: 'files:UpdateFileMetadata', + resource: srn, + context, + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const tags = ctx.request.body as Record; + ctx.body = await updateFileTags({ id: ctx.params.id, tags, merge: true }); +}); + export { filesRouter }; diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index 15eb01a6..35bbc5c8 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -2,7 +2,7 @@ import { Router } from '@ttoss/http-server'; import { agentsRouter } from './agents'; import { actorsRouter } from './actors'; -import { apiKeysRouter } from './apiKeys'; +import { projectKeysRouter } from './projectKeys'; import { conversationsRouter } from './conversations'; import { documentsRouter } from './documents'; import { filesRouter } from './files'; @@ -13,7 +13,7 @@ const v1Router = new Router(); v1Router.use(agentsRouter.routes()); v1Router.use(actorsRouter.routes()); -v1Router.use(apiKeysRouter.routes()); +v1Router.use(projectKeysRouter.routes()); v1Router.use(conversationsRouter.routes()); v1Router.use(documentsRouter.routes()); v1Router.use(filesRouter.routes()); diff --git a/packages/server/src/rest/v1/apiKeys.ts b/packages/server/src/rest/v1/projectKeys.ts similarity index 74% rename from packages/server/src/rest/v1/apiKeys.ts rename to packages/server/src/rest/v1/projectKeys.ts index d3320ef5..cfcfe9be 100644 --- a/packages/server/src/rest/v1/apiKeys.ts +++ b/packages/server/src/rest/v1/projectKeys.ts @@ -1,19 +1,23 @@ import { Router } from '@ttoss/http-server'; import type { Context } from 'src/Context'; import { db } from 'src/db'; -import { createApiKey, getApiKey, updateApiKey } from 'src/lib/apiKeys'; +import { + createProjectKey, + getProjectKey, + updateProjectKey, +} from 'src/lib/projectKeys'; -const apiKeysRouter = new Router(); +const projectKeysRouter = new Router(); /** * @openapi - * /api-keys: + * /project-keys: * post: * tags: - * - API Keys - * summary: Create a new API key - * description: Creates a new API key for a user in a project with specified policy - * operationId: createApiKey + * - Project Keys + * summary: Create a new project key + * description: Creates a new project key for a user in a project with specified policy + * operationId: createProjectKey * requestBody: * required: true * content: @@ -33,10 +37,10 @@ const apiKeysRouter = new Router(); * description: Policy ID * name: * type: string - * description: API key name + * description: project key name * responses: * '201': - * description: API key created successfully + * description: Project key created successfully * content: * application/json: * schema: @@ -48,7 +52,7 @@ const apiKeysRouter = new Router(); * type: string * key: * type: string - * description: The full API key (shown only once) + * description: The full project key (shown only once) * keyPrefix: * type: string * createdAt: @@ -62,7 +66,7 @@ const apiKeysRouter = new Router(); * '500': * description: Internal server error */ -apiKeysRouter.post('/api-keys', async (ctx: Context) => { +projectKeysRouter.post('/project-keys', async (ctx: Context) => { if (!ctx.authUser) { ctx.status = 401; ctx.body = { error: 'Unauthorized' }; @@ -120,7 +124,7 @@ apiKeysRouter.post('/api-keys', async (ctx: Context) => { return; } - const apiKey = await createApiKey({ + const projectKey = await createProjectKey({ userId: ctx.authUser.id, projectId: project.id, policyId: policy.id, @@ -128,28 +132,28 @@ apiKeysRouter.post('/api-keys', async (ctx: Context) => { }); ctx.status = 201; - ctx.body = apiKey; + ctx.body = projectKey; }); /** * @openapi - * /api-keys/{id}: + * /project-keys/{id}: * get: * tags: - * - API Keys - * summary: Get an API key by ID - * description: Returns the data and metadata of a specific API key - * operationId: getApiKey + * - Project Keys + * summary: Get a project key by ID + * description: Returns the data and metadata of a specific project key + * operationId: getProjectKey * parameters: * - name: id * in: path * required: true - * description: API key ID + * description: Project key ID * schema: * type: string * responses: * '200': - * description: API key found + * description: Project key found * content: * application/json: * schema: @@ -172,51 +176,51 @@ apiKeysRouter.post('/api-keys', async (ctx: Context) => { * updatedAt: * type: string * '404': - * description: API key not found + * description: Project key not found * '403': * description: Forbidden * '500': * description: Internal server error */ -apiKeysRouter.get('/api-keys/:id', async (ctx: Context) => { +projectKeysRouter.get('/project-keys/:id', async (ctx: Context) => { if (!ctx.authUser) { ctx.status = 401; ctx.body = { error: 'Unauthorized' }; return; } - const apiKey = await getApiKey({ id: ctx.params.id }); + const projectKey = await getProjectKey({ id: ctx.params.id }); - if (!apiKey) { + if (!projectKey) { ctx.status = 404; - ctx.body = { error: 'API key not found' }; + ctx.body = { error: 'Project key not found' }; return; } - // Check if user owns the API key - if (apiKey.userId !== ctx.authUser.publicId) { + // Check if user owns the project key + if (projectKey.userId !== ctx.authUser.publicId) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; return; } - ctx.body = apiKey; + ctx.body = projectKey; }); /** * @openapi - * /api-keys/{id}: + * /project-keys/{id}: * put: * tags: - * - API Keys - * summary: Update an API key - * description: Updates the policy of a specific API key - * operationId: updateApiKey + * - Project Keys + * summary: Update a project key + * description: Updates the policy of a specific project key + * operationId: updateProjectKey * parameters: * - name: id * in: path * required: true - * description: API key ID + * description: Project key ID * schema: * type: string * requestBody: @@ -233,7 +237,7 @@ apiKeysRouter.get('/api-keys/:id', async (ctx: Context) => { * description: New policy ID * responses: * '200': - * description: API key updated successfully + * description: Project key updated successfully * content: * application/json: * schema: @@ -256,13 +260,13 @@ apiKeysRouter.get('/api-keys/:id', async (ctx: Context) => { * updatedAt: * type: string * '404': - * description: API key not found + * description: Project key not found * '403': * description: Forbidden * '500': * description: Internal server error */ -apiKeysRouter.put('/api-keys/:id', async (ctx: Context) => { +projectKeysRouter.put('/project-keys/:id', async (ctx: Context) => { if (!ctx.authUser) { ctx.status = 401; ctx.body = { error: 'Unauthorized' }; @@ -290,37 +294,37 @@ apiKeysRouter.put('/api-keys/:id', async (ctx: Context) => { return; } - // Check if user owns the API key - const apiKeyRecord = await db.ApiKey.findOne({ + // Check if user owns the project key + const projectKeyRecord = await ctx.db.ProjectKey.findOne({ where: { publicId: ctx.params.id }, include: [{ model: db.User }, { model: db.Project }], }); - if (!apiKeyRecord) { + if (!projectKeyRecord) { ctx.status = 404; - ctx.body = { error: 'API key not found' }; + ctx.body = { error: 'Project key not found' }; return; } - if (apiKeyRecord.userId !== ctx.authUser.id) { + if (projectKeyRecord.userId !== ctx.authUser.id) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; return; } - // Check if policy belongs to the same project as the API key - if (policy.projectId !== apiKeyRecord.projectId) { + // Check if policy belongs to the same project as the project key + if (policy.projectId !== projectKeyRecord.projectId) { ctx.status = 400; ctx.body = { error: 'Policy does not belong to the same project' }; return; } - const updatedApiKey = await updateApiKey({ + const updatedProjectKey = await updateProjectKey({ id: ctx.params.id, policyId: policy.id, }); - ctx.body = updatedApiKey; + ctx.body = updatedProjectKey; }); -export { apiKeysRouter }; +export { projectKeysRouter }; diff --git a/packages/server/src/rest/v1/projects.ts b/packages/server/src/rest/v1/projects.ts index 416d2e74..53e444b8 100644 --- a/packages/server/src/rest/v1/projects.ts +++ b/packages/server/src/rest/v1/projects.ts @@ -5,9 +5,14 @@ import { createProject, createProjectPolicy, deleteProject, + deleteProjectPolicy, getProject, + getProjectPolicy, + getUserProjectPolicies, listProjectPolicies, listProjects, + updateProjectPolicy, + updateUserProjectPolicies, } from 'src/lib/projects'; const projectsRouter = new Router(); @@ -127,10 +132,10 @@ projectsRouter.get('/projects/:projectId/policies', async (ctx: Context) => { } // Check if user is member of the project - const allowed = await ctx.authUser.isAllowed( - ctx.params.projectId, - 'projects:GetProject' - ); + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: ctx.params.projectId, + action: 'projects:GetProject', + }); if (!allowed) { ctx.status = 403; ctx.body = { error: 'Forbidden' }; @@ -163,23 +168,17 @@ projectsRouter.get('/projects/:projectId/policies', async (ctx: Context) => { * example: 'proj_V1StGXR8Z5jdHi6B' * requestBody: * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - permissions + * content:document * properties: - * permissions: - * type: array - * items: - * type: string - * example: ['files:read'] - * notPermissions: - * type: array - * items: - * type: string - * example: ['files:delete'] + * name: + * type: string + * example: 'Document Readers' + * description: + * type: string + * example: 'Read-only access to documents' + * document: + * type: object + * description: PolicyDocument JSON * responses: * '201': * description: Policy created successfully @@ -187,6 +186,12 @@ projectsRouter.get('/projects/:projectId/policies', async (ctx: Context) => { * application/json: * schema: * $ref: '#/components/schemas/ProjectPolicyRecord' + * '400': + * description: Invalid policy document + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' * '401': * description: Unauthorized * content: @@ -219,36 +224,57 @@ projectsRouter.post('/projects/:projectId/policies', async (ctx: Context) => { return; } - const { permissions, notPermissions } = ctx.request.body as { + const { name, description, permissions, notPermissions } = ctx.request + .body as { + name?: string; + description?: string; permissions: string[]; notPermissions?: string[]; }; - const policy = await createProjectPolicy({ + const document: import('src/lib/iam').PolicyDocument = { + statement: [ + ...(permissions?.length + ? [{ effect: 'Allow' as const, action: permissions }] + : []), + ...(notPermissions?.length + ? [{ effect: 'Deny' as const, action: notPermissions }] + : []), + ], + }; + + const result = await createProjectPolicy({ projectId: ctx.params.projectId, - permissions, - notPermissions, + name, + description, + document, }); - if (!policy) { + if (result === 'not_found') { ctx.status = 404; ctx.body = { error: 'Project not found' }; return; } + if ('invalid' in result) { + ctx.status = 400; + ctx.body = { error: 'Invalid policy document', details: result.errors }; + return; + } + ctx.status = 201; - ctx.body = policy; + ctx.body = result; }); /** * @openapi - * /projects/{projectId}/members: - * post: + * /projects/{projectId}/policies/{policyId}: + * put: * tags: * - Projects - * summary: Add user to project - * description: Adds a user to a project with a specific policy. Only admins can add members. - * operationId: addUserToProject + * summary: Update a project policy + * description: Replaces a policy document. Only admins can update policies. + * operationId: updateProjectPolicy * parameters: * - name: projectId * in: path @@ -256,7 +282,12 @@ projectsRouter.post('/projects/:projectId/policies', async (ctx: Context) => { * description: Project ID * schema: * type: string - * example: 'proj_V1StGXR8Z5jdHi6B' + * - name: policyId + * in: path + * required: true + * description: Policy ID + * schema: + * type: string * requestBody: * required: true * content: @@ -264,15 +295,263 @@ projectsRouter.post('/projects/:projectId/policies', async (ctx: Context) => { * schema: * type: object * required: - * - userId - * - policyId + * - document + * properties: + * name: + * type: string + * description: + * type: string + * document: + * type: object + * responses: + * '200': + * description: Policy updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProjectPolicyRecord' + * '400': + * description: Invalid policy document + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project or policy not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.put( + '/projects/:projectId/policies/:policyId', + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const { name, description, document } = ctx.request.body as { + name?: string; + description?: string; + document: object; + }; + + const result = await updateProjectPolicy({ + projectId: ctx.params.projectId, + policyId: ctx.params.policyId, + name, + description, + document: document as import('src/lib/iam').PolicyDocument, + }); + + if (result === 'not_found') { + ctx.status = 404; + ctx.body = { error: 'Project or policy not found' }; + return; + } + + if ('invalid' in result) { + ctx.status = 400; + ctx.body = { error: 'Invalid policy document', details: result.errors }; + return; + } + + ctx.body = result; + } +); + +/** + * @openapi + * /projects/{projectId}/policies/{policyId}: + * delete: + * tags: + * - Projects + * summary: Delete a project policy + * description: Deletes a policy. Only admins can delete policies. + * operationId: deleteProjectPolicy + * parameters: + * - name: projectId + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * - name: policyId + * in: path + * required: true + * description: Policy ID + * schema: + * type: string + * responses: + * '204': + * description: Policy deleted successfully + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project or policy not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.delete( + '/projects/:projectId/policies/:policyId', + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const result = await deleteProjectPolicy({ + projectId: ctx.params.projectId, + policyId: ctx.params.policyId, + }); + + if (result === 'not_found') { + ctx.status = 404; + ctx.body = { error: 'Project or policy not found' }; + return; + } + + ctx.status = 204; + } +); + +/** + * @openapi + * /projects/{projectId}/policies/{policyId}: + * get: + * tags: + * - Projects + * summary: Get a project policy + * description: Returns a single policy for a project. + * operationId: getProjectPolicy + * parameters: + * - name: projectId + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * - name: policyId + * in: path + * required: true + * description: Policy ID + * schema: + * type: string + * responses: + * '200': + * description: Policy returned successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProjectPolicyRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Policy not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.get( + '/projects/:projectId/policies/:policyId', + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: ctx.params.projectId, + action: 'projects:GetProject', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const policy = await getProjectPolicy({ + projectId: ctx.params.projectId, + policyId: ctx.params.policyId, + }); + + if (!policy) { + ctx.status = 404; + ctx.body = { error: 'Policy not found' }; + return; + } + + ctx.body = policy; + } +); + +/** + * @openapi + * /projects/{projectId}/policies/{policyId}: + * put: + * tags: + * - Projects + * summary: Update a project policy * properties: * userId: * type: string * example: 'usr_V1StGXR8Z5jdHi6B' - * policyId: - * type: string - * example: 'pol_V1StGXR8Z5jdHi6B' + * policyIds: + * type: array + * items: + * type: string + * example: ['pol_V1StGXR8Z5jdHi6B'] * responses: * '201': * description: User added to project successfully @@ -308,15 +587,18 @@ projectsRouter.post('/projects/:projectId/members', async (ctx: Context) => { return; } - const { userId, policyId } = ctx.request.body as { + const { userId, policyId, policyIds } = ctx.request.body as { userId: string; - policyId: string; + policyId?: string; + policyIds?: string[]; }; + const resolvedPolicyIds = policyIds ?? (policyId ? [policyId] : undefined); + const success = await addUserToProject({ projectId: ctx.params.projectId, userId, - policyId, + policyIds: resolvedPolicyIds, }); if (!success) { @@ -328,6 +610,176 @@ projectsRouter.post('/projects/:projectId/members', async (ctx: Context) => { ctx.status = 201; }); +/** + * @openapi + * /projects/{projectId}/members/{userId}/policies: + * put: + * tags: + * - Projects + * summary: Update member policies + * description: Replaces the list of policies attached to a member. Only admins can update member policies. + * operationId: updateUserProjectPolicies + * parameters: + * - name: projectId + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * - name: userId + * in: path + * required: true + * description: User ID + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - policyIds + * properties: + * policyIds: + * type: array + * items: + * type: string + * responses: + * '204': + * description: Member policies updated successfully + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project, user, or policy not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.put( + '/projects/:projectId/members/:userId/policies', + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const { policyIds } = ctx.request.body as { policyIds: string[] }; + + const result = await updateUserProjectPolicies({ + projectId: ctx.params.projectId, + userId: ctx.params.userId, + policyIds, + }); + + if (result === 'not_found') { + ctx.status = 404; + ctx.body = { error: 'Project, user, membership, or policy not found' }; + return; + } + + ctx.status = 204; + } +); + +/** + * @openapi + * /projects/{projectId}/members/{userId}/policies: + * get: + * tags: + * - Projects + * summary: Get member policies + * description: Returns the list of policies attached to a project member. + * operationId: getUserProjectPolicies + * parameters: + * - name: projectId + * in: path + * required: true + * description: Project ID + * schema: + * type: string + * - name: userId + * in: path + * required: true + * description: User ID + * schema: + * type: string + * responses: + * '200': + * description: Member policies returned successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ProjectPolicyRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Project or user not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +projectsRouter.get( + '/projects/:projectId/members/:userId/policies', + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + if (ctx.authUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const policies = await getUserProjectPolicies({ + projectId: ctx.params.projectId, + userId: ctx.params.userId, + }); + + if (policies === null) { + ctx.status = 404; + ctx.body = { error: 'Project or user not found' }; + return; + } + + ctx.body = policies; + } +); + /** * @openapi * /projects: diff --git a/packages/server/tests/unit/tests/documents.test.ts b/packages/server/tests/unit/tests/documents.test.ts index 46478440..67028677 100644 --- a/packages/server/tests/unit/tests/documents.test.ts +++ b/packages/server/tests/unit/tests/documents.test.ts @@ -465,8 +465,8 @@ describe('Documents', () => { }); }); - describe('API key access', () => { - let apiKey: string; + describe('project key access', () => { + let projectKey: string; beforeAll(async () => { const policyRes = await authenticatedTestClient(adminToken) @@ -474,39 +474,39 @@ describe('Documents', () => { .send({ permissions: ['documents:ListDocuments', 'documents:SearchDocuments'], }); - const apiKeyPolicyId = policyRes.body.id; + const projectKeyPolicyId = policyRes.body.id; - const apiKeyRes = await authenticatedTestClient(userToken) - .post('/api/v1/api-keys') + const projectKeyRes = await authenticatedTestClient(userToken) + .post('/api/v1/project-keys') .send({ projectId, - policyId: apiKeyPolicyId, - name: 'Docs Test API Key', + policyId: projectKeyPolicyId, + name: 'Docs Test project key', }); - apiKey = apiKeyRes.body.key; + projectKey = projectKeyRes.body.key; await authenticatedTestClient(userToken).post('/api/v1/documents').send({ projectId, - content: 'API key test document.', - filename: 'apikey-doc.txt', + content: 'project key test document.', + filename: 'projectkey-doc.txt', }); }); - test('API key can list documents without providing projectId', async () => { + test('project key can list documents without providing projectId', async () => { const response = await testClient .get('/api/v1/documents') - .set('Authorization', `Bearer ${apiKey}`); + .set('Authorization', `Bearer ${projectKey}`); expect(response.status).toBe(200); expect(Array.isArray(response.body.data)).toBe(true); expect(response.body.data.length).toBeGreaterThan(0); }); - test('API key can search documents without providing projectId', async () => { + test('project key can search documents without providing projectId', async () => { const response = await testClient .post('/api/v1/documents/search') - .set('Authorization', `Bearer ${apiKey}`) - .send({ query: 'API key test' }); + .set('Authorization', `Bearer ${projectKey}`) + .send({ query: 'project key test' }); expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); diff --git a/packages/server/tests/unit/tests/iam.test.ts b/packages/server/tests/unit/tests/iam.test.ts new file mode 100644 index 00000000..2ecf912c --- /dev/null +++ b/packages/server/tests/unit/tests/iam.test.ts @@ -0,0 +1,436 @@ +import { + buildSrn, + evaluateCondition, + evaluatePolicies, + matchesPattern, + statementMatches, + validatePolicyDocument, + type PolicyDocument, +} from '../../../src/lib/iam'; + +describe('IAM', () => { + describe('validatePolicyDocument', () => { + test('valid document with Allow statement passes', () => { + const doc: PolicyDocument = { + statement: [ + { + effect: 'Allow', + action: ['files:GetFile'], + }, + ], + }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + test('valid document with all fields passes', () => { + const doc: PolicyDocument = { + statement: [ + { + effect: 'Allow', + action: ['files:GetFile', 'files:*'], + resource: ['soat:proj_ABC:file:*'], + condition: { + StringEquals: { 'soat:tag:env': 'prod' }, + }, + }, + ], + }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(true); + }); + + test('invalid effect fails', () => { + const doc = { + statement: [{ effect: 'Grant', action: ['files:GetFile'] }], + }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.includes('effect'))).toBe(true); + }); + + test('empty action array fails', () => { + const doc = { statement: [{ effect: 'Allow', action: [] }] }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.includes('action'))).toBe(true); + }); + + test('invalid action format fails', () => { + const doc = { + statement: [{ effect: 'Allow', action: ['invalid-action'] }], + }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(false); + }); + + test('wildcard * action passes', () => { + const doc = { statement: [{ effect: 'Allow', action: ['*'] }] }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(true); + }); + + test('module:* action passes', () => { + const doc = { statement: [{ effect: 'Allow', action: ['files:*'] }] }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(true); + }); + + test('invalid SRN format fails', () => { + const doc = { + statement: [ + { effect: 'Allow', action: ['files:GetFile'], resource: ['invalid'] }, + ], + }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.includes('resource'))).toBe(true); + }); + + test('valid SRN with wildcard passes', () => { + const doc = { + statement: [ + { + effect: 'Allow', + action: ['files:GetFile'], + resource: ['soat:proj_123:file:*'], + }, + ], + }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(true); + }); + + test('invalid condition operator fails', () => { + const doc = { + statement: [ + { + effect: 'Allow', + action: ['files:GetFile'], + condition: { InvalidOp: { 'soat:tag:env': 'prod' } }, + }, + ], + }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.includes('operator'))).toBe(true); + }); + + test('condition key not starting with soat: fails', () => { + const doc = { + statement: [ + { + effect: 'Allow', + action: ['files:GetFile'], + condition: { StringEquals: { env: 'prod' } }, + }, + ], + }; + const result = validatePolicyDocument(doc); + expect(result.valid).toBe(false); + }); + + test('non-object input fails', () => { + const result = validatePolicyDocument('invalid'); + expect(result.valid).toBe(false); + }); + + test('missing statement array fails', () => { + const result = validatePolicyDocument({ foo: 'bar' }); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.includes('statement'))).toBe(true); + }); + }); + + describe('buildSrn', () => { + test('produces correct SRN string', () => { + const srn = buildSrn({ + projectPublicId: 'proj_ABC', + resourceType: 'file', + resourceId: 'file_123', + }); + expect(srn).toBe('soat:proj_ABC:file:file_123'); + }); + + test('works with different resource types', () => { + expect( + buildSrn({ + projectPublicId: 'p', + resourceType: 'document', + resourceId: 'd', + }) + ).toBe('soat:p:document:d'); + }); + }); + + describe('matchesPattern', () => { + test('* matches everything', () => { + expect(matchesPattern({ pattern: '*', value: 'files:GetFile' })).toBe( + true + ); + }); + + test('module:* matches module actions', () => { + expect( + matchesPattern({ pattern: 'files:*', value: 'files:GetFile' }) + ).toBe(true); + expect( + matchesPattern({ pattern: 'files:*', value: 'actors:GetActor' }) + ).toBe(false); + }); + + test('exact match works', () => { + expect( + matchesPattern({ pattern: 'files:GetFile', value: 'files:GetFile' }) + ).toBe(true); + expect( + matchesPattern({ pattern: 'files:GetFile', value: 'files:DeleteFile' }) + ).toBe(false); + }); + + test('SRN resource wildcard matches', () => { + expect( + matchesPattern({ + pattern: 'soat:proj_ABC:file:*', + value: 'soat:proj_ABC:file:file_123', + }) + ).toBe(true); + expect( + matchesPattern({ + pattern: 'soat:proj_ABC:file:*', + value: 'soat:proj_XYZ:file:file_123', + }) + ).toBe(false); + }); + }); + + describe('evaluateCondition', () => { + test('StringEquals passes when values match', () => { + expect( + evaluateCondition({ + condition: { StringEquals: { 'soat:tag:env': 'prod' } }, + context: { 'soat:tag:env': 'prod' }, + }) + ).toBe(true); + }); + + test('StringEquals fails when values differ', () => { + expect( + evaluateCondition({ + condition: { StringEquals: { 'soat:tag:env': 'prod' } }, + context: { 'soat:tag:env': 'dev' }, + }) + ).toBe(false); + }); + + test('StringNotEquals passes when values differ', () => { + expect( + evaluateCondition({ + condition: { StringNotEquals: { 'soat:tag:env': 'prod' } }, + context: { 'soat:tag:env': 'dev' }, + }) + ).toBe(true); + }); + + test('StringLike with glob matches', () => { + expect( + evaluateCondition({ + condition: { StringLike: { 'soat:tag:env': 'prod*' } }, + context: { 'soat:tag:env': 'production' }, + }) + ).toBe(true); + }); + + test('multiple operators require all to pass (AND logic)', () => { + expect( + evaluateCondition({ + condition: { + StringEquals: { 'soat:tag:env': 'prod' }, + StringNotEquals: { 'soat:tag:region': 'us-east-1' }, + }, + context: { 'soat:tag:env': 'prod', 'soat:tag:region': 'eu-west-1' }, + }) + ).toBe(true); + + expect( + evaluateCondition({ + condition: { + StringEquals: { 'soat:tag:env': 'prod' }, + StringNotEquals: { 'soat:tag:region': 'us-east-1' }, + }, + context: { 'soat:tag:env': 'prod', 'soat:tag:region': 'us-east-1' }, + }) + ).toBe(false); + }); + }); + + describe('statementMatches', () => { + const baseStatement = { + effect: 'Allow' as const, + action: ['files:GetFile'], + resource: ['soat:proj_ABC:file:*'], + }; + + test('matches when action and resource match', () => { + expect( + statementMatches({ + statement: baseStatement, + action: 'files:GetFile', + resource: 'soat:proj_ABC:file:file_123', + context: {}, + }) + ).toBe(true); + }); + + test('does not match wrong action', () => { + expect( + statementMatches({ + statement: baseStatement, + action: 'files:DeleteFile', + resource: 'soat:proj_ABC:file:file_123', + context: {}, + }) + ).toBe(false); + }); + + test('does not match wrong resource', () => { + expect( + statementMatches({ + statement: baseStatement, + action: 'files:GetFile', + resource: 'soat:proj_XYZ:file:file_123', + context: {}, + }) + ).toBe(false); + }); + + test('statement without resource matches any resource', () => { + const stmt = { effect: 'Allow' as const, action: ['files:GetFile'] }; + expect( + statementMatches({ + statement: stmt, + action: 'files:GetFile', + resource: 'soat:proj_ABC:file:file_123', + context: {}, + }) + ).toBe(true); + }); + + test('does not match when condition fails', () => { + const stmt = { + ...baseStatement, + condition: { StringEquals: { 'soat:tag:env': 'prod' } }, + }; + expect( + statementMatches({ + statement: stmt, + action: 'files:GetFile', + resource: 'soat:proj_ABC:file:file_123', + context: { 'soat:tag:env': 'dev' }, + }) + ).toBe(false); + }); + }); + + describe('evaluatePolicies', () => { + const allowPolicy: PolicyDocument = { + statement: [{ effect: 'Allow', action: ['files:GetFile'] }], + }; + + const denyPolicy: PolicyDocument = { + statement: [{ effect: 'Deny', action: ['files:GetFile'] }], + }; + + test('returns false when no policies', () => { + expect(evaluatePolicies({ policies: [], action: 'files:GetFile' })).toBe( + false + ); + }); + + test('returns false when no matching statements', () => { + expect( + evaluatePolicies({ + policies: [allowPolicy], + action: 'files:DeleteFile', + }) + ).toBe(false); + }); + + test('returns true when Allow matches', () => { + expect( + evaluatePolicies({ policies: [allowPolicy], action: 'files:GetFile' }) + ).toBe(true); + }); + + test('explicit Deny overrides Allow (Deny wins)', () => { + expect( + evaluatePolicies({ + policies: [allowPolicy, denyPolicy], + action: 'files:GetFile', + }) + ).toBe(false); + }); + + test('Deny short-circuits even if Allow comes first', () => { + expect( + evaluatePolicies({ + policies: [denyPolicy, allowPolicy], + action: 'files:GetFile', + }) + ).toBe(false); + }); + + test('resource filtering works', () => { + const scopedPolicy: PolicyDocument = { + statement: [ + { + effect: 'Allow', + action: ['files:GetFile'], + resource: ['soat:proj_A:file:*'], + }, + ], + }; + expect( + evaluatePolicies({ + policies: [scopedPolicy], + action: 'files:GetFile', + resource: 'soat:proj_A:file:file_123', + }) + ).toBe(true); + expect( + evaluatePolicies({ + policies: [scopedPolicy], + action: 'files:GetFile', + resource: 'soat:proj_B:file:file_123', + }) + ).toBe(false); + }); + + test('condition filtering works', () => { + const condPolicy: PolicyDocument = { + statement: [ + { + effect: 'Allow', + action: ['files:GetFile'], + condition: { StringEquals: { 'soat:tag:env': 'prod' } }, + }, + ], + }; + expect( + evaluatePolicies({ + policies: [condPolicy], + action: 'files:GetFile', + context: { 'soat:tag:env': 'prod' }, + }) + ).toBe(true); + expect( + evaluatePolicies({ + policies: [condPolicy], + action: 'files:GetFile', + context: { 'soat:tag:env': 'dev' }, + }) + ).toBe(false); + }); + }); +}); diff --git a/packages/server/tests/unit/tests/permissionsFlow.test.ts b/packages/server/tests/unit/tests/permissionsFlow.test.ts index 0c8023a9..54ecc723 100644 --- a/packages/server/tests/unit/tests/permissionsFlow.test.ts +++ b/packages/server/tests/unit/tests/permissionsFlow.test.ts @@ -8,10 +8,10 @@ import { authenticatedTestClient, loginAs, testClient } from '../testClient'; * 3. admin assigns the user to the project with a read-only policy * 4. user can read a file using JWT * 5. user cannot delete that file using JWT - * 6. user creates an API key scoped to the project - * 7. user assigns the deleteFile action to the API key - * 8. user can read the file using the API key - * 9. user cannot delete the file using the API key (membership policy intersection) + * 6. user creates an project key scoped to the project + * 7. user assigns the deleteFile action to the project key + * 8. user can read the file using the project key + * 9. user cannot delete the file using the project key (membership policy intersection) */ // ─── Group 1: Setup (Steps 1-3) ────────────────────────────────────────── @@ -174,14 +174,14 @@ describe('Group 2: JWT Permissions - User can read but not delete file', () => { }); }); -// ─── Group 3: API Key Permissions (Steps 6-9) ───────────────────────────── +// ─── Group 3: project key Permissions (Steps 6-9) ───────────────────────────── -describe('Group 3: API Key Permissions - Create key, assign permissions, test access', () => { +describe('Group 3: project key Permissions - Create key, assign permissions, test access', () => { let adminToken: string; let projectId: string; let userId: string; let userToken: string; - let apiKey: string; + let projectKey: string; let fileId: string; beforeAll(async () => { @@ -220,7 +220,7 @@ describe('Group 3: API Key Permissions - Create key, assign permissions, test ac policyId, }); - // Create API key with delete permission + // Create project key with delete permission const newPolicyResponse = await authenticatedTestClient(adminToken) .post(`/api/v1/projects/${projectId}/policies`) .send({ @@ -228,14 +228,14 @@ describe('Group 3: API Key Permissions - Create key, assign permissions, test ac }); const newPolicyId = newPolicyResponse.body.id; - const apiKeyResponse = await authenticatedTestClient(userToken) - .post('/api/v1/api-keys') + const projectKeyResponse = await authenticatedTestClient(userToken) + .post('/api/v1/project-keys') .send({ projectId, policyId: newPolicyId, - name: 'Test API Key', + name: 'Test project key', }); - apiKey = apiKeyResponse.body.key; + projectKey = projectKeyResponse.body.key; // Create file const fileResponse = await authenticatedTestClient(adminToken) @@ -249,7 +249,7 @@ describe('Group 3: API Key Permissions - Create key, assign permissions, test ac fileId = fileResponse.body.id; }); - test('user can create an API key for the project', async () => { + test('user can create an project key for the project', async () => { // First get the policy ID const policiesResponse = await authenticatedTestClient(userToken).get( `/api/v1/projects/${projectId}/policies` @@ -257,34 +257,34 @@ describe('Group 3: API Key Permissions - Create key, assign permissions, test ac const policyId = policiesResponse.body[0].id; const response = await authenticatedTestClient(userToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId, - name: 'Test API Key', + name: 'Test project key', }); expect(response.status).toBe(201); expect(response.body.id).toBeDefined(); - expect(response.body.name).toBe('Test API Key'); + expect(response.body.name).toBe('Test project key'); expect(response.body.key).toMatch(/^sk_/); expect(response.body.keyPrefix).toBe(response.body.key.slice(0, 8)); }); - test('user can read file using the API key', async () => { + test('user can read file using the project key', async () => { const response = await testClient .get(`/api/v1/files/${fileId}`) - .set('Authorization', `Bearer ${apiKey}`); + .set('Authorization', `Bearer ${projectKey}`); expect(response.status).toBe(200); expect(response.body.id).toBe(fileId); expect(response.body.filename).toBe('test.txt'); }); - test('user cannot delete file using the API key due to policy intersection', async () => { + test('user cannot delete file using the project key due to policy intersection', async () => { const response = await testClient .delete(`/api/v1/files/${fileId}`) - .set('Authorization', `Bearer ${apiKey}`); + .set('Authorization', `Bearer ${projectKey}`); expect(response.status).toBe(403); expect(response.body.error).toBe('Forbidden'); @@ -389,14 +389,14 @@ describe('Group 4: Two users in the same project with different policies', () => }); }); -// ─── Group 5: User with Multiple API Keys ───────────────────────────────── +// ─── Group 5: User with Multiple project keys ───────────────────────────────── -describe('Group 5: User with multiple API keys scoped to different permissions', () => { +describe('Group 5: User with multiple project keys scoped to different permissions', () => { let adminToken: string; let projectId: string; let userToken: string; - let readOnlyApiKey: string; - let deleteOnlyApiKey: string; + let readOnlyProjectKey: string; + let deleteOnlyProjectKey: string; let fileId: string; beforeAll(async () => { @@ -408,7 +408,7 @@ describe('Group 5: User with multiple API keys scoped to different permissions', const projectResponse = await authenticatedTestClient(adminToken) .post('/api/v1/projects') - .send({ name: 'Multiple API Keys Project' }); + .send({ name: 'Multiple project keys Project' }); projectId = projectResponse.body.id; const userResponse = await authenticatedTestClient(adminToken) @@ -429,7 +429,7 @@ describe('Group 5: User with multiple API keys scoped to different permissions', .post(`/api/v1/projects/${projectId}/members`) .send({ userId: userResponse.body.id, policyId: memberPolicyId }); - // API key policy 1: read-only + // project key policy 1: read-only const readKeyPolicyResponse = await authenticatedTestClient(adminToken) .post(`/api/v1/projects/${projectId}/policies`) .send({ @@ -438,7 +438,7 @@ describe('Group 5: User with multiple API keys scoped to different permissions', }); const readKeyPolicyId = readKeyPolicyResponse.body.id; - // API key policy 2: delete-only + // project key policy 2: delete-only const deleteKeyPolicyResponse = await authenticatedTestClient(adminToken) .post(`/api/v1/projects/${projectId}/policies`) .send({ @@ -448,14 +448,14 @@ describe('Group 5: User with multiple API keys scoped to different permissions', const deleteKeyPolicyId = deleteKeyPolicyResponse.body.id; const readKeyResponse = await authenticatedTestClient(userToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId: readKeyPolicyId, name: 'Read Key' }); - readOnlyApiKey = readKeyResponse.body.key; + readOnlyProjectKey = readKeyResponse.body.key; const deleteKeyResponse = await authenticatedTestClient(userToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId: deleteKeyPolicyId, name: 'Delete Key' }); - deleteOnlyApiKey = deleteKeyResponse.body.key; + deleteOnlyProjectKey = deleteKeyResponse.body.key; const fileResponse = await authenticatedTestClient(adminToken) .post('/api/v1/files') @@ -468,46 +468,46 @@ describe('Group 5: User with multiple API keys scoped to different permissions', fileId = fileResponse.body.id; }); - test('read-only API key can read the file', async () => { + test('read-only project key can read the file', async () => { const response = await testClient .get(`/api/v1/files/${fileId}`) - .set('Authorization', `Bearer ${readOnlyApiKey}`); + .set('Authorization', `Bearer ${readOnlyProjectKey}`); expect(response.status).toBe(200); expect(response.body.id).toBe(fileId); }); - test('read-only API key cannot delete the file', async () => { + test('read-only project key cannot delete the file', async () => { const response = await testClient .delete(`/api/v1/files/${fileId}`) - .set('Authorization', `Bearer ${readOnlyApiKey}`); + .set('Authorization', `Bearer ${readOnlyProjectKey}`); expect(response.status).toBe(403); expect(response.body.error).toBe('Forbidden'); }); - test('delete-only API key cannot read the file', async () => { + test('delete-only project key cannot read the file', async () => { const response = await testClient .get(`/api/v1/files/${fileId}`) - .set('Authorization', `Bearer ${deleteOnlyApiKey}`); + .set('Authorization', `Bearer ${deleteOnlyProjectKey}`); expect(response.status).toBe(403); expect(response.body.error).toBe('Forbidden'); }); - test('delete-only API key can delete the file', async () => { + test('delete-only project key can delete the file', async () => { const response = await testClient .delete(`/api/v1/files/${fileId}`) - .set('Authorization', `Bearer ${deleteOnlyApiKey}`); + .set('Authorization', `Bearer ${deleteOnlyProjectKey}`); expect(response.status).toBe(204); }); }); -// ─── Group 6: API Key Project Isolation ────────────────────────────────── +// ─── Group 6: project key Project Isolation ────────────────────────────────── -describe('Group 6: API key cannot access files in a different project', () => { +describe('Group 6: project key cannot access files in a different project', () => { let adminToken: string; let projectAId: string; let projectBId: string; let userToken: string; - let apiKey: string; + let projectKey: string; let fileInProjectA: string; let fileInProjectB: string; @@ -545,10 +545,10 @@ describe('Group 6: API key cannot access files in a different project', () => { .post(`/api/v1/projects/${projectAId}/members`) .send({ userId: userResponse.body.id, policyId }); - const apiKeyResponse = await authenticatedTestClient(userToken) - .post('/api/v1/api-keys') + const projectKeyResponse = await authenticatedTestClient(userToken) + .post('/api/v1/project-keys') .send({ projectId: projectAId, policyId, name: 'Project A Key' }); - apiKey = apiKeyResponse.body.key; + projectKey = projectKeyResponse.body.key; const fileAResponse = await authenticatedTestClient(adminToken) .post('/api/v1/files') @@ -571,18 +571,18 @@ describe('Group 6: API key cannot access files in a different project', () => { fileInProjectB = fileBResponse.body.id; }); - test('API key can read a file from its own project', async () => { + test('project key can read a file from its own project', async () => { const response = await testClient .get(`/api/v1/files/${fileInProjectA}`) - .set('Authorization', `Bearer ${apiKey}`); + .set('Authorization', `Bearer ${projectKey}`); expect(response.status).toBe(200); expect(response.body.id).toBe(fileInProjectA); }); - test('API key cannot read a file from a different project', async () => { + test('project key cannot read a file from a different project', async () => { const response = await testClient .get(`/api/v1/files/${fileInProjectB}`) - .set('Authorization', `Bearer ${apiKey}`); + .set('Authorization', `Bearer ${projectKey}`); expect(response.status).toBe(403); expect(response.body.error).toBe('Forbidden'); }); @@ -908,9 +908,9 @@ describe('Group 11: Multiple admins can all manage projects and bypass policy ch }); }); -// ─── Group 12: Multiple Users with Multiple API Keys ────────────────────── +// ─── Group 12: Multiple Users with Multiple project keys ────────────────────── -describe('Group 12: Multiple users each with multiple API keys in the same project', () => { +describe('Group 12: Multiple users each with multiple project keys in the same project', () => { let adminToken: string; let projectId: string; let user1ReadKey: string; @@ -927,7 +927,7 @@ describe('Group 12: Multiple users each with multiple API keys in the same proje const projectResponse = await authenticatedTestClient(adminToken) .post('/api/v1/projects') - .send({ name: 'Multi-User API Keys Project' }); + .send({ name: 'Multi-User project keys Project' }); projectId = projectResponse.body.id; // User1 membership: full permissions @@ -966,7 +966,7 @@ describe('Group 12: Multiple users each with multiple API keys in the same proje .post(`/api/v1/projects/${projectId}/members`) .send({ userId: user2Response.body.id, policyId: readOnlyPolicyId }); - // API key policies + // project key policies const readKeyPolicyResponse = await authenticatedTestClient(adminToken) .post(`/api/v1/projects/${projectId}/policies`) .send({ @@ -983,14 +983,14 @@ describe('Group 12: Multiple users each with multiple API keys in the same proje }); const deleteKeyPolicyId = deleteKeyPolicyResponse.body.id; - // User1 creates two API keys: one read-only, one full + // User1 creates two project keys: one read-only, one full const u1ReadKeyResponse = await authenticatedTestClient(user1Token) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId: readKeyPolicyId, name: 'Oscar Read Key' }); user1ReadKey = u1ReadKeyResponse.body.key; const u1DeleteKeyResponse = await authenticatedTestClient(user1Token) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId: deleteKeyPolicyId, @@ -1001,7 +1001,7 @@ describe('Group 12: Multiple users each with multiple API keys in the same proje // User2 creates a key with full key policy — but membership is read-only, // so the effective permission (intersection) is still read-only const u2KeyResponse = await authenticatedTestClient(user2Token) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId: deleteKeyPolicyId, diff --git a/packages/server/tests/unit/tests/apiKeys.test.ts b/packages/server/tests/unit/tests/projectKeys.test.ts similarity index 79% rename from packages/server/tests/unit/tests/apiKeys.test.ts rename to packages/server/tests/unit/tests/projectKeys.test.ts index 81e1f42e..6b78dea0 100644 --- a/packages/server/tests/unit/tests/apiKeys.test.ts +++ b/packages/server/tests/unit/tests/projectKeys.test.ts @@ -1,6 +1,6 @@ import { authenticatedTestClient, loginAs, testClient } from '../testClient'; -describe('API Keys', () => { +describe('Project Keys', () => { let adminToken: string; let aliceToken: string; let aliceId: string; @@ -41,10 +41,10 @@ describe('API Keys', () => { .send({ userId: aliceId, policyId }); }); - describe('POST /api/v1/api-keys', () => { + describe('POST /api/v1/project-keys', () => { test('returns 401 if not authenticated', async () => { const response = await testClient - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId, name: 'My Key' }); expect(response.status).toBe(401); @@ -52,7 +52,7 @@ describe('API Keys', () => { test('returns 400 if required fields are missing', async () => { const response = await authenticatedTestClient(aliceToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ name: 'My Key' }); expect(response.status).toBe(400); @@ -60,7 +60,7 @@ describe('API Keys', () => { test('returns 400 if project does not exist', async () => { const response = await authenticatedTestClient(aliceToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId: 'proj_nonexistent', policyId, name: 'My Key' }); expect(response.status).toBe(400); @@ -68,7 +68,7 @@ describe('API Keys', () => { test('returns 403 if user is not a member of the project', async () => { const response = await authenticatedTestClient(bobToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId, name: 'Bob Key' }); expect(response.status).toBe(403); @@ -86,7 +86,7 @@ describe('API Keys', () => { const otherPolicyId = otherPolicyRes.body.id; const response = await authenticatedTestClient(aliceToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId: otherPolicyId, name: 'My Key' }); expect(response.status).toBe(400); @@ -94,7 +94,7 @@ describe('API Keys', () => { test('returns 201 and the full key on success', async () => { const response = await authenticatedTestClient(aliceToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId, name: 'Alice Key' }); expect(response.status).toBe(201); @@ -107,45 +107,47 @@ describe('API Keys', () => { }); }); - describe('GET /api/v1/api-keys/:id', () => { - let apiKeyId: string; + describe('GET /api/v1/project-keys/:id', () => { + let projectKeyId: string; beforeAll(async () => { const res = await authenticatedTestClient(aliceToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId, name: 'Get Test Key' }); - apiKeyId = res.body.id; + projectKeyId = res.body.id; }); test('returns 401 if not authenticated', async () => { - const response = await testClient.get(`/api/v1/api-keys/${apiKeyId}`); + const response = await testClient.get( + `/api/v1/project-keys/${projectKeyId}` + ); expect(response.status).toBe(401); }); - test('returns 404 if api key does not exist', async () => { + test('returns 404 if project key does not exist', async () => { const response = await authenticatedTestClient(aliceToken).get( - '/api/v1/api-keys/key_nonexistent' + '/api/v1/project-keys/key_nonexistent' ); expect(response.status).toBe(404); }); - test('returns 403 if user does not own the api key', async () => { + test('returns 403 if user does not own the project key', async () => { const response = await authenticatedTestClient(bobToken).get( - `/api/v1/api-keys/${apiKeyId}` + `/api/v1/project-keys/${projectKeyId}` ); expect(response.status).toBe(403); }); - test('returns 200 and api key data without the full key', async () => { + test('returns 200 and project key data without the full key', async () => { const response = await authenticatedTestClient(aliceToken).get( - `/api/v1/api-keys/${apiKeyId}` + `/api/v1/project-keys/${projectKeyId}` ); expect(response.status).toBe(200); - expect(response.body.id).toBe(apiKeyId); + expect(response.body.id).toBe(projectKeyId); expect(response.body.name).toBe('Get Test Key'); expect(response.body.keyPrefix).toBeDefined(); expect(response.body.key).toBeUndefined(); @@ -154,15 +156,15 @@ describe('API Keys', () => { }); }); - describe('PUT /api/v1/api-keys/:id', () => { - let apiKeyId: string; + describe('PUT /api/v1/project-keys/:id', () => { + let projectKeyId: string; let newPolicyId: string; beforeAll(async () => { const keyRes = await authenticatedTestClient(aliceToken) - .post('/api/v1/api-keys') + .post('/api/v1/project-keys') .send({ projectId, policyId, name: 'Update Test Key' }); - apiKeyId = keyRes.body.id; + projectKeyId = keyRes.body.id; const newPolicyRes = await authenticatedTestClient(adminToken) .post(`/api/v1/projects/${projectId}/policies`) @@ -172,7 +174,7 @@ describe('API Keys', () => { test('returns 401 if not authenticated', async () => { const response = await testClient - .put(`/api/v1/api-keys/${apiKeyId}`) + .put(`/api/v1/project-keys/${projectKeyId}`) .send({ policyId: newPolicyId }); expect(response.status).toBe(401); @@ -180,7 +182,7 @@ describe('API Keys', () => { test('returns 400 if policyId is missing', async () => { const response = await authenticatedTestClient(aliceToken) - .put(`/api/v1/api-keys/${apiKeyId}`) + .put(`/api/v1/project-keys/${projectKeyId}`) .send({}); expect(response.status).toBe(400); @@ -188,23 +190,23 @@ describe('API Keys', () => { test('returns 400 if policy does not exist', async () => { const response = await authenticatedTestClient(aliceToken) - .put(`/api/v1/api-keys/${apiKeyId}`) + .put(`/api/v1/project-keys/${projectKeyId}`) .send({ policyId: 'policy_nonexistent' }); expect(response.status).toBe(400); }); - test('returns 404 if api key does not exist', async () => { + test('returns 404 if project key does not exist', async () => { const response = await authenticatedTestClient(aliceToken) - .put('/api/v1/api-keys/key_nonexistent') + .put('/api/v1/project-keys/key_nonexistent') .send({ policyId: newPolicyId }); expect(response.status).toBe(404); }); - test('returns 403 if user does not own the api key', async () => { + test('returns 403 if user does not own the project key', async () => { const response = await authenticatedTestClient(bobToken) - .put(`/api/v1/api-keys/${apiKeyId}`) + .put(`/api/v1/project-keys/${projectKeyId}`) .send({ policyId: newPolicyId }); expect(response.status).toBe(403); @@ -222,19 +224,19 @@ describe('API Keys', () => { const otherPolicyId = otherPolicyRes.body.id; const response = await authenticatedTestClient(aliceToken) - .put(`/api/v1/api-keys/${apiKeyId}`) + .put(`/api/v1/project-keys/${projectKeyId}`) .send({ policyId: otherPolicyId }); expect(response.status).toBe(400); }); - test('returns 200 and the updated api key', async () => { + test('returns 200 and the updated project key', async () => { const response = await authenticatedTestClient(aliceToken) - .put(`/api/v1/api-keys/${apiKeyId}`) + .put(`/api/v1/project-keys/${projectKeyId}`) .send({ policyId: newPolicyId }); expect(response.status).toBe(200); - expect(response.body.id).toBe(apiKeyId); + expect(response.body.id).toBe(projectKeyId); expect(response.body.policyId).toBe(newPolicyId); }); }); diff --git a/packages/server/tests/unit/tests/projects.test.ts b/packages/server/tests/unit/tests/projects.test.ts index d855320b..1885a313 100644 --- a/packages/server/tests/unit/tests/projects.test.ts +++ b/packages/server/tests/unit/tests/projects.test.ts @@ -96,19 +96,19 @@ describe('Projects', () => { ).toBe(true); expect(response.body.length).toBe(1); }); - describe('api key only sees its scoped project', () => { + describe('project key only sees its scoped project', () => { let projectAId: string; - let rawApiKey: string; + let rawProjectKey: string; beforeAll(async () => { const projARes = await authenticatedTestClient(adminToken) .post('/api/v1/projects') - .send({ name: 'API Key Project A' }); + .send({ name: 'project key Project A' }); projectAId = projARes.body.id; const projBRes = await authenticatedTestClient(adminToken) .post('/api/v1/projects') - .send({ name: 'API Key Project B' }); + .send({ name: 'project key Project B' }); const projectBId = projBRes.body.id; const policyARes = await authenticatedTestClient(adminToken) @@ -129,19 +129,19 @@ describe('Projects', () => { .post(`/api/v1/projects/${projectBId}/members`) .send({ userId, policyId: policyBId }); - const apiKeyRes = await authenticatedTestClient(userToken) - .post('/api/v1/api-keys') + const projectKeyRes = await authenticatedTestClient(userToken) + .post('/api/v1/project-keys') .send({ projectId: projectAId, policyId: policyAId, name: 'Scoped Key', }); - rawApiKey = apiKeyRes.body.key; + rawProjectKey = projectKeyRes.body.key; }); - test('api key user only sees the scoped project', async () => { + test('project key user only sees the scoped project', async () => { const response = - await authenticatedTestClient(rawApiKey).get('/api/v1/projects'); + await authenticatedTestClient(rawProjectKey).get('/api/v1/projects'); expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); @@ -433,7 +433,7 @@ describe('Projects', () => { }); describe('cascade deletion when a project is deleted', () => { - test('deleting a project removes its policies, memberships, and api keys', async () => { + test('deleting a project removes its policies, memberships, and project keys', async () => { const projRes = await authenticatedTestClient(adminToken) .post('/api/v1/projects') .send({ name: 'Cascade Test Project' }); @@ -451,11 +451,11 @@ describe('Projects', () => { .send({ userId, policyId }); expect(memberRes.status).toBe(201); - const apiKeyRes = await authenticatedTestClient(userToken) - .post('/api/v1/api-keys') + const projectKeyRes = await authenticatedTestClient(userToken) + .post('/api/v1/project-keys') .send({ projectId, policyId, name: 'Cascade Test Key' }); - expect(apiKeyRes.status).toBe(201); - const apiKeyId = apiKeyRes.body.id; + expect(projectKeyRes.status).toBe(201); + const projectKeyId = projectKeyRes.body.id; const deleteRes = await authenticatedTestClient(adminToken).delete( `/api/v1/projects/${projectId}` @@ -476,11 +476,11 @@ describe('Projects', () => { }); expect(projectIds).not.toContain(projectId); - // ApiKey cascade-deleted: key no longer found - const apiKeyGetRes = await authenticatedTestClient(userToken).get( - `/api/v1/api-keys/${apiKeyId}` + // ProjectKey cascade-deleted: key no longer found + const projectKeyGetRes = await authenticatedTestClient(userToken).get( + `/api/v1/project-keys/${projectKeyId}` ); - expect(apiKeyGetRes.status).toBe(404); + expect(projectKeyGetRes.status).toBe(404); }); }); }); diff --git a/packages/website/docs/modules/documents.md b/packages/website/docs/modules/documents.md index 102b7c77..98651870 100644 --- a/packages/website/docs/modules/documents.md +++ b/packages/website/docs/modules/documents.md @@ -42,8 +42,8 @@ OLLAMA_BASE_URL=http://localhost:11434 | Field | Type | Description | | ----------- | ------ | ------------------------------------------------------------- | | `id` | string | Public identifier prefixed with `doc_` | -| `fileId` | string | ID of the underlying File record | -| `projectId` | string | ID of the owning project | +| `fileId` | string | ID of the underlying File record | +| `projectId` | string | ID of the owning project | | `filename` | string | Original filename (`.txt` extension) | | `size` | number | File size in bytes | | `content` | string | Text content — only present in `GET /documents/:id` responses | @@ -62,6 +62,7 @@ Document operations are governed by per-project policies. Grant the following pe | Get a document | `documents:GetDocument` | `GET /api/v1/documents/:id` | `get-document` | | Create a document | `documents:CreateDocument` | `POST /api/v1/documents` | `create-document` | | Delete a document | `documents:DeleteDocument` | `DELETE /api/v1/documents/:id` | `delete-document` | +| Update a document | `documents:UpdateDocument` | `PATCH /api/v1/documents/:id` | `update-document` | | Semantic search | `documents:SearchDocuments` | `POST /api/v1/documents/search` | `search-documents` | ## Operations @@ -109,6 +110,23 @@ DELETE /api/v1/documents/doc_xxx Authorization: Bearer ``` +### Update a document + +Updates text content (and regenerates the embedding), title, metadata, or tags. All fields are optional — supply at least one: + +```http +PATCH /api/v1/documents/doc_xxx +Authorization: Bearer +Content-Type: application/json + +{ + "content": "Updated document text.", + "title": "New title", + "metadata": { "author": "Alice" }, + "tags": { "env": "production" } +} +``` + ### Semantic search Embeds the query and returns the closest documents by cosine distance. `projectId` is optional — omit it to search across all accessible projects: @@ -136,7 +154,7 @@ For endpoints that accept `projectId`, the field is optional. When omitted, the | Caller type | Behavior when `projectId` is omitted | | ----------- | ---------------------------------------------------------------------------- | -| API key | Infers the project from the key's own scope (single project) | +| project key | Infers the project from the key's own scope (single project) | | JWT admin | No project filter — returns results across all projects | | JWT user | Enumerates all projects the user is a member of with the required permission | @@ -152,4 +170,5 @@ The following MCP tools are available for AI assistants: | `get-document` | Retrieve a document including its text content | | `create-document` | Create a new text document with automatic embedding | | `delete-document` | Delete a document and its underlying file | +| `update-document` | Update document content, title, metadata, or tags | | `search-documents` | Semantic search; omit `projectId` to search across all accessible projects | diff --git a/packages/website/docs/modules/files.md b/packages/website/docs/modules/files.md index e0e4eb33..8fa2fe3a 100644 --- a/packages/website/docs/modules/files.md +++ b/packages/website/docs/modules/files.md @@ -48,7 +48,7 @@ File operations are governed by per-project policies. Grant the following permis | Action | Permission | REST Endpoint | MCP Tool | | ----------------------------- | -------------------------- | ---------------------------------- | ---------------------- | -| List files | `files:ListFiles` | `GET /api/v1/files` | `list-files` | +| List files | `files:GetFile` | `GET /api/v1/files` | `list-files` | | Get file metadata | `files:GetFile` | `GET /api/v1/files/:id` | `get-file` | | Create a metadata-only record | `files:CreateFile` | `POST /api/v1/files` | `create-file` | | Upload a file | `files:UploadFile` | `POST /api/v1/files/upload` | `upload-file` | diff --git a/packages/website/docs/modules/iam.md b/packages/website/docs/modules/iam.md index 44482677..5c9b1987 100644 --- a/packages/website/docs/modules/iam.md +++ b/packages/website/docs/modules/iam.md @@ -1,113 +1,396 @@ -# IAM Module +--- +sidebar_position: 1 +--- -The IAM (Identity and Access Management) module controls who can access SOAT and what they are allowed to do. It provides user authentication, project-based organization, fine-grained permission policies, and API keys for programmatic access. +# IAM + +The IAM (Identity and Access Management) module provides authentication, identity management, and fine-grained authorization for the SOAT platform. It implements an AWS IAM-inspired policy engine with structured policy statements supporting `Effect`, `Action`, `Resource`, and `Condition`. ## Overview -Every resource in SOAT (files, projects, and more) is protected by IAM. Before any action is performed, IAM verifies: +SOAT uses a policy-based access control model. Every API request is authenticated via JWT (for users) or project key (for project-scoped clients). Authorization is evaluated by loading the caller's attached policy documents and running them through the policy engine. + +The IAM module covers: -1. **Who are you?** — Authentication via a login session (JWT) or an API key. -2. **Are you allowed to do that?** — Authorization via policies attached to your project membership. +- **Users** — identity management, roles, and JWT authentication (see [Users](#users) below) +- **Projects** — multi-tenant namespaces with membership and policy attachment (see [Projects](projects.md)) +- **Project Keys** — scoped project keys for programmatic access (see [Project Keys](projects.md#project-keys)) +- **Policy Documents** — structured permission rules attached to memberships and project keys +- **Policy Engine** — evaluation logic that resolves allow/deny decisions at request time ## Authentication -### Login (JWT) +SOAT supports two authentication methods. Both use the `Authorization: Bearer ` header. + +### JWT (Users) + +Users authenticate via `POST /api/v1/users/login` with username and password. The server returns a signed JWT containing the user's public ID and role. Admin users bypass policy evaluation and have unrestricted access. Regular users are authorized through their project membership policies. + +### Project Keys + +Project keys are prefixed with `pk_` and scoped to a single project. When an project key is used, authorization applies **intersection semantics**: both the owning user's membership policies _and_ the key's own attached policy must independently allow the action. This ensures project keys can never exceed the permissions of the user who created them. + +## Policy Documents -Human users authenticate by logging in with their credentials. SOAT returns a session token that you include in every subsequent request. +A policy document is a JSON object containing one or more statements. Each statement describes a permission rule. +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["documents:GetDocument", "documents:ListDocuments"], + "resource": ["soat:proj_ABC:document:doc_XYZ"] + }, + { + "effect": "Deny", + "action": ["secrets:*"], + "resource": ["soat:proj_ABC:secret:sec_PROD_KEY"] + } + ] +} ``` -POST /api/v1/users/login + +### Statement + +| Field | Type | Required | Description | +| ----------- | ---------- | -------- | ------------------------------------------------------- | +| `effect` | `string` | Yes | `"Allow"` or `"Deny"` | +| `action` | `string[]` | Yes | Actions this statement applies to (supports wildcards) | +| `resource` | `string[]` | No | SRNs this statement applies to (default: `["*"]`) | +| `condition` | `object` | No | Conditions that must be true for the statement to apply | + +Policy documents are created and managed under a project via the project policy endpoints (see [Projects](projects.md)). + +## SOAT Resource Names (SRNs) + +Every addressable entity has a canonical identifier called a SOAT Resource Name: + +``` +soat::: +``` + +Examples: + +| SRN | Description | +| -------------------------------- | -------------------------- | +| `soat:proj_ABC:document:doc_XYZ` | A specific document | +| `soat:proj_ABC:document:*` | All documents in a project | +| `soat:proj_ABC:file:*` | All files in a project | +| `soat:proj_ABC:actor:act_123` | A specific actor | +| `soat:*:*:*` | Everything (admin-level) | + +### Resource Types + +| Resource Type | Public ID Prefix | Module | +| -------------- | ---------------- | ------------- | +| `document` | `doc_` | Documents | +| `file` | `file_` | Files | +| `actor` | `act_` | Actors | +| `conversation` | `conv_` | Conversations | +| `project` | `proj_` | Projects | +| `policy` | `pol_` | Policies | +| `api-key` | `key_` | project keys | + +## Actions + +Actions follow the `module:Operation` pattern. Each module defines its own set of actions documented in the **Permissions** section of the respective module page: + +- [Actors Permissions](actors.md#permissions) +- [Conversations Permissions](conversations.md#permissions) +- [Documents Permissions](documents.md#permissions) +- [Files Permissions](files.md#permissions) +- [Projects Permissions](projects.md#permissions) +- [Project Keys Permissions](projects.md#project-key-permissions) +- [Users Permissions](#user-permissions) + +### Wildcards + +- `*` — matches all actions across all modules +- `module:*` — matches all actions in a specific module (e.g., `documents:*`) + +## Conditions + +Conditions add attribute-based constraints to statements. A condition block maps an operator to one or more key-value pairs that must all evaluate to true. + +```json +{ + "condition": { + "StringEquals": { + "soat:ResourceTag/environment": "production" + }, + "StringLike": { + "soat:ResourceTag/team": "engineering-*" + } + } +} ``` +### Condition Operators + +| Operator | Description | +| ----------------- | ----------------------------- | +| `StringEquals` | Exact string match | +| `StringNotEquals` | Negated exact match | +| `StringLike` | Glob pattern match (`*`, `?`) | + +### Condition Keys + +| Key | Source | Description | +| ------------------------ | ------------- | --------------------------------------- | +| `soat:ResourceTag/` | Resource tags | Tag value on the target resource | +| `soat:ResourceType` | Request | The type of the resource being accessed | + +## Policy Evaluation + +Policy evaluation follows AWS IAM semantics: + +1. **Default deny** — if no statement matches, access is denied. +2. **Explicit deny wins** — if any statement explicitly denies, access is denied regardless of allows. +3. **Allow** — if at least one statement allows and no statement denies, access is granted. + +### Statement Matching + +A statement matches a request when **all** of the following are true: + +1. At least one pattern in `action` matches the requested action. +2. At least one pattern in `resource` matches the target SRN (or `resource` is omitted / `["*"]`). +3. All `condition` blocks evaluate to true (or `condition` is omitted). + +### Pattern Matching + +- `*` matches everything. +- `module:*` matches all actions in a module. +- `soat:proj_ABC:document:*` matches all documents in a project. +- Wildcards apply only at segment boundaries — partial wildcards like `doc_X*` are not supported. + +## Tags + +Tags are key-value pairs attached to resources. They enable attribute-based access control (ABAC) via conditions. Taggable resources include documents, files, actors, and conversations. + ```json { - "email": "you@example.com", - "password": "your-password" + "tags": { + "environment": "production", + "team": "engineering", + "sensitivity": "high" + } } ``` -Include the token in the `Authorization` header: +Tags are managed via each resource's create/update endpoints using the `tags` field, or through dedicated tag sub-endpoints: ``` -Authorization: Bearer +PUT /api/v1//:id/tags Replace all tags +PATCH /api/v1//:id/tags Merge tags +GET /api/v1//:id/tags Get tags ``` -Session tokens expire after 7 days. +## Roles + +| Role | Description | +| ------- | --------------------------------------------------------------------------- | +| `admin` | Full access to all resources and operations. Bypasses policy evaluation. | +| `user` | Access determined by project membership policies. Must be a project member. | -### API Keys +--- -For scripts, integrations, or any machine-to-machine access, use an API key instead of a login token. API keys are long-lived and can be scoped to a specific set of permissions. +## Users -Create a key via `POST /api/v1/api-keys`. The full key value is returned **only once** at creation — store it securely. Use it exactly like a session token: +The Users section covers identity management and authentication. Users authenticate via username and password and receive a JWT for subsequent requests. +A User has a username, a hashed password, and a role (`admin` or `user`). Users are identified by an `id` prefixed with `user_`. Passwords are hashed with bcrypt and never returned in API responses. + +The first user is created via the **bootstrap** endpoint, which is only available when no users exist. Subsequent users are created by admins. + +### User Data Model + +| Field | Type | Description | +| ----------- | ------ | --------------------------------------- | +| `id` | string | Public identifier prefixed with `user_` | +| `username` | string | Unique username | +| `role` | string | `"admin"` or `"user"` | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +Sensitive fields (`passwordHash`, internal numeric ID) are never exposed in responses. + +### Bootstrap + +The `POST /api/v1/users/bootstrap` endpoint creates the first admin user. It is only available when the user table is empty and returns `409 Conflict` if any user already exists. This endpoint does not require authentication. + +### User Authentication + +Users authenticate with `POST /api/v1/users/login`, providing `username` and `password`. On success, the server returns a signed JWT containing the user's public ID and role. The token is passed as `Authorization: Bearer ` on subsequent requests. + +### User Permissions + +User management is restricted to admin users. These operations are not governed by the policy engine — they require the `admin` role directly. + +| Action | Permission | REST Endpoint | MCP Tool | +| -------------- | --------------- | ------------------------------ | -------- | +| List users | Admin only | `GET /api/v1/users` | — | +| Get user by ID | Admin only | `GET /api/v1/users/:id` | — | +| Create user | Admin only | `POST /api/v1/users` | — | +| Delete user | Admin only | `DELETE /api/v1/users/:id` | — | +| Bootstrap | Unauthenticated | `POST /api/v1/users/bootstrap` | — | +| Login | Unauthenticated | `POST /api/v1/users/login` | — | + +### User Operations + +#### Bootstrap the First Admin + +```http +POST /api/v1/users/bootstrap +Content-Type: application/json + +{ + "username": "admin", + "password": "supersecret" +} ``` -Authorization: Bearer sk_ + +**Response** `201 Created` + +```json +{ + "id": "user_abc123", + "username": "admin", + "role": "admin", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" +} ``` -An API key's effective permissions are the **intersection** of the key's own policy and the key owner's project membership policy. Both must allow an action for it to be permitted. A key can never grant more access than the owner's membership policy allows, and can be scoped to even less via the key's own policy. +#### Login -## Roles +```http +POST /api/v1/users/login +Content-Type: application/json -| Role | Description | -| ------- | ------------------------------------------------------- | -| `admin` | Full access to all resources and management operations. | -| `user` | Access governed by project membership and policies. | +{ + "username": "admin", + "password": "supersecret" +} +``` -The first user registered via `POST /api/v1/users/bootstrap` becomes the admin. All subsequent users have the `user` role. +**Response** `200 OK` -## Projects +```json +{ + "token": "eyJhbGciOiJIUzI1NiIs..." +} +``` -A project is the top-level organizational unit in SOAT. All resources — files, policies, and API keys — belong to a project. +#### Create a User -- **Admins** can create, manage, and delete any project. -- **Users** can access only the projects they are members of. -- Each project membership carries a **policy** that defines what that member is allowed to do. +```http +POST /api/v1/users +Authorization: Bearer +Content-Type: application/json -## Policies +{ + "username": "alice", + "password": "alicepass", + "role": "user" +} +``` -A policy defines what actions a member (or API key) is allowed or denied within a project. +**Response** `201 Created` ```json { - "name": "read-only", - "projectId": "proj_...", - "permissions": ["files:*"], - "notPermissions": ["files:DeleteFile"] + "id": "user_def456", + "username": "alice", + "role": "user", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" } ``` -| Field | Description | -| ---------------- | -------------------------------------------------------------------------- | -| `permissions` | Actions the policy allows. Supports wildcards (`files:*`, `*`). | -| `notPermissions` | Actions the policy explicitly denies. Denials take precedence over allows. | +## Examples -### Permission Actions +### Full Admin Policy -Permission strings follow the format `resource:Action`. Each module documents its own permission actions — see the individual module pages for the full tables: +Equivalent to unrestricted access: -- [Files](../modules/files.md#permissions) -- [Actors](../modules/actors.md#permissions) -- [Documents](../modules/documents.md#permissions) -- [Conversations](../modules/conversations.md#permissions) +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["*"], + "resource": ["*"] + } + ] +} +``` -Use wildcards to grant broader access: +### Read-only Across All Modules -| Pattern | Effect | -| --------------- | --------------------------- | -| `*` | Allow everything | -| `files:*` | Allow all file operations | -| `files:GetFile` | Allow only retrieving files | +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": [ + "documents:GetDocument", + "documents:ListDocuments", + "documents:SearchDocuments", + "files:GetFile", + "files:DownloadFile", + "actors:ListActors", + "actors:GetActor", + "conversations:ListConversations", + "conversations:GetConversation" + ], + "resource": ["*"] + } + ] +} +``` -## API Keys +### Allow All File Operations Except Delete -Each API key is attached to a policy, which limits the key to a specific set of actions. When a request uses an API key, both the key's policy and the owner's membership policy must allow the action. +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["files:*"], + "resource": ["soat:proj_ABC:file:*"] + }, + { + "effect": "Deny", + "action": ["files:DeleteFile"], + "resource": ["soat:proj_ABC:file:*"] + } + ] +} +``` -| Field | Description | -| ---------- | ---------------------------------------------------------------------------- | -| `name` | A human-readable label for the key | -| `policyId` | The policy that governs this key's permissions | -| `key` | The raw key value — **shown only once at creation, never retrievable again** | +### Condition-based Access -## OAuth +Allow only actors tagged `"internal"`: -> **Coming soon.** OAuth 2.0 support (Authorization Code and Client Credentials flows) is planned for a future release. +```json +{ + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["actors:GetActor"], + "resource": ["soat:proj_ABC:actor:*"], + "condition": { + "StringEquals": { + "soat:ResourceTag/visibility": "internal" + } + } + } + ] +} +``` diff --git a/packages/website/docs/modules/projects.md b/packages/website/docs/modules/projects.md new file mode 100644 index 00000000..8b53aa18 --- /dev/null +++ b/packages/website/docs/modules/projects.md @@ -0,0 +1,281 @@ +# Projects Module + +The Projects module provides multi-tenant namespaces in SOAT. Every resource (document, file, actor, conversation) belongs to a project. Projects also own policy documents, manage user membership, and issue project keys for programmatic access. + +## Overview + +A Project is a top-level container that scopes all resources. Users access projects through membership, and their permissions within a project are determined by attached policy documents. Projects are identified by an `id` prefixed with `proj_`. + +## Data Model + +| Field | Type | Description | +| ----------- | ------ | --------------------------------------- | +| `id` | string | Public identifier prefixed with `proj_` | +| `name` | string | Human-readable project name | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +## Key Concepts + +### Membership + +Users are added to projects as members. Each membership associates the user with one or more policy documents that define what the user can do within that project. A user can be a member of multiple projects, each with different policies. + +### Policy Documents + +Policy documents are scoped to a project and contain structured IAM statements. See [IAM Module](iam.md) for the full policy format, evaluation logic, and examples. + +**Policy data model:** + +| Field | Type | Description | +| ------------- | ------ | -------------------------------------- | +| `id` | string | Public identifier prefixed with `pol_` | +| `name` | string | Human-readable label | +| `description` | string | Optional description | +| `document` | object | Policy document (see [IAM](iam.md)) | +| `projectId` | string | ID of the owning project | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +### Visibility Rules + +- **Admin users** see all projects. +- **project key callers** are restricted to the project the key is scoped to. +- **Regular users** see only projects they are members of. + +## Permissions + +Project CRUD and management operations are restricted to admin users. The `projects:GetProject` action is used by the policy engine for listing and reading policies as a member. + +| Action | Permission | REST Endpoint | MCP Tool | +| ---------------------- | --------------------- | ---------------------------------------------------------- | --------------- | +| List projects | Authenticated | `GET /api/v1/projects` | `list-projects` | +| Get project by ID | Authenticated | `GET /api/v1/projects/:id` | `get-project` | +| Create project | Admin only | `POST /api/v1/projects` | — | +| Delete project | Admin only | `DELETE /api/v1/projects/:id` | — | +| List policies | `projects:GetProject` | `GET /api/v1/projects/:projectId/policies` | — | +| Get policy | `projects:GetProject` | `GET /api/v1/projects/:projectId/policies/:policyId` | — | +| Create policy | Admin only | `POST /api/v1/projects/:projectId/policies` | — | +| Update policy | Admin only | `PUT /api/v1/projects/:projectId/policies/:policyId` | — | +| Delete policy | Admin only | `DELETE /api/v1/projects/:projectId/policies/:policyId` | — | +| Add member | Admin only | `POST /api/v1/projects/:projectId/members` | — | +| Update member policies | Admin only | `PUT /api/v1/projects/:projectId/members/:userId/policies` | — | +| Get member policies | Admin only | `GET /api/v1/projects/:projectId/members/:userId/policies` | — | + +## Operations + +### Create a Project + +```http +POST /api/v1/projects +Authorization: Bearer +Content-Type: application/json + +{ + "name": "My Project" +} +``` + +**Response** `201 Created` + +```json +{ + "id": "proj_abc123", + "name": "My Project", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" +} +``` + +### Create a Policy + +```http +POST /api/v1/projects/proj_abc123/policies +Authorization: Bearer +Content-Type: application/json + +{ + "name": "Read-only Documents", + "description": "Allows reading all documents", + "document": { + "version": "2025-01-01", + "statement": [ + { + "effect": "Allow", + "action": ["documents:GetDocument", "documents:ListDocuments"], + "resource": ["*"] + } + ] + } +} +``` + +**Response** `201 Created` + +```json +{ + "id": "pol_def456", + "name": "Read-only Documents", + "description": "Allows reading all documents", + "document": { "...": "..." }, + "projectId": "proj_abc123", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" +} +``` + +### Add a Member to a Project + +```http +POST /api/v1/projects/proj_abc123/members +Authorization: Bearer +Content-Type: application/json + +{ + "userId": "user_def456", + "policyIds": ["pol_def456"] +} +``` + +**Response** `201 Created` + +### Update Member Policies + +```http +PUT /api/v1/projects/proj_abc123/members/user_def456/policies +Authorization: Bearer +Content-Type: application/json + +{ + "policyIds": ["pol_def456", "pol_ghi789"] +} +``` + +**Response** `200 OK` + +--- + +## Project Keys + +Project Keys provide project key-based authentication for programmatic access to SOAT. Each key is scoped to a single project and bound to a single policy document. The raw key is returned only once at creation time — it cannot be retrieved afterwards. + +Project Keys are identified by an `id` prefixed with `key_`. + +### Project Key Data Model + +| Field | Type | Description | +| ----------- | ------ | ---------------------------------------------- | +| `id` | string | Public identifier prefixed with `key_` | +| `name` | string | Human-readable label | +| `keyPrefix` | string | First 8 characters of the raw key (for lookup) | +| `userId` | string | Public ID of the user who created the key | +| `projectId` | string | Public ID of the project the key is scoped to | +| `policyId` | string | Public ID of the attached policy | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +The raw secret key is only returned in the `POST` response. Only `keyPrefix` and a bcrypt hash are stored. + +### Security Model + +- The raw key is a 32-byte random value prefixed with `pk_`. +- Only the `keyPrefix` (first 8 characters) and a bcrypt hash of the full key are stored. +- Authentication works by matching the prefix to candidate rows, then verifying the full key against each hash. + +### Intersection Authorization + +When an project key is used to make a request, authorization applies **intersection semantics**: + +1. The owning user's project membership policies must allow the action. +2. The key's own attached policy must also allow the action. + +Both must independently evaluate to `Allow`. This ensures a key can never exceed the permissions of the user who created it. + +### Scoping + +A project key is scoped to exactly one project. Requests made with the key can only access resources within that project. The project is resolved automatically from the key — callers do not need to specify the project explicitly. + +### Project Key Permissions + +Project key operations require authentication. The creator of a key is the only user who can read or update it (ownership enforcement). + +| Action | Permission | REST Endpoint | MCP Tool | +| ----------------- | -------------- | ------------------------------ | -------- | +| Create key | Project member | `POST /api/v1/project-keys` | — | +| Get key by ID | Key owner only | `GET /api/v1/project-keys/:id` | — | +| Update key policy | Key owner only | `PUT /api/v1/project-keys/:id` | — | + +### Project Key Operations + +#### Create a Project Key + +The caller must be a member of the target project. The policy must belong to the same project. + +```http +POST /api/v1/project-keys +Authorization: Bearer +Content-Type: application/json + +{ + "name": "CI/CD Key", + "projectId": "proj_abc123", + "policyId": "pol_def456" +} +``` + +**Response** `201 Created` + +```json +{ + "id": "key_ghi789", + "name": "CI/CD Key", + "keyPrefix": "pk_abcd", + "key": "pk_abcdefghijklmnopqrstuvwxyz123456", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" +} +``` + +:::caution +The `key` field is only returned once at creation time. Store it securely — it cannot be retrieved again. +::: + +#### Get a Project Key + +Returns metadata about the key. Only the key owner can access this. + +```http +GET /api/v1/project-keys/key_ghi789 +Authorization: Bearer +``` + +**Response** `200 OK` + +```json +{ + "id": "key_ghi789", + "name": "CI/CD Key", + "keyPrefix": "pk_abcd", + "userId": "user_abc123", + "projectId": "proj_abc123", + "policyId": "pol_def456", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" +} +``` + +#### Update a Key's Policy + +Swap the attached policy. The new policy must belong to the same project as the key. + +```http +PUT /api/v1/project-keys/key_ghi789 +Authorization: Bearer +Content-Type: application/json + +{ + "policyId": "pol_xyz999" +} +``` + +**Response** `200 OK` From 914afb81ae3f3f548429122014434fb193a70adf Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Sun, 12 Apr 2026 21:47:09 -0300 Subject: [PATCH 14/27] chore: update docs --- packages/website/docs/modules/documents.md | 83 +--------- packages/website/docs/modules/files.md | 68 +------- packages/website/docs/modules/iam.md | 177 ++++++--------------- packages/website/docs/modules/projects.md | 100 ------------ 4 files changed, 54 insertions(+), 374 deletions(-) diff --git a/packages/website/docs/modules/documents.md b/packages/website/docs/modules/documents.md index 98651870..c3412443 100644 --- a/packages/website/docs/modules/documents.md +++ b/packages/website/docs/modules/documents.md @@ -65,88 +65,7 @@ Document operations are governed by per-project policies. Grant the following pe | Update a document | `documents:UpdateDocument` | `PATCH /api/v1/documents/:id` | `update-document` | | Semantic search | `documents:SearchDocuments` | `POST /api/v1/documents/search` | `search-documents` | -## Operations - -### List documents - -Returns documents visible to the caller (no content field). `projectId` is optional — omit it to retrieve documents across all accessible projects: - -```http -GET /api/v1/documents?projectId=proj_xxx -Authorization: Bearer -``` - -### Get a document - -Returns the document record including its full text `content`: - -```http -GET /api/v1/documents/doc_xxx -Authorization: Bearer -``` - -### Create a document - -Writes the text to disk, generates an embedding, and persists both: - -```http -POST /api/v1/documents -Authorization: Bearer -Content-Type: application/json - -{ - "projectId": "proj_xxx", - "content": "The quick brown fox jumps over the lazy dog.", - "filename": "my-doc.txt" -} -``` - -### Delete a document - -Deletes the Document record, the underlying File record, and removes the file from disk: - -```http -DELETE /api/v1/documents/doc_xxx -Authorization: Bearer -``` - -### Update a document - -Updates text content (and regenerates the embedding), title, metadata, or tags. All fields are optional — supply at least one: - -```http -PATCH /api/v1/documents/doc_xxx -Authorization: Bearer -Content-Type: application/json - -{ - "content": "Updated document text.", - "title": "New title", - "metadata": { "author": "Alice" }, - "tags": { "env": "production" } -} -``` - -### Semantic search - -Embeds the query and returns the closest documents by cosine distance. `projectId` is optional — omit it to search across all accessible projects: - -```http -POST /api/v1/documents/search -Authorization: Bearer -Content-Type: application/json - -{ - "projectId": "proj_xxx", - "query": "What is the capital of France?", - "limit": 5, - "threshold": 0.7 -} -``` - -The `limit` field defaults to `10` when omitted. The optional `threshold` field (between 0 and 1) filters out results whose similarity score falls below the given value. - -Each result in the response includes a `score` field (1 − cosine distance) and a `content` field with the full document text. +See the [API Reference](../api/documents/list-documents) for full endpoint details, request/response schemas, and status codes. ## Project ID Resolution diff --git a/packages/website/docs/modules/files.md b/packages/website/docs/modules/files.md index 8fa2fe3a..c1dc16d7 100644 --- a/packages/website/docs/modules/files.md +++ b/packages/website/docs/modules/files.md @@ -56,70 +56,4 @@ File operations are governed by per-project policies. Grant the following permis | Update metadata | `files:UpdateFileMetadata` | `PATCH /api/v1/files/:id/metadata` | `update-file-metadata` | | Delete a file | `files:DeleteFile` | `DELETE /api/v1/files/:id` | `delete-file` | -## Operations - -### Upload a file - -`POST /api/v1/files/upload` — Multipart form-data. The server stores the file on disk and creates a database record. - -Required fields: `file` (binary), `projectId`. -Optional fields: `metadata` (JSON string). - -### Download a file - -`GET /api/v1/files/{id}/download` — Returns the raw file bytes with the original `Content-Type` and `Content-Disposition: attachment` header. - -### Get file metadata - -`GET /api/v1/files/{id}` — Returns the file record without transferring the file content. - -### Update metadata - -`PATCH /api/v1/files/{id}/metadata` — Updates the `metadata` and/or `filename` fields on an existing file record. Both fields are optional; supply at least one. - -Request body: - -```json -{ - "metadata": "{\"author\":\"Alice\",\"tags\":[\"report\"]}", - "filename": "new-name.pdf" -} -``` - -### Delete a file - -`DELETE /api/v1/files/{id}` — Removes the file from disk and deletes the database record. Returns `204 No Content`. - -### Upload a file (Base64) - -`POST /api/v1/files/upload/base64` — JSON-based upload that accepts the file content as a Base64-encoded string. Useful when multipart form-data is not practical (e.g., from MCP tools). - -Request body: - -```json -{ - "projectId": "proj_xxx", - "content": "", - "filename": "report.pdf", - "contentType": "application/pdf" -} -``` - -### Download a file (Base64) - -`GET /api/v1/files/{id}/download/base64` — Returns the file content as a Base64-encoded JSON response instead of raw bytes. Useful for MCP tool integrations. - -Response body: - -```json -{ - "content": "", - "filename": "report.pdf", - "contentType": "application/pdf", - "size": 12345 -} -``` - -### List files with project filter - -`GET /api/v1/files` accepts an optional `projectId` query parameter to filter files by project. When omitted, the server resolves accessible projects based on the caller's identity (same resolution rules as the Documents module). +See the [API Reference](../api/files/list-files) for full endpoint details, request/response schemas, and status codes. diff --git a/packages/website/docs/modules/iam.md b/packages/website/docs/modules/iam.md index 5c9b1987..ec1a5c20 100644 --- a/packages/website/docs/modules/iam.md +++ b/packages/website/docs/modules/iam.md @@ -13,8 +13,6 @@ SOAT uses a policy-based access control model. Every API request is authenticate The IAM module covers: - **Users** — identity management, roles, and JWT authentication (see [Users](#users) below) -- **Projects** — multi-tenant namespaces with membership and policy attachment (see [Projects](projects.md)) -- **Project Keys** — scoped project keys for programmatic access (see [Project Keys](projects.md#project-keys)) - **Policy Documents** — structured permission rules attached to memberships and project keys - **Policy Engine** — evaluation logic that resolves allow/deny decisions at request time @@ -36,7 +34,6 @@ A policy document is a JSON object containing one or more statements. Each state ```json { - "version": "2025-01-01", "statement": [ { "effect": "Allow", @@ -187,128 +184,6 @@ PATCH /api/v1//:id/tags Merge tags GET /api/v1//:id/tags Get tags ``` -## Roles - -| Role | Description | -| ------- | --------------------------------------------------------------------------- | -| `admin` | Full access to all resources and operations. Bypasses policy evaluation. | -| `user` | Access determined by project membership policies. Must be a project member. | - ---- - -## Users - -The Users section covers identity management and authentication. Users authenticate via username and password and receive a JWT for subsequent requests. - -A User has a username, a hashed password, and a role (`admin` or `user`). Users are identified by an `id` prefixed with `user_`. Passwords are hashed with bcrypt and never returned in API responses. - -The first user is created via the **bootstrap** endpoint, which is only available when no users exist. Subsequent users are created by admins. - -### User Data Model - -| Field | Type | Description | -| ----------- | ------ | --------------------------------------- | -| `id` | string | Public identifier prefixed with `user_` | -| `username` | string | Unique username | -| `role` | string | `"admin"` or `"user"` | -| `createdAt` | string | ISO 8601 creation timestamp | -| `updatedAt` | string | ISO 8601 last-updated timestamp | - -Sensitive fields (`passwordHash`, internal numeric ID) are never exposed in responses. - -### Bootstrap - -The `POST /api/v1/users/bootstrap` endpoint creates the first admin user. It is only available when the user table is empty and returns `409 Conflict` if any user already exists. This endpoint does not require authentication. - -### User Authentication - -Users authenticate with `POST /api/v1/users/login`, providing `username` and `password`. On success, the server returns a signed JWT containing the user's public ID and role. The token is passed as `Authorization: Bearer ` on subsequent requests. - -### User Permissions - -User management is restricted to admin users. These operations are not governed by the policy engine — they require the `admin` role directly. - -| Action | Permission | REST Endpoint | MCP Tool | -| -------------- | --------------- | ------------------------------ | -------- | -| List users | Admin only | `GET /api/v1/users` | — | -| Get user by ID | Admin only | `GET /api/v1/users/:id` | — | -| Create user | Admin only | `POST /api/v1/users` | — | -| Delete user | Admin only | `DELETE /api/v1/users/:id` | — | -| Bootstrap | Unauthenticated | `POST /api/v1/users/bootstrap` | — | -| Login | Unauthenticated | `POST /api/v1/users/login` | — | - -### User Operations - -#### Bootstrap the First Admin - -```http -POST /api/v1/users/bootstrap -Content-Type: application/json - -{ - "username": "admin", - "password": "supersecret" -} -``` - -**Response** `201 Created` - -```json -{ - "id": "user_abc123", - "username": "admin", - "role": "admin", - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": "2025-01-01T00:00:00.000Z" -} -``` - -#### Login - -```http -POST /api/v1/users/login -Content-Type: application/json - -{ - "username": "admin", - "password": "supersecret" -} -``` - -**Response** `200 OK` - -```json -{ - "token": "eyJhbGciOiJIUzI1NiIs..." -} -``` - -#### Create a User - -```http -POST /api/v1/users -Authorization: Bearer -Content-Type: application/json - -{ - "username": "alice", - "password": "alicepass", - "role": "user" -} -``` - -**Response** `201 Created` - -```json -{ - "id": "user_def456", - "username": "alice", - "role": "user", - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": "2025-01-01T00:00:00.000Z" -} -``` - ## Examples ### Full Admin Policy @@ -394,3 +269,55 @@ Allow only actors tagged `"internal"`: ] } ``` + +--- + +## Users + +The Users section covers identity management and authentication. Users authenticate via username and password and receive a JWT for subsequent requests. + +A User has a username, a hashed password, and a role (`admin` or `user`). Users are identified by an `id` prefixed with `user_`. Passwords are hashed with bcrypt and never returned in API responses. + +The first user is created via the **bootstrap** endpoint, which is only available when no users exist. Subsequent users are created by admins. + +## Roles + +| Role | Description | +| ------- | --------------------------------------------------------------------------- | +| `admin` | Full access to all resources and operations. Bypasses policy evaluation. | +| `user` | Access determined by project membership policies. Must be a project member. | + +### User Data Model + +| Field | Type | Description | +| ----------- | ------ | --------------------------------------- | +| `id` | string | Public identifier prefixed with `user_` | +| `username` | string | Unique username | +| `role` | string | `"admin"` or `"user"` | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +Sensitive fields (`passwordHash`, internal numeric ID) are never exposed in responses. + +### Bootstrap + +The `POST /api/v1/users/bootstrap` endpoint creates the first admin user. It is only available when the user table is empty and returns `409 Conflict` if any user already exists. This endpoint does not require authentication. + +### User Authentication + +Users authenticate with `POST /api/v1/users/login`, providing `username` and `password`. On success, the server returns a signed JWT containing the user's public ID and role. The token is passed as `Authorization: Bearer ` on subsequent requests. + +### User Permissions + +User management is restricted to admin users. These operations are not governed by the policy engine — they require the `admin` role directly. + +| Action | Permission | REST Endpoint | MCP Tool | +| -------------- | --------------- | ------------------------------ | -------- | +| List users | Admin only | `GET /api/v1/users` | — | +| Get user by ID | Admin only | `GET /api/v1/users/:id` | — | +| Create user | Admin only | `POST /api/v1/users` | — | +| Delete user | Admin only | `DELETE /api/v1/users/:id` | — | +| Bootstrap | Unauthenticated | `POST /api/v1/users/bootstrap` | — | +| Login | Unauthenticated | `POST /api/v1/users/login` | — | + +See the [API Reference](../api/users/list-users) for full endpoint details, request/response schemas, and status codes. diff --git a/packages/website/docs/modules/projects.md b/packages/website/docs/modules/projects.md index 8b53aa18..594d977c 100644 --- a/packages/website/docs/modules/projects.md +++ b/packages/website/docs/modules/projects.md @@ -62,31 +62,6 @@ Project CRUD and management operations are restricted to admin users. The `proje | Update member policies | Admin only | `PUT /api/v1/projects/:projectId/members/:userId/policies` | — | | Get member policies | Admin only | `GET /api/v1/projects/:projectId/members/:userId/policies` | — | -## Operations - -### Create a Project - -```http -POST /api/v1/projects -Authorization: Bearer -Content-Type: application/json - -{ - "name": "My Project" -} -``` - -**Response** `201 Created` - -```json -{ - "id": "proj_abc123", - "name": "My Project", - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": "2025-01-01T00:00:00.000Z" -} -``` - ### Create a Policy ```http @@ -204,78 +179,3 @@ Project key operations require authentication. The creator of a key is the only | Create key | Project member | `POST /api/v1/project-keys` | — | | Get key by ID | Key owner only | `GET /api/v1/project-keys/:id` | — | | Update key policy | Key owner only | `PUT /api/v1/project-keys/:id` | — | - -### Project Key Operations - -#### Create a Project Key - -The caller must be a member of the target project. The policy must belong to the same project. - -```http -POST /api/v1/project-keys -Authorization: Bearer -Content-Type: application/json - -{ - "name": "CI/CD Key", - "projectId": "proj_abc123", - "policyId": "pol_def456" -} -``` - -**Response** `201 Created` - -```json -{ - "id": "key_ghi789", - "name": "CI/CD Key", - "keyPrefix": "pk_abcd", - "key": "pk_abcdefghijklmnopqrstuvwxyz123456", - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": "2025-01-01T00:00:00.000Z" -} -``` - -:::caution -The `key` field is only returned once at creation time. Store it securely — it cannot be retrieved again. -::: - -#### Get a Project Key - -Returns metadata about the key. Only the key owner can access this. - -```http -GET /api/v1/project-keys/key_ghi789 -Authorization: Bearer -``` - -**Response** `200 OK` - -```json -{ - "id": "key_ghi789", - "name": "CI/CD Key", - "keyPrefix": "pk_abcd", - "userId": "user_abc123", - "projectId": "proj_abc123", - "policyId": "pol_def456", - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": "2025-01-01T00:00:00.000Z" -} -``` - -#### Update a Key's Policy - -Swap the attached policy. The new policy must belong to the same project as the key. - -```http -PUT /api/v1/project-keys/key_ghi789 -Authorization: Bearer -Content-Type: application/json - -{ - "policyId": "pol_xyz999" -} -``` - -**Response** `200 OK` From d32b2c8407a20b8f57d350e9134624e9c0918688 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 06:41:53 -0300 Subject: [PATCH 15/27] feat: ai provider --- packages/postgresdb/src/index.ts | 2 + packages/postgresdb/src/models/AiProvider.ts | 92 ++++ packages/postgresdb/src/models/Secret.ts | 53 ++ packages/postgresdb/src/models/index.ts | 2 + packages/postgresdb/src/utils/publicId.ts | 2 + packages/server/src/lib/aiProviders.ts | 139 +++++ packages/server/src/lib/secrets.ts | 136 +++++ packages/server/src/mcp/tools/aiProviders.ts | 144 +++++ packages/server/src/mcp/tools/index.ts | 4 + packages/server/src/mcp/tools/secrets.ts | 107 ++++ packages/server/src/rest/v1/aiProviders.ts | 508 ++++++++++++++++++ packages/server/src/rest/v1/index.ts | 4 + packages/server/src/rest/v1/secrets.ts | 441 +++++++++++++++ .../tests/unit/tests/aiProviders.test.ts | 365 +++++++++++++ .../server/tests/unit/tests/secrets.test.ts | 300 +++++++++++ packages/website/docs/modules/ai-providers.md | 53 ++ packages/website/docs/modules/secrets.md | 48 ++ 17 files changed, 2400 insertions(+) create mode 100644 packages/postgresdb/src/models/AiProvider.ts create mode 100644 packages/postgresdb/src/models/Secret.ts create mode 100644 packages/server/src/lib/aiProviders.ts create mode 100644 packages/server/src/lib/secrets.ts create mode 100644 packages/server/src/mcp/tools/aiProviders.ts create mode 100644 packages/server/src/mcp/tools/secrets.ts create mode 100644 packages/server/src/rest/v1/aiProviders.ts create mode 100644 packages/server/src/rest/v1/secrets.ts create mode 100644 packages/server/tests/unit/tests/aiProviders.test.ts create mode 100644 packages/server/tests/unit/tests/secrets.test.ts create mode 100644 packages/website/docs/modules/ai-providers.md create mode 100644 packages/website/docs/modules/secrets.md diff --git a/packages/postgresdb/src/index.ts b/packages/postgresdb/src/index.ts index b8fea7f7..9a133291 100644 --- a/packages/postgresdb/src/index.ts +++ b/packages/postgresdb/src/index.ts @@ -1,2 +1,4 @@ export * as models from './models'; +export { AI_PROVIDER_SLUGS } from './models/AiProvider'; +export type { AiProviderSlug } from './models/AiProvider'; export * from './utils/publicId'; diff --git a/packages/postgresdb/src/models/AiProvider.ts b/packages/postgresdb/src/models/AiProvider.ts new file mode 100644 index 00000000..ecb28a58 --- /dev/null +++ b/packages/postgresdb/src/models/AiProvider.ts @@ -0,0 +1,92 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; +import { Project } from './Project'; +import { Secret } from './Secret'; + +export const AI_PROVIDER_SLUGS = [ + 'openai', + 'anthropic', + 'google', + 'xai', + 'groq', + 'ollama', + 'azure', + 'bedrock', + 'gateway', + 'custom', +] as const; + +export type AiProviderSlug = (typeof AI_PROVIDER_SLUGS)[number]; + +@Table({ + tableName: 'ai_providers', + hooks: { + beforeValidate: (instance: AiProvider) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.aiProvider); + } + }, + }, +}) +export class AiProvider extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @ForeignKey(() => { + return Project; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare projectId: number; + + @BelongsTo(() => { + return Project; + }) + declare project: Project; + + @ForeignKey(() => { + return Secret; + }) + @Column({ type: DataType.INTEGER, allowNull: true }) + declare secretId: number | null; + + @BelongsTo(() => { + return Secret; + }) + declare secret: Secret | null; + + @Column({ type: DataType.STRING, allowNull: false }) + declare name: string; + + @Column({ + type: DataType.ENUM(...AI_PROVIDER_SLUGS), + allowNull: false, + }) + declare provider: AiProviderSlug; + + @Column({ type: DataType.STRING, allowNull: false }) + declare defaultModel: string; + + @Column({ type: DataType.STRING, allowNull: true }) + declare baseUrl: string | null; + + @Column({ type: DataType.JSONB, allowNull: true }) + declare config: Record | null; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/Secret.ts b/packages/postgresdb/src/models/Secret.ts new file mode 100644 index 00000000..46b97004 --- /dev/null +++ b/packages/postgresdb/src/models/Secret.ts @@ -0,0 +1,53 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from '@ttoss/postgresdb'; + +import { generatePublicId, PUBLIC_ID_PREFIXES } from '../utils/publicId'; +import { Project } from './Project'; + +@Table({ + tableName: 'secrets', + hooks: { + beforeValidate: (instance: Secret) => { + if (!instance.publicId) { + instance.publicId = generatePublicId(PUBLIC_ID_PREFIXES.secret); + } + }, + }, +}) +export class Secret extends Model { + @Column({ + type: DataType.STRING(32), + unique: true, + allowNull: false, + }) + declare publicId: string; + + @ForeignKey(() => { + return Project; + }) + @Column({ type: DataType.INTEGER, allowNull: false }) + declare projectId: number; + + @BelongsTo(() => { + return Project; + }) + declare project: Project; + + @Column({ type: DataType.STRING, allowNull: false }) + declare name: string; + + @Column({ type: DataType.TEXT, allowNull: true }) + declare encryptedValue: string | null; + + @Column({ type: DataType.DATE }) + declare createdAt: Date; + + @Column({ type: DataType.DATE }) + declare updatedAt: Date; +} diff --git a/packages/postgresdb/src/models/index.ts b/packages/postgresdb/src/models/index.ts index 8d011128..b7ae945a 100644 --- a/packages/postgresdb/src/models/index.ts +++ b/packages/postgresdb/src/models/index.ts @@ -1,4 +1,5 @@ export { Actor } from './Actor'; +export { AiProvider } from './AiProvider'; export { ProjectKey } from './ProjectKey'; export { Conversation } from './Conversation'; export { ConversationMessage } from './ConversationMessage'; @@ -6,5 +7,6 @@ export { Document } from './Document'; export { File } from './File'; export { Project } from './Project'; export { ProjectPolicy } from './ProjectPolicy'; +export { Secret } from './Secret'; export { User } from './User'; export { UserProject } from './UserProject'; diff --git a/packages/postgresdb/src/utils/publicId.ts b/packages/postgresdb/src/utils/publicId.ts index e1bc212a..11a9a0ea 100644 --- a/packages/postgresdb/src/utils/publicId.ts +++ b/packages/postgresdb/src/utils/publicId.ts @@ -13,6 +13,8 @@ export const PUBLIC_ID_PREFIXES = { document: 'doc_', actor: 'act_', conversation: 'conv_', + secret: 'sec_', + aiProvider: 'aip_', } as const; /** diff --git a/packages/server/src/lib/aiProviders.ts b/packages/server/src/lib/aiProviders.ts new file mode 100644 index 00000000..b314087d --- /dev/null +++ b/packages/server/src/lib/aiProviders.ts @@ -0,0 +1,139 @@ +import type { AiProviderSlug } from '@soat/postgresdb'; + +import { db } from 'src/db'; +import { decryptValue } from 'src/lib/secrets'; + +const getAiProviderIncludes = () => [ + { model: db.Project, as: 'project' }, + { model: db.Secret, as: 'secret' }, +]; + +const mapAiProvider = ( + instance: InstanceType<(typeof db)['AiProvider']> & { + project?: InstanceType<(typeof db)['Project']>; + secret?: InstanceType<(typeof db)['Secret']> | null; + } +) => ({ + id: instance.publicId, + projectId: instance.project?.publicId, + secretId: instance.secret?.publicId ?? null, + name: instance.name, + provider: instance.provider, + defaultModel: instance.defaultModel, + baseUrl: instance.baseUrl ?? undefined, + config: instance.config ?? undefined, + createdAt: instance.createdAt, + updatedAt: instance.updatedAt, +}); + +export const listAiProviders = async (args: { projectIds: number[] }) => { + const providers = await db.AiProvider.findAll({ + where: { projectId: args.projectIds }, + include: getAiProviderIncludes(), + }); + return providers.map(mapAiProvider); +}; + +export const getAiProvider = async (args: { id: string }) => { + const provider = await db.AiProvider.findOne({ + where: { publicId: args.id }, + include: getAiProviderIncludes(), + }); + if (!provider) return null; + return mapAiProvider(provider); +}; + +export const createAiProvider = async (args: { + projectId: number; + secretId?: number; + name: string; + provider: AiProviderSlug; + defaultModel: string; + baseUrl?: string; + config?: Record; +}) => { + const instance = await db.AiProvider.create({ + projectId: args.projectId, + secretId: args.secretId ?? null, + name: args.name, + provider: args.provider, + defaultModel: args.defaultModel, + baseUrl: args.baseUrl ?? null, + config: args.config ?? null, + }); + const withAssociations = await db.AiProvider.findOne({ + where: { id: instance.id }, + include: getAiProviderIncludes(), + }); + return mapAiProvider(withAssociations!); +}; + +export const updateAiProvider = async (args: { + id: string; + secretId?: number; + name?: string; + provider?: AiProviderSlug; + defaultModel?: string; + baseUrl?: string | null; + config?: Record | null; +}) => { + const instance = await db.AiProvider.findOne({ + where: { publicId: args.id }, + include: getAiProviderIncludes(), + }); + if (!instance) return null; + + if (args.name !== undefined) instance.name = args.name; + if (args.provider !== undefined) instance.provider = args.provider; + if (args.defaultModel !== undefined) + instance.defaultModel = args.defaultModel; + if (args.baseUrl !== undefined) instance.baseUrl = args.baseUrl; + if (args.config !== undefined) instance.config = args.config; + if (args.secretId !== undefined) instance.secretId = args.secretId; + + await instance.save(); + const refreshed = await db.AiProvider.findOne({ + where: { id: instance.id }, + include: getAiProviderIncludes(), + }); + return mapAiProvider(refreshed!); +}; + +export const deleteAiProvider = async (args: { + id: string; + force?: boolean; +}) => { + const instance = await db.AiProvider.findOne({ + where: { publicId: args.id }, + }); + if (!instance) return null; + + // TODO: add cascade check against chats when that module is implemented + await instance.destroy(); + return 'deleted' as const; +}; + +export const resolveAiProviderSecret = async (args: { + aiProviderId: string; +}) => { + const instance = await db.AiProvider.findOne({ + where: { publicId: args.aiProviderId }, + }); + if (!instance) return null; + + let decryptedValue: string | null = null; + if (instance.secretId) { + const secret = await db.Secret.findByPk(instance.secretId); + if (secret?.encryptedValue) { + decryptedValue = decryptValue(secret.encryptedValue); + } + } + + return { + provider: instance.provider, + defaultModel: instance.defaultModel, + baseUrl: instance.baseUrl ?? undefined, + config: instance.config ?? undefined, + secretValue: decryptedValue, + }; +}; diff --git a/packages/server/src/lib/secrets.ts b/packages/server/src/lib/secrets.ts new file mode 100644 index 00000000..5a2df462 --- /dev/null +++ b/packages/server/src/lib/secrets.ts @@ -0,0 +1,136 @@ +import crypto from 'node:crypto'; + +import { db } from 'src/db'; + +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 12; +const AUTH_TAG_LENGTH = 16; + +const getEncryptionKey = () => { + const key = process.env.SECRETS_ENCRYPTION_KEY; + if (!key) { + throw new Error('SECRETS_ENCRYPTION_KEY environment variable is not set'); + } + const buf = Buffer.from(key, 'hex'); + if (buf.length !== 32) { + throw new Error( + 'SECRETS_ENCRYPTION_KEY must be a 64-character hex string (32 bytes)' + ); + } + return buf; +}; + +export const encryptValue = (plaintext: string): string => { + const key = getEncryptionKey(); + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv(ALGORITHM, key, iv, { + authTagLength: AUTH_TAG_LENGTH, + }); + const encrypted = Buffer.concat([ + cipher.update(plaintext, 'utf8'), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + return Buffer.concat([iv, authTag, encrypted]).toString('base64'); +}; + +export const decryptValue = (ciphertext: string): string => { + const key = getEncryptionKey(); + const buf = Buffer.from(ciphertext, 'base64'); + const iv = buf.subarray(0, IV_LENGTH); + const authTag = buf.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH); + const encrypted = buf.subarray(IV_LENGTH + AUTH_TAG_LENGTH); + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, { + authTagLength: AUTH_TAG_LENGTH, + }); + decipher.setAuthTag(authTag); + return decipher.update(encrypted) + decipher.final('utf8'); +}; + +const mapSecret = ( + instance: InstanceType<(typeof db)['Secret']> & { + project?: InstanceType<(typeof db)['Project']>; + } +) => ({ + id: instance.publicId, + projectId: instance.project?.publicId, + name: instance.name, + hasValue: instance.encryptedValue !== null, + createdAt: instance.createdAt, + updatedAt: instance.updatedAt, +}); + +export const listSecrets = async (args: { projectIds: number[] }) => { + const secrets = await db.Secret.findAll({ + where: { projectId: args.projectIds }, + include: [{ model: db.Project, as: 'project' }], + }); + return secrets.map(mapSecret); +}; + +export const getSecret = async (args: { id: string }) => { + const secret = await db.Secret.findOne({ + where: { publicId: args.id }, + include: [{ model: db.Project, as: 'project' }], + }); + if (!secret) return null; + return mapSecret(secret); +}; + +export const createSecret = async (args: { + projectId: number; + name: string; + value?: string; +}) => { + const secret = await db.Secret.create({ + projectId: args.projectId, + name: args.name, + encryptedValue: args.value ? encryptValue(args.value) : null, + }); + const withProject = await db.Secret.findOne({ + where: { id: secret.id }, + include: [{ model: db.Project, as: 'project' }], + }); + return mapSecret(withProject!); +}; + +export const updateSecret = async (args: { + id: string; + name?: string; + value?: string; +}) => { + const secret = await db.Secret.findOne({ + where: { publicId: args.id }, + include: [{ model: db.Project, as: 'project' }], + }); + if (!secret) return null; + + if (args.name !== undefined) { + secret.name = args.name; + } + if (args.value !== undefined) { + secret.encryptedValue = encryptValue(args.value); + } + await secret.save(); + return mapSecret(secret); +}; + +export const deleteSecret = async (args: { id: string; force?: boolean }) => { + const secret = await db.Secret.findOne({ where: { publicId: args.id } }); + if (!secret) return null; + + const dependentCount = await db.AiProvider.count({ + where: { secretId: secret.id }, + }); + + if (dependentCount > 0 && !args.force) { + return 'conflict' as const; + } + + if (args.force) { + await db.AiProvider.destroy({ where: { secretId: secret.id } }); + } + + await secret.destroy(); + return 'deleted' as const; +}; diff --git a/packages/server/src/mcp/tools/aiProviders.ts b/packages/server/src/mcp/tools/aiProviders.ts new file mode 100644 index 00000000..da392a98 --- /dev/null +++ b/packages/server/src/mcp/tools/aiProviders.ts @@ -0,0 +1,144 @@ +import type { McpServer } from '@ttoss/http-server-mcp'; +import { apiCall, z } from '@ttoss/http-server-mcp'; + +const AI_PROVIDER_SLUGS = [ + 'openai', + 'anthropic', + 'google', + 'xai', + 'groq', + 'ollama', + 'azure', + 'bedrock', + 'gateway', + 'custom', +] as const; + +const registerTools = (server: McpServer) => { + server.registerTool( + 'list-ai-providers', + { + description: 'List AI providers in a project.', + inputSchema: { + projectId: z.string().optional().describe('Project ID to filter by'), + }, + }, + async ({ projectId }) => { + const params = new URLSearchParams(); + if (projectId) params.set('projectId', projectId); + const qs = params.toString() ? `?${params.toString()}` : ''; + const data = await apiCall('GET', `/ai-providers${qs}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'get-ai-provider', + { + description: 'Get an AI provider by ID.', + inputSchema: { + id: z.string().describe('AI Provider ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/ai-providers/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'create-ai-provider', + { + description: 'Create a new AI provider configuration.', + inputSchema: { + projectId: z + .string() + .optional() + .describe( + 'Project ID. Required for JWT auth; omit when using a project key.' + ), + secretId: z + .string() + .optional() + .describe('Secret ID containing the provider credentials'), + name: z.string().describe('Display name for this AI provider'), + provider: z.enum(AI_PROVIDER_SLUGS).describe('Provider type'), + defaultModel: z.string().describe('Default model to use'), + baseUrl: z + .string() + .optional() + .describe('Custom base URL for the provider API'), + config: z + .record(z.unknown()) + .optional() + .describe('Provider-specific configuration as a JSON object'), + }, + }, + async ({ + projectId, + secretId, + name, + provider, + defaultModel, + baseUrl, + config, + }) => { + const data = await apiCall('POST', '/ai-providers', { + body: { + projectId, + secretId, + name, + provider, + defaultModel, + baseUrl, + config, + }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'update-ai-provider', + { + description: 'Update an AI provider configuration.', + inputSchema: { + id: z.string().describe('AI Provider ID'), + secretId: z.string().optional().describe('New Secret ID'), + name: z.string().optional().describe('New display name'), + provider: z + .enum(AI_PROVIDER_SLUGS) + .optional() + .describe('New provider type'), + defaultModel: z.string().optional().describe('New default model'), + baseUrl: z.string().optional().describe('New base URL'), + config: z + .record(z.unknown()) + .optional() + .describe('New provider-specific configuration'), + }, + }, + async ({ id, secretId, name, provider, defaultModel, baseUrl, config }) => { + const data = await apiCall('PATCH', `/ai-providers/${id}`, { + body: { secretId, name, provider, defaultModel, baseUrl, config }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'delete-ai-provider', + { + description: 'Delete an AI provider.', + inputSchema: { + id: z.string().describe('AI Provider ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('DELETE', `/ai-providers/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); +}; + +export { registerTools }; diff --git a/packages/server/src/mcp/tools/index.ts b/packages/server/src/mcp/tools/index.ts index 4a236d8e..c1620f3d 100644 --- a/packages/server/src/mcp/tools/index.ts +++ b/packages/server/src/mcp/tools/index.ts @@ -1,17 +1,21 @@ import type { McpServer } from '@ttoss/http-server-mcp'; import { registerTools as registerActorTools } from './actors'; +import { registerTools as registerAiProviderTools } from './aiProviders'; import { registerTools as registerConversationTools } from './conversations'; import { registerTools as registerDocumentTools } from './documents'; import { registerTools as registerFileTools } from './files'; import { registerTools as registerProjectTools } from './projects'; +import { registerTools as registerSecretTools } from './secrets'; const registerTools = (server: McpServer) => { registerActorTools(server); + registerAiProviderTools(server); registerConversationTools(server); registerDocumentTools(server); registerFileTools(server); registerProjectTools(server); + registerSecretTools(server); }; export { registerTools }; diff --git a/packages/server/src/mcp/tools/secrets.ts b/packages/server/src/mcp/tools/secrets.ts new file mode 100644 index 00000000..9f9c0bfe --- /dev/null +++ b/packages/server/src/mcp/tools/secrets.ts @@ -0,0 +1,107 @@ +import type { McpServer } from '@ttoss/http-server-mcp'; +import { apiCall, z } from '@ttoss/http-server-mcp'; + +const registerTools = (server: McpServer) => { + server.registerTool( + 'list-secrets', + { + description: 'List secrets in a project. Values are never returned.', + inputSchema: { + projectId: z.string().optional().describe('Project ID to filter by'), + }, + }, + async ({ projectId }) => { + const params = new URLSearchParams(); + if (projectId) params.set('projectId', projectId); + const qs = params.toString() ? `?${params.toString()}` : ''; + const data = await apiCall('GET', `/secrets${qs}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'get-secret', + { + description: 'Get a secret by ID. The value is never returned.', + inputSchema: { + id: z.string().describe('Secret ID'), + }, + }, + async ({ id }) => { + const data = await apiCall('GET', `/secrets/${id}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'create-secret', + { + description: + 'Create a new secret. The value is encrypted at rest and never returned.', + inputSchema: { + projectId: z + .string() + .optional() + .describe( + 'Project ID. Required for JWT auth; omit when using a project key.' + ), + name: z.string().describe('Secret name'), + value: z + .string() + .optional() + .describe('Secret value to encrypt and store'), + }, + }, + async ({ projectId, name, value }) => { + const data = await apiCall('POST', '/secrets', { + body: { projectId, name, value }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'update-secret', + { + description: 'Update the name or value of a secret.', + inputSchema: { + id: z.string().describe('Secret ID'), + name: z.string().optional().describe('New name'), + value: z + .string() + .optional() + .describe('New secret value to encrypt and store'), + }, + }, + async ({ id, name, value }) => { + const data = await apiCall('PATCH', `/secrets/${id}`, { + body: { name, value }, + }); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); + + server.registerTool( + 'delete-secret', + { + description: + 'Delete a secret. Returns a conflict error if referenced by an AI provider unless force is true.', + inputSchema: { + id: z.string().describe('Secret ID'), + force: z + .boolean() + .optional() + .describe('If true, also delete dependent AI providers'), + }, + }, + async ({ id, force }) => { + const params = new URLSearchParams(); + if (force) params.set('force', 'true'); + const qs = params.toString() ? `?${params.toString()}` : ''; + const data = await apiCall('DELETE', `/secrets/${id}${qs}`); + return { content: [{ type: 'text', text: JSON.stringify(data) }] }; + } + ); +}; + +export { registerTools }; diff --git a/packages/server/src/rest/v1/aiProviders.ts b/packages/server/src/rest/v1/aiProviders.ts new file mode 100644 index 00000000..97c37bea --- /dev/null +++ b/packages/server/src/rest/v1/aiProviders.ts @@ -0,0 +1,508 @@ +import { Router } from '@ttoss/http-server'; +import type { AiProviderSlug } from '@soat/postgresdb'; +import { AI_PROVIDER_SLUGS } from '@soat/postgresdb'; +import type { Context } from 'src/Context'; +import { db } from 'src/db'; +import { + createAiProvider, + deleteAiProvider, + getAiProvider, + listAiProviders, + updateAiProvider, +} from 'src/lib/aiProviders'; + +const aiProvidersRouter = new Router(); + +/** + * @openapi + * /ai-providers: + * get: + * tags: + * - AI Providers + * summary: List AI providers + * description: Returns all AI providers in the project. + * operationId: listAiProviders + * parameters: + * - name: projectId + * in: query + * required: false + * description: Project ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: List of AI providers + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/AiProviderRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +aiProvidersRouter.get('/ai-providers', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const projectPublicId = ctx.query.projectId as string | undefined; + + const projectIds = await ctx.authUser.resolveProjectIds({ + projectPublicId, + action: 'aiProviders:ListAiProviders', + }); + + if (projectIds === null) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await listAiProviders({ projectIds: projectIds ?? [] }); +}); + +/** + * @openapi + * /ai-providers/{aiProviderId}: + * get: + * tags: + * - AI Providers + * summary: Get an AI provider by ID + * description: Returns AI provider details. The secret value is never returned. + * operationId: getAiProvider + * parameters: + * - name: aiProviderId + * in: path + * required: true + * description: AI Provider ID + * schema: + * type: string + * example: 'aip_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: AI provider found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AiProviderRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: AI provider not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +aiProvidersRouter.get('/ai-providers/:aiProviderId', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const provider = await getAiProvider({ id: ctx.params.aiProviderId }); + if (!provider) { + ctx.status = 404; + ctx.body = { error: 'AI provider not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: provider.projectId!, + action: 'aiProviders:GetAiProvider', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = provider; +}); + +/** + * @openapi + * /ai-providers: + * post: + * tags: + * - AI Providers + * summary: Create an AI provider + * description: Creates a new AI provider configuration. + * operationId: createAiProvider + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - provider + * - defaultModel + * properties: + * projectId: + * type: string + * description: Project ID. Required for JWT auth; omit when using a project key. + * example: 'proj_V1StGXR8Z5jdHi6B' + * secretId: + * type: string + * description: Secret ID containing the provider credentials + * example: 'sec_V1StGXR8Z5jdHi6B' + * name: + * type: string + * example: 'OpenAI Production' + * provider: + * type: string + * enum: [openai, anthropic, google, xai, groq, ollama, azure, bedrock, gateway, custom] + * example: 'openai' + * defaultModel: + * type: string + * example: 'gpt-4o' + * baseUrl: + * type: string + * example: 'https://api.openai.com/v1' + * config: + * type: object + * description: Provider-specific configuration + * responses: + * '201': + * description: AI provider created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AiProviderRecord' + * '400': + * description: Invalid request body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +aiProvidersRouter.post('/ai-providers', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { + projectId?: string; + secretId?: string; + name?: string; + provider?: string; + defaultModel?: string; + baseUrl?: string; + config?: Record; + }; + + if (!body.name) { + ctx.status = 400; + ctx.body = { error: 'name is required' }; + return; + } + if ( + !body.provider || + !AI_PROVIDER_SLUGS.includes(body.provider as AiProviderSlug) + ) { + ctx.status = 400; + ctx.body = { + error: `provider must be one of: ${AI_PROVIDER_SLUGS.join(', ')}`, + }; + return; + } + if (!body.defaultModel) { + ctx.status = 400; + ctx.body = { error: 'defaultModel is required' }; + return; + } + + let resolvedProjectPublicId = body.projectId; + if (!resolvedProjectPublicId) { + if (ctx.authUser.projectKeyProjectId) { + resolvedProjectPublicId = ctx.authUser.projectKeyProjectId; + } else { + ctx.status = 400; + ctx.body = { error: 'projectId is required' }; + return; + } + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: resolvedProjectPublicId, + action: 'aiProviders:CreateAiProvider', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ + where: { publicId: resolvedProjectPublicId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + let resolvedSecretId: number | undefined; + if (body.secretId) { + const secret = await db.Secret.findOne({ + where: { publicId: body.secretId, projectId: project.id }, + }); + if (!secret) { + ctx.status = 400; + ctx.body = { error: 'Invalid secret ID' }; + return; + } + resolvedSecretId = secret.id; + } + + const provider = await createAiProvider({ + projectId: project.id, + secretId: resolvedSecretId, + name: body.name, + provider: body.provider as AiProviderSlug, + defaultModel: body.defaultModel, + baseUrl: body.baseUrl, + config: body.config, + }); + + ctx.status = 201; + ctx.body = provider; +}); + +/** + * @openapi + * /ai-providers/{aiProviderId}: + * patch: + * tags: + * - AI Providers + * summary: Update an AI provider + * description: Updates the configuration of an AI provider. + * operationId: updateAiProvider + * parameters: + * - name: aiProviderId + * in: path + * required: true + * description: AI Provider ID + * schema: + * type: string + * example: 'aip_V1StGXR8Z5jdHi6B' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * secretId: + * type: string + * name: + * type: string + * provider: + * type: string + * enum: [openai, anthropic, google, xai, groq, ollama, azure, bedrock, gateway, custom] + * defaultModel: + * type: string + * baseUrl: + * type: string + * config: + * type: object + * responses: + * '200': + * description: AI provider updated + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AiProviderRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: AI provider not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +aiProvidersRouter.patch('/ai-providers/:aiProviderId', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const existing = await getAiProvider({ id: ctx.params.aiProviderId }); + if (!existing) { + ctx.status = 404; + ctx.body = { error: 'AI provider not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: existing.projectId!, + action: 'aiProviders:UpdateAiProvider', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const body = ctx.request.body as { + secretId?: string; + name?: string; + provider?: string; + defaultModel?: string; + baseUrl?: string | null; + config?: Record | null; + }; + + let resolvedSecretId: number | undefined; + if (body.secretId !== undefined) { + const project = await db.Project.findOne({ + where: { publicId: existing.projectId! }, + }); + const secret = await db.Secret.findOne({ + where: { publicId: body.secretId, projectId: project!.id }, + }); + if (!secret) { + ctx.status = 400; + ctx.body = { error: 'Invalid secret ID' }; + return; + } + resolvedSecretId = secret.id; + } + + const updated = await updateAiProvider({ + id: ctx.params.aiProviderId, + secretId: resolvedSecretId, + name: body.name, + provider: body.provider as AiProviderSlug | undefined, + defaultModel: body.defaultModel, + baseUrl: body.baseUrl, + config: body.config, + }); + + ctx.body = updated; +}); + +/** + * @openapi + * /ai-providers/{aiProviderId}: + * delete: + * tags: + * - AI Providers + * summary: Delete an AI provider + * description: Deletes an AI provider. + * operationId: deleteAiProvider + * parameters: + * - name: aiProviderId + * in: path + * required: true + * description: AI Provider ID + * schema: + * type: string + * example: 'aip_V1StGXR8Z5jdHi6B' + * responses: + * '204': + * description: AI provider deleted + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: AI provider not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +aiProvidersRouter.delete( + '/ai-providers/:aiProviderId', + async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const existing = await getAiProvider({ id: ctx.params.aiProviderId }); + if (!existing) { + ctx.status = 404; + ctx.body = { error: 'AI provider not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: existing.projectId!, + action: 'aiProviders:DeleteAiProvider', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + await deleteAiProvider({ id: ctx.params.aiProviderId }); + ctx.status = 204; + } +); + +export { aiProvidersRouter }; diff --git a/packages/server/src/rest/v1/index.ts b/packages/server/src/rest/v1/index.ts index 35bbc5c8..7e6373ec 100644 --- a/packages/server/src/rest/v1/index.ts +++ b/packages/server/src/rest/v1/index.ts @@ -2,22 +2,26 @@ import { Router } from '@ttoss/http-server'; import { agentsRouter } from './agents'; import { actorsRouter } from './actors'; +import { aiProvidersRouter } from './aiProviders'; import { projectKeysRouter } from './projectKeys'; import { conversationsRouter } from './conversations'; import { documentsRouter } from './documents'; import { filesRouter } from './files'; import { projectsRouter } from './projects'; +import { secretsRouter } from './secrets'; import { usersRouter } from './users'; const v1Router = new Router(); v1Router.use(agentsRouter.routes()); v1Router.use(actorsRouter.routes()); +v1Router.use(aiProvidersRouter.routes()); v1Router.use(projectKeysRouter.routes()); v1Router.use(conversationsRouter.routes()); v1Router.use(documentsRouter.routes()); v1Router.use(filesRouter.routes()); v1Router.use(projectsRouter.routes()); +v1Router.use(secretsRouter.routes()); v1Router.use(usersRouter.routes()); export { v1Router }; diff --git a/packages/server/src/rest/v1/secrets.ts b/packages/server/src/rest/v1/secrets.ts new file mode 100644 index 00000000..d963ff20 --- /dev/null +++ b/packages/server/src/rest/v1/secrets.ts @@ -0,0 +1,441 @@ +import { Router } from '@ttoss/http-server'; +import type { Context } from 'src/Context'; +import { db } from 'src/db'; +import { + createSecret, + deleteSecret, + getSecret, + listSecrets, + updateSecret, +} from 'src/lib/secrets'; + +const secretsRouter = new Router(); + +/** + * @openapi + * /secrets: + * get: + * tags: + * - Secrets + * summary: List secrets + * description: Returns all secrets in the project. Values are never returned. + * operationId: listSecrets + * parameters: + * - name: projectId + * in: query + * required: false + * description: Project ID + * schema: + * type: string + * example: 'proj_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: List of secrets + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/SecretRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +secretsRouter.get('/secrets', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const projectPublicId = ctx.query.projectId as string | undefined; + + const projectIds = await ctx.authUser.resolveProjectIds({ + projectPublicId, + action: 'secrets:ListSecrets', + }); + + if (projectIds === null) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = await listSecrets({ projectIds: projectIds ?? [] }); +}); + +/** + * @openapi + * /secrets/{secretId}: + * get: + * tags: + * - Secrets + * summary: Get a secret by ID + * description: Returns secret metadata. The value is never returned. + * operationId: getSecret + * parameters: + * - name: secretId + * in: path + * required: true + * description: Secret ID + * schema: + * type: string + * example: 'sec_V1StGXR8Z5jdHi6B' + * responses: + * '200': + * description: Secret found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SecretRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Secret not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +secretsRouter.get('/secrets/:secretId', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const secret = await getSecret({ id: ctx.params.secretId }); + + if (!secret) { + ctx.status = 404; + ctx.body = { error: 'Secret not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: secret.projectId!, + action: 'secrets:GetSecret', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + ctx.body = secret; +}); + +/** + * @openapi + * /secrets: + * post: + * tags: + * - Secrets + * summary: Create a secret + * description: Creates a new secret. The value is encrypted at rest and never returned. + * operationId: createSecret + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * properties: + * projectId: + * type: string + * description: Project ID. Required for JWT auth; omit when using a project key. + * example: 'proj_V1StGXR8Z5jdHi6B' + * name: + * type: string + * example: 'OpenAI Production Key' + * value: + * type: string + * description: The secret value to encrypt and store + * example: 'sk-...' + * responses: + * '201': + * description: Secret created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SecretRecord' + * '400': + * description: Invalid request body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +secretsRouter.post('/secrets', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const body = ctx.request.body as { + projectId?: string; + name?: string; + value?: string; + }; + + if (!body.name) { + ctx.status = 400; + ctx.body = { error: 'name is required' }; + return; + } + + let resolvedProjectPublicId = body.projectId; + if (!resolvedProjectPublicId) { + if (ctx.authUser.projectKeyProjectId) { + resolvedProjectPublicId = ctx.authUser.projectKeyProjectId; + } else { + ctx.status = 400; + ctx.body = { error: 'projectId is required' }; + return; + } + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: resolvedProjectPublicId, + action: 'secrets:CreateSecret', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const project = await db.Project.findOne({ + where: { publicId: resolvedProjectPublicId }, + }); + if (!project) { + ctx.status = 400; + ctx.body = { error: 'Invalid project ID' }; + return; + } + + const secret = await createSecret({ + projectId: project.id, + name: body.name, + value: body.value, + }); + + ctx.status = 201; + ctx.body = secret; +}); + +/** + * @openapi + * /secrets/{secretId}: + * patch: + * tags: + * - Secrets + * summary: Update a secret + * description: Updates the name or value of a secret. + * operationId: updateSecret + * parameters: + * - name: secretId + * in: path + * required: true + * description: Secret ID + * schema: + * type: string + * example: 'sec_V1StGXR8Z5jdHi6B' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * value: + * type: string + * description: New secret value to encrypt and store + * responses: + * '200': + * description: Secret updated + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SecretRecord' + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Secret not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +secretsRouter.patch('/secrets/:secretId', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const secret = await getSecret({ id: ctx.params.secretId }); + if (!secret) { + ctx.status = 404; + ctx.body = { error: 'Secret not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: secret.projectId!, + action: 'secrets:UpdateSecret', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const body = ctx.request.body as { name?: string; value?: string }; + + const updated = await updateSecret({ + id: ctx.params.secretId, + name: body.name, + value: body.value, + }); + + ctx.body = updated; +}); + +/** + * @openapi + * /secrets/{secretId}: + * delete: + * tags: + * - Secrets + * summary: Delete a secret + * description: Deletes a secret. Returns 409 if referenced by an AI provider unless force=true. + * operationId: deleteSecret + * parameters: + * - name: secretId + * in: path + * required: true + * description: Secret ID + * schema: + * type: string + * example: 'sec_V1StGXR8Z5jdHi6B' + * - name: force + * in: query + * required: false + * description: If true, also delete dependent AI providers + * schema: + * type: boolean + * responses: + * '204': + * description: Secret deleted + * '401': + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '403': + * description: Forbidden + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '404': + * description: Secret not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * '409': + * description: Conflict — secret is referenced by one or more AI providers + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ +secretsRouter.delete('/secrets/:secretId', async (ctx: Context) => { + if (!ctx.authUser) { + ctx.status = 401; + ctx.body = { error: 'Unauthorized' }; + return; + } + + const secret = await getSecret({ id: ctx.params.secretId }); + if (!secret) { + ctx.status = 404; + ctx.body = { error: 'Secret not found' }; + return; + } + + const allowed = await ctx.authUser.isAllowed({ + projectPublicId: secret.projectId!, + action: 'secrets:DeleteSecret', + }); + if (!allowed) { + ctx.status = 403; + ctx.body = { error: 'Forbidden' }; + return; + } + + const force = ctx.query.force === 'true'; + const result = await deleteSecret({ id: ctx.params.secretId, force }); + + if (result === 'conflict') { + ctx.status = 409; + ctx.body = { + error: + 'Secret is referenced by one or more AI providers. Use force=true to delete them as well.', + }; + return; + } + + ctx.status = 204; +}); + +export { secretsRouter }; diff --git a/packages/server/tests/unit/tests/aiProviders.test.ts b/packages/server/tests/unit/tests/aiProviders.test.ts new file mode 100644 index 00000000..653712dd --- /dev/null +++ b/packages/server/tests/unit/tests/aiProviders.test.ts @@ -0,0 +1,365 @@ +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +describe('AI Providers', () => { + let adminToken: string; + let userToken: string; + let userId: string; + let projectId: string; + let otherProjectId: string; + let policyId: string; + let secretId: string; + + beforeAll(async () => { + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.SECRETS_ENCRYPTION_KEY = '0'.repeat(64); + + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'aiprovadmin', password: 'supersecret' }); + + adminToken = await loginAs('aiprovadmin', 'supersecret'); + + const createUserRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'aiprovuser', password: 'aiprovpass' }); + + userId = createUserRes.body.id; + userToken = await loginAs('aiprovuser', 'aiprovpass'); + + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'AI Providers Test Project' }); + projectId = projectRes.body.id; + + const otherProjectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'AI Providers Other Project' }); + otherProjectId = otherProjectRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: [ + 'aiProviders:ListAiProviders', + 'aiProviders:GetAiProvider', + 'aiProviders:CreateAiProvider', + 'aiProviders:UpdateAiProvider', + 'aiProviders:DeleteAiProvider', + ], + }); + policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + + const secretRes = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId, name: 'AI Provider Secret', value: 'sk-test' }); + secretId = secretRes.body.id; + }); + + describe('GET /api/v1/ai-providers', () => { + test('authenticated user can list AI providers', async () => { + const response = await authenticatedTestClient(userToken) + .get('/api/v1/ai-providers') + .query({ projectId }); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get('/api/v1/ai-providers'); + expect(response.status).toBe(401); + }); + + test('user without access to project returns 403', async () => { + const response = await authenticatedTestClient(userToken) + .get('/api/v1/ai-providers') + .query({ projectId: otherProjectId }); + + expect(response.status).toBe(403); + }); + }); + + describe('POST /api/v1/ai-providers', () => { + test('authenticated user with permission can create an AI provider', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + name: 'My OpenAI', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.name).toBe('My OpenAI'); + expect(response.body.projectId).toBe(projectId); + expect(response.body.provider).toBe('openai'); + expect(response.body.defaultModel).toBe('gpt-4o'); + expect(response.body.secretId).toBeNull(); + }); + + test('can create AI provider linked to a secret', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + secretId, + name: 'My OpenAI With Key', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + expect(response.status).toBe(201); + expect(response.body.secretId).toBe(secretId); + }); + + test('create without name returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/ai-providers') + .send({ projectId, provider: 'openai', defaultModel: 'gpt-4o' }); + + expect(response.status).toBe(400); + }); + + test('create with invalid provider returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + name: 'x', + provider: 'invalid', + defaultModel: 'gpt-4o', + }); + + expect(response.status).toBe(400); + }); + + test('create without defaultModel returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/ai-providers') + .send({ projectId, name: 'x', provider: 'openai' }); + + expect(response.status).toBe(400); + }); + + test('create with secretId from wrong project returns 400', async () => { + const otherSecretRes = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId: otherProjectId, name: 'Other Project Secret' }); + const otherSecretId = otherSecretRes.body.id; + + const response = await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + secretId: otherSecretId, + name: 'x', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + expect(response.status).toBe(400); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.post('/api/v1/ai-providers').send({ + projectId, + name: 'x', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + expect(response.status).toBe(401); + }); + + test('user without permission on project returns 403', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/ai-providers') + .send({ + projectId: otherProjectId, + name: 'x', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + expect(response.status).toBe(403); + }); + }); + + describe('GET /api/v1/ai-providers/:aiProviderId', () => { + let aiProviderId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + name: 'Get Test Provider', + provider: 'anthropic', + defaultModel: 'claude-3-5-haiku-latest', + }); + aiProviderId = res.body.id; + }); + + test('authenticated user with permission can get an AI provider', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/ai-providers/${aiProviderId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(aiProviderId); + expect(response.body.projectId).toBe(projectId); + expect(response.body.provider).toBe('anthropic'); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get( + `/api/v1/ai-providers/${aiProviderId}` + ); + expect(response.status).toBe(401); + }); + + test('user without permission returns 403', async () => { + const adminRes = await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId: otherProjectId, + name: 'Other Provider', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + const response = await authenticatedTestClient(userToken).get( + `/api/v1/ai-providers/${adminRes.body.id}` + ); + expect(response.status).toBe(403); + }); + + test('unknown ID returns 404', async () => { + const response = await authenticatedTestClient(userToken).get( + '/api/v1/ai-providers/aip_doesnotexist' + ); + expect(response.status).toBe(404); + }); + }); + + describe('PATCH /api/v1/ai-providers/:aiProviderId', () => { + let aiProviderId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + name: 'Patch Test Provider', + provider: 'openai', + defaultModel: 'gpt-4o-mini', + }); + aiProviderId = res.body.id; + }); + + test('authenticated user with permission can update an AI provider', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/ai-providers/${aiProviderId}`) + .send({ name: 'Updated Provider', defaultModel: 'gpt-4o' }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(aiProviderId); + expect(response.body.name).toBe('Updated Provider'); + expect(response.body.defaultModel).toBe('gpt-4o'); + }); + + test('can link a secret when updating', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/ai-providers/${aiProviderId}`) + .send({ secretId }); + + expect(response.status).toBe(200); + expect(response.body.secretId).toBe(secretId); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .patch(`/api/v1/ai-providers/${aiProviderId}`) + .send({ name: 'x' }); + expect(response.status).toBe(401); + }); + + test('user without permission returns 403', async () => { + const adminRes = await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId: otherProjectId, + name: 'Other Patch Provider', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/ai-providers/${adminRes.body.id}`) + .send({ name: 'x' }); + expect(response.status).toBe(403); + }); + + test('unknown ID returns 404', async () => { + const response = await authenticatedTestClient(userToken) + .patch('/api/v1/ai-providers/aip_doesnotexist') + .send({ name: 'x' }); + expect(response.status).toBe(404); + }); + }); + + describe('DELETE /api/v1/ai-providers/:aiProviderId', () => { + test('authenticated user with permission can delete an AI provider', async () => { + const createRes = await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + name: 'To Delete', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + const aiProviderId = createRes.body.id; + + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/ai-providers/${aiProviderId}` + ); + expect(response.status).toBe(204); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.delete( + '/api/v1/ai-providers/aip_doesnotexist' + ); + expect(response.status).toBe(401); + }); + + test('user without permission returns 403', async () => { + const adminRes = await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId: otherProjectId, + name: 'Other Delete Provider', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/ai-providers/${adminRes.body.id}` + ); + expect(response.status).toBe(403); + }); + + test('unknown ID returns 404', async () => { + const response = await authenticatedTestClient(userToken).delete( + '/api/v1/ai-providers/aip_doesnotexist' + ); + expect(response.status).toBe(404); + }); + }); +}); diff --git a/packages/server/tests/unit/tests/secrets.test.ts b/packages/server/tests/unit/tests/secrets.test.ts new file mode 100644 index 00000000..c4e8eb13 --- /dev/null +++ b/packages/server/tests/unit/tests/secrets.test.ts @@ -0,0 +1,300 @@ +import { authenticatedTestClient, loginAs, testClient } from '../testClient'; + +describe('Secrets', () => { + let adminToken: string; + let userToken: string; + let userId: string; + let projectId: string; + let otherProjectId: string; + let policyId: string; + + beforeAll(async () => { + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.SECRETS_ENCRYPTION_KEY = '0'.repeat(64); + + await testClient + .post('/api/v1/users/bootstrap') + .send({ username: 'secretsadmin', password: 'supersecret' }); + + adminToken = await loginAs('secretsadmin', 'supersecret'); + + const createUserRes = await authenticatedTestClient(adminToken) + .post('/api/v1/users') + .send({ username: 'secretsuser', password: 'secretspass' }); + + userId = createUserRes.body.id; + userToken = await loginAs('secretsuser', 'secretspass'); + + const projectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Secrets Test Project' }); + projectId = projectRes.body.id; + + const otherProjectRes = await authenticatedTestClient(adminToken) + .post('/api/v1/projects') + .send({ name: 'Secrets Other Project' }); + otherProjectId = otherProjectRes.body.id; + + const policyRes = await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/policies`) + .send({ + permissions: [ + 'secrets:ListSecrets', + 'secrets:GetSecret', + 'secrets:CreateSecret', + 'secrets:UpdateSecret', + 'secrets:DeleteSecret', + ], + }); + policyId = policyRes.body.id; + + await authenticatedTestClient(adminToken) + .post(`/api/v1/projects/${projectId}/members`) + .send({ userId, policyId }); + }); + + describe('GET /api/v1/secrets', () => { + test('authenticated user can list secrets', async () => { + const response = await authenticatedTestClient(userToken) + .get('/api/v1/secrets') + .query({ projectId }); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get('/api/v1/secrets'); + expect(response.status).toBe(401); + }); + + test('user without access to project returns 403', async () => { + const response = await authenticatedTestClient(userToken) + .get('/api/v1/secrets') + .query({ projectId: otherProjectId }); + + expect(response.status).toBe(403); + }); + }); + + describe('POST /api/v1/secrets', () => { + test('authenticated user with permission can create a secret', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/secrets') + .send({ projectId, name: 'Test Secret', value: 'supersecretvalue' }); + + expect(response.status).toBe(201); + expect(response.body.id).toBeDefined(); + expect(response.body.name).toBe('Test Secret'); + expect(response.body.projectId).toBe(projectId); + expect(response.body.hasValue).toBe(true); + // value must never be returned + expect(response.body.value).toBeUndefined(); + }); + + test('create without name returns 400', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/secrets') + .send({ projectId }); + + expect(response.status).toBe(400); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .post('/api/v1/secrets') + .send({ projectId, name: 'Test' }); + + expect(response.status).toBe(401); + }); + + test('user without permission on project returns 403', async () => { + const response = await authenticatedTestClient(userToken) + .post('/api/v1/secrets') + .send({ projectId: otherProjectId, name: 'Test' }); + + expect(response.status).toBe(403); + }); + }); + + describe('GET /api/v1/secrets/:secretId', () => { + let secretId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId, name: 'Get Test Secret' }); + secretId = res.body.id; + }); + + test('authenticated user with permission can get a secret', async () => { + const response = await authenticatedTestClient(userToken).get( + `/api/v1/secrets/${secretId}` + ); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(secretId); + expect(response.body.projectId).toBe(projectId); + expect(response.body.value).toBeUndefined(); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.get(`/api/v1/secrets/${secretId}`); + expect(response.status).toBe(401); + }); + + test('user without permission returns 403', async () => { + // Create a secret in otherProject (as admin) and try to access it as user + const adminRes = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId: otherProjectId, name: 'Other Secret' }); + const otherId = adminRes.body.id; + + const response = await authenticatedTestClient(userToken).get( + `/api/v1/secrets/${otherId}` + ); + expect(response.status).toBe(403); + }); + + test('unknown ID returns 404', async () => { + const response = await authenticatedTestClient(userToken).get( + '/api/v1/secrets/sec_doesnotexist' + ); + expect(response.status).toBe(404); + }); + }); + + describe('PATCH /api/v1/secrets/:secretId', () => { + let secretId: string; + + beforeAll(async () => { + const res = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId, name: 'Patch Test Secret' }); + secretId = res.body.id; + }); + + test('authenticated user with permission can update a secret', async () => { + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/secrets/${secretId}`) + .send({ name: 'Updated Name', value: 'newvalue' }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(secretId); + expect(response.body.name).toBe('Updated Name'); + expect(response.body.hasValue).toBe(true); + expect(response.body.value).toBeUndefined(); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient + .patch(`/api/v1/secrets/${secretId}`) + .send({ name: 'x' }); + expect(response.status).toBe(401); + }); + + test('user without permission returns 403', async () => { + const adminRes = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId: otherProjectId, name: 'Other Patch Secret' }); + + const response = await authenticatedTestClient(userToken) + .patch(`/api/v1/secrets/${adminRes.body.id}`) + .send({ name: 'x' }); + expect(response.status).toBe(403); + }); + + test('unknown ID returns 404', async () => { + const response = await authenticatedTestClient(userToken) + .patch('/api/v1/secrets/sec_doesnotexist') + .send({ name: 'x' }); + expect(response.status).toBe(404); + }); + }); + + describe('DELETE /api/v1/secrets/:secretId', () => { + test('authenticated user with permission can delete a secret', async () => { + const createRes = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId, name: 'To Delete' }); + const secretId = createRes.body.id; + + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/secrets/${secretId}` + ); + expect(response.status).toBe(204); + }); + + test('unauthenticated request returns 401', async () => { + const response = await testClient.delete( + '/api/v1/secrets/sec_doesnotexist' + ); + expect(response.status).toBe(401); + }); + + test('user without permission returns 403', async () => { + const adminRes = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId: otherProjectId, name: 'Other Delete Secret' }); + + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/secrets/${adminRes.body.id}` + ); + expect(response.status).toBe(403); + }); + + test('unknown ID returns 404', async () => { + const response = await authenticatedTestClient(userToken).delete( + '/api/v1/secrets/sec_doesnotexist' + ); + expect(response.status).toBe(404); + }); + + test('secret referenced by AI provider returns 409 without force', async () => { + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.SECRETS_ENCRYPTION_KEY = '0'.repeat(64); + + const secretRes = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId, name: 'Linked Secret' }); + const linkedSecretId = secretRes.body.id; + + await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + secretId: linkedSecretId, + name: 'Test Provider', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/secrets/${linkedSecretId}` + ); + expect(response.status).toBe(409); + }); + + test('secret referenced by AI provider deleted with force=true returns 204', async () => { + const secretRes = await authenticatedTestClient(adminToken) + .post('/api/v1/secrets') + .send({ projectId, name: 'Force Delete Secret' }); + const linkedSecretId = secretRes.body.id; + + await authenticatedTestClient(adminToken) + .post('/api/v1/ai-providers') + .send({ + projectId, + secretId: linkedSecretId, + name: 'Test Provider Force', + provider: 'openai', + defaultModel: 'gpt-4o', + }); + + const response = await authenticatedTestClient(userToken).delete( + `/api/v1/secrets/${linkedSecretId}?force=true` + ); + expect(response.status).toBe(204); + }); + }); +}); diff --git a/packages/website/docs/modules/ai-providers.md b/packages/website/docs/modules/ai-providers.md new file mode 100644 index 00000000..1790da4e --- /dev/null +++ b/packages/website/docs/modules/ai-providers.md @@ -0,0 +1,53 @@ +# AI Providers Module + +The AI Providers module lets you register and manage LLM provider configurations for a project. Each provider record stores the model slug, optional base URL, optional configuration, and an optional link to a [Secret](./secrets.md) that supplies the API key. + +## Overview + +An AI provider is a named configuration that tells the system how to reach a specific LLM endpoint. A project can have multiple providers — for example, one for GPT-4o and another for Claude 3.5. + +When a provider is linked to a secret the secret's encrypted value is retrieved and passed as the API key when calling the LLM. The key is never exposed through the API. + +## Data Model + +| Field | Type | Description | +| -------------- | ---------------- | --------------------------------------------------------- | +| `id` | string | Public identifier (e.g. `aip_…`) | +| `projectId` | string | ID of the owning project | +| `secretId` | string \| null | Public ID of the linked secret, or `null` | +| `name` | string | Human-readable label | +| `provider` | `AiProviderSlug` | Provider slug (see below) | +| `defaultModel` | string | Default model name sent to the provider API | +| `baseUrl` | string \| null | Override base URL (optional, useful for self-hosted LLMs) | +| `config` | object \| null | Arbitrary provider-specific configuration object | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +### Provider Slugs + +Valid values for the `provider` field: + +| Slug | Description | +| ----------- | -------------------------- | +| `openai` | OpenAI | +| `anthropic` | Anthropic | +| `google` | Google Gemini | +| `xai` | xAI (Grok) | +| `groq` | Groq | +| `ollama` | Ollama (local) | +| `azure` | Azure OpenAI | +| `bedrock` | Amazon Bedrock | +| `gateway` | Generic API gateway | +| `custom` | Custom / self-hosted model | + +## Permissions + +| Action | Permission | REST Endpoint | MCP Tool | +| --------------- | ------------------------------ | ------------------------------------------- | -------------------- | +| List providers | `aiProviders:ListAiProviders` | `GET /api/v1/ai-providers` | `list-ai-providers` | +| Get a provider | `aiProviders:GetAiProvider` | `GET /api/v1/ai-providers/:aiProviderId` | `get-ai-provider` | +| Create provider | `aiProviders:CreateAiProvider` | `POST /api/v1/ai-providers` | `create-ai-provider` | +| Update provider | `aiProviders:UpdateAiProvider` | `PATCH /api/v1/ai-providers/:aiProviderId` | `update-ai-provider` | +| Delete provider | `aiProviders:DeleteAiProvider` | `DELETE /api/v1/ai-providers/:aiProviderId` | `delete-ai-provider` | + +See the [API Reference](../api/ai-providers/list-ai-providers) for full endpoint details, request/response schemas, and status codes. diff --git a/packages/website/docs/modules/secrets.md b/packages/website/docs/modules/secrets.md new file mode 100644 index 00000000..dc8d08b2 --- /dev/null +++ b/packages/website/docs/modules/secrets.md @@ -0,0 +1,48 @@ +# Secrets Module + +The Secrets module provides encrypted storage for sensitive values such as API keys and credentials. Values are encrypted at rest using AES-256-GCM and are never returned by any API response. + +## Overview + +Secrets are associated with a project. Once stored, a secret's value can only be replaced — it is never readable again. All operations return a `hasValue` boolean to indicate whether an encrypted value is on file. + +Secrets can be linked to [AI Providers](./ai-providers.md) to supply credentials at inference time. + +## Configuration + +| Environment Variable | Required | Description | +| ------------------------ | -------- | ------------------------------------------------------------------------------------------------ | +| `SECRETS_ENCRYPTION_KEY` | Yes | 64-character hex string (32 bytes). Used for AES-256-GCM encryption of all stored secret values. | + +Generate a key with: + +```bash +openssl rand -hex 32 +``` + +## Data Model + +| Field | Type | Description | +| ----------- | ------- | ---------------------------------------- | +| `id` | string | Public identifier (e.g. `sec_…`) | +| `projectId` | string | ID of the owning project | +| `name` | string | Human-readable label | +| `hasValue` | boolean | `true` when an encrypted value is stored | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 last-updated timestamp | + +## Deletion behaviour + +By default, deleting a secret that is still referenced by one or more AI providers returns `409 Conflict`. Pass `?force=true` to cascade-delete the dependent AI providers along with the secret. + +## Permissions + +| Action | Permission | REST Endpoint | MCP Tool | +| ------------- | ---------------------- | ---------------------------------- | --------------- | +| List secrets | `secrets:ListSecrets` | `GET /api/v1/secrets` | `list-secrets` | +| Get a secret | `secrets:GetSecret` | `GET /api/v1/secrets/:secretId` | `get-secret` | +| Create secret | `secrets:CreateSecret` | `POST /api/v1/secrets` | `create-secret` | +| Update secret | `secrets:UpdateSecret` | `PATCH /api/v1/secrets/:secretId` | `update-secret` | +| Delete secret | `secrets:DeleteSecret` | `DELETE /api/v1/secrets/:secretId` | `delete-secret` | + +See the [API Reference](../api/secrets/list-secrets) for full endpoint details, request/response schemas, and status codes. From d54c5ed553d7537d75c20d82d603760a9e68f3e8 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 06:51:56 -0300 Subject: [PATCH 16/27] fix: pr pipeline --- .github/instructions/general.instructions.md | 16 +++++++ .github/workflows/pr.yml | 46 ++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/.github/instructions/general.instructions.md b/.github/instructions/general.instructions.md index 12d6e966..bb6c8886 100644 --- a/.github/instructions/general.instructions.md +++ b/.github/instructions/general.instructions.md @@ -35,3 +35,19 @@ If you need to change the database schema, read the instructions in `packages/po ## Documentation All documentation must be written in English. The target audience is developers with technical skills — write concisely and precisely, assume familiarity with REST APIs, JWT, and common backend concepts, and prefer code examples over prose descriptions. + +## Implementation Checklist + +Every implementation — whether adding a new feature or changing existing behavior — must complete all of the following steps before being considered done: + +1. **Implement business logic** — Write or update code in `packages/server/src/lib/.ts`. All database access goes here; route handlers must stay free of direct DB calls. + +2. **REST API** — Add or update route handlers in `packages/server/src/rest/v1/.ts`. Every handler must have an `@openapi` JSDoc block and the corresponding OpenAPI spec in `packages/server/src/rest/openapi/v1/.yaml` must be kept in sync. + +3. **Module docs** — Update the module documentation page at `packages/website/docs/modules/.md`, including any changes to the data model, key concepts, or the `## Permissions` table. + +4. **MCP tool** (project-scoped changes only) — If the change affects a resource that is exposed through the MCP server, add or update the tool in `packages/server/src/mcp/tools/.ts` and ensure it is registered in `packages/server/src/mcp/tools/index.ts`. + +5. **Tests** — Add or update tests in `packages/server/tests/unit/tests/.test.ts`. Every new route and every changed lib function must have coverage (happy path, `401`, `403`, and relevant edge cases). + +6. **Smoke test** (when applicable) — If the change introduces a new user-facing flow (e.g., a new resource lifecycle), add the corresponding steps to `tests/smoke-test.sh`. Run it with `pnpm run -w smoke-test` to verify end-to-end behaviour against a live server. diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 842e313b..95623593 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,8 +18,54 @@ jobs: node-version: '24' cache: 'pnpm' + - name: Cache Turbo + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build and test run: pnpm turbo run build test + + smoke-test: + name: Smoke Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run smoke tests + run: pnpm run smoke-test + + all-checks: + name: All Checks Passed + runs-on: ubuntu-latest + needs: + - build-and-test + - smoke-test + if: always() + + steps: + - name: Check all jobs passed + run: | + if [[ "${{ needs.build-and-test.result }}" != "success" || \ + "${{ needs.smoke-test.result }}" != "success" ]]; then + echo "One or more jobs failed." + exit 1 + fi + echo "All checks passed." From eb2473de3ab3f7289755d4533fa2f34d8677f8cd Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 06:53:24 -0300 Subject: [PATCH 17/27] fix: remove blog --- .../blog/2019-05-28-first-blog-post.md | 12 ----- .../website/blog/2019-05-29-long-blog-post.md | 44 ------------------ .../website/blog/2021-08-01-mdx-blog-post.mdx | 24 ---------- .../docusaurus-plushie-banner.jpeg | Bin 96122 -> 0 bytes .../website/blog/2021-08-26-welcome/index.md | 29 ------------ packages/website/blog/authors.yml | 25 ---------- packages/website/blog/tags.yml | 19 -------- 7 files changed, 153 deletions(-) delete mode 100644 packages/website/blog/2019-05-28-first-blog-post.md delete mode 100644 packages/website/blog/2019-05-29-long-blog-post.md delete mode 100644 packages/website/blog/2021-08-01-mdx-blog-post.mdx delete mode 100644 packages/website/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg delete mode 100644 packages/website/blog/2021-08-26-welcome/index.md delete mode 100644 packages/website/blog/authors.yml delete mode 100644 packages/website/blog/tags.yml diff --git a/packages/website/blog/2019-05-28-first-blog-post.md b/packages/website/blog/2019-05-28-first-blog-post.md deleted file mode 100644 index 986f0429..00000000 --- a/packages/website/blog/2019-05-28-first-blog-post.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -slug: first-blog-post -title: First Blog Post -authors: [slorber, yangshun] -tags: [hola, docusaurus] ---- - -Lorem ipsum dolor sit amet... - -{/_ truncate _/} - -...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/packages/website/blog/2019-05-29-long-blog-post.md b/packages/website/blog/2019-05-29-long-blog-post.md deleted file mode 100644 index 3cd8c101..00000000 --- a/packages/website/blog/2019-05-29-long-blog-post.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -slug: long-blog-post -title: Long Blog Post -authors: yangshun -tags: [hello, docusaurus] ---- - -This is the summary of a very long blog post, - -Use a `` comment to limit blog post size in the list view. - -{/_ truncate _/} - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/packages/website/blog/2021-08-01-mdx-blog-post.mdx b/packages/website/blog/2021-08-01-mdx-blog-post.mdx deleted file mode 100644 index 0c4b4a48..00000000 --- a/packages/website/blog/2021-08-01-mdx-blog-post.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -slug: mdx-blog-post -title: MDX Blog Post -authors: [slorber] -tags: [docusaurus] ---- - -Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/). - -:::tip - -Use the power of React to create interactive blog posts. - -::: - -{/* truncate */} - -For example, use JSX to create an interactive button: - -```js - -``` - - diff --git a/packages/website/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg b/packages/website/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg deleted file mode 100644 index 11bda0928456b12f8e53d0ba5709212a4058d449..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96122 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 diff --git a/packages/website/blog/2021-08-26-welcome/index.md b/packages/website/blog/2021-08-26-welcome/index.md deleted file mode 100644 index 7cc2a6ab..00000000 --- a/packages/website/blog/2021-08-26-welcome/index.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -slug: welcome -title: Welcome -authors: [slorber, yangshun] -tags: [facebook, hello, docusaurus] ---- - -[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). - -Here are a few tips you might find useful. - -{/_ truncate _/} - -Simply add Markdown files (or folders) to the `blog` directory. - -Regular blog authors can be added to `authors.yml`. - -The blog post date can be extracted from filenames, such as: - -- `2019-05-30-welcome.md` -- `2019-05-30-welcome/index.md` - -A blog post folder can be convenient to co-locate blog post images: - -![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) - -The blog supports tags as well! - -**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. diff --git a/packages/website/blog/authors.yml b/packages/website/blog/authors.yml deleted file mode 100644 index 0fd39873..00000000 --- a/packages/website/blog/authors.yml +++ /dev/null @@ -1,25 +0,0 @@ -yangshun: - name: Yangshun Tay - title: Ex-Meta Staff Engineer, Co-founder GreatFrontEnd - url: https://linkedin.com/in/yangshun - image_url: https://github.com/yangshun.png - page: true - socials: - x: yangshunz - linkedin: yangshun - github: yangshun - newsletter: https://www.greatfrontend.com - -slorber: - name: Sébastien Lorber - title: Docusaurus maintainer - url: https://sebastienlorber.com - image_url: https://github.com/slorber.png - page: - # customize the url of the author page at /blog/authors/ - permalink: '/all-sebastien-lorber-articles' - socials: - x: sebastienlorber - linkedin: sebastienlorber - github: slorber - newsletter: https://thisweekinreact.com diff --git a/packages/website/blog/tags.yml b/packages/website/blog/tags.yml deleted file mode 100644 index bfaa778f..00000000 --- a/packages/website/blog/tags.yml +++ /dev/null @@ -1,19 +0,0 @@ -facebook: - label: Facebook - permalink: /facebook - description: Facebook tag description - -hello: - label: Hello - permalink: /hello - description: Hello tag description - -docusaurus: - label: Docusaurus - permalink: /docusaurus - description: Docusaurus tag description - -hola: - label: Hola - permalink: /hola - description: Hola tag description From deef777fcd6407ad0fbea4217089b548c2fbd095 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 06:54:34 -0300 Subject: [PATCH 18/27] fix: build --- packages/cli/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index e0459f68..943c7ec2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -2,8 +2,7 @@ "name": "@soat/cli", "version": "0.0.0-alpha.2", "scripts": { - "build": "tsup", - "test": "jest --projects tests/unit" + "build": "tsup" }, "dependencies": { "@ttoss/logger": "^0.7.1", From cbeae21aa1c943f60ee70677fc604d95e7e2f42a Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 07:16:05 -0300 Subject: [PATCH 19/27] fix: actions --- .github/workflows/main.yml | 14 +++++++------- .github/workflows/pr.yml | 14 +++++++------- packages/website/package.json | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bc4e320f..05416c5e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '24' cache: 'pnpm' @@ -34,7 +34,7 @@ jobs: packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -58,13 +58,13 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '24' cache: 'pnpm' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 95623593..5ab67bcd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -9,17 +9,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '24' cache: 'pnpm' - name: Cache Turbo - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .turbo key: ${{ runner.os }}-turbo-${{ github.sha }} @@ -37,11 +37,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '24' cache: 'pnpm' diff --git a/packages/website/package.json b/packages/website/package.json index b7e2562a..b0437b9d 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -5,7 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "dev": "docusaurus start", - "build": "docusaurus build", + "build": "docusaurus gen-api-docs all && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", From 7d8ea057e55e261e6da71a17316762a0d4ac773a Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 07:20:54 -0300 Subject: [PATCH 20/27] fix: docker cache --- .github/workflows/main.yml | 90 -------------------------------------- .github/workflows/pr.yml | 11 +++++ docker-compose.test.yml | 7 ++- 3 files changed, 17 insertions(+), 91 deletions(-) delete mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 05416c5e..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Main - -on: - push: - branches: - - main - -jobs: - test-and-build: - name: Test and Build - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Test and build - run: pnpm turbo run build test - - deploy-docker: - name: Deploy Docker Image - runs-on: ubuntu-latest - needs: test-and-build - permissions: - packages: write - - steps: - - uses: actions/checkout@v6 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - push: true - tags: ghcr.io/${{ github.repository }}:latest - - deploy-website: - name: Deploy Website - runs-on: ubuntu-latest - needs: test-and-build - permissions: - contents: write - - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - uses: pnpm/action-setup@v5 - - - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build server (required for OpenAPI docs generation) - run: pnpm --filter @soat/postgresdb build && pnpm --filter @soat/server build - - - name: Configure git credentials - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git - - - name: Deploy website - run: pnpm --filter website deploy - env: - GIT_USER: ${{ github.actor }} - USE_SSH: 'false' - CURRENT_BRANCH: main - DEPLOYMENT_BRANCH: gh-pages diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5ab67bcd..35c2dbe7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -49,8 +49,19 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Ollama models + uses: actions/cache@v5 + with: + path: ~/.ollama/models + key: ollama-models-qwen3-embedding-0.6b-qwen2.5-0.5b + - name: Run smoke tests run: pnpm run smoke-test + env: + COMPOSE_BAKE: '1' all-checks: name: All Checks Passed diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 8afc7c92..fba9ebf3 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -33,7 +33,12 @@ services: start_period: 30s server: - build: . + build: + context: . + cache_from: + - type=gha + cache_to: + - type=gha,mode=max depends_on: database: condition: service_healthy From b334249cf5d2a048a285873f36a356a2219faee6 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 07:23:26 -0300 Subject: [PATCH 21/27] fix: build docs --- packages/website/docs/modules/ai-providers.md | 2 -- packages/website/docs/modules/secrets.md | 2 -- turbo.json | 1 - 3 files changed, 5 deletions(-) diff --git a/packages/website/docs/modules/ai-providers.md b/packages/website/docs/modules/ai-providers.md index 1790da4e..230d8698 100644 --- a/packages/website/docs/modules/ai-providers.md +++ b/packages/website/docs/modules/ai-providers.md @@ -49,5 +49,3 @@ Valid values for the `provider` field: | Create provider | `aiProviders:CreateAiProvider` | `POST /api/v1/ai-providers` | `create-ai-provider` | | Update provider | `aiProviders:UpdateAiProvider` | `PATCH /api/v1/ai-providers/:aiProviderId` | `update-ai-provider` | | Delete provider | `aiProviders:DeleteAiProvider` | `DELETE /api/v1/ai-providers/:aiProviderId` | `delete-ai-provider` | - -See the [API Reference](../api/ai-providers/list-ai-providers) for full endpoint details, request/response schemas, and status codes. diff --git a/packages/website/docs/modules/secrets.md b/packages/website/docs/modules/secrets.md index dc8d08b2..b4da5f43 100644 --- a/packages/website/docs/modules/secrets.md +++ b/packages/website/docs/modules/secrets.md @@ -44,5 +44,3 @@ By default, deleting a secret that is still referenced by one or more AI provide | Create secret | `secrets:CreateSecret` | `POST /api/v1/secrets` | `create-secret` | | Update secret | `secrets:UpdateSecret` | `PATCH /api/v1/secrets/:secretId` | `update-secret` | | Delete secret | `secrets:DeleteSecret` | `DELETE /api/v1/secrets/:secretId` | `delete-secret` | - -See the [API Reference](../api/secrets/list-secrets) for full endpoint details, request/response schemas, and status codes. diff --git a/turbo.json b/turbo.json index 553af844..befb4e55 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,5 @@ { "$schema": "./node_modules/turbo/schema.json", - "globalEnv": ["DATABASE_*"], "tasks": { "build": { "dependsOn": ["^build"], From fae53b23cb63b8d23524f0c637b6848e0cf70592 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 07:24:33 -0300 Subject: [PATCH 22/27] chore: remove unused type --- packages/server/package.json | 1 - pnpm-lock.yaml | 79 ++++-------------------------------- 2 files changed, 8 insertions(+), 72 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 5dcd53ac..344c7019 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -15,7 +15,6 @@ "@ttoss/http-server": "^0.5.9", "@ttoss/http-server-mcp": "^0.11.1", "@ttoss/postgresdb": "^0.8.0", - "@types/koa__multer": "^2.0.8", "bcryptjs": "^3.0.3", "dotenv": "^17.4.1", "jsonwebtoken": "^9.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0332e92..1f5cb7e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,9 +127,6 @@ importers: '@ttoss/postgresdb': specifier: ^0.8.0 version: 0.8.0(@types/node@25.5.2)(@types/validator@13.15.10)(reflect-metadata@0.2.2) - '@types/koa__multer': - specifier: ^2.0.8 - version: 2.0.8 bcryptjs: specifier: ^3.0.3 version: 3.0.3 @@ -3598,9 +3595,6 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/accepts@1.3.7': - resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} - '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -3638,15 +3632,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/content-disposition@0.5.9': - resolution: {integrity: sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==} - '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} - '@types/cookies@0.9.2': - resolution: {integrity: sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -3692,9 +3680,6 @@ packages: '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} - '@types/http-assert@1.5.6': - resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} - '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -3728,18 +3713,6 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/keygrip@1.0.6': - resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} - - '@types/koa-compose@3.2.9': - resolution: {integrity: sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==} - - '@types/koa@3.0.2': - resolution: {integrity: sha512-7TRzVOBcH/q8CfPh9AmHBQ8TZtimT4Sn+rw8//hXveI6+F41z93W8a+0B0O8L7apKQv+vKBIEZSECiL0Oo1JFA==} - - '@types/koa__multer@2.0.8': - resolution: {integrity: sha512-XkD9YzmGKzKfOkxwHepueQkP61okAeu6ibAieL4NSqwM1lyGOkgbT2XmP+xROd2agQVsewPqsFOsfgVmlEqJmg==} - '@types/markdown-escape@1.1.3': resolution: {integrity: sha512-JIc1+s3y5ujKnt/+N+wq6s/QdL2qZ11fP79MijrVXsAAnzSxCbT2j/3prHRouJdZ2yFLN3vkP0HytfnoCczjOw==} @@ -15548,9 +15521,9 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-config-turbo: 2.9.4(eslint@9.39.2(jiti@2.6.1))(turbo@2.9.4) - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-formatjs: 6.4.4(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jest: 29.15.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1))(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(typescript@6.0.2) eslint-plugin-jest-dom: 5.5.0(@testing-library/dom@10.4.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -15682,10 +15655,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/accepts@1.3.7': - dependencies: - '@types/node': 25.5.2 - '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -15740,17 +15709,8 @@ snapshots: dependencies: '@types/node': 25.5.2 - '@types/content-disposition@0.5.9': {} - '@types/cookiejar@2.1.5': {} - '@types/cookies@0.9.2': - dependencies: - '@types/connect': 3.4.38 - '@types/express': 4.17.25 - '@types/keygrip': 1.0.6 - '@types/node': 25.5.2 - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -15812,8 +15772,6 @@ snapshots: '@types/html-minifier-terser@6.1.0': {} - '@types/http-assert@1.5.6': {} - '@types/http-cache-semantics@4.0.4': {} '@types/http-errors@2.0.5': {} @@ -15852,27 +15810,6 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 25.5.2 - '@types/keygrip@1.0.6': {} - - '@types/koa-compose@3.2.9': - dependencies: - '@types/koa': 3.0.2 - - '@types/koa@3.0.2': - dependencies: - '@types/accepts': 1.3.7 - '@types/content-disposition': 0.5.9 - '@types/cookies': 0.9.2 - '@types/http-assert': 1.5.6 - '@types/http-errors': 2.0.5 - '@types/keygrip': 1.0.6 - '@types/koa-compose': 3.2.9 - '@types/node': 25.5.2 - - '@types/koa__multer@2.0.8': - dependencies: - '@types/koa': 3.0.2 - '@types/markdown-escape@1.1.3': {} '@types/mdast@4.0.4': @@ -18213,7 +18150,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) @@ -18224,18 +18161,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -18251,7 +18188,7 @@ snapshots: transitivePeerDependencies: - ts-jest - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -18262,7 +18199,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From 2cb95e468c81031262cdf19e44c4ef199dbf14ab Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 07:34:32 -0300 Subject: [PATCH 23/27] fix: test --- .github/workflows/pr.yml | 4 ++++ packages/server/tests/unit/jest.config.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 35c2dbe7..0f483c9c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -3,6 +3,10 @@ name: PR on: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + jobs: build-and-test: name: Build and Test diff --git a/packages/server/tests/unit/jest.config.ts b/packages/server/tests/unit/jest.config.ts index 156b093e..d6687461 100644 --- a/packages/server/tests/unit/jest.config.ts +++ b/packages/server/tests/unit/jest.config.ts @@ -5,6 +5,7 @@ export default jestUnitConfig({ coverageThreshold: { global: {}, }, + maxWorkers: 2, // setupFiles: ['/setupTests.ts'], setupFilesAfterEnv: ['/setupTestsAfterEnv.ts'], transformIgnorePatterns: getTransformIgnorePatterns({ From 559e578fed3817f685a0cdb5feb19b02f77f8119 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 08:09:16 -0300 Subject: [PATCH 24/27] fix: test --- .../server/tests/unit/setupTestsAfterEnv.ts | 17 ++++++++++++ .../tests/unit/tests/conversations.test.ts | 26 +++++++++---------- .../server/tests/unit/tests/documents.test.ts | 14 +++++----- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/server/tests/unit/setupTestsAfterEnv.ts b/packages/server/tests/unit/setupTestsAfterEnv.ts index 9449f273..1606a87b 100644 --- a/packages/server/tests/unit/setupTestsAfterEnv.ts +++ b/packages/server/tests/unit/setupTestsAfterEnv.ts @@ -1,3 +1,20 @@ +jest.mock('ollama', () => { + return { + Ollama: jest.fn().mockImplementation(() => { + return { + embed: jest.fn().mockResolvedValue({ + embeddings: [Array(1024).fill(0.1)], + }), + chat: jest.fn().mockResolvedValue( + (async function* () { + yield { message: { content: 'mock', role: 'assistant' } }; + })() + ), + }; + }), + }; +}); + import { models } from '@soat/postgresdb'; import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql'; import { PostgreSqlContainer } from '@testcontainers/postgresql'; diff --git a/packages/server/tests/unit/tests/conversations.test.ts b/packages/server/tests/unit/tests/conversations.test.ts index 5768d85d..d96b8587 100644 --- a/packages/server/tests/unit/tests/conversations.test.ts +++ b/packages/server/tests/unit/tests/conversations.test.ts @@ -4,12 +4,6 @@ import path from 'node:path'; import { authenticatedTestClient, loginAs, testClient } from '../testClient'; -jest.mock('src/lib/embedding', () => { - return { - getEmbedding: jest.fn().mockResolvedValue(Array(1024).fill(0.1)), - }; -}); - describe('Conversations', () => { let adminToken: string; let userToken: string; @@ -162,7 +156,9 @@ describe('Conversations', () => { expect(response.status).toBe(200); expect(Array.isArray(response.body.data)).toBe(true); expect( - response.body.data.some((c: { id: string }) => c.id === filteredConvId) + response.body.data.some((c: { id: string }) => { + return c.id === filteredConvId; + }) ).toBe(true); }); @@ -325,9 +321,9 @@ describe('Conversations', () => { expect(listRes.status).toBe(200); expect( - listRes.body.data.some( - (m: { documentId: string }) => m.documentId === addedDocumentId - ) + listRes.body.data.some((m: { documentId: string }) => { + return m.documentId === addedDocumentId; + }) ).toBe(true); }); @@ -383,9 +379,9 @@ describe('Conversations', () => { `/api/v1/conversations/${conversationId}/messages` ); expect( - listRes.body.data.some( - (m: { documentId: string }) => m.documentId === secondDocumentId - ) + listRes.body.data.some((m: { documentId: string }) => { + return m.documentId === secondDocumentId; + }) ).toBe(false); }); @@ -483,7 +479,9 @@ describe('Conversations', () => { expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body.length).toBe(2); - const ids = response.body.map((a: { id: string }) => a.id); + const ids = response.body.map((a: { id: string }) => { + return a.id; + }); expect(ids).toContain(actorId); expect(ids).toContain(secondActorIdForActorsTest); }); diff --git a/packages/server/tests/unit/tests/documents.test.ts b/packages/server/tests/unit/tests/documents.test.ts index 67028677..ad1b8746 100644 --- a/packages/server/tests/unit/tests/documents.test.ts +++ b/packages/server/tests/unit/tests/documents.test.ts @@ -4,12 +4,6 @@ import path from 'node:path'; import { authenticatedTestClient, loginAs, testClient } from '../testClient'; -jest.mock('src/lib/embedding', () => { - return { - getEmbedding: jest.fn().mockResolvedValue(Array(1024).fill(0.1)), - }; -}); - describe('Documents', () => { let adminToken: string; let userToken: string; @@ -450,7 +444,9 @@ describe('Documents', () => { expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); - const ids = response.body.map((d: { id: string }) => d.id); + const ids = response.body.map((d: { id: string }) => { + return d.id; + }); expect(ids).toContain(taggedDocId); }); @@ -460,7 +456,9 @@ describe('Documents', () => { .send({ projectId, query: 'tag-filtered', tags: ['no-such-tag-xyz'] }); expect(response.status).toBe(200); - const ids = response.body.map((d: { id: string }) => d.id); + const ids = response.body.map((d: { id: string }) => { + return d.id; + }); expect(ids).not.toContain(taggedDocId); }); }); From f499f0ae4f3e4c84145fc3248cc47948e97b1051 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 10:25:19 -0300 Subject: [PATCH 25/27] fix: ci --- .github/workflows/pr.yml | 28 ++++++++++++++++++++++------ carlin.yml | 0 packages/website/carlin.yml | 1 + packages/website/package.json | 3 ++- turbo.json | 8 ++++++++ 5 files changed, 33 insertions(+), 7 deletions(-) delete mode 100644 carlin.yml create mode 100644 packages/website/carlin.yml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0f483c9c..41d9a9ea 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,9 +8,11 @@ concurrency: cancel-in-progress: false jobs: - build-and-test: - name: Build and Test + build-test-and-deploy: + name: Build, Test and Deploy runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - uses: actions/checkout@v6 @@ -33,8 +35,22 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build and test - run: pnpm turbo run build test + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Build, test and deploy + run: pnpm turbo run deploy + + - name: Deploy report + run: pnpm turbo run deploy-report + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + CARLIN_BRANCH: ${{ github.event.pull_request.head.ref }} smoke-test: name: Smoke Tests @@ -71,14 +87,14 @@ jobs: name: All Checks Passed runs-on: ubuntu-latest needs: - - build-and-test + - build-test-and-deploy - smoke-test if: always() steps: - name: Check all jobs passed run: | - if [[ "${{ needs.build-and-test.result }}" != "success" || \ + if [[ "${{ needs.build-test-and-deploy.result }}" != "success" || \ "${{ needs.smoke-test.result }}" != "success" ]]; then echo "One or more jobs failed." exit 1 diff --git a/carlin.yml b/carlin.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/website/carlin.yml b/packages/website/carlin.yml new file mode 100644 index 00000000..58e9fc70 --- /dev/null +++ b/packages/website/carlin.yml @@ -0,0 +1 @@ +appendIndexHtml: true diff --git a/packages/website/package.json b/packages/website/package.json index b0437b9d..d3dda81c 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -7,7 +7,8 @@ "dev": "docusaurus start", "build": "docusaurus gen-api-docs all && docusaurus build", "swizzle": "docusaurus swizzle", - "deploy": "docusaurus deploy", + "deploy": "carlin deploy static-app", + "deploy-report": "carlin deploy report --channel=github-pr", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", diff --git a/turbo.json b/turbo.json index befb4e55..61e5b81d 100644 --- a/turbo.json +++ b/turbo.json @@ -8,6 +8,14 @@ "test": { "dependsOn": ["^build"], "outputs": [] + }, + "deploy": { + "dependsOn": ["build", "test"], + "outputs": [] + }, + "deploy-report": { + "dependsOn": ["deploy"], + "outputs": [] } } } From 6bf2d2d8ab01a32730b0b3fd9d184045565b6917 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 10:27:10 -0300 Subject: [PATCH 26/27] fix: install carlin --- package.json | 1 + pnpm-lock.yaml | 3058 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 3017 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 26fbffdf..966201a2 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@ttoss/eslint-config": "^1.26.14", "@ttoss/monorepo": "^1.29.8", "@types/node": "^25.5.2", + "carlin": "^1.48.3", "eslint": "^9.39.1", "husky": "^9.1.7", "lint-staged": "^16.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f5cb7e6..4da84e58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@types/node': specifier: ^25.5.2 version: 25.5.2 + carlin: + specifier: ^1.48.3 + version: 1.48.3(@swc/core@1.15.24)(@types/node@25.5.2)(encoding@0.1.13)(rollup@4.53.3)(typescript@6.0.2) eslint: specifier: ^9.39.1 version: 9.39.2(jiti@2.6.1) @@ -345,6 +348,175 @@ packages: '@asyncapi/specs@6.10.0': resolution: {integrity: sha512-vB5oKLsdrLUORIZ5BXortZTlVyGWWMC1Nud/0LtgxQ3Yn2738HigAD6EVqScvpPsDUI/bcLVsYEXN4dtXQHVng==} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-cloudformation@3.1029.0': + resolution: {integrity: sha512-Tr4W2pQ9XDaYRrgrmGeNWVV3xVZ3hovxKgu33pr4oyJWBOXOOP/002QZgkER3mxncx//ESPXyTMTX1JpP3r2+Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1029.0': + resolution: {integrity: sha512-OuA8RZTxsAaHDcI25j2NGLMaYFI2WpJdDzK3uLmVBmaHwjQKQZOUDVVBcln8pNo3IgkY+HRSJhRR4/xlM//UyQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.27': + resolution: {integrity: sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.6': + resolution: {integrity: sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.25': + resolution: {integrity: sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.27': + resolution: {integrity: sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.29': + resolution: {integrity: sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.29': + resolution: {integrity: sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.30': + resolution: {integrity: sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.25': + resolution: {integrity: sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.29': + resolution: {integrity: sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.29': + resolution: {integrity: sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/lib-storage@3.1029.0': + resolution: {integrity: sha512-Un9PaYUUHKCbO+A2w4Hse7Fahg9siRSilR7MR5Eu9wbgzG79fCc/Jgm8x7q35KhRMW2z5Iy4tF5NjSw4nxnksA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@aws-sdk/client-s3': ^3.1029.0 + + '@aws-sdk/middleware-bucket-endpoint@3.972.9': + resolution: {integrity: sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.9': + resolution: {integrity: sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.7': + resolution: {integrity: sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.9': + resolution: {integrity: sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.9': + resolution: {integrity: sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.9': + resolution: {integrity: sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.10': + resolution: {integrity: sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.28': + resolution: {integrity: sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.9': + resolution: {integrity: sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.29': + resolution: {integrity: sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.996.19': + resolution: {integrity: sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.11': + resolution: {integrity: sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.16': + resolution: {integrity: sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1026.0': + resolution: {integrity: sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.7': + resolution: {integrity: sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.6': + resolution: {integrity: sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.9': + resolution: {integrity: sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==} + + '@aws-sdk/util-user-agent-node@3.973.15': + resolution: {integrity: sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.17': + resolution: {integrity: sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1694,6 +1866,26 @@ packages: resolution: {integrity: sha512-T3B0WTigsIthe0D4LQa2k+7bJY+c3WS+Wq2JhcznOSpn1lSN64yNtHQXboCj3QnUs1EuAZszQG1SHKu5w5ZrlA==} engines: {node: '>=20.0'} + '@edge-runtime/format@2.2.1': + resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==} + engines: {node: '>=16'} + + '@edge-runtime/node-utils@2.3.0': + resolution: {integrity: sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ==} + engines: {node: '>=16'} + + '@edge-runtime/ponyfill@2.4.2': + resolution: {integrity: sha512-oN17GjFr69chu6sDLvXxdhg0Qe8EZviGSuqzR9qOiKh4MhFYGdBBcqRNzdmYeAdeRzOW2mM9yil4RftUQ7sUOA==} + engines: {node: '>=16'} + + '@edge-runtime/primitives@4.1.0': + resolution: {integrity: sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ==} + engines: {node: '>=16'} + + '@edge-runtime/vm@3.2.0': + resolution: {integrity: sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==} + engines: {node: '>=16'} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -2104,6 +2296,10 @@ packages: resolution: {integrity: sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==} deprecated: Please update to a newer version. + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@formatjs/bigdecimal@0.2.0': resolution: {integrity: sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==} @@ -2167,6 +2363,10 @@ packages: react-dom: '>=16.8.0' react-hook-form: ^7.0.0 + '@httptoolkit/esm@3.3.2': + resolution: {integrity: sha512-mpB6FdMtn+c17RA6b6PDdsJXBLHG4P8ZJFK7sjS4IZn689kcOwjt90Ct+oxTaYo9pvCSb/GeTSjYEzoBv+bnbQ==} + engines: {node: '>=6'} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2248,6 +2448,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -2485,6 +2689,9 @@ packages: '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} @@ -2541,6 +2748,11 @@ packages: resolution: {integrity: sha512-WfsCoRsrfQm1gYi6XKB+Yh5tvAGkL0PYBEEjo8hzfBO+YynxAyI1VWbZwI/fLNNZcDR9E8u7ceUE9B6gFbkGTA==} engines: {node: ^22.17.0 || >=24.0.0} + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -2644,6 +2856,9 @@ packages: resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} engines: {node: '>= 20'} + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + '@octokit/openapi-types@27.0.0': resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} @@ -2668,6 +2883,10 @@ packages: peerDependencies: '@octokit/core': '>=6' + '@octokit/request-error@5.1.1': + resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} + engines: {node: '>= 18'} + '@octokit/request-error@7.1.0': resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} engines: {node: '>= 20'} @@ -2680,9 +2899,23 @@ packages: resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} engines: {node: '>= 20'} + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@octokit/webhooks-methods@4.1.0': + resolution: {integrity: sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==} + engines: {node: '>= 18'} + + '@octokit/webhooks-types@7.6.1': + resolution: {integrity: sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==} + + '@octokit/webhooks@12.3.2': + resolution: {integrity: sha512-exj1MzVXoP7xnAcAB3jZ97pTvVPkQF9y6GA/dvYC47HV7vLv+24XRS6b/v/XnyikpEuvMhugEXdGtAlU086WkQ==} + engines: {node: '>= 18'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -2867,6 +3100,15 @@ packages: peerDependencies: rollup: ^1.20.0||^2.0.0 + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.53.3': resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} cpu: [arm] @@ -3071,6 +3313,12 @@ packages: '@sideway/pinpoint@2.0.0': resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + '@simple-git/args-pathspec@1.0.3': + resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==} + + '@simple-git/argv-parser@1.1.0': + resolution: {integrity: sha512-sUKOu2lb5vGIWADNNLpscyj07DAeQZU3KLbnE2Tj53tW6BbDQKMly2CCfnR4oYzqtRELCPWfwaPg+Q0T8qfKBg==} + '@simple-libs/child-process-utils@1.0.1': resolution: {integrity: sha512-3nWd8irxvDI6v856wpPCHZ+08iQR0oHTZfzAZmnbsLzf+Sf1odraP6uKOHDZToXq3RPRV/LbqGVlSCogm9cJjg==} engines: {node: '>=18'} @@ -3083,6 +3331,9 @@ packages: resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} engines: {node: '>=18'} + '@sinclair/typebox@0.25.24': + resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -3110,6 +3361,14 @@ packages: '@sinonjs/fake-timers@15.3.0': resolution: {integrity: sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==} + '@slack/types@2.20.1': + resolution: {integrity: sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + + '@slack/webhook@7.0.8': + resolution: {integrity: sha512-x3gttyRJGZSeIm+juObzYsYEMa/weoeZUU3WDWwOCwmHhShSwtq1sj1jvuKx0mLNIA6VveVlc4zWvluhA6qcxg==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + '@slorber/react-helmet-async@1.3.0': resolution: {integrity: sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==} peerDependencies: @@ -3119,6 +3378,218 @@ packages: '@slorber/remark-comment@1.0.0': resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} + '@smithy/chunked-blob-reader-native@4.2.3': + resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.2': + resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.14': + resolution: {integrity: sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.14': + resolution: {integrity: sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.13': + resolution: {integrity: sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.13': + resolution: {integrity: sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.13': + resolution: {integrity: sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.13': + resolution: {integrity: sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.13': + resolution: {integrity: sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.13': + resolution: {integrity: sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.16': + resolution: {integrity: sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.14': + resolution: {integrity: sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.13': + resolution: {integrity: sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.13': + resolution: {integrity: sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.13': + resolution: {integrity: sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.13': + resolution: {integrity: sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.13': + resolution: {integrity: sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.29': + resolution: {integrity: sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.5.1': + resolution: {integrity: sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.17': + resolution: {integrity: sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.13': + resolution: {integrity: sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.13': + resolution: {integrity: sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.5.2': + resolution: {integrity: sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.13': + resolution: {integrity: sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.13': + resolution: {integrity: sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.13': + resolution: {integrity: sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.13': + resolution: {integrity: sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.13': + resolution: {integrity: sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.8': + resolution: {integrity: sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.13': + resolution: {integrity: sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.9': + resolution: {integrity: sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.0': + resolution: {integrity: sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.13': + resolution: {integrity: sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.45': + resolution: {integrity: sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.49': + resolution: {integrity: sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.3.4': + resolution: {integrity: sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.13': + resolution: {integrity: sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.3.1': + resolution: {integrity: sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.22': + resolution: {integrity: sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.15': + resolution: {integrity: sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -3511,10 +3982,17 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@ts-morph/common@0.11.1': + resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} + '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -3527,6 +4005,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@ttoss/cloudformation@0.12.10': + resolution: {integrity: sha512-b0QCxfqBQ88VbCLMiDGbOvw4RmaJHQ9QSQEbXWDfzE1CLpsXAqDEPypP4d/30TqMKr6QHvlM8VyJ1SgR5+jNfg==} + '@ttoss/config@1.37.8': resolution: {integrity: sha512-Ikk44m4mM+xiuh4JaubdFCVtTKDRpM4lfZ2I/bU7uNJ9uGk+Vv7d+PxpYXwvb3aaNhTEHYfs4OMRnC4KSRyHmQ==} @@ -3555,6 +4036,9 @@ packages: '@ttoss/postgresdb@0.8.0': resolution: {integrity: sha512-icGG5SBGjWomyEXUJ/n4hUMVowyRSoGEGI4zpmJ/+pTGilel/vQYig+R8Sco1Lwq+rVwnfpC2yGCMzir3VZkVw==} + '@ttoss/read-config-file@2.2.8': + resolution: {integrity: sha512-Iw8RL0Pow5evda9iFUBexAZZ5Es9OhhEwhah32rKuCrgetHy7KohDvP6mXA1bMCx6AtPS8uKraGRAVicJ29+hg==} + '@ttoss/test-utils@4.2.8': resolution: {integrity: sha512-3Tw3miD59SZOQK3GQJwsmh5yd957zVauMB0wnHDH6L/nSELuhS9+T2uuLWXiO+pIt/GA025zjQDDGxvOgVmhrw==} peerDependencies: @@ -3734,6 +4218,9 @@ packages: '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + '@types/node@16.18.11': + resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==} + '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} @@ -4069,10 +4556,64 @@ packages: cpu: [x64] os: [win32] + '@vercel/build-utils@9.1.0': + resolution: {integrity: sha512-ccknvdKH6LDB9ZzZaX8a8cOvFbI441APLHvKrunJE/wezY0skmfuEUK1qnfPApXMs4FMWzZQj2LO9qpzfgBPsQ==} + + '@vercel/error-utils@2.0.3': + resolution: {integrity: sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ==} + + '@vercel/fun@1.1.2': + resolution: {integrity: sha512-n13RO1BUy8u6+kzDQ2++BRj4Y5EAiQPt+aV+Tb2HNTmToNr4Mu3dE1kFlaTVTxQzAT3hvIRlVEU/OMvF8LCFJw==} + engines: {node: '>= 16'} + + '@vercel/gatsby-plugin-vercel-analytics@1.0.11': + resolution: {integrity: sha512-iTEA0vY6RBPuEzkwUTVzSHDATo1aF6bdLLspI68mQ/BTbi5UQEGjpjyzdKOVcSYApDtFU6M6vypZ1t4vIEnHvw==} + + '@vercel/gatsby-plugin-vercel-builder@2.0.65': + resolution: {integrity: sha512-MQX56fuL4WHDhT/fvKy9FMJigOymTAcCqw8rteF1wpRBAGhapSJkhT34I4mkfRRMFk1kIV7ijwuX+w1mpRrLjA==} + + '@vercel/go@3.2.1': + resolution: {integrity: sha512-ezjmuUvLigH9V4egEaX0SZ+phILx8lb+Zkp1iTqKI+yl/ibPAtVo5o+dLSRAXU9U01LBmaLu3O8Oxd/JpWYCOw==} + + '@vercel/hydrogen@1.0.11': + resolution: {integrity: sha512-nkSQ0LC7rFRdfkTUGm9pIbAfRb2Aat05u8ouN0FoUl7/I/YVgd0G6iRBN9bOMFUIiBiaKB4KqaZEFzVfUHpwYw==} + + '@vercel/next@4.4.4': + resolution: {integrity: sha512-/xMzlOMY8UHzCehRZzx8TIdzVRCu3O2O+Gb7R8uRX0/ci9cLIjJvi0WfLyR06Ny4fMqMzzUuRADp5ezfJjaO1Q==} + + '@vercel/nft@0.27.10': + resolution: {integrity: sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg==} + engines: {node: '>=16'} + hasBin: true + + '@vercel/node@5.0.4': + resolution: {integrity: sha512-AXpTFDzomabvi/FmxDDTwmnuqRBDfy2i0nzjKwVPM3ch94EucPbiAk3+18iZOX/A+o2mBO4jKc1DmB0ifQF2Rw==} + '@vercel/oidc@3.0.5': resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} engines: {node: '>= 20'} + '@vercel/python@4.7.1': + resolution: {integrity: sha512-H4g/5e8unII4oQ+KN5IUvTZSzHmj+lLYDkAK15QGYgAxBtE/mHUvEZpPPo7DPUDIyfq8ybWB1bmk7H5kEahubQ==} + + '@vercel/redwood@2.1.13': + resolution: {integrity: sha512-e+4odfP2akWQq3WQ8mBkjqqwUcOvjhYmAhfg66IqTdIG15tIY6EOTMx/DhqXlvSDCyBbZPcqHb4/Xe662yPiEw==} + + '@vercel/remix-builder@5.1.1': + resolution: {integrity: sha512-OP1f6GI8MdylL4aUrX6n7OkN93jqmkWyLzQMeQMapVOXKvRFj05STZ4SQ/kNJkXdh3rEzjJWuCsJ6bklTHkJ7Q==} + + '@vercel/routing-utils@5.0.1': + resolution: {integrity: sha512-CH8sulzI8VNySWyJP+536fEX+oBnRuIVpw79jrn/0JwgCl7xb6E2JkKrMBT/mUCkZXh4vZZIOt23/QiIRK9Dyw==} + + '@vercel/ruby@2.2.0': + resolution: {integrity: sha512-FJF9gKVNHAljGOgV6zS5ou2N7ZgjOqMMtcPA5lsJEUI5/AZzVDWCmtcowTP80wEtHuupkd7d7M399FA082kXYQ==} + + '@vercel/static-build@2.5.43': + resolution: {integrity: sha512-r6Pi/yC1nUCuq6V7xDxfMKDkwla4qnqpJVohd7cTsWRDKlRzHJJX/YaDp/6yKrDaNH9UY6cBhj9ryL8QJWY63w==} + + '@vercel/static-config@3.0.0': + resolution: {integrity: sha512-2qtvcBJ1bGY0dYGYh3iM7yGKkk971FujLEDXzuW5wcZsPr1GSEjO/w2iSr3qve6nDDtBImsGoDEnus5FI4+fIw==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -4124,6 +4665,10 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + abbrev@4.0.0: resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -4140,7 +4685,12 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - acorn-import-phases@1.0.4: + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-import-phases@1.0.4: resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} engines: {node: '>=10.13.0'} peerDependencies: @@ -4164,6 +4714,10 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} + adm-zip@0.5.17: + resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} + engines: {node: '>=12.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -4226,6 +4780,9 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.6.3: + resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==} + algoliasearch-helper@3.27.0: resolution: {integrity: sha512-eNYchRerbsvk2doHOMfdS1/B6Tm70oGtu8mzQlrNzbCeQ8p1MjCW8t/BL6iZ5PD+cL5NNMgTMyMnmiXZ1sgmNw==} peerDependencies: @@ -4284,6 +4841,9 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + archiver-utils@5.0.2: resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} engines: {node: '>= 14'} @@ -4292,6 +4852,14 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} + are-we-there-yet@4.0.2: + resolution: {integrity: sha512-ncSWAawFhKMJDTdoAeOV+jyW1VCMj5QIAwULIBV0SSR7B/RLPPEQiknKcg/RIIZlUQrxELpsxMiTUoAQ4sIUyg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + + arg@4.1.0: + resolution: {integrity: sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -4377,9 +4945,23 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-listen@1.2.0: + resolution: {integrity: sha512-CcEtRh/oc9Jc4uWeUwdpG/+Mb2YUHKmdaTf0gUr7Wa+bfp4xx70HOb3RuSTJMvqKNB1TkdTfjLdrcz2X4rkkZA==} + + async-listen@3.0.0: + resolution: {integrity: sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg==} + engines: {node: '>= 14'} + + async-listen@3.0.1: + resolution: {integrity: sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA==} + engines: {node: '>= 14'} + async-lock@1.4.1: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -4401,10 +4983,18 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-sdk@2.1693.0: + resolution: {integrity: sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==} + 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 + axe-core@4.11.0: resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -4555,6 +5145,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -4572,6 +5165,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + boxen@6.2.1: resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4602,6 +5198,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} @@ -4612,6 +5211,12 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + + buffer@5.6.0: + resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -4651,6 +5256,10 @@ packages: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} + bytes@3.1.0: + resolution: {integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==} + engines: {node: '>= 0.8'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -4711,6 +5320,10 @@ packages: caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + carlin@1.48.3: + resolution: {integrity: sha512-SYWq5Q9WIz8vocxrS1BNKFn2u4gPAw/dseNwL5i7leawSsk+heXzvqlQdCYHqW9AhQmSS6rWdJ53aaoJO5eG4g==} + hasBin: true + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -4756,6 +5369,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.0: + resolution: {integrity: sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==} + engines: {node: '>= 14.16.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -4783,6 +5400,9 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} + cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + cjs-module-lexer@2.1.1: resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==} @@ -4849,6 +5469,9 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + code-block-writer@10.1.1: + resolution: {integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -4862,6 +5485,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -4972,6 +5599,9 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + content-disposition@0.5.2: resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} engines: {node: '>= 0.6'} @@ -4984,6 +5614,10 @@ packages: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} + content-type@1.0.4: + resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} + engines: {node: '>= 0.6'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -5024,6 +5658,10 @@ packages: engines: {node: '>=18'} hasBin: true + convert-hrtime@3.0.0: + resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==} + engines: {node: '>=8'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -5294,6 +5932,15 @@ packages: supports-color: optional: true + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -5332,6 +5979,10 @@ packages: deep-equal@1.0.1: resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -5393,6 +6044,9 @@ packages: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} + deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -5401,6 +6055,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + detect-indent@7.0.2: resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} engines: {node: '>=12.20'} @@ -5561,6 +6219,11 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + edge-runtime@2.5.9: + resolution: {integrity: sha512-pk+k0oK0PVXdlT4oRp4lwh+unuKB7Ng4iZ2HB+EZ7QCEQizX360Rp/F4aRpgpRgdP2ufB35N+1KppHmYjqIGSg==} + engines: {node: '>=16'} + hasBin: true + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -5597,6 +6260,9 @@ packages: encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.1.0: + resolution: {integrity: sha512-EoulkdKF/1xa92q25PbjuDcgJ9RDHYU2Rs3SCIvs2/dSQ3BpmxneNHmA/M7fe60M3PrV7nNGTTNbkK62l6vXiQ==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -5645,10 +6311,16 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-iterator-helpers@1.2.2: resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} engines: {node: '>= 0.4'} + es-module-lexer@1.4.1: + resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -5677,6 +6349,131 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + esbuild-android-64@0.14.47: + resolution: {integrity: sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + esbuild-android-arm64@0.14.47: + resolution: {integrity: sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + esbuild-darwin-64@0.14.47: + resolution: {integrity: sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + esbuild-darwin-arm64@0.14.47: + resolution: {integrity: sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + esbuild-freebsd-64@0.14.47: + resolution: {integrity: sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + esbuild-freebsd-arm64@0.14.47: + resolution: {integrity: sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + esbuild-linux-32@0.14.47: + resolution: {integrity: sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + esbuild-linux-64@0.14.47: + resolution: {integrity: sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + esbuild-linux-arm64@0.14.47: + resolution: {integrity: sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + esbuild-linux-arm@0.14.47: + resolution: {integrity: sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + esbuild-linux-mips64le@0.14.47: + resolution: {integrity: sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + esbuild-linux-ppc64le@0.14.47: + resolution: {integrity: sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + esbuild-linux-riscv64@0.14.47: + resolution: {integrity: sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + esbuild-linux-s390x@0.14.47: + resolution: {integrity: sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + esbuild-netbsd-64@0.14.47: + resolution: {integrity: sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + esbuild-openbsd-64@0.14.47: + resolution: {integrity: sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + esbuild-sunos-64@0.14.47: + resolution: {integrity: sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + esbuild-windows-32@0.14.47: + resolution: {integrity: sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + esbuild-windows-64@0.14.47: + resolution: {integrity: sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + esbuild-windows-arm64@0.14.47: + resolution: {integrity: sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + esbuild@0.14.47: + resolution: {integrity: sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.27.1: resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} engines: {node: '>=18'} @@ -5992,9 +6789,16 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-intercept@2.0.0: + resolution: {integrity: sha512-blk1va0zol9QOrdZt0rFXo5KMkNPVSp92Eju/Qz8THwKWKRKeE0T8Br/1aW6+Edkyq9xHYgYxn2QtOnUKPUp+Q==} + events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@1.1.1: + resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} + engines: {node: '>=0.4.x'} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -6007,6 +6811,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + execa@3.2.0: + resolution: {integrity: sha512-kJJfVbI/lZE1PZYDI5VPxp8zXPO9rtxOkhpZ0jMKha56AI9y2gGVC6bkukStQf0ka5Rh15BA5m7cCCH4jmHqkw==} + engines: {node: ^8.12.0 || >=9.7.0} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -6022,6 +6830,10 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + expect@30.2.0: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -6098,6 +6910,13 @@ packages: fast-wrap-ansi@0.2.0: resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + + fast-xml-parser@5.5.8: + resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -6120,6 +6939,9 @@ packages: fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -6158,6 +6980,9 @@ packages: resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} engines: {node: '>=0.10.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -6190,6 +7015,10 @@ packages: resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + findup-sync@5.0.0: + resolution: {integrity: sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==} + engines: {node: '>= 10.13.0'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -6262,10 +7091,21 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@11.1.0: + resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==} + engines: {node: '>=14.14'} + fs-extra@11.3.4: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-minipass@1.2.7: + resolution: {integrity: sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==} + fs-minipass@3.0.3: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -6288,10 +7128,19 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gauge@5.0.2: + resolution: {integrity: sha512-pMaFftXPtiGIHCJHdcUUx9Rby/rFT/Kkt3fIIGCs+9PMDIljSyRiqraTlxNtBReJRDfUefpa263RQ3vnp5G/LQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + generic-pool@3.4.2: + resolution: {integrity: sha512-H7cUpwCQSiJmAHM4c/aFu6fUfrhWXW1ncyh8ftxEPMu6AiYkHw9K8br720TGPZJbk5eOH2bynjZD1yPvdDAmag==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6326,6 +7175,10 @@ packages: get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -6377,6 +7230,12 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@13.0.0: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} @@ -6397,6 +7256,14 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -6486,6 +7353,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + has-yarn@3.0.0: resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6534,6 +7404,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + hono@4.12.12: resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} engines: {node: '>=16.9.0'} @@ -6608,10 +7482,18 @@ packages: http-deceiver@1.2.7: resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + http-errors@1.4.0: + resolution: {integrity: sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw==} + engines: {node: '>= 0.6'} + http-errors@1.6.3: resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} engines: {node: '>= 0.6'} + http-errors@1.7.3: + resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} + engines: {node: '>= 0.6'} + http-errors@1.8.1: resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} engines: {node: '>= 0.6'} @@ -6654,6 +7536,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -6693,6 +7579,9 @@ packages: resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==} engines: {node: '>=4'} + ieee754@1.1.13: + resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -6734,6 +7623,9 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + import-sync@2.2.3: + resolution: {integrity: sha512-ZnF84+eGjetsXwYEuFZADO4eCYr1ngBo6UK476Oq4q6dkiDM1TN+6D5iQ5/e3erCyjo7O6xT3xHE6xdtCgDYhw==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -6762,6 +7654,9 @@ packages: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.1: + resolution: {integrity: sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==} + inherits@2.0.3: resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} @@ -6815,6 +7710,10 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -7037,6 +7936,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -7096,6 +7999,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + jest-changed-files@30.3.0: resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7273,6 +8180,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} @@ -7344,6 +8255,9 @@ packages: resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==} engines: {node: '>=12.0.0'} + json-schema-to-ts@1.6.4: + resolution: {integrity: sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA==} + json-schema-to-ts@2.7.2: resolution: {integrity: sha512-R1JfqKqbBR4qE8UyBR56Ms30LL62/nlhoz+1UkfI/VE7p54Awu919FZ6ZUPG8zIa3XB65usPJgr1ONVncUGSaQ==} engines: {node: '>=16'} @@ -7379,6 +8293,9 @@ packages: jsonc-parser@2.2.1: resolution: {integrity: sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==} + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -7677,6 +8594,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -7816,6 +8737,11 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micro@9.3.5-canary.3: + resolution: {integrity: sha512-viYIo9PefV+w9dvoIBh1gI44Mvx1BOk67B4BpC2QK77qdY0xZF0Q+vWLt/BII6cLkIc8rLmSIcJaB/OrXXKe1g==} + engines: {node: '>= 8.0.0'} + hasBin: true + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -8059,6 +8985,9 @@ packages: resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} engines: {node: '>=8'} + minipass@2.9.0: + resolution: {integrity: sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==} + minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} @@ -8067,6 +8996,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@1.3.3: + resolution: {integrity: sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==} + minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} @@ -8078,6 +9010,11 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} @@ -8092,6 +9029,10 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -8099,6 +9040,12 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.1: + resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8183,6 +9130,24 @@ packages: resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==} engines: {node: 4.x || >=6.0.0} + node-fetch@2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@2.6.9: + resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -8196,6 +9161,10 @@ packages: resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} engines: {node: '>= 6.13.0'} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-gyp@12.1.0: resolution: {integrity: sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==} engines: {node: ^20.17.0 || >=22.9.0} @@ -8214,6 +9183,11 @@ packages: resolution: {integrity: sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==} engines: {node: '>=14'} + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + nopt@9.0.0: resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -8255,6 +9229,11 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} + npmlog@7.0.1: + resolution: {integrity: sha512-uJ0YFk/mCQpLBt+bxN88AKd+gyqZvZDbtiNxk6Waqcj2aPRyfVx8ITawkyQynxUagInjdYT1+qj4NfA5KJJUxg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + nprogress@0.2.0: resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} @@ -8302,6 +9281,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -8340,6 +9323,9 @@ packages: resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} + once@1.3.3: + resolution: {integrity: sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -8375,6 +9361,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + os-paths@4.4.0: + resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==} + engines: {node: '>= 6.0'} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -8387,6 +9377,10 @@ packages: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} + p-finally@2.0.1: + resolution: {integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==} + engines: {node: '>=8'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -8475,6 +9469,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -8482,6 +9480,10 @@ packages: parse-numeric-range@1.3.0: resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + parse-path@7.1.0: resolution: {integrity: sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==} @@ -8513,6 +9515,10 @@ packages: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -8528,6 +9534,9 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-match@1.2.4: + resolution: {integrity: sha512-UWlehEdqu36jmh4h5CWJ7tARp1OEVKGHKm6+dg9qMq5RKUTV5WJrGgaZ3dN2m7WFAXDbjlHzvJvL/IUpy84Ktw==} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -8548,6 +9557,15 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@6.1.0: + resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==} + + path-to-regexp@6.2.1: + resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -8558,6 +9576,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -8596,6 +9617,9 @@ packages: resolution: {integrity: sha512-nKaQY9wtuiidwLMdVIce1O3kL0d+FxrigCVzsShnoqzOSaWWWOvuctb/sYwlai5cTwwzRSNa+a/NtN2kVZGNJw==} engines: {node: '>= 18'} + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -9111,6 +10135,10 @@ packages: resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -9149,6 +10177,9 @@ packages: promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + promisepipe@3.0.0: + resolution: {integrity: sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -9180,9 +10211,16 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -9201,6 +10239,11 @@ packages: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} + querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9219,6 +10262,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@2.4.1: + resolution: {integrity: sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==} + engines: {node: '>= 0.8'} + raw-body@2.5.3: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} @@ -9503,6 +10550,10 @@ packages: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -9635,6 +10686,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + sax@1.4.3: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} @@ -9678,6 +10732,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -9769,6 +10828,9 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -9787,6 +10849,9 @@ packages: setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + setprototypeof@1.1.1: + resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -9851,6 +10916,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.0.2: + resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} + engines: {node: '>=14'} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -9859,6 +10928,9 @@ packages: resolution: {integrity: sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==} engines: {node: '>=12'} + simple-git@3.36.0: + resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -10007,6 +11079,9 @@ packages: stacktracey@2.1.8: resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + stat-mode@0.3.0: + resolution: {integrity: sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -10022,6 +11097,15 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + + stream-to-array@2.3.0: + resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} + + stream-to-promise@2.2.0: + resolution: {integrity: sha512-HAGUASw8NT0k8JvIVutB2Y/9iBk7gpgEyAudXwNJmZERdMITGdajOa4VJfD/kNiA3TppQpTP4J+CtcHwdzKBAw==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -10133,6 +11217,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -10269,6 +11356,11 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tar@4.4.18: + resolution: {integrity: sha512-ZuOtqqmkV9RE1+4odd+MhBpibmCxNP6PJhH/h2OqNuotTX7/XHPZQJv2pKvWMplFH9SIZZhitehh6vBH6LO8Pg==} + engines: {node: '>=4.5'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} @@ -10328,6 +11420,10 @@ packages: thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + time-span@4.0.0: + resolution: {integrity: sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==} + engines: {node: '>=10'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -10371,6 +11467,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.0: + resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==} + engines: {node: '>=0.6'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -10427,6 +11527,23 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-morph@12.0.0: + resolution: {integrity: sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==} + + ts-node@10.9.1: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -10441,6 +11558,9 @@ packages: '@swc/wasm': optional: true + ts-toolbelt@6.15.5: + resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -10542,6 +11662,11 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + typescript@6.0.2: resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} @@ -10559,6 +11684,9 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + uid-promise@1.0.0: + resolution: {integrity: sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10572,6 +11700,10 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + undici@7.24.7: resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} engines: {node: '>=20.18.1'} @@ -10636,6 +11768,10 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -10673,6 +11809,9 @@ packages: file-loader: optional: true + url@0.10.3: + resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} + url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} @@ -10690,6 +11829,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} @@ -10705,6 +11847,15 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@3.3.2: + resolution: {integrity: sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==} + 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. + hasBin: true + + uuid@8.0.0: + resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -10752,6 +11903,11 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vercel@39.4.2: + resolution: {integrity: sha512-A3ilkwJ83xLwAYAI733hHthJ4DO0zXjQOvCWS9QYklWQTBEj0RllyRkrfGd2jypgNDZuAbDS/iFMsV+GuuaTHw==} + engines: {node: '>= 16'} + hasBin: true + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -10788,6 +11944,9 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-vitals@0.2.4: + resolution: {integrity: sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -10893,6 +12052,10 @@ packages: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -10903,6 +12066,9 @@ packages: engines: {node: ^20.17.0 || >=22.9.0} hasBin: true + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + widest-line@4.0.1: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} @@ -10982,10 +12148,18 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + xdg-app-paths@5.1.0: + resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==} + engines: {node: '>=6'} + xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} + xdg-portable@7.3.0: + resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==} + engines: {node: '>= 6.0'} + xml-formatter@3.7.0: resolution: {integrity: sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==} engines: {node: '>= 16'} @@ -11002,6 +12176,14 @@ packages: resolution: {integrity: sha512-TxyRxk9sTOUg3glxSIY6f0nfuqRll2OEF8TspLgh5mZkLuBgheCn3zClcDSGJ58TvNmiwyCCuat4UajPud/5Og==} engines: {node: '>= 16'} + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -11056,6 +12238,17 @@ packages: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yauzl-clone@1.0.4: + resolution: {integrity: sha512-igM2RRCf3k8TvZoxR2oguuw4z1xasOnA31joCqHIyLkeWrvAc2Jgay5ISQ2ZplinkoGaJ6orCz56Ey456c5ESA==} + engines: {node: '>=6'} + + yauzl-promise@2.1.3: + resolution: {integrity: sha512-A1pf6fzh6eYkK0L4Qp7g9jzJSDrM6nN0bOn5T0IbY4Yo3w+YkWlHFkJP7mzknMXjqusHFHlKsK2N+4OLsK2MRA==} + engines: {node: '>=6'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -11256,6 +12449,507 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-cloudformation@3.1029.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/credential-provider-node': 3.972.30 + '@aws-sdk/middleware-host-header': 3.972.9 + '@aws-sdk/middleware-logger': 3.972.9 + '@aws-sdk/middleware-recursion-detection': 3.972.10 + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/region-config-resolver': 3.972.11 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@aws-sdk/util-user-agent-browser': 3.972.9 + '@aws-sdk/util-user-agent-node': 3.973.15 + '@smithy/config-resolver': 4.4.14 + '@smithy/core': 3.23.14 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/hash-node': 4.2.13 + '@smithy/invalid-dependency': 4.2.13 + '@smithy/middleware-content-length': 4.2.13 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-retry': 4.5.1 + '@smithy/middleware-serde': 4.2.17 + '@smithy/middleware-stack': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/node-http-handler': 4.5.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.45 + '@smithy/util-defaults-mode-node': 4.2.49 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.15 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-s3@3.1029.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/credential-provider-node': 3.972.30 + '@aws-sdk/middleware-bucket-endpoint': 3.972.9 + '@aws-sdk/middleware-expect-continue': 3.972.9 + '@aws-sdk/middleware-flexible-checksums': 3.974.7 + '@aws-sdk/middleware-host-header': 3.972.9 + '@aws-sdk/middleware-location-constraint': 3.972.9 + '@aws-sdk/middleware-logger': 3.972.9 + '@aws-sdk/middleware-recursion-detection': 3.972.10 + '@aws-sdk/middleware-sdk-s3': 3.972.28 + '@aws-sdk/middleware-ssec': 3.972.9 + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/region-config-resolver': 3.972.11 + '@aws-sdk/signature-v4-multi-region': 3.996.16 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@aws-sdk/util-user-agent-browser': 3.972.9 + '@aws-sdk/util-user-agent-node': 3.973.15 + '@smithy/config-resolver': 4.4.14 + '@smithy/core': 3.23.14 + '@smithy/eventstream-serde-browser': 4.2.13 + '@smithy/eventstream-serde-config-resolver': 4.3.13 + '@smithy/eventstream-serde-node': 4.2.13 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/hash-blob-browser': 4.2.14 + '@smithy/hash-node': 4.2.13 + '@smithy/hash-stream-node': 4.2.13 + '@smithy/invalid-dependency': 4.2.13 + '@smithy/md5-js': 4.2.13 + '@smithy/middleware-content-length': 4.2.13 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-retry': 4.5.1 + '@smithy/middleware-serde': 4.2.17 + '@smithy/middleware-stack': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/node-http-handler': 4.5.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.45 + '@smithy/util-defaults-mode-node': 4.2.49 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.15 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.27': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws-sdk/xml-builder': 3.972.17 + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.6': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.27': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/node-http-handler': 4.5.2 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-stream': 4.5.22 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/credential-provider-env': 3.972.25 + '@aws-sdk/credential-provider-http': 3.972.27 + '@aws-sdk/credential-provider-login': 3.972.29 + '@aws-sdk/credential-provider-process': 3.972.25 + '@aws-sdk/credential-provider-sso': 3.972.29 + '@aws-sdk/credential-provider-web-identity': 3.972.29 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.30': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.25 + '@aws-sdk/credential-provider-http': 3.972.27 + '@aws-sdk/credential-provider-ini': 3.972.29 + '@aws-sdk/credential-provider-process': 3.972.25 + '@aws-sdk/credential-provider-sso': 3.972.29 + '@aws-sdk/credential-provider-web-identity': 3.972.29 + '@aws-sdk/types': 3.973.7 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/token-providers': 3.1026.0 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/lib-storage@3.1029.0(@aws-sdk/client-s3@3.1029.0)': + dependencies: + '@aws-sdk/client-s3': 3.1029.0 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-bucket-endpoint@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.7': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/crc64-nvme': 3.972.6 + '@aws-sdk/types': 3.973.7 + '@smithy/is-array-buffer': 4.2.2 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@smithy/core': 3.23.14 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-retry': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.996.19': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/middleware-host-header': 3.972.9 + '@aws-sdk/middleware-logger': 3.972.9 + '@aws-sdk/middleware-recursion-detection': 3.972.10 + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/region-config-resolver': 3.972.11 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@aws-sdk/util-user-agent-browser': 3.972.9 + '@aws-sdk/util-user-agent-node': 3.973.15 + '@smithy/config-resolver': 4.4.14 + '@smithy/core': 3.23.14 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/hash-node': 4.2.13 + '@smithy/invalid-dependency': 4.2.13 + '@smithy/middleware-content-length': 4.2.13 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-retry': 4.5.1 + '@smithy/middleware-serde': 4.2.17 + '@smithy/middleware-stack': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/node-http-handler': 4.5.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.45 + '@smithy/util-defaults-mode-node': 4.2.49 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/config-resolver': 4.4.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.16': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.28 + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1026.0': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.7': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.6': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-endpoints': 3.3.4 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.15': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/types': 3.973.7 + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.17': + dependencies: + '@smithy/types': 4.14.0 + fast-xml-parser: 5.5.8 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -12378,7 +14072,6 @@ snapshots: '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - optional: true '@csstools/cascade-layer-name-parser@2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: @@ -13491,6 +15184,18 @@ snapshots: - uglify-js - webpack-cli + '@edge-runtime/format@2.2.1': {} + + '@edge-runtime/node-utils@2.3.0': {} + + '@edge-runtime/ponyfill@2.4.2': {} + + '@edge-runtime/primitives@4.1.0': {} + + '@edge-runtime/vm@3.2.0': + dependencies: + '@edge-runtime/primitives': 4.1.0 + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -13761,6 +15466,8 @@ snapshots: '@faker-js/faker@5.5.3': {} + '@fastify/busboy@2.1.1': {} + '@formatjs/bigdecimal@0.2.0': {} '@formatjs/ecma402-abstract@3.2.0': @@ -13828,6 +15535,8 @@ snapshots: react-dom: 19.2.5(react@19.2.5) react-hook-form: 7.70.0(react@19.2.5) + '@httptoolkit/esm@3.3.2': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -13897,6 +15606,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/cliui@9.0.0': {} + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -14172,7 +15883,6 @@ snapshots: dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - optional: true '@js-sdsl/ordered-map@4.4.2': {} @@ -14257,6 +15967,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@kwsites/promise-deferred@1.1.1': {} + '@leichtgewicht/ip-codec@2.0.5': {} '@lerna-lite/changed@5.0.0(@lerna-lite/version@5.0.0)(@types/node@25.5.2)': @@ -14396,6 +16108,19 @@ snapshots: - conventional-commits-filter - supports-color + '@mapbox/node-pre-gyp@2.0.3(encoding@0.1.13)': + dependencies: + consola: 3.4.2 + detect-libc: 2.1.2 + https-proxy-agent: 7.0.6 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 8.1.0 + semver: 7.7.4 + tar: 7.5.2 + transitivePeerDependencies: + - encoding + - supports-color + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.8 @@ -14581,6 +16306,8 @@ snapshots: '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 + '@octokit/openapi-types@24.2.0': {} + '@octokit/openapi-types@27.0.0': {} '@octokit/plugin-enterprise-rest@6.0.1': {} @@ -14599,6 +16326,12 @@ snapshots: '@octokit/core': 7.0.6 '@octokit/types': 16.0.0 + '@octokit/request-error@5.1.1': + dependencies: + '@octokit/types': 13.10.0 + deprecation: 2.3.1 + once: 1.4.0 + '@octokit/request-error@7.1.0': dependencies: '@octokit/types': 16.0.0 @@ -14618,10 +16351,25 @@ snapshots: '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.6) '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6) + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + '@octokit/types@16.0.0': dependencies: '@octokit/openapi-types': 27.0.0 + '@octokit/webhooks-methods@4.1.0': {} + + '@octokit/webhooks-types@7.6.1': {} + + '@octokit/webhooks@12.3.2': + dependencies: + '@octokit/request-error': 5.1.1 + '@octokit/webhooks-methods': 4.1.0 + '@octokit/webhooks-types': 7.6.1 + aggregate-error: 3.1.0 + '@opentelemetry/api@1.9.0': {} '@paralleldrive/cuid2@2.3.1': @@ -14792,6 +16540,14 @@ snapshots: picomatch: 2.3.1 rollup: 2.79.2 + '@rollup/pluginutils@5.3.0(rollup@4.53.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.53.3 + '@rollup/rollup-android-arm-eabi@4.53.3': optional: true @@ -14923,6 +16679,12 @@ snapshots: '@sideway/pinpoint@2.0.0': {} + '@simple-git/args-pathspec@1.0.3': {} + + '@simple-git/argv-parser@1.1.0': + dependencies: + '@simple-git/args-pathspec': 1.0.3 + '@simple-libs/child-process-utils@1.0.1': dependencies: '@simple-libs/stream-utils': 1.2.0 @@ -14932,6 +16694,8 @@ snapshots: '@simple-libs/stream-utils@1.2.0': {} + '@sinclair/typebox@0.25.24': {} + '@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.34.45': {} @@ -14954,6 +16718,16 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@slack/types@2.20.1': {} + + '@slack/webhook@7.0.8': + dependencies: + '@slack/types': 2.20.1 + '@types/node': 25.5.2 + axios: 1.15.0 + transitivePeerDependencies: + - debug + '@slorber/react-helmet-async@1.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.28.4 @@ -14970,27 +16744,360 @@ snapshots: micromark-util-character: 1.2.0 micromark-util-symbol: 1.1.0 - '@standard-schema/spec@1.0.0': {} - - '@standard-schema/utils@0.3.0': {} - - '@stoplight/better-ajv-errors@1.0.3(ajv@8.17.1)': + '@smithy/chunked-blob-reader-native@4.2.3': dependencies: - ajv: 8.17.1 - jsonpointer: 5.0.1 - leven: 3.1.0 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 - '@stoplight/json-ref-readers@1.2.2(encoding@0.1.13)': + '@smithy/chunked-blob-reader@5.2.2': dependencies: - node-fetch: 2.7.0(encoding@0.1.13) - tslib: 1.14.1 - transitivePeerDependencies: - - encoding + tslib: 2.8.1 - '@stoplight/json-ref-resolver@3.1.6': + '@smithy/config-resolver@4.4.14': dependencies: - '@stoplight/json': 3.21.7 - '@stoplight/path': 1.3.2 + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + tslib: 2.8.1 + + '@smithy/core@3.23.14': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.13': + dependencies: + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.13': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.0 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.13': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.13': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.13': + dependencies: + '@smithy/eventstream-codec': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.16': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/querystring-builder': 4.2.13 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.2.14': + dependencies: + '@smithy/chunked-blob-reader': 5.2.2 + '@smithy/chunked-blob-reader-native': 4.2.3 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.13': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.29': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/middleware-serde': 4.2.17 + '@smithy/node-config-provider': 4.3.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-middleware': 4.2.13 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.5.1': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/service-error-classification': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.17': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.13': + dependencies: + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.5.2': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/querystring-builder': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + + '@smithy/shared-ini-file-loader@4.4.8': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.13': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.9': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-stack': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-stream': 4.5.22 + tslib: 2.8.1 + + '@smithy/types@4.14.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.13': + dependencies: + '@smithy/querystring-parser': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.45': + dependencies: + '@smithy/property-provider': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.49': + dependencies: + '@smithy/config-resolver': 4.4.14 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.3.4': + dependencies: + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.3.1': + dependencies: + '@smithy/service-error-classification': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.22': + dependencies: + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/node-http-handler': 4.5.2 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.15': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + + '@standard-schema/spec@1.0.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@stoplight/better-ajv-errors@1.0.3(ajv@8.17.1)': + dependencies: + ajv: 8.17.1 + jsonpointer: 5.0.1 + leven: 3.1.0 + + '@stoplight/json-ref-readers@1.2.2(encoding@0.1.13)': + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + tslib: 1.14.1 + transitivePeerDependencies: + - encoding + + '@stoplight/json-ref-resolver@3.1.6': + dependencies: + '@stoplight/json': 3.21.7 + '@stoplight/path': 1.3.2 '@stoplight/types': 13.20.0 '@types/urijs': 1.19.26 dependency-graph: 0.11.0 @@ -15480,19 +17587,29 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@tootallnate/once@2.0.0': {} + '@trysound/sax@0.2.0': {} - '@tsconfig/node10@1.0.12': - optional: true + '@ts-morph/common@0.11.1': + dependencies: + fast-glob: 3.3.3 + minimatch: 3.1.5 + mkdirp: 1.0.4 + path-browserify: 1.0.1 - '@tsconfig/node12@1.0.11': - optional: true + '@tsconfig/node10@1.0.12': {} - '@tsconfig/node14@1.0.3': - optional: true + '@tsconfig/node12@1.0.11': {} - '@tsconfig/node16@1.0.4': - optional: true + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@ttoss/cloudformation@0.12.10': + dependencies: + '@ttoss/read-config-file': 2.2.8 + js-yaml: 4.1.1 '@ttoss/config@1.37.8': dependencies: @@ -15606,6 +17723,12 @@ snapshots: - supports-color - tedious + '@ttoss/read-config-file@2.2.8': + dependencies: + esbuild: 0.27.7 + import-sync: 2.2.3 + js-yaml: 4.1.1 + '@ttoss/test-utils@4.2.8(@types/jest@30.0.0)(@types/react@19.2.7)(encoding@0.1.13)(jest@30.3.0(@types/node@25.5.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@emotion/jest': 11.14.2(@types/jest@30.0.0) @@ -15828,6 +17951,8 @@ snapshots: dependencies: '@types/node': 25.5.2 + '@types/node@16.18.11': {} + '@types/node@17.0.45': {} '@types/node@18.19.130': @@ -16185,8 +18310,162 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vercel/build-utils@9.1.0': {} + + '@vercel/error-utils@2.0.3': {} + + '@vercel/fun@1.1.2(encoding@0.1.13)': + dependencies: + '@tootallnate/once': 2.0.0 + async-listen: 1.2.0 + debug: 4.3.4 + execa: 3.2.0 + fs-extra: 8.1.0 + generic-pool: 3.4.2 + micro: 9.3.5-canary.3 + ms: 2.1.1 + node-fetch: 2.6.7(encoding@0.1.13) + path-match: 1.2.4 + promisepipe: 3.0.0 + semver: 7.5.4 + stat-mode: 0.3.0 + stream-to-promise: 2.2.0 + tar: 4.4.18 + tree-kill: 1.2.2 + uid-promise: 1.0.0 + uuid: 3.3.2 + xdg-app-paths: 5.1.0 + yauzl-promise: 2.1.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@vercel/gatsby-plugin-vercel-analytics@1.0.11': + dependencies: + web-vitals: 0.2.4 + + '@vercel/gatsby-plugin-vercel-builder@2.0.65': + dependencies: + '@sinclair/typebox': 0.25.24 + '@vercel/build-utils': 9.1.0 + '@vercel/routing-utils': 5.0.1 + esbuild: 0.14.47 + etag: 1.8.1 + fs-extra: 11.1.0 + + '@vercel/go@3.2.1': {} + + '@vercel/hydrogen@1.0.11': + dependencies: + '@vercel/static-config': 3.0.0 + ts-morph: 12.0.0 + + '@vercel/next@4.4.4(encoding@0.1.13)(rollup@4.53.3)': + dependencies: + '@vercel/nft': 0.27.10(encoding@0.1.13)(rollup@4.53.3) + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/nft@0.27.10(encoding@0.1.13)(rollup@4.53.3)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13) + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.4 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/node@5.0.4(@swc/core@1.15.24)(encoding@0.1.13)(rollup@4.53.3)': + dependencies: + '@edge-runtime/node-utils': 2.3.0 + '@edge-runtime/primitives': 4.1.0 + '@edge-runtime/vm': 3.2.0 + '@types/node': 16.18.11 + '@vercel/build-utils': 9.1.0 + '@vercel/error-utils': 2.0.3 + '@vercel/nft': 0.27.10(encoding@0.1.13)(rollup@4.53.3) + '@vercel/static-config': 3.0.0 + async-listen: 3.0.0 + cjs-module-lexer: 1.2.3 + edge-runtime: 2.5.9 + es-module-lexer: 1.4.1 + esbuild: 0.14.47 + etag: 1.8.1 + node-fetch: 2.6.9(encoding@0.1.13) + path-to-regexp: 6.2.1 + path-to-regexp-updated: path-to-regexp@6.3.0 + ts-morph: 12.0.0 + ts-node: 10.9.1(@swc/core@1.15.24)(@types/node@16.18.11)(typescript@4.9.5) + typescript: 4.9.5 + undici: 5.28.4 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - encoding + - rollup + - supports-color + '@vercel/oidc@3.0.5': {} + '@vercel/python@4.7.1': {} + + '@vercel/redwood@2.1.13(encoding@0.1.13)(rollup@4.53.3)': + dependencies: + '@vercel/nft': 0.27.10(encoding@0.1.13)(rollup@4.53.3) + '@vercel/routing-utils': 5.0.1 + '@vercel/static-config': 3.0.0 + semver: 6.3.1 + ts-morph: 12.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/remix-builder@5.1.1(encoding@0.1.13)(rollup@4.53.3)': + dependencies: + '@vercel/error-utils': 2.0.3 + '@vercel/nft': 0.27.10(encoding@0.1.13)(rollup@4.53.3) + '@vercel/static-config': 3.0.0 + ts-morph: 12.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/routing-utils@5.0.1': + dependencies: + path-to-regexp: 6.1.0 + path-to-regexp-updated: path-to-regexp@6.3.0 + optionalDependencies: + ajv: 6.14.0 + + '@vercel/ruby@2.2.0': {} + + '@vercel/static-build@2.5.43': + dependencies: + '@vercel/gatsby-plugin-vercel-analytics': 1.0.11 + '@vercel/gatsby-plugin-vercel-builder': 2.0.65 + '@vercel/static-config': 3.0.0 + ts-morph: 12.0.0 + + '@vercel/static-config@3.0.0': + dependencies: + ajv: 8.6.3 + json-schema-to-ts: 1.6.4 + ts-morph: 12.0.0 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -16267,6 +18546,8 @@ snapshots: '@xtuc/long@4.2.2': {} + abbrev@3.0.1: {} + abbrev@4.0.0: {} abort-controller@3.0.0: @@ -16283,6 +18564,10 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-import-phases@1.0.4(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -16299,6 +18584,8 @@ snapshots: address@1.2.2: {} + adm-zip@0.5.17: {} + agent-base@7.1.4: {} aggregate-error@3.1.0: @@ -16364,6 +18651,13 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ajv@8.6.3: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + algoliasearch-helper@3.27.0(algoliasearch@5.46.2): dependencies: '@algolia/events': 4.0.1 @@ -16425,6 +18719,8 @@ snapshots: append-field@1.0.0: {} + aproba@2.1.0: {} + archiver-utils@5.0.2: dependencies: glob: 10.5.0 @@ -16448,8 +18744,11 @@ snapshots: - bare-abort-controller - react-native-b4a - arg@4.1.3: - optional: true + are-we-there-yet@4.0.2: {} + + arg@4.1.0: {} + + arg@4.1.3: {} arg@5.0.2: {} @@ -16558,8 +18857,16 @@ snapshots: async-function@1.0.0: {} + async-listen@1.2.0: {} + + async-listen@3.0.0: {} + + async-listen@3.0.1: {} + async-lock@1.4.1: {} + async-sema@3.1.1: {} + async@3.2.6: {} asynckit@0.4.0: {} @@ -16579,8 +18886,29 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws-sdk@2.1693.0: + dependencies: + buffer: 4.9.2 + events: 1.1.1 + ieee754: 1.1.13 + jmespath: 0.16.0 + querystring: 0.2.0 + sax: 1.2.1 + url: 0.10.3 + util: 0.12.5 + uuid: 8.0.0 + xml2js: 0.6.2 + axe-core@4.11.0: {} + axios@1.15.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} b4a@1.7.3: {} @@ -16763,6 +19091,10 @@ snapshots: binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -16807,6 +19139,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: {} + boxen@6.2.1: dependencies: ansi-align: 3.0.1 @@ -16858,12 +19192,25 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-crc32@0.2.13: {} + buffer-crc32@1.0.0: {} buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} + buffer@4.9.2: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + + buffer@5.6.0: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -16898,6 +19245,8 @@ snapshots: bytes@3.0.0: {} + bytes@3.1.0: {} + bytes@3.1.2: {} cac@6.7.14: {} @@ -16969,6 +19318,48 @@ snapshots: caniuse-lite@1.0.30001760: {} + carlin@1.48.3(@swc/core@1.15.24)(@types/node@25.5.2)(encoding@0.1.13)(rollup@4.53.3)(typescript@6.0.2): + dependencies: + '@aws-sdk/client-cloudformation': 3.1029.0 + '@aws-sdk/client-s3': 3.1029.0 + '@aws-sdk/lib-storage': 3.1029.0(@aws-sdk/client-s3@3.1029.0) + '@octokit/webhooks': 12.3.2 + '@slack/webhook': 7.0.8 + '@ttoss/cloudformation': 0.12.10 + '@ttoss/config': 1.37.8 + '@ttoss/read-config-file': 2.2.8 + adm-zip: 0.5.17 + aws-sdk: 2.1693.0 + change-case: 5.4.4 + deep-equal: 2.2.3 + deepmerge: 4.3.1 + dotenv: 17.4.1 + esbuild: 0.27.7 + findup-sync: 5.0.0 + glob: 11.1.0 + import-sync: 2.2.3 + js-yaml: 4.1.1 + mime-types: 2.1.35 + npmlog: 7.0.1 + prettier: 3.8.1 + semver: 7.7.4 + simple-git: 3.36.0 + ts-node: 10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2) + uglify-js: 3.19.3 + vercel: 39.4.2(@swc/core@1.15.24)(encoding@0.1.13)(rollup@4.53.3) + yargs: 17.7.2 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - aws-crt + - debug + - encoding + - rollup + - supports-color + - ts-jest + - typescript + ccount@2.0.1: {} chalk@4.1.2: @@ -17023,6 +19414,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.0: + dependencies: + readdirp: 4.1.2 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -17039,6 +19434,8 @@ snapshots: ci-info@4.4.0: {} + cjs-module-lexer@1.2.3: {} + cjs-module-lexer@2.1.1: {} clean-css@5.3.3: @@ -17108,6 +19505,8 @@ snapshots: co@4.6.0: {} + code-block-writer@10.1.1: {} + collapse-white-space@2.1.0: {} collect-v8-coverage@1.0.3: {} @@ -17118,6 +19517,8 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + colord@2.9.3: {} colorette@1.4.0: {} @@ -17229,6 +19630,8 @@ snapshots: consola@3.4.2: {} + console-control-strings@1.1.0: {} + content-disposition@0.5.2: {} content-disposition@0.5.4: @@ -17237,6 +19640,8 @@ snapshots: content-disposition@1.0.1: {} + content-type@1.0.4: {} + content-type@1.0.5: {} conventional-changelog-angular@8.3.1: @@ -17286,6 +19691,8 @@ snapshots: conventional-commits-parser: 6.4.0 meow: 13.2.0 + convert-hrtime@3.0.0: {} + convert-source-map@2.0.0: {} cookie-signature@1.0.7: {} @@ -17372,8 +19779,7 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 - create-require@1.1.1: - optional: true + create-require@1.1.1: {} cross-fetch@3.2.0(encoding@0.1.13): dependencies: @@ -17576,6 +19982,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.4: + dependencies: + ms: 2.1.2 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -17596,6 +20006,27 @@ snapshots: deep-equal@1.0.1: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -17641,10 +20072,14 @@ snapshots: dependency-graph@0.11.0: {} + deprecation@2.3.1: {} + dequal@2.0.3: {} destroy@1.2.0: {} + detect-file@1.0.0: {} + detect-indent@7.0.2: {} detect-libc@1.0.3: @@ -17676,8 +20111,7 @@ snapshots: asap: 2.0.6 wrappy: 1.0.2 - diff@4.0.2: - optional: true + diff@4.0.2: {} dir-glob@3.0.1: dependencies: @@ -17873,6 +20307,18 @@ snapshots: dependencies: safe-buffer: 5.2.1 + edge-runtime@2.5.9: + dependencies: + '@edge-runtime/format': 2.2.1 + '@edge-runtime/ponyfill': 2.4.2 + '@edge-runtime/vm': 3.2.0 + async-listen: 3.0.1 + mri: 1.2.0 + picocolors: 1.0.0 + pretty-ms: 7.0.1 + signal-exit: 4.0.2 + time-span: 4.0.0 + ee-first@1.1.1: {} electron-to-chromium@1.5.267: {} @@ -17898,6 +20344,10 @@ snapshots: iconv-lite: 0.6.3 optional: true + end-of-stream@1.1.0: + dependencies: + once: 1.3.3 + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -17995,6 +20445,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-iterator-helpers@1.2.2: dependencies: call-bind: 1.0.8 @@ -18014,6 +20476,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@1.4.1: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: @@ -18053,6 +20517,89 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 + esbuild-android-64@0.14.47: + optional: true + + esbuild-android-arm64@0.14.47: + optional: true + + esbuild-darwin-64@0.14.47: + optional: true + + esbuild-darwin-arm64@0.14.47: + optional: true + + esbuild-freebsd-64@0.14.47: + optional: true + + esbuild-freebsd-arm64@0.14.47: + optional: true + + esbuild-linux-32@0.14.47: + optional: true + + esbuild-linux-64@0.14.47: + optional: true + + esbuild-linux-arm64@0.14.47: + optional: true + + esbuild-linux-arm@0.14.47: + optional: true + + esbuild-linux-mips64le@0.14.47: + optional: true + + esbuild-linux-ppc64le@0.14.47: + optional: true + + esbuild-linux-riscv64@0.14.47: + optional: true + + esbuild-linux-s390x@0.14.47: + optional: true + + esbuild-netbsd-64@0.14.47: + optional: true + + esbuild-openbsd-64@0.14.47: + optional: true + + esbuild-sunos-64@0.14.47: + optional: true + + esbuild-windows-32@0.14.47: + optional: true + + esbuild-windows-64@0.14.47: + optional: true + + esbuild-windows-arm64@0.14.47: + optional: true + + esbuild@0.14.47: + optionalDependencies: + esbuild-android-64: 0.14.47 + esbuild-android-arm64: 0.14.47 + esbuild-darwin-64: 0.14.47 + esbuild-darwin-arm64: 0.14.47 + esbuild-freebsd-64: 0.14.47 + esbuild-freebsd-arm64: 0.14.47 + esbuild-linux-32: 0.14.47 + esbuild-linux-64: 0.14.47 + esbuild-linux-arm: 0.14.47 + esbuild-linux-arm64: 0.14.47 + esbuild-linux-mips64le: 0.14.47 + esbuild-linux-ppc64le: 0.14.47 + esbuild-linux-riscv64: 0.14.47 + esbuild-linux-s390x: 0.14.47 + esbuild-netbsd-64: 0.14.47 + esbuild-openbsd-64: 0.14.47 + esbuild-sunos-64: 0.14.47 + esbuild-windows-32: 0.14.47 + esbuild-windows-64: 0.14.47 + esbuild-windows-arm64: 0.14.47 + esbuild@0.27.1: optionalDependencies: '@esbuild/aix-ppc64': 0.27.1 @@ -18484,12 +21031,16 @@ snapshots: eventemitter3@5.0.1: {} + events-intercept@2.0.0: {} + events-universal@1.0.1: dependencies: bare-events: 2.8.2 transitivePeerDependencies: - bare-abort-controller + events@1.1.1: {} + events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -18498,6 +21049,19 @@ snapshots: dependencies: eventsource-parser: 3.0.6 + execa@3.2.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + p-finally: 2.0.1 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -18529,6 +21093,10 @@ snapshots: exit-x@0.2.2: {} + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + expect@30.2.0: dependencies: '@jest/expect-utils': 30.2.0 @@ -18673,6 +21241,16 @@ snapshots: dependencies: fast-string-width: 3.0.2 + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.5.0 + + fast-xml-parser@5.5.8: + dependencies: + fast-xml-builder: 1.1.4 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -18707,6 +21285,10 @@ snapshots: dependencies: walk-up-path: 4.0.0 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -18737,6 +21319,8 @@ snapshots: file-type@3.9.0: {} + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -18786,6 +21370,13 @@ snapshots: locate-path: 7.2.0 path-exists: 5.0.0 + findup-sync@5.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 @@ -18848,12 +21439,28 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@11.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-minipass@1.2.7: + dependencies: + minipass: 2.9.0 + fs-minipass@3.0.3: dependencies: minipass: 7.1.2 @@ -18876,8 +21483,21 @@ snapshots: functions-have-names@1.2.3: {} + gauge@5.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 4.1.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + generator-function@2.0.1: {} + generic-pool@3.4.2: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -18913,6 +21533,10 @@ snapshots: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + get-stream@6.0.1: {} get-stream@9.0.1: @@ -18972,6 +21596,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.1 + glob@13.0.0: dependencies: minimatch: 10.1.1 @@ -19004,6 +21637,20 @@ snapshots: dependencies: ini: 2.0.0 + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + globals@14.0.0: {} globals@17.4.0: {} @@ -19102,6 +21749,8 @@ snapshots: dependencies: has-symbols: 1.1.0 + has-unicode@2.0.1: {} + has-yarn@3.0.0: {} hasown@2.0.2: @@ -19223,6 +21872,10 @@ snapshots: dependencies: react-is: 16.13.1 + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + hono@4.12.12: {} hosted-git-info@8.1.0: @@ -19308,6 +21961,11 @@ snapshots: http-deceiver@1.2.7: {} + http-errors@1.4.0: + dependencies: + inherits: 2.0.1 + statuses: 1.5.0 + http-errors@1.6.3: dependencies: depd: 1.1.2 @@ -19315,6 +21973,14 @@ snapshots: setprototypeof: 1.1.0 statuses: 1.5.0 + http-errors@1.7.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.1.1 + statuses: 1.5.0 + toidentifier: 1.0.0 + http-errors@1.8.1: dependencies: depd: 1.1.2 @@ -19376,6 +22042,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@1.1.1: {} + human-signals@2.1.0: {} human-signals@8.0.1: {} @@ -19404,6 +22072,8 @@ snapshots: dependencies: harmony-reflect: 1.6.2 + ieee754@1.1.13: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -19432,6 +22102,10 @@ snapshots: import-meta-resolve@4.2.0: {} + import-sync@2.2.3: + dependencies: + '@httptoolkit/esm': 3.3.2 + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -19449,6 +22123,8 @@ snapshots: once: 1.4.0 wrappy: 1.0.2 + inherits@2.0.1: {} + inherits@2.0.3: {} inherits@2.0.4: {} @@ -19488,6 +22164,11 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -19681,6 +22362,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-windows@1.0.2: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -19749,6 +22432,10 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + jest-changed-files@30.3.0: dependencies: execa: 5.1.1 @@ -20139,6 +22826,8 @@ snapshots: jiti@2.6.1: {} + jmespath@0.16.0: {} + joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -20221,6 +22910,11 @@ snapshots: json-schema-compare: 0.2.2 lodash: 4.17.21 + json-schema-to-ts@1.6.4: + dependencies: + '@types/json-schema': 7.0.15 + ts-toolbelt: 6.15.5 + json-schema-to-ts@2.7.2: dependencies: '@babel/runtime': 7.28.4 @@ -20253,6 +22947,10 @@ snapshots: jsonc-parser@2.2.1: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -20543,6 +23241,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lz-string@1.5.0: {} magic-string@0.25.9: @@ -20557,8 +23259,7 @@ snapshots: dependencies: semver: 7.7.4 - make-error@1.3.6: - optional: true + make-error@1.3.6: {} make-fetch-happen@15.0.3: dependencies: @@ -20811,6 +23512,12 @@ snapshots: methods@1.1.2: {} + micro@9.3.5-canary.3: + dependencies: + arg: 4.1.0 + content-type: 1.0.4 + raw-body: 2.4.1 + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -21207,12 +23914,21 @@ snapshots: dependencies: minipass: 3.3.6 + minipass@2.9.0: + dependencies: + safe-buffer: 5.2.1 + yallist: 3.1.1 + minipass@3.3.6: dependencies: yallist: 4.0.0 minipass@7.1.2: {} + minizlib@1.3.3: + dependencies: + minipass: 2.9.0 + minizlib@3.1.0: dependencies: minipass: 7.1.2 @@ -21223,6 +23939,8 @@ snapshots: dependencies: minimist: 1.2.8 + mkdirp@1.0.4: {} + mkdirp@3.0.1: {} mlly@1.8.0: @@ -21238,10 +23956,16 @@ snapshots: moment@2.30.1: {} + mri@1.2.0: {} + mrmime@2.0.1: {} ms@2.0.0: {} + ms@2.1.1: {} + + ms@2.1.2: {} + ms@2.1.3: {} multer@2.0.2: @@ -21323,6 +24047,18 @@ snapshots: dependencies: http2-client: 1.3.5 + node-fetch@2.6.7(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-fetch@2.6.9(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -21331,6 +24067,8 @@ snapshots: node-forge@1.3.3: {} + node-gyp-build@4.8.4: {} + node-gyp@12.1.0: dependencies: env-paths: 2.2.1 @@ -21359,6 +24097,10 @@ snapshots: '@types/sarif': 2.1.7 fs-extra: 10.1.0 + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + nopt@9.0.0: dependencies: abbrev: 4.0.0 @@ -21402,6 +24144,13 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 + npmlog@7.0.1: + dependencies: + are-we-there-yet: 4.0.2 + console-control-strings: 1.1.0 + gauge: 5.0.2 + set-blocking: 2.0.0 + nprogress@0.2.0: {} nth-check@2.1.1: @@ -21462,6 +24211,11 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + object-keys@1.1.1: {} object.assign@4.1.7: @@ -21512,6 +24266,10 @@ snapshots: on-headers@1.1.0: {} + once@1.3.3: + dependencies: + wrappy: 1.0.2 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -21573,6 +24331,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + os-paths@4.4.0: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -21583,6 +24343,8 @@ snapshots: p-finally@1.0.0: {} + p-finally@2.0.1: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -21682,10 +24444,14 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-ms@2.1.0: {} + parse-ms@4.0.0: {} parse-numeric-range@1.3.0: {} + parse-passwd@1.0.0: {} + parse-path@7.1.0: dependencies: protocols: 2.0.2 @@ -21717,6 +24483,8 @@ snapshots: path-exists@5.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-is-inside@1.0.2: {} @@ -21725,6 +24493,11 @@ snapshots: path-key@4.0.0: {} + path-match@1.2.4: + dependencies: + http-errors: 1.4.0 + path-to-regexp: 1.9.0 + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -21745,12 +24518,20 @@ snapshots: path-to-regexp@3.3.0: {} + path-to-regexp@6.1.0: {} + + path-to-regexp@6.2.1: {} + + path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} path-type@4.0.0: {} pathe@2.0.3: {} + pend@1.2.0: {} + pg-cloudflare@1.3.0: optional: true @@ -21788,6 +24569,8 @@ snapshots: pgvector@0.2.1: {} + picocolors@1.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -22349,6 +25132,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-ms@7.0.1: + dependencies: + parse-ms: 2.1.0 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -22380,6 +25167,8 @@ snapshots: dependencies: asap: 2.0.6 + promisepipe@3.0.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -22430,11 +25219,15 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@2.1.0: {} + pump@3.0.3: dependencies: end-of-stream: 1.4.5 once: 1.4.0 + punycode@1.3.2: {} + punycode@1.4.1: {} punycode@2.3.1: {} @@ -22449,6 +25242,8 @@ snapshots: dependencies: side-channel: 1.1.0 + querystring@0.2.0: {} + queue-microtask@1.2.3: {} quick-lru@5.1.1: {} @@ -22461,6 +25256,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@2.4.1: + dependencies: + bytes: 3.1.0 + http-errors: 1.7.3 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@2.5.3: dependencies: bytes: 3.1.2 @@ -22854,6 +25656,11 @@ snapshots: dependencies: resolve-from: 5.0.0 + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -22998,6 +25805,8 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.1 + sax@1.2.1: {} + sax@1.4.3: {} saxes@6.0.0: @@ -23041,6 +25850,10 @@ snapshots: semver@6.3.1: {} + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.7.1: {} semver@7.7.3: {} @@ -23163,6 +25976,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -23189,6 +26004,8 @@ snapshots: setprototypeof@1.1.0: {} + setprototypeof@1.1.1: {} + setprototypeof@1.2.0: {} shallow-clone@3.0.1: @@ -23267,12 +26084,24 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.0.2: {} + signal-exit@4.1.0: {} simple-eval@1.0.1: dependencies: jsep: 1.4.0 + simple-git@3.36.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + '@simple-git/args-pathspec': 1.0.3 + '@simple-git/argv-parser': 1.1.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29 @@ -23432,6 +26261,8 @@ snapshots: as-table: 1.0.55 get-source: 2.0.12 + stat-mode@0.3.0: {} + statuses@1.5.0: {} statuses@2.0.2: {} @@ -23443,6 +26274,21 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + stream-to-array@2.3.0: + dependencies: + any-promise: 1.3.0 + + stream-to-promise@2.2.0: + dependencies: + any-promise: 1.3.0 + end-of-stream: 1.1.0 + stream-to-array: 2.3.0 + streamsearch@1.1.0: {} streamx@2.23.0: @@ -23581,6 +26427,8 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.2.3: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -23762,6 +26610,16 @@ snapshots: - bare-abort-controller - react-native-b4a + tar@4.4.18: + dependencies: + chownr: 1.1.4 + fs-minipass: 1.2.7 + minipass: 2.9.0 + minizlib: 1.3.3 + mkdirp: 0.5.6 + safe-buffer: 5.2.1 + yallist: 3.1.1 + tar@7.5.2: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -23841,6 +26699,10 @@ snapshots: thunky@1.1.0: {} + time-span@4.0.0: + dependencies: + convert-hrtime: 3.0.0 + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -23872,6 +26734,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.0: {} + toidentifier@1.0.1: {} toposort-class@1.0.1: {} @@ -23910,6 +26774,31 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-morph@12.0.0: + dependencies: + '@ts-morph/common': 0.11.1 + code-block-writer: 10.1.1 + + ts-node@10.9.1(@swc/core@1.15.24)(@types/node@16.18.11)(typescript@4.9.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 16.18.11 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.24 + ts-node@10.9.2(@swc/core@1.15.24)(@types/node@25.5.2)(typescript@6.0.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -23929,7 +26818,8 @@ snapshots: yn: 3.1.1 optionalDependencies: '@swc/core': 1.15.24 - optional: true + + ts-toolbelt@6.15.5: {} tsconfig-paths@3.15.0: dependencies: @@ -24064,14 +26954,17 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@4.9.5: {} + typescript@6.0.2: {} ua-parser-js@1.0.41: {} ufo@1.6.1: {} - uglify-js@3.19.3: - optional: true + uglify-js@3.19.3: {} + + uid-promise@1.0.0: {} unbox-primitive@1.1.0: dependencies: @@ -24086,6 +26979,10 @@ snapshots: undici-types@7.18.2: {} + undici@5.28.4: + dependencies: + '@fastify/busboy': 2.1.1 + undici@7.24.7: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -24154,6 +27051,8 @@ snapshots: universal-user-agent@7.0.3: {} + universalify@0.1.2: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -24220,6 +27119,11 @@ snapshots: optionalDependencies: file-loader: 6.2.0(webpack@5.104.1(@swc/core@1.15.24)) + url@0.10.3: + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + url@0.11.4: dependencies: punycode: 1.4.1 @@ -24235,6 +27139,14 @@ snapshots: util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + utila@0.4.0: {} utility-types@3.11.0: {} @@ -24243,10 +27155,13 @@ snapshots: uuid@10.0.0: {} + uuid@3.3.2: {} + + uuid@8.0.0: {} + uuid@8.3.2: {} - v8-compile-cache-lib@3.0.1: - optional: true + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: dependencies: @@ -24286,6 +27201,27 @@ snapshots: vary@1.1.2: {} + vercel@39.4.2(@swc/core@1.15.24)(encoding@0.1.13)(rollup@4.53.3): + dependencies: + '@vercel/build-utils': 9.1.0 + '@vercel/fun': 1.1.2(encoding@0.1.13) + '@vercel/go': 3.2.1 + '@vercel/hydrogen': 1.0.11 + '@vercel/next': 4.4.4(encoding@0.1.13)(rollup@4.53.3) + '@vercel/node': 5.0.4(@swc/core@1.15.24)(encoding@0.1.13)(rollup@4.53.3) + '@vercel/python': 4.7.1 + '@vercel/redwood': 2.1.13(encoding@0.1.13)(rollup@4.53.3) + '@vercel/remix-builder': 5.1.1(encoding@0.1.13)(rollup@4.53.3) + '@vercel/ruby': 2.2.0 + '@vercel/static-build': 2.5.43 + chokidar: 4.0.0 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - encoding + - rollup + - supports-color + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -24330,6 +27266,8 @@ snapshots: web-namespaces@2.0.1: {} + web-vitals@0.2.4: {} + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -24526,6 +27464,10 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -24534,6 +27476,10 @@ snapshots: dependencies: isexe: 3.1.1 + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + widest-line@4.0.1: dependencies: string-width: 5.1.2 @@ -24604,8 +27550,16 @@ snapshots: dependencies: is-wsl: 3.1.0 + xdg-app-paths@5.1.0: + dependencies: + xdg-portable: 7.3.0 + xdg-basedir@5.1.0: {} + xdg-portable@7.3.0: + dependencies: + os-paths: 4.4.0 + xml-formatter@3.7.0: dependencies: xml-parser-xo: 4.1.5 @@ -24618,6 +27572,13 @@ snapshots: xml-parser-xo@4.1.5: {} + xml2js@0.6.2: + dependencies: + sax: 1.4.3 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + xmlchars@2.2.0: {} xtend@4.0.2: {} @@ -24661,8 +27622,21 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 - yn@3.1.1: - optional: true + yauzl-clone@1.0.4: + dependencies: + events-intercept: 2.0.0 + + yauzl-promise@2.1.3: + dependencies: + yauzl: 2.10.0 + yauzl-clone: 1.0.4 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yn@3.1.1: {} yocto-queue@0.1.0: {} From aa6bc09a64bb0252d882c25fa27427221c8216c8 Mon Sep 17 00:00:00 2001 From: Pedro Arantes Date: Mon, 13 Apr 2026 10:38:02 -0300 Subject: [PATCH 27/27] fix: install carlin --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 41d9a9ea..572a8669 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -36,7 +36,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}