From b05b54033fdd392c976954c291d03485f3d7bc08 Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Sat, 31 Jan 2026 13:25:09 +0000 Subject: [PATCH] feat: Initial release MCP Outline PostgreSQL v1.0.0 86 tools across 12 modules for direct PostgreSQL access to Outline Wiki: - Documents (19), Collections (14), Users (9), Groups (8) - Comments (6), Shares (5), Revisions (3), Events (3) - Attachments (5), File Operations (4), OAuth (8), Auth (2) Co-Authored-By: Claude Opus 4.5 --- .env.example | 9 + .gitignore | 33 + CHANGELOG.md | 38 + CLAUDE.md | 130 + SPEC-MCP-OUTLINE.md | 842 ++++++ package-lock.json | 5001 ++++++++++++++++++++++++++++++++++ package.json | 29 + src/config/database.ts | 52 + src/index.ts | 199 ++ src/pg-client.ts | 158 ++ src/tools/attachments.ts | 490 ++++ src/tools/auth.ts | 159 ++ src/tools/collections.ts | 1334 +++++++++ src/tools/comments.ts | 480 ++++ src/tools/documents.ts | 1342 +++++++++ src/tools/events.ts | 370 +++ src/tools/file-operations.ts | 303 ++ src/tools/groups.ts | 564 ++++ src/tools/index.ts | 41 + src/tools/oauth.ts | 546 ++++ src/tools/revisions.ts | 335 +++ src/tools/shares.ts | 470 ++++ src/tools/users.ts | 660 +++++ src/types/db.ts | 324 +++ src/types/index.ts | 7 + src/types/tools.ts | 292 ++ src/utils/index.ts | 7 + src/utils/logger.ts | 90 + src/utils/security.ts | 115 + tsconfig.json | 19 + 30 files changed, 14439 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 SPEC-MCP-OUTLINE.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/config/database.ts create mode 100644 src/index.ts create mode 100644 src/pg-client.ts create mode 100644 src/tools/attachments.ts create mode 100644 src/tools/auth.ts create mode 100644 src/tools/collections.ts create mode 100644 src/tools/comments.ts create mode 100644 src/tools/documents.ts create mode 100644 src/tools/events.ts create mode 100644 src/tools/file-operations.ts create mode 100644 src/tools/groups.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/oauth.ts create mode 100644 src/tools/revisions.ts create mode 100644 src/tools/shares.ts create mode 100644 src/tools/users.ts create mode 100644 src/types/db.ts create mode 100644 src/types/index.ts create mode 100644 src/types/tools.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/logger.ts create mode 100644 src/utils/security.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9a720a3 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Outline PostgreSQL +DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/DATABASE + +# Componentes +DB_HOST=localhost +DB_PORT=5432 +DB_USER=outline +DB_PASSWORD=your_password +DB_NAME=outline diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..271482f --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log +npm-debug.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Temporary +tmp/ +*.tmp +*.bak + +# Test coverage +coverage/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4d19458 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [1.0.0] - 2026-01-31 + +### Added + +- Initial release of MCP Outline PostgreSQL +- 86 tools across 12 modules for direct PostgreSQL access to Outline Wiki +- **Documents (19 tools):** CRUD, search, archive, move, templates, memberships +- **Collections (14 tools):** CRUD, user/group memberships, export +- **Users (9 tools):** CRUD, suspend, activate, promote, demote +- **Groups (8 tools):** CRUD, memberships management +- **Comments (6 tools):** CRUD, resolve functionality +- **Shares (5 tools):** CRUD, revoke public links +- **Revisions (3 tools):** list, info, compare versions +- **Events (3 tools):** audit log, statistics +- **Attachments (5 tools):** CRUD, storage statistics +- **File Operations (4 tools):** import/export job management +- **OAuth (8 tools):** OAuth clients and authentications +- **Auth (2 tools):** authentication info and config +- PostgreSQL client with connection pooling +- Rate limiting and security utilities +- Full TypeScript implementation with type safety +- MCP SDK v1.0.0 compatibility + +### Technical + +- Direct SQL access (not Outline API) for better performance +- Parameterized queries for SQL injection protection +- Soft delete support across all entities +- Full-text search using PostgreSQL tsvector +- Pagination and sorting on all list operations + +--- + +*Developed by Descomplicar® | descomplicar.pt* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f87abdb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,130 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +MCP server for direct PostgreSQL access to Outline Wiki database. Follows patterns established by `mcp-desk-crm-sql-v3`. + +**Architecture:** Claude Code -> MCP Outline (stdio) -> PostgreSQL (Outline DB) + +**Total Tools:** 86 tools across 12 modules + +## Commands + +```bash +# Build TypeScript to dist/ +npm run build + +# Run production server +npm start + +# Development with ts-node +npm run dev + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # MCP entry point +├── pg-client.ts # PostgreSQL client wrapper +├── config/ +│ └── database.ts # DB configuration +├── types/ +│ ├── index.ts +│ ├── tools.ts # Base tool types +│ └── db.ts # Database table types +├── tools/ +│ ├── index.ts # Export all tools +│ ├── documents.ts # 19 tools +│ ├── collections.ts # 14 tools +│ ├── users.ts # 9 tools +│ ├── groups.ts # 8 tools +│ ├── comments.ts # 6 tools +│ ├── shares.ts # 5 tools +│ ├── revisions.ts # 3 tools +│ ├── events.ts # 3 tools +│ ├── attachments.ts # 5 tools +│ ├── file-operations.ts # 4 tools +│ ├── oauth.ts # 8 tools +│ └── auth.ts # 2 tools +└── utils/ + ├── logger.ts + └── security.ts +``` + +## Tools Summary (86 total) + +| Module | Tools | Description | +|--------|-------|-------------| +| documents | 19 | CRUD, search, archive, move, templates, memberships | +| collections | 14 | CRUD, memberships, groups, export | +| users | 9 | CRUD, suspend, activate, promote, demote | +| groups | 8 | CRUD, memberships | +| comments | 6 | CRUD, resolve | +| shares | 5 | CRUD, revoke | +| revisions | 3 | list, info, compare | +| events | 3 | list, info, stats | +| attachments | 5 | CRUD, stats | +| file-operations | 4 | import/export jobs | +| oauth | 8 | OAuth clients, authentications | +| auth | 2 | auth info, config | + +## Configuration + +Add to `~/.claude.json` under `mcpServers`: + +```json +{ + "outline": { + "command": "node", + "args": ["/home/ealmeida/mcp-servers/mcp-outline-postgresql/dist/index.js"], + "env": { + "DATABASE_URL": "postgres://outline:password@localhost:5432/outline" + } + } +} +``` + +## Environment + +Required in `.env`: +``` +DATABASE_URL=postgres://user:password@host:port/outline +``` + +## Key Patterns + +### Tool Response Format + +```typescript +return { + content: [{ + type: 'text', + text: JSON.stringify(data, null, 2) + }] +}; +``` + +### Naming Conventions + +| Type | Convention | Example | +|------|------------|---------| +| Tool name | snake_case with prefix | `outline_list_documents` | +| Function | camelCase | `listDocuments` | +| Type | PascalCase | `DocumentRow` | +| File | kebab-case | `documents.ts` | + +## Database + +**PostgreSQL 15** - Direct SQL access (not Outline API) + +Key tables: `documents`, `collections`, `users`, `groups`, `comments`, `revisions`, `shares`, `attachments`, `events`, `stars`, `pins`, `views`, `file_operations`, `oauth_clients` + +Soft deletes: Most entities use `deletedAt` column, not hard deletes. + +See `SPEC-MCP-OUTLINE.md` for complete database schema. diff --git a/SPEC-MCP-OUTLINE.md b/SPEC-MCP-OUTLINE.md new file mode 100644 index 0000000..ce1e1a8 --- /dev/null +++ b/SPEC-MCP-OUTLINE.md @@ -0,0 +1,842 @@ +# MCP Outline - Especificação Completa + +**Versão:** 1.0.0 +**Data:** 2026-01-31 +**Autor:** Descomplicar® + +--- + +## 1. Visão Geral + +### Objectivo + +MCP para acesso directo à base de dados PostgreSQL do Outline, seguindo os padrões estabelecidos pelo `mcp-desk-crm-sql-v3`. + +### Arquitectura + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Claude Code │────▶│ MCP Outline │────▶│ PostgreSQL │ +│ (Cliente) │◀────│ (Servidor) │◀────│ (Outline DB) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + │ MCP Protocol │ SQL Directo + │ (stdio/SSE) │ (pg client) +``` + +### Decisões Técnicas + +| Aspecto | Decisão | Razão | +|---------|---------|-------| +| **Acesso** | SQL Directo (não API) | Performance, controlo total | +| **BD** | PostgreSQL 15 | Compatível com Outline | +| **Transporte** | stdio (dev), SSE (prod) | Padrão MCP | +| **Linguagem** | TypeScript | Type safety, padrão Desk | + +--- + +## 2. Ambiente de Desenvolvimento + +### Configuração Local + +```bash +# Base de dados +Host: localhost +Port: 5432 +User: outline +Password: outline_dev_2026 +Database: outline + +# Connection string +postgres://outline:outline_dev_2026@localhost:5432/outline +``` + +### Estrutura do Projecto + +``` +~/mcp-servers/mcp-outline/ +├── package.json +├── tsconfig.json +├── .env +├── .env.example +├── CHANGELOG.md +├── README.md +├── SPEC-MCP-OUTLINE.md ← Este ficheiro +├── src/ +│ ├── index.ts ← Entry point MCP +│ ├── pg-client.ts ← Cliente PostgreSQL +│ ├── config/ +│ │ └── database.ts ← Configuração BD +│ ├── types/ +│ │ ├── index.ts +│ │ ├── tools.ts ← Tipos base das tools +│ │ └── db.ts ← Tipos das tabelas +│ ├── tools/ +│ │ ├── index.ts ← Exporta todas as tools +│ │ ├── documents.ts +│ │ ├── collections.ts +│ │ ├── users.ts +│ │ ├── groups.ts +│ │ ├── comments.ts +│ │ ├── attachments.ts +│ │ ├── shares.ts +│ │ ├── revisions.ts +│ │ └── events.ts +│ └── utils/ +│ ├── logger.ts +│ └── security.ts +├── dist/ ← Build output +└── tests/ + └── tools/ +``` + +--- + +## 3. Dependências + +### package.json + +```json +{ + "name": "mcp-outline", + "version": "1.0.0", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts", + "test": "vitest" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "pg": "^8.11.0", + "dotenv": "^16.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/pg": "^8.10.0", + "typescript": "^5.0.0", + "tsx": "^4.0.0", + "vitest": "^1.0.0" + } +} +``` + +### tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +--- + +## 4. Tipos Base (Padrão Desk) + +### src/types/tools.ts + +```typescript +import { Pool } from 'pg'; + +export interface ToolResponse { + content: Array<{ + type: 'text'; + text: string; + }>; + [key: string]: unknown; +} + +export interface BaseTool> { + name: string; + description: string; + inputSchema: { + type: string; + properties: Record; + required?: string[]; + }; + handler: (args: TArgs, pgClient: Pool) => Promise; +} + +// Argumentos comuns +export interface PaginationArgs { + limit?: number; + offset?: number; +} + +export interface DateRangeArgs { + dateFrom?: string; + dateTo?: string; +} +``` + +--- + +## 5. API Outline → Tools MCP + +### 5.1 Documents (17 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `documents.list` | `list_documents` | SELECT | P1 | +| `documents.info` | `get_document` | SELECT | P1 | +| `documents.create` | `create_document` | INSERT | P1 | +| `documents.update` | `update_document` | UPDATE | P1 | +| `documents.delete` | `delete_document` | UPDATE (soft) | P1 | +| `documents.search` | `search_documents` | SELECT FTS | P1 | +| `documents.drafts` | `list_drafts` | SELECT | P2 | +| `documents.viewed` | `list_viewed_documents` | SELECT | P2 | +| `documents.archive` | `archive_document` | UPDATE | P2 | +| `documents.restore` | `restore_document` | UPDATE | P2 | +| `documents.move` | `move_document` | UPDATE | P2 | +| `documents.unpublish` | `unpublish_document` | UPDATE | P3 | +| `documents.templatize` | `templatize_document` | INSERT | P3 | +| `documents.export` | `export_document` | SELECT | P2 | +| `documents.import` | `import_document` | INSERT | P3 | +| `documents.users` | `list_document_users` | SELECT | P2 | +| `documents.memberships` | `list_document_memberships` | SELECT | P3 | + +#### Exemplo: list_documents + +```typescript +export const listDocuments: BaseTool = { + name: 'list_documents', + description: 'Lista documentos com filtros. Suporta paginação e ordenação.', + inputSchema: { + type: 'object', + properties: { + collection_id: { + type: 'string', + description: 'UUID da collection para filtrar' + }, + user_id: { + type: 'string', + description: 'UUID do autor para filtrar' + }, + template: { + type: 'boolean', + description: 'Filtrar apenas templates' + }, + published: { + type: 'boolean', + description: 'Filtrar apenas publicados (default: true)' + }, + limit: { + type: 'number', + description: 'Máximo de resultados (default: 25, max: 100)' + }, + offset: { + type: 'number', + description: 'Offset para paginação' + }, + sort: { + type: 'string', + enum: ['updatedAt', 'createdAt', 'title', 'index'], + description: 'Campo para ordenação' + }, + direction: { + type: 'string', + enum: ['ASC', 'DESC'], + description: 'Direcção da ordenação' + } + } + }, + handler: async (args, pgClient) => { + const limit = Math.min(args.limit || 25, 100); + const offset = args.offset || 0; + const sort = args.sort || 'updatedAt'; + const direction = args.direction || 'DESC'; + + let sql = ` + SELECT + d.id, + d.title, + d.text, + d."collectionId", + d."parentDocumentId", + d."createdById", + d."publishedAt", + d."updatedAt", + d."archivedAt", + d.template, + c.name as collection_name, + u.name as author_name + FROM documents d + LEFT JOIN collections c ON d."collectionId" = c.id + LEFT JOIN users u ON d."createdById" = u.id + WHERE d."deletedAt" IS NULL + `; + + const params: any[] = []; + let paramIndex = 1; + + if (args.collection_id) { + sql += ` AND d."collectionId" = $${paramIndex++}`; + params.push(args.collection_id); + } + + if (args.user_id) { + sql += ` AND d."createdById" = $${paramIndex++}`; + params.push(args.user_id); + } + + if (args.template !== undefined) { + sql += ` AND d.template = $${paramIndex++}`; + params.push(args.template); + } + + if (args.published !== false) { + sql += ` AND d."publishedAt" IS NOT NULL`; + } + + sql += ` ORDER BY d."${sort}" ${direction}`; + sql += ` LIMIT $${paramIndex++} OFFSET $${paramIndex++}`; + params.push(limit, offset); + + const result = await pgClient.query(sql, params); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + documents: result.rows, + pagination: { limit, offset, count: result.rows.length } + }, null, 2) + }] + }; + } +}; +``` + +### 5.2 Collections (13 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `collections.list` | `list_collections` | SELECT | P1 | +| `collections.info` | `get_collection` | SELECT | P1 | +| `collections.create` | `create_collection` | INSERT | P1 | +| `collections.update` | `update_collection` | UPDATE | P1 | +| `collections.delete` | `delete_collection` | UPDATE (soft) | P2 | +| `collections.documents` | `list_collection_documents` | SELECT | P1 | +| `collections.add_user` | `add_user_to_collection` | INSERT | P2 | +| `collections.remove_user` | `remove_user_from_collection` | DELETE | P2 | +| `collections.memberships` | `list_collection_memberships` | SELECT | P2 | +| `collections.add_group` | `add_group_to_collection` | INSERT | P3 | +| `collections.remove_group` | `remove_group_from_collection` | DELETE | P3 | +| `collections.group_memberships` | `list_collection_group_memberships` | SELECT | P3 | +| `collections.export` | `export_collection` | SELECT | P3 | + +### 5.3 Users (7 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `users.list` | `list_users` | SELECT | P1 | +| `users.info` | `get_user` | SELECT | P1 | +| `users.create` | `create_user` | INSERT | P2 | +| `users.update` | `update_user` | UPDATE | P2 | +| `users.delete` | `delete_user` | UPDATE (soft) | P3 | +| `users.suspend` | `suspend_user` | UPDATE | P2 | +| `users.activate` | `activate_user` | UPDATE | P2 | + +### 5.4 Groups (7 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `groups.list` | `list_groups` | SELECT | P1 | +| `groups.info` | `get_group` | SELECT | P1 | +| `groups.create` | `create_group` | INSERT | P2 | +| `groups.update` | `update_group` | UPDATE | P2 | +| `groups.delete` | `delete_group` | DELETE | P3 | +| `groups.memberships` | `list_group_members` | SELECT | P2 | +| `groups.add_user` | `add_user_to_group` | INSERT | P2 | + +### 5.5 Comments (5 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `comments.list` | `list_comments` | SELECT | P1 | +| `comments.info` | `get_comment` | SELECT | P2 | +| `comments.create` | `create_comment` | INSERT | P1 | +| `comments.update` | `update_comment` | UPDATE | P2 | +| `comments.delete` | `delete_comment` | DELETE | P2 | + +### 5.6 Shares (4 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `shares.info` | `get_share` | SELECT | P2 | +| `shares.create` | `create_share` | INSERT | P2 | +| `shares.update` | `update_share` | UPDATE | P3 | +| `shares.delete` | `delete_share` | DELETE | P2 | + +### 5.7 Revisions (2 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `revisions.list` | `list_revisions` | SELECT | P2 | +| `revisions.info` | `get_revision` | SELECT | P2 | + +### 5.8 Events (1 tool) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `events.list` | `list_events` | SELECT | P2 | + +### 5.9 Attachments (3 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `attachments.create` | `create_attachment` | INSERT | P3 | +| `attachments.redirect` | `get_attachment_url` | SELECT | P3 | +| `attachments.delete` | `delete_attachment` | DELETE | P3 | + +### 5.10 Auth (2 tools) + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `auth.info` | `get_auth_info` | SELECT | P3 | +| `auth.config` | `get_auth_config` | SELECT | P3 | + +### 5.11 Stars (3 tools) - NOVO + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `stars.list` | `list_stars` | SELECT | P2 | +| `stars.create` | `star_document` | INSERT | P2 | +| `stars.delete` | `unstar_document` | DELETE | P2 | + +### 5.12 Pins (3 tools) - NOVO + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `pins.list` | `list_pins` | SELECT | P2 | +| `pins.create` | `pin_document` | INSERT | P2 | +| `pins.delete` | `unpin_document` | DELETE | P2 | + +### 5.13 Views (2 tools) - NOVO + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `views.list` | `list_document_views` | SELECT | P2 | +| `views.create` | `register_view` | INSERT | P3 | + +### 5.14 Reactions (3 tools) - NOVO + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `reactions.list` | `list_reactions` | SELECT | P3 | +| `reactions.create` | `add_reaction` | INSERT | P3 | +| `reactions.delete` | `remove_reaction` | DELETE | P3 | + +### 5.15 API Keys (4 tools) - NOVO + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `apiKeys.list` | `list_api_keys` | SELECT | P2 | +| `apiKeys.create` | `create_api_key` | INSERT | P2 | +| `apiKeys.delete` | `delete_api_key` | DELETE | P2 | +| `apiKeys.info` | `get_api_key` | SELECT | P3 | + +### 5.16 Webhooks (4 tools) - NOVO + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `webhooks.list` | `list_webhooks` | SELECT | P3 | +| `webhooks.create` | `create_webhook` | INSERT | P3 | +| `webhooks.update` | `update_webhook` | UPDATE | P3 | +| `webhooks.delete` | `delete_webhook` | DELETE | P3 | + +### 5.17 Backlinks (1 tool) - NOVO + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `backlinks.list` | `list_backlinks` | SELECT | P2 | + +### 5.18 Search Queries (2 tools) - NOVO + +| API Endpoint | Tool MCP | Operação | Prioridade | +|--------------|----------|----------|------------| +| `searchQueries.list` | `list_search_queries` | SELECT | P3 | +| `searchQueries.popular` | `get_popular_searches` | SELECT | P3 | + +--- + +## 6. Resumo de Tools + +### Por Prioridade + +| Prioridade | Quantidade | Descrição | +|------------|------------|-----------| +| P1 | 18 | Core: CRUD documentos, collections, users, search | +| P2 | 37 | Secundárias: memberships, comments, shares, stars, pins, views, apiKeys | +| P3 | 28 | Avançadas: templates, OAuth, attachments, reactions, webhooks | +| **Total** | **83** | | + +### Por Módulo + +| Módulo | Tools | Estado | +|--------|-------|--------| +| Documents | 17 | A implementar | +| Collections | 13 | A implementar | +| Users | 7 | A implementar | +| Groups | 7 | A implementar | +| Comments | 5 | A implementar | +| Shares | 4 | A implementar | +| Revisions | 2 | A implementar | +| Events | 1 | A implementar | +| Attachments | 3 | A implementar | +| Auth | 2 | A implementar | +| Stars | 3 | A implementar | +| Pins | 3 | A implementar | +| Views | 2 | A implementar | +| Reactions | 3 | A implementar | +| API Keys | 4 | A implementar | +| Webhooks | 4 | A implementar | +| Backlinks | 1 | A implementar | +| Search Queries | 2 | A implementar | + +--- + +## 7. Schema PostgreSQL (Outline) + +### Tabelas Reais (41 tabelas - verificado 2026-01-31) + +```sql +-- Core Content +documents -- Documentos e drafts +collections -- Organizações de documentos +revisions -- Histórico de versões +comments -- Comentários em documentos +attachments -- Ficheiros anexados +backlinks -- Links entre documentos + +-- Users & Auth +users -- Utilizadores +teams -- Workspaces +groups -- Grupos de utilizadores +group_users -- Membros de grupos +user_permissions -- Permissões de utilizadores +user_authentications -- Autenticações de utilizadores +user_passkeys -- Passkeys WebAuthn +authentications -- Sessões de autenticação +authentication_providers -- Providers OAuth +oauth_authentications -- Tokens OAuth +oauth_authorization_codes -- Códigos OAuth +oauth_clients -- Clientes OAuth registados +apiKeys -- API tokens + +-- Permissions +collection_users -- Permissões collection-user +collection_groups -- Permissões collection-group +group_permissions -- Permissões de grupos + +-- Sharing & Social +shares -- Links públicos +stars -- Favoritos +pins -- Documentos fixados +views -- Visualizações de documentos +reactions -- Reacções (emojis em docs) +emojis -- Emojis customizados +relationships -- Relações entre entidades + +-- Notifications & Events +events -- Audit log +notifications -- Alertas +subscriptions -- Subscrições de notificações + +-- Import/Export +imports -- Imports em curso +import_tasks -- Tarefas de import +file_operations -- Operações de ficheiros + +-- Integrations +integrations -- Integrações externas +webhook_subscriptions -- Webhooks registados +webhook_deliveries -- Entregas de webhooks + +-- System +SequelizeMeta -- Migrações Sequelize +team_domains -- Domínios por team +search_queries -- Histórico de pesquisas +``` + +### Relações Chave + +``` +teams (1) ─────────< (N) users +teams (1) ─────────< (N) collections +teams (1) ─────────< (N) groups +teams (1) ─────────< (N) team_domains +teams (1) ─────────< (N) integrations + +collections (1) ────< (N) documents +collections (1) ────< (N) collection_users +collections (1) ────< (N) collection_groups + +documents (1) ──────< (N) comments +documents (1) ──────< (N) revisions +documents (1) ──────< (N) shares +documents (1) ──────< (N) attachments +documents (1) ──────< (N) views +documents (1) ──────< (N) stars +documents (1) ──────< (N) pins +documents (1) ──────< (N) reactions +documents (1) ──────< (N) backlinks + +users (1) ──────────< (N) documents (createdById) +users (1) ──────────< (N) apiKeys +users (1) ──────────< (N) user_authentications +users (1) ──────────< (N) notifications +users (1) ──────────< (N) subscriptions +users (N) ─────────<>───── (N) groups (via group_users) + +groups (N) ────────<>───── (N) collections (via collection_groups) + +integrations (1) ───< (N) webhook_subscriptions +webhook_subscriptions (1) < (N) webhook_deliveries +``` + +--- + +## 8. Implementação + +### Fase 1: Setup (MVP) + +```bash +# 1. Criar estrutura +mkdir -p ~/mcp-servers/mcp-outline/{src/{tools,types,config,utils},tests} +cd ~/mcp-servers/mcp-outline + +# 2. Inicializar +npm init -y +npm install @modelcontextprotocol/sdk pg dotenv +npm install -D typescript @types/node @types/pg tsx vitest + +# 3. Configurar +cp .env.example .env +# Editar .env com credenciais + +# 4. Build +npm run build + +# 5. Testar +npm run dev +``` + +### Fase 2: Core Tools (P1) + +Implementar por ordem: +1. `list_documents`, `get_document`, `search_documents` +2. `list_collections`, `get_collection` +3. `list_users`, `get_user` +4. `create_document`, `update_document` +5. `create_collection`, `update_collection` + +### Fase 3: Secondary Tools (P2) + +1. Comments (CRUD) +2. Memberships +3. Revisions +4. Shares +5. Events + +### Fase 4: Advanced Tools (P3) + +1. Templates +2. Attachments +3. OAuth +4. Import/Export + +--- + +## 9. Configuração Claude Code + +### ~/.claude.json + +```json +{ + "mcpServers": { + "outline": { + "command": "node", + "args": ["/home/ealmeida/mcp-servers/mcp-outline/dist/index.js"], + "env": { + "DATABASE_URL": "postgres://outline:outline_dev_2026@localhost:5432/outline" + } + } + } +} +``` + +--- + +## 10. Testes + +### Estrutura + +``` +tests/ +├── setup.ts # Configuração vitest +├── tools/ +│ ├── documents.test.ts +│ ├── collections.test.ts +│ └── users.test.ts +└── integration/ + └── full-flow.test.ts +``` + +### Exemplo Test + +```typescript +import { describe, it, expect, beforeAll } from 'vitest'; +import { Pool } from 'pg'; +import { listDocuments } from '../src/tools/documents'; + +describe('Documents Tools', () => { + let pool: Pool; + + beforeAll(() => { + pool = new Pool({ + connectionString: process.env.DATABASE_URL + }); + }); + + it('list_documents returns array', async () => { + const result = await listDocuments.handler({}, pool); + const data = JSON.parse(result.content[0].text); + + expect(Array.isArray(data.documents)).toBe(true); + expect(data.pagination).toBeDefined(); + }); +}); +``` + +--- + +## 11. Padrões de Código (do Desk) + +### Response Format + +```typescript +// SEMPRE retornar neste formato +return { + content: [{ + type: 'text', + text: JSON.stringify(data, null, 2) + }] +}; +``` + +### Error Handling + +```typescript +try { + const result = await pgClient.query(sql, params); + return { + content: [{ + type: 'text', + text: JSON.stringify({ success: true, data: result.rows }, null, 2) + }] + }; +} catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }, null, 2) + }] + }; +} +``` + +### Naming Conventions + +| Tipo | Padrão | Exemplo | +|------|--------|---------| +| Tool name | snake_case | `list_documents` | +| Function | camelCase | `listDocuments` | +| Type | PascalCase | `DocumentRow` | +| File | kebab-case | `documents.ts` | + +--- + +## 12. Checklist de Implementação + +### Setup +- [ ] Estrutura de pastas criada +- [ ] package.json configurado +- [ ] tsconfig.json configurado +- [ ] .env configurado +- [ ] pg-client.ts implementado + +### Tools P1 +- [ ] list_documents +- [ ] get_document +- [ ] create_document +- [ ] update_document +- [ ] delete_document +- [ ] search_documents +- [ ] list_collections +- [ ] get_collection +- [ ] create_collection +- [ ] update_collection +- [ ] list_collection_documents +- [ ] list_users +- [ ] get_user +- [ ] list_groups +- [ ] get_group +- [ ] list_comments +- [ ] create_comment +- [ ] list_group_members + +### Tools P2 (novas) +- [ ] list_stars +- [ ] star_document +- [ ] unstar_document +- [ ] list_pins +- [ ] pin_document +- [ ] unpin_document +- [ ] list_document_views +- [ ] list_backlinks +- [ ] list_api_keys +- [ ] create_api_key +- [ ] delete_api_key + +### Integração +- [ ] Build sem erros +- [ ] Configurado em ~/.claude.json +- [ ] Tools visíveis no Claude Code +- [ ] Testes P1 passam + +### Documentação +- [ ] README.md +- [ ] CHANGELOG.md +- [ ] Obsidian docs actualizados + +--- + +## 13. Referências + +- [Outline API Docs](https://www.getoutline.com/developers) +- [MCP SDK](https://modelcontextprotocol.io/) +- [mcp-desk-crm-sql-v3](~/mcp-servers/mcp-desk-crm-sql-v3/) +- [PostgreSQL pg docs](https://node-postgres.com/) + +--- + +*Documento gerado: 2026-01-31* +*Autor: Descomplicar®* diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..69919e6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5001 @@ +{ + "name": "mcp-outline-postgresql", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-outline-postgresql", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "dotenv": "^16.3.1", + "pg": "^8.11.3", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "@types/node": "^20.10.0", + "@types/pg": "^8.10.9", + "jest": "^29.7.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", + "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "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 + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b59ae4 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "mcp-outline-postgresql", + "version": "1.0.0", + "description": "MCP Server for Outline Wiki via PostgreSQL direct access", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "test": "jest" + }, + "keywords": ["mcp", "outline", "postgresql", "wiki"], + "author": "Descomplicar", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "pg": "^8.11.3", + "dotenv": "^16.3.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/pg": "^8.10.9", + "typescript": "^5.3.2", + "ts-node": "^10.9.2", + "jest": "^29.7.0", + "@types/jest": "^29.5.11" + } +} diff --git a/src/config/database.ts b/src/config/database.ts new file mode 100644 index 0000000..0a86de6 --- /dev/null +++ b/src/config/database.ts @@ -0,0 +1,52 @@ +/** + * MCP Outline PostgreSQL - Database Configuration + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import * as dotenv from 'dotenv'; + +dotenv.config(); + +export interface DatabaseConfig { + host: string; + port: number; + user: string; + password: string; + database: string; + ssl?: boolean; + connectionString?: string; + max?: number; // Max pool size + idleTimeoutMillis?: number; + connectionTimeoutMillis?: number; +} + +export function getDatabaseConfig(): DatabaseConfig { + // If DATABASE_URL is provided, use it + if (process.env.DATABASE_URL) { + return { + connectionString: process.env.DATABASE_URL, + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + user: process.env.DB_USER || 'outline', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'outline', + ssl: process.env.DB_SSL === 'true', + max: parseInt(process.env.DB_POOL_SIZE || '10', 10), + idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT || '30000', 10), + connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT || '5000', 10) + }; + } + + // Otherwise, use individual environment variables + return { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + user: process.env.DB_USER || 'outline', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'outline', + ssl: process.env.DB_SSL === 'true', + max: parseInt(process.env.DB_POOL_SIZE || '10', 10), + idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT || '30000', 10), + connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT || '5000', 10) + }; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2e54534 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,199 @@ +#!/usr/bin/env node +/** + * MCP Outline PostgreSQL - Main Server + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + ListToolsRequestSchema, + CallToolRequestSchema, + ListResourcesRequestSchema, + ListPromptsRequestSchema +} from '@modelcontextprotocol/sdk/types.js'; +import * as dotenv from 'dotenv'; + +import { PgClient } from './pg-client.js'; +import { getDatabaseConfig } from './config/database.js'; +import { logger } from './utils/logger.js'; +import { checkRateLimit } from './utils/security.js'; +import { BaseTool } from './types/tools.js'; + +// Import ALL tools +import { + documentsTools, + collectionsTools, + usersTools, + groupsTools, + commentsTools, + sharesTools, + revisionsTools, + eventsTools, + attachmentsTools, + fileOperationsTools, + oauthTools, + authTools +} from './tools/index.js'; + +dotenv.config(); + +// Combine ALL tools into single array +const allTools: BaseTool[] = [ + // Core functionality + ...documentsTools, + ...collectionsTools, + ...usersTools, + ...groupsTools, + + // Collaboration + ...commentsTools, + ...sharesTools, + ...revisionsTools, + + // System + ...eventsTools, + ...attachmentsTools, + ...fileOperationsTools, + + // Authentication + ...oauthTools, + ...authTools +]; + +// Validate all tools have required properties +const invalidTools = allTools.filter((tool) => !tool.name || !tool.handler); +if (invalidTools.length > 0) { + logger.error(`${invalidTools.length} invalid tools found`); + process.exit(1); +} + +async function main() { + // Get database configuration + const config = getDatabaseConfig(); + + // Initialize PostgreSQL client + const pgClient = new PgClient(config); + + // Test database connection + const isConnected = await pgClient.testConnection(); + if (!isConnected) { + throw new Error('Failed to connect to PostgreSQL database'); + } + + // Initialize MCP server + const server = new Server({ + name: 'mcp-outline', + version: '1.0.0' + }); + + // Set capabilities (required for MCP v2.2+) + (server as any)._capabilities = { + tools: {}, + resources: {}, + prompts: {} + }; + + // Connect transport BEFORE registering handlers + const transport = new StdioServerTransport(); + await server.connect(transport); + + // Register tools list handler + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: allTools.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema + })) + })); + + // Register resources handler (required even if empty) + server.setRequestHandler(ListResourcesRequestSchema, async () => { + logger.debug('Resources list requested'); + return { resources: [] }; + }); + + // Register prompts handler (required even if empty) + server.setRequestHandler(ListPromptsRequestSchema, async () => { + logger.debug('Prompts list requested'); + return { prompts: [] }; + }); + + // Register tool call handler + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + // Rate limiting (using 'default' as clientId for now) + const clientId = process.env.CLIENT_ID || 'default'; + if (!checkRateLimit('api', clientId)) { + return { + content: [ + { type: 'text', text: 'Too Many Requests: rate limit exceeded. Try again later.' } + ] + }; + } + + // Find the tool handler + const tool = allTools.find((t) => t.name === name); + + if (!tool) { + return { + content: [ + { + type: 'text', + text: `Tool '${name}' not found` + } + ] + }; + } + + try { + // Pass the pool directly to tool handlers + return await tool.handler(args as Record, pgClient.getPool()); + } catch (error) { + logger.error(`Error in tool ${name}:`, { + error: error instanceof Error ? error.message : String(error) + }); + return { + content: [ + { + type: 'text', + text: `Error in tool ${name}: ${error instanceof Error ? error.message : String(error)}` + } + ] + }; + } + }); + + // Log startup (minimal logging for MCP protocol compatibility) + if (process.env.LOG_LEVEL !== 'error' && process.env.LOG_LEVEL !== 'none') { + logger.info('MCP Server started'); + } + + // Debug logging + logger.debug('MCP Outline PostgreSQL Server running', { + totalTools: allTools.length, + toolsByModule: { + documents: documentsTools.length, + collections: collectionsTools.length, + users: usersTools.length, + groups: groupsTools.length, + comments: commentsTools.length, + shares: sharesTools.length, + revisions: revisionsTools.length, + events: eventsTools.length, + attachments: attachmentsTools.length, + fileOperations: fileOperationsTools.length, + oauth: oauthTools.length, + auth: authTools.length + } + }); +} + +main().catch((error) => { + logger.error('Fatal error', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }); + process.exit(1); +}); diff --git a/src/pg-client.ts b/src/pg-client.ts new file mode 100644 index 0000000..687ae9d --- /dev/null +++ b/src/pg-client.ts @@ -0,0 +1,158 @@ +/** + * MCP Outline PostgreSQL - PostgreSQL Client + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool, PoolConfig, QueryResult, QueryResultRow } from 'pg'; +import { DatabaseConfig } from './config/database.js'; +import { logger } from './utils/logger.js'; + +export class PgClient { + private pool: Pool; + private isConnected: boolean = false; + + constructor(config: DatabaseConfig) { + const poolConfig: PoolConfig = config.connectionString + ? { + connectionString: config.connectionString, + max: config.max, + idleTimeoutMillis: config.idleTimeoutMillis, + connectionTimeoutMillis: config.connectionTimeoutMillis + } + : { + host: config.host, + port: config.port, + user: config.user, + password: config.password, + database: config.database, + ssl: config.ssl ? { rejectUnauthorized: false } : false, + max: config.max, + idleTimeoutMillis: config.idleTimeoutMillis, + connectionTimeoutMillis: config.connectionTimeoutMillis + }; + + this.pool = new Pool(poolConfig); + + // Handle pool errors + this.pool.on('error', (err) => { + logger.error('Unexpected PostgreSQL pool error', { error: err.message }); + }); + } + + /** + * Get the underlying pool for direct access + */ + getPool(): Pool { + return this.pool; + } + + /** + * Test database connection + */ + async testConnection(): Promise { + try { + const client = await this.pool.connect(); + await client.query('SELECT 1'); + client.release(); + this.isConnected = true; + logger.info('PostgreSQL connection successful'); + return true; + } catch (error) { + logger.error('PostgreSQL connection failed', { + error: error instanceof Error ? error.message : String(error) + }); + this.isConnected = false; + return false; + } + } + + /** + * Execute a query with parameters + */ + async query(sql: string, params?: any[]): Promise { + const start = Date.now(); + try { + const result = await this.pool.query(sql, params); + const duration = Date.now() - start; + + logger.debug('Query executed', { + sql: sql.substring(0, 100), + duration, + rowCount: result.rowCount + }); + + return result.rows; + } catch (error) { + const duration = Date.now() - start; + logger.error('Query failed', { + sql: sql.substring(0, 100), + duration, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + } + + /** + * Execute a query and return the full result + */ + async queryRaw(sql: string, params?: any[]): Promise> { + return this.pool.query(sql, params); + } + + /** + * Execute a query and return a single row + */ + async queryOne(sql: string, params?: any[]): Promise { + const rows = await this.query(sql, params); + return rows.length > 0 ? rows[0] : null; + } + + /** + * Execute multiple queries in a transaction + */ + async transaction(callback: (client: any) => Promise): Promise { + const client = await this.pool.connect(); + try { + await client.query('BEGIN'); + const result = await callback(client); + await client.query('COMMIT'); + return result; + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } + + /** + * Close the pool + */ + async close(): Promise { + await this.pool.end(); + this.isConnected = false; + logger.info('PostgreSQL pool closed'); + } + + /** + * Check if connected + */ + isPoolConnected(): boolean { + return this.isConnected; + } +} + +// Export a singleton factory +let instance: PgClient | null = null; + +export function createPgClient(config: DatabaseConfig): PgClient { + if (!instance) { + instance = new PgClient(config); + } + return instance; +} + +export function getPgClient(): PgClient | null { + return instance; +} diff --git a/src/tools/attachments.ts b/src/tools/attachments.ts new file mode 100644 index 0000000..b5a3e0c --- /dev/null +++ b/src/tools/attachments.ts @@ -0,0 +1,490 @@ +/** + * MCP Outline PostgreSQL - Attachments Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse, CreateAttachmentArgs, GetAttachmentArgs, PaginationArgs } from '../types/tools.js'; +import { validatePagination, isValidUUID } from '../utils/security.js'; + +interface AttachmentListArgs extends PaginationArgs { + document_id?: string; + user_id?: string; + team_id?: string; +} + +/** + * attachments.list - List attachments with optional filters + */ +const listAttachments: BaseTool = { + name: 'outline_attachments_list', + description: 'List file attachments with optional filtering by document, user, or team. Supports pagination.', + inputSchema: { + type: 'object', + properties: { + document_id: { + type: 'string', + description: 'Filter by document ID (UUID)', + }, + user_id: { + type: 'string', + description: 'Filter by user ID who uploaded (UUID)', + }, + team_id: { + type: 'string', + description: 'Filter by team ID (UUID)', + }, + limit: { + type: 'number', + description: 'Maximum number of results (default: 25, max: 100)', + }, + offset: { + type: 'number', + description: 'Number of results to skip (default: 0)', + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { limit, offset } = validatePagination(args.limit, args.offset); + const conditions: string[] = ['a."deletedAt" IS NULL']; + const params: any[] = []; + let paramIndex = 1; + + if (args.document_id) { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + conditions.push(`a."documentId" = $${paramIndex++}`); + params.push(args.document_id); + } + + if (args.user_id) { + if (!isValidUUID(args.user_id)) { + throw new Error('Invalid user_id format'); + } + conditions.push(`a."userId" = $${paramIndex++}`); + params.push(args.user_id); + } + + if (args.team_id) { + if (!isValidUUID(args.team_id)) { + throw new Error('Invalid team_id format'); + } + conditions.push(`a."teamId" = $${paramIndex++}`); + params.push(args.team_id); + } + + const whereClause = `WHERE ${conditions.join(' AND ')}`; + + const query = ` + SELECT + a.id, + a.key, + a.url, + a."contentType", + a.size, + a.acl, + a."documentId", + a."userId", + a."teamId", + a."createdAt", + a."updatedAt", + d.title as "documentTitle", + u.name as "uploadedByName", + u.email as "uploadedByEmail" + FROM attachments a + LEFT JOIN documents d ON a."documentId" = d.id + LEFT JOIN users u ON a."userId" = u.id + ${whereClause} + ORDER BY a."createdAt" DESC + LIMIT $${paramIndex++} OFFSET $${paramIndex} + `; + + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows, + pagination: { + limit, + offset, + total: result.rows.length, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * attachments.info - Get detailed information about a specific attachment + */ +const getAttachment: BaseTool = { + name: 'outline_attachments_info', + description: 'Get detailed information about a specific attachment by ID.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Attachment ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid attachment ID format'); + } + + const query = ` + SELECT + a.id, + a.key, + a.url, + a."contentType", + a.size, + a.acl, + a."documentId", + a."userId", + a."teamId", + a."createdAt", + a."updatedAt", + a."deletedAt", + d.title as "documentTitle", + d."collectionId", + u.name as "uploadedByName", + u.email as "uploadedByEmail", + t.name as "teamName" + FROM attachments a + LEFT JOIN documents d ON a."documentId" = d.id + LEFT JOIN users u ON a."userId" = u.id + LEFT JOIN teams t ON a."teamId" = t.id + WHERE a.id = $1 + `; + + const result = await pgClient.query(query, [args.id]); + + if (result.rows.length === 0) { + throw new Error('Attachment not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * attachments.create - Create a new attachment record + */ +const createAttachment: BaseTool = { + name: 'outline_attachments_create', + description: 'Create a new attachment record. Note: This creates the database record only, actual file upload is handled separately.', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Attachment filename/key', + }, + document_id: { + type: 'string', + description: 'Document ID to attach to (UUID, optional)', + }, + content_type: { + type: 'string', + description: 'MIME type (e.g., "image/png", "application/pdf")', + }, + size: { + type: 'number', + description: 'File size in bytes', + }, + }, + required: ['name', 'content_type', 'size'], + }, + handler: async (args, pgClient): Promise => { + if (args.document_id && !isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + + // Verify document exists if provided + if (args.document_id) { + const docCheck = await pgClient.query( + 'SELECT id, "teamId" FROM documents WHERE id = $1 AND "deletedAt" IS NULL', + [args.document_id] + ); + + if (docCheck.rows.length === 0) { + throw new Error('Document not found or deleted'); + } + } + + // Get first admin user and team + const userQuery = await pgClient.query( + 'SELECT u.id, u."teamId" FROM users u WHERE u."isAdmin" = true AND u."deletedAt" IS NULL LIMIT 1' + ); + + if (userQuery.rows.length === 0) { + throw new Error('No valid user found to create attachment'); + } + + const userId = userQuery.rows[0].id; + const teamId = userQuery.rows[0].teamId; + + // Generate URL and key (in real implementation, this would be S3/storage URL) + const key = `attachments/${Date.now()}-${args.name}`; + const url = `/api/attachments.redirect?id=PLACEHOLDER`; + + const query = ` + INSERT INTO attachments ( + key, + url, + "contentType", + size, + acl, + "documentId", + "userId", + "teamId", + "createdAt", + "updatedAt" + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW()) + RETURNING * + `; + + const result = await pgClient.query(query, [ + key, + url, + args.content_type, + args.size, + 'private', // Default ACL + args.document_id || null, + userId, + teamId, + ]); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * attachments.delete - Delete an attachment (soft delete) + */ +const deleteAttachment: BaseTool = { + name: 'outline_attachments_delete', + description: 'Soft delete an attachment. The attachment record is marked as deleted but not removed from the database.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Attachment ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid attachment ID format'); + } + + const query = ` + UPDATE attachments + SET + "deletedAt" = NOW(), + "updatedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL + RETURNING id, key, "documentId" + `; + + const result = await pgClient.query(query, [args.id]); + + if (result.rows.length === 0) { + throw new Error('Attachment not found or already deleted'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: 'Attachment deleted successfully', + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * attachments.stats - Get attachment statistics + */ +const getAttachmentStats: BaseTool<{ team_id?: string; document_id?: string }> = { + name: 'outline_attachments_stats', + description: 'Get statistics about attachments including total count, size, and breakdown by content type.', + inputSchema: { + type: 'object', + properties: { + team_id: { + type: 'string', + description: 'Filter statistics by team ID (UUID)', + }, + document_id: { + type: 'string', + description: 'Filter statistics by document ID (UUID)', + }, + }, + }, + handler: async (args, pgClient): Promise => { + const conditions: string[] = ['a."deletedAt" IS NULL']; + const params: any[] = []; + let paramIndex = 1; + + if (args.team_id) { + if (!isValidUUID(args.team_id)) { + throw new Error('Invalid team_id format'); + } + conditions.push(`a."teamId" = $${paramIndex++}`); + params.push(args.team_id); + } + + if (args.document_id) { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + conditions.push(`a."documentId" = $${paramIndex++}`); + params.push(args.document_id); + } + + const whereClause = `WHERE ${conditions.join(' AND ')}`; + + // Overall statistics + const overallStatsQuery = await pgClient.query( + `SELECT + COUNT(*) as "totalAttachments", + SUM(size) as "totalSize", + AVG(size) as "averageSize", + COUNT(DISTINCT "documentId") as "documentsWithAttachments", + COUNT(DISTINCT "userId") as "uniqueUploaders" + FROM attachments a + ${whereClause}`, + params + ); + + // By content type + const byContentTypeQuery = await pgClient.query( + `SELECT + a."contentType", + COUNT(*) as count, + SUM(size) as "totalSize" + FROM attachments a + ${whereClause} + GROUP BY a."contentType" + ORDER BY count DESC`, + params + ); + + // Top uploaders + const topUploadersQuery = await pgClient.query( + `SELECT + a."userId", + u.name as "userName", + u.email as "userEmail", + COUNT(*) as "attachmentCount", + SUM(a.size) as "totalSize" + FROM attachments a + LEFT JOIN users u ON a."userId" = u.id + ${whereClause} + GROUP BY a."userId", u.name, u.email + ORDER BY "attachmentCount" DESC + LIMIT 10`, + params + ); + + // Recent uploads + const recentUploadsQuery = await pgClient.query( + `SELECT + a.id, + a.key, + a."contentType", + a.size, + a."createdAt", + u.name as "uploadedByName", + d.title as "documentTitle" + FROM attachments a + LEFT JOIN users u ON a."userId" = u.id + LEFT JOIN documents d ON a."documentId" = d.id + ${whereClause} + ORDER BY a."createdAt" DESC + LIMIT 10`, + params + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + overall: overallStatsQuery.rows[0], + byContentType: byContentTypeQuery.rows, + topUploaders: topUploadersQuery.rows, + recentUploads: recentUploadsQuery.rows, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all attachment tools +export const attachmentsTools: BaseTool[] = [ + listAttachments, + getAttachment, + createAttachment, + deleteAttachment, + getAttachmentStats, +]; diff --git a/src/tools/auth.ts b/src/tools/auth.ts new file mode 100644 index 0000000..075024f --- /dev/null +++ b/src/tools/auth.ts @@ -0,0 +1,159 @@ +/** + * MCP Outline PostgreSQL - Authentication Tools + * Provides authentication information and configuration + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse } from '../types/tools.js'; + +interface AuthenticationProvider { + id: string; + name: string; + enabled: boolean; + teamId: string; + createdAt: Date; + updatedAt: Date; +} + +/** + * auth.info - Get current authentication details + */ +const getAuthInfo: BaseTool> = { + name: 'outline_auth_info', + description: 'Get information about the current authentication context. Returns team and user information based on the database connection.', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async (args, pgClient): Promise => { + // Get general authentication statistics + const statsQuery = await pgClient.query(` + SELECT + (SELECT COUNT(*) FROM users WHERE "deletedAt" IS NULL) as total_users, + (SELECT COUNT(*) FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL) as admin_users, + (SELECT COUNT(*) FROM users WHERE "isSuspended" = true) as suspended_users, + (SELECT COUNT(*) FROM teams) as total_teams, + (SELECT COUNT(*) FROM oauth_clients) as oauth_clients, + (SELECT COUNT(*) FROM oauth_authentications) as oauth_authentications + `); + + const stats = statsQuery.rows[0]; + + // Get recent authentication activity + const recentActivity = await pgClient.query(` + SELECT + u.id, + u.name, + u.email, + u."lastActiveAt", + u."lastSignedInAt", + u."isAdmin", + u."isSuspended" + FROM users u + WHERE u."deletedAt" IS NULL + ORDER BY u."lastActiveAt" DESC NULLS LAST + LIMIT 10 + `); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + statistics: { + totalUsers: stats.total_users, + adminUsers: stats.admin_users, + suspendedUsers: stats.suspended_users, + totalTeams: stats.total_teams, + oauthClients: stats.oauth_clients, + oauthAuthentications: stats.oauth_authentications, + }, + recentActivity: recentActivity.rows, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * auth.config - Get authentication provider configuration + */ +const getAuthConfig: BaseTool> = { + name: 'outline_auth_config', + description: 'Get authentication provider configuration. Returns enabled authentication methods and their settings.', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async (args, pgClient): Promise => { + // Get authentication providers + const providersQuery = await pgClient.query(` + SELECT + ap.id, + ap.name, + ap.enabled, + ap."teamId", + ap."createdAt", + ap."updatedAt", + t.name as "teamName" + FROM authentication_providers ap + LEFT JOIN teams t ON ap."teamId" = t.id + ORDER BY ap.name + `); + + // Get team authentication settings + const teamsQuery = await pgClient.query(` + SELECT + id, + name, + subdomain, + domain, + "guestSignin", + "inviteRequired", + "defaultUserRole", + "createdAt" + FROM teams + `); + + // Get OAuth client statistics per team + const oauthStatsQuery = await pgClient.query(` + SELECT + t.id as "teamId", + t.name as "teamName", + COUNT(oc.id) as "clientCount" + FROM teams t + LEFT JOIN oauth_clients oc ON oc."teamId" = t.id + GROUP BY t.id, t.name + `); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + providers: providersQuery.rows, + teams: teamsQuery.rows, + oauthStatistics: oauthStatsQuery.rows, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all authentication tools +export const authTools: BaseTool[] = [getAuthInfo, getAuthConfig]; diff --git a/src/tools/collections.ts b/src/tools/collections.ts new file mode 100644 index 0000000..e732d73 --- /dev/null +++ b/src/tools/collections.ts @@ -0,0 +1,1334 @@ +/** + * MCP Outline PostgreSQL - Collections Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse } from '../types/tools.js'; +import { validatePagination, isValidUUID } from '../utils/security.js'; + +export const collectionsTools: BaseTool[] = [ + // 1. LIST COLLECTIONS + { + name: 'list_collections', + description: 'List all collections in the team. Supports pagination and filtering by teamId.', + inputSchema: { + type: 'object', + properties: { + teamId: { + type: 'string', + description: 'Filter collections by team ID (UUID format)', + }, + offset: { + type: 'number', + description: 'Number of records to skip (default: 0)', + default: 0, + }, + limit: { + type: 'number', + description: 'Maximum number of records to return (default: 25, max: 100)', + default: 25, + }, + }, + }, + handler: async (args: { teamId?: string; offset?: number; limit?: number }, pool: Pool): Promise => { + try { + const { offset = 0, limit = 25, teamId } = args; + validatePagination(offset, limit); + + let query = ` + SELECT + c.id, + c."urlId", + c.name, + c.description, + c.icon, + c.color, + c.index, + c.permission, + c."maintainerApprovalRequired", + c."documentStructure", + c.sharing, + c.sort, + c."teamId", + c."createdById", + c."createdAt", + c."updatedAt", + c."deletedAt", + c."archivedAt", + u.name as "createdByName", + u.email as "createdByEmail", + (SELECT COUNT(*) FROM documents WHERE "collectionId" = c.id AND "deletedAt" IS NULL) as "documentCount", + (SELECT COUNT(*) FROM collection_users WHERE "collectionId" = c.id) as "memberCount" + FROM collections c + LEFT JOIN users u ON c."createdById" = u.id + WHERE c."deletedAt" IS NULL + `; + const params: any[] = []; + + if (teamId) { + if (!isValidUUID(teamId)) { + return { + content: [{ type: 'text', text: 'Invalid teamId format. Must be a valid UUID.' }], + isError: true, + }; + } + params.push(teamId); + query += ` AND c."teamId" = $${params.length}`; + } + + query += ` + ORDER BY c.index ASC, c."createdAt" DESC + LIMIT $${params.length + 1} OFFSET $${params.length + 2} + `; + params.push(limit, offset); + + const result = await pool.query(query, params); + + // Get total count + let countQuery = 'SELECT COUNT(*) FROM collections WHERE "deletedAt" IS NULL'; + const countParams: any[] = []; + if (teamId) { + countParams.push(teamId); + countQuery += ` AND "teamId" = $1`; + } + const countResult = await pool.query(countQuery, countParams); + const totalCount = parseInt(countResult.rows[0].count); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + collections: result.rows, + pagination: { + total: totalCount, + offset, + limit, + hasMore: offset + limit < totalCount, + }, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error listing collections: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 2. GET COLLECTION + { + name: 'get_collection', + description: 'Get detailed information about a specific collection by ID or urlId.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Collection ID (UUID) or urlId', + }, + }, + required: ['id'], + }, + handler: async (args: { id: string }, pool: Pool): Promise => { + try { + const { id } = args; + + const query = ` + SELECT + c.id, + c."urlId", + c.name, + c.description, + c.icon, + c.color, + c.index, + c.permission, + c."maintainerApprovalRequired", + c."documentStructure", + c.sharing, + c.sort, + c."teamId", + c."createdById", + c."createdAt", + c."updatedAt", + c."deletedAt", + c."archivedAt", + u.name as "createdByName", + u.email as "createdByEmail", + t.name as "teamName", + (SELECT COUNT(*) FROM documents WHERE "collectionId" = c.id AND "deletedAt" IS NULL) as "documentCount", + (SELECT COUNT(*) FROM collection_users WHERE "collectionId" = c.id) as "memberCount", + (SELECT COUNT(*) FROM collection_groups WHERE "collectionId" = c.id) as "groupCount" + FROM collections c + LEFT JOIN users u ON c."createdById" = u.id + LEFT JOIN teams t ON c."teamId" = t.id + WHERE (c.id = $1 OR c."urlId" = $1) + AND c."deletedAt" IS NULL + `; + + const result = await pool.query(query, [id]); + + if (result.rows.length === 0) { + return { + content: [{ type: 'text', text: 'Collection not found or has been deleted.' }], + isError: true, + }; + } + + return { + content: [{ type: 'text', text: JSON.stringify(result.rows[0], null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error getting collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 3. CREATE COLLECTION + { + name: 'create_collection', + description: 'Create a new collection in a team.', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Collection name', + }, + teamId: { + type: 'string', + description: 'Team ID (UUID)', + }, + createdById: { + type: 'string', + description: 'User ID creating the collection (UUID)', + }, + description: { + type: 'string', + description: 'Collection description', + }, + icon: { + type: 'string', + description: 'Collection icon (emoji or icon name)', + }, + color: { + type: 'string', + description: 'Collection color (hex format)', + }, + permission: { + type: 'string', + description: 'Default permission level (read, read_write)', + enum: ['read', 'read_write'], + default: 'read_write', + }, + sharing: { + type: 'boolean', + description: 'Allow public sharing', + default: true, + }, + index: { + type: 'string', + description: 'Custom index for sorting', + }, + }, + required: ['name', 'teamId', 'createdById'], + }, + handler: async (args: { + name: string; + teamId: string; + createdById: string; + description?: string; + icon?: string; + color?: string; + permission?: string; + sharing?: boolean; + index?: string; + }, pool: Pool): Promise => { + try { + const { name, teamId, createdById, description, icon, color, permission = 'read_write', sharing = true, index } = args; + + // Validate UUIDs + if (!isValidUUID(teamId) || !isValidUUID(createdById)) { + return { + content: [{ type: 'text', text: 'Invalid teamId or createdById format. Must be valid UUIDs.' }], + isError: true, + }; + } + + // Generate urlId from name + const urlId = name.toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .substring(0, 50); + + const query = ` + INSERT INTO collections ( + name, "urlId", "teamId", "createdById", description, icon, color, + permission, sharing, index, "createdAt", "updatedAt" + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW()) + RETURNING + id, "urlId", name, description, icon, color, index, permission, + sharing, "teamId", "createdById", "createdAt", "updatedAt" + `; + + const result = await pool.query(query, [ + name, + urlId, + teamId, + createdById, + description || null, + icon || null, + color || null, + permission, + sharing, + index || null, + ]); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + message: 'Collection created successfully', + collection: result.rows[0], + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error creating collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 4. UPDATE COLLECTION + { + name: 'update_collection', + description: 'Update an existing collection.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Collection ID (UUID)', + }, + name: { + type: 'string', + description: 'New collection name', + }, + description: { + type: 'string', + description: 'New description', + }, + icon: { + type: 'string', + description: 'New icon', + }, + color: { + type: 'string', + description: 'New color (hex format)', + }, + permission: { + type: 'string', + description: 'New default permission', + enum: ['read', 'read_write'], + }, + sharing: { + type: 'boolean', + description: 'New sharing setting', + }, + index: { + type: 'string', + description: 'New index for sorting', + }, + }, + required: ['id'], + }, + handler: async (args: { + id: string; + name?: string; + description?: string; + icon?: string; + color?: string; + permission?: string; + sharing?: boolean; + index?: string; + }, pool: Pool): Promise => { + try { + const { id, name, description, icon, color, permission, sharing, index } = args; + + if (!isValidUUID(id)) { + return { + content: [{ type: 'text', text: 'Invalid collection ID format. Must be a valid UUID.' }], + isError: true, + }; + } + + // Build dynamic UPDATE query + const updates: string[] = []; + const values: any[] = []; + let paramCount = 1; + + if (name !== undefined) { + updates.push(`name = $${paramCount}`); + values.push(name); + paramCount++; + + // Update urlId if name changes + const urlId = name.toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .substring(0, 50); + updates.push(`"urlId" = $${paramCount}`); + values.push(urlId); + paramCount++; + } + + if (description !== undefined) { + updates.push(`description = $${paramCount}`); + values.push(description); + paramCount++; + } + + if (icon !== undefined) { + updates.push(`icon = $${paramCount}`); + values.push(icon); + paramCount++; + } + + if (color !== undefined) { + updates.push(`color = $${paramCount}`); + values.push(color); + paramCount++; + } + + if (permission !== undefined) { + updates.push(`permission = $${paramCount}`); + values.push(permission); + paramCount++; + } + + if (sharing !== undefined) { + updates.push(`sharing = $${paramCount}`); + values.push(sharing); + paramCount++; + } + + if (index !== undefined) { + updates.push(`index = $${paramCount}`); + values.push(index); + paramCount++; + } + + if (updates.length === 0) { + return { + content: [{ type: 'text', text: 'No fields to update provided.' }], + isError: true, + }; + } + + updates.push(`"updatedAt" = NOW()`); + values.push(id); + + const query = ` + UPDATE collections + SET ${updates.join(', ')} + WHERE id = $${paramCount} AND "deletedAt" IS NULL + RETURNING + id, "urlId", name, description, icon, color, index, permission, + sharing, "teamId", "createdById", "createdAt", "updatedAt" + `; + + const result = await pool.query(query, values); + + if (result.rows.length === 0) { + return { + content: [{ type: 'text', text: 'Collection not found or has been deleted.' }], + isError: true, + }; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + message: 'Collection updated successfully', + collection: result.rows[0], + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error updating collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 5. DELETE COLLECTION + { + name: 'delete_collection', + description: 'Soft delete a collection (sets deletedAt timestamp).', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Collection ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args: { id: string }, pool: Pool): Promise => { + try { + const { id } = args; + + if (!isValidUUID(id)) { + return { + content: [{ type: 'text', text: 'Invalid collection ID format. Must be a valid UUID.' }], + isError: true, + }; + } + + const query = ` + UPDATE collections + SET "deletedAt" = NOW(), "updatedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL + RETURNING id, name, "deletedAt" + `; + + const result = await pool.query(query, [id]); + + if (result.rows.length === 0) { + return { + content: [{ type: 'text', text: 'Collection not found or already deleted.' }], + isError: true, + }; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + message: 'Collection deleted successfully', + collection: result.rows[0], + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error deleting collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 6. LIST COLLECTION DOCUMENTS + { + name: 'list_collection_documents', + description: 'List all documents in a collection with pagination.', + inputSchema: { + type: 'object', + properties: { + collectionId: { + type: 'string', + description: 'Collection ID (UUID)', + }, + offset: { + type: 'number', + description: 'Number of records to skip (default: 0)', + default: 0, + }, + limit: { + type: 'number', + description: 'Maximum number of records to return (default: 25, max: 100)', + default: 25, + }, + }, + required: ['collectionId'], + }, + handler: async (args: { collectionId: string; offset?: number; limit?: number }, pool: Pool): Promise => { + try { + const { collectionId, offset = 0, limit = 25 } = args; + + if (!isValidUUID(collectionId)) { + return { + content: [{ type: 'text', text: 'Invalid collectionId format. Must be a valid UUID.' }], + isError: true, + }; + } + + validatePagination(offset, limit); + + const query = ` + SELECT + d.id, + d."urlId", + d.title, + d.emoji, + d."collectionId", + d."parentDocumentId", + d.template, + d.fullWidth, + d.insightsEnabled, + d.publish, + d."createdById", + d."updatedById", + d."createdAt", + d."updatedAt", + d."publishedAt", + d."archivedAt", + d."deletedAt", + creator.name as "createdByName", + creator.email as "createdByEmail", + updater.name as "updatedByName", + updater.email as "updatedByEmail" + FROM documents d + LEFT JOIN users creator ON d."createdById" = creator.id + LEFT JOIN users updater ON d."updatedById" = updater.id + WHERE d."collectionId" = $1 AND d."deletedAt" IS NULL + ORDER BY d."updatedAt" DESC + LIMIT $2 OFFSET $3 + `; + + const result = await pool.query(query, [collectionId, limit, offset]); + + // Get total count + const countQuery = 'SELECT COUNT(*) FROM documents WHERE "collectionId" = $1 AND "deletedAt" IS NULL'; + const countResult = await pool.query(countQuery, [collectionId]); + const totalCount = parseInt(countResult.rows[0].count); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + documents: result.rows, + pagination: { + total: totalCount, + offset, + limit, + hasMore: offset + limit < totalCount, + }, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error listing collection documents: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 7. ADD USER TO COLLECTION + { + name: 'add_user_to_collection', + description: 'Add a user to a collection with specific permissions.', + inputSchema: { + type: 'object', + properties: { + collectionId: { + type: 'string', + description: 'Collection ID (UUID)', + }, + userId: { + type: 'string', + description: 'User ID (UUID)', + }, + permission: { + type: 'string', + description: 'Permission level for the user', + enum: ['read', 'read_write', 'maintain'], + default: 'read_write', + }, + createdById: { + type: 'string', + description: 'User ID adding the member (UUID)', + }, + }, + required: ['collectionId', 'userId', 'createdById'], + }, + handler: async (args: { + collectionId: string; + userId: string; + permission?: string; + createdById: string; + }, pool: Pool): Promise => { + try { + const { collectionId, userId, permission = 'read_write', createdById } = args; + + if (!isValidUUID(collectionId) || !isValidUUID(userId) || !isValidUUID(createdById)) { + return { + content: [{ type: 'text', text: 'Invalid UUID format for collectionId, userId, or createdById.' }], + isError: true, + }; + } + + const query = ` + INSERT INTO collection_users ("collectionId", "userId", permission, "createdById", "createdAt", "updatedAt") + VALUES ($1, $2, $3, $4, NOW(), NOW()) + ON CONFLICT ("collectionId", "userId") + DO UPDATE SET permission = $3, "updatedAt" = NOW() + RETURNING + id, "collectionId", "userId", permission, "createdById", "createdAt", "updatedAt" + `; + + const result = await pool.query(query, [collectionId, userId, permission, createdById]); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + message: 'User added to collection successfully', + membership: result.rows[0], + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error adding user to collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 8. REMOVE USER FROM COLLECTION + { + name: 'remove_user_from_collection', + description: 'Remove a user from a collection.', + inputSchema: { + type: 'object', + properties: { + collectionId: { + type: 'string', + description: 'Collection ID (UUID)', + }, + userId: { + type: 'string', + description: 'User ID (UUID)', + }, + }, + required: ['collectionId', 'userId'], + }, + handler: async (args: { collectionId: string; userId: string }, pool: Pool): Promise => { + try { + const { collectionId, userId } = args; + + if (!isValidUUID(collectionId) || !isValidUUID(userId)) { + return { + content: [{ type: 'text', text: 'Invalid UUID format for collectionId or userId.' }], + isError: true, + }; + } + + const query = ` + DELETE FROM collection_users + WHERE "collectionId" = $1 AND "userId" = $2 + RETURNING id, "collectionId", "userId" + `; + + const result = await pool.query(query, [collectionId, userId]); + + if (result.rows.length === 0) { + return { + content: [{ type: 'text', text: 'User membership not found in collection.' }], + isError: true, + }; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + message: 'User removed from collection successfully', + membership: result.rows[0], + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error removing user from collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 9. LIST COLLECTION MEMBERSHIPS + { + name: 'list_collection_memberships', + description: 'List all user memberships for a collection.', + inputSchema: { + type: 'object', + properties: { + collectionId: { + type: 'string', + description: 'Collection ID (UUID)', + }, + offset: { + type: 'number', + description: 'Number of records to skip (default: 0)', + default: 0, + }, + limit: { + type: 'number', + description: 'Maximum number of records to return (default: 25, max: 100)', + default: 25, + }, + }, + required: ['collectionId'], + }, + handler: async (args: { collectionId: string; offset?: number; limit?: number }, pool: Pool): Promise => { + try { + const { collectionId, offset = 0, limit = 25 } = args; + + if (!isValidUUID(collectionId)) { + return { + content: [{ type: 'text', text: 'Invalid collectionId format. Must be a valid UUID.' }], + isError: true, + }; + } + + validatePagination(offset, limit); + + const query = ` + SELECT + cu.id, + cu."collectionId", + cu."userId", + cu.permission, + cu."createdById", + cu."createdAt", + cu."updatedAt", + u.name as "userName", + u.email as "userEmail", + creator.name as "addedByName" + FROM collection_users cu + LEFT JOIN users u ON cu."userId" = u.id + LEFT JOIN users creator ON cu."createdById" = creator.id + WHERE cu."collectionId" = $1 + ORDER BY cu."createdAt" DESC + LIMIT $2 OFFSET $3 + `; + + const result = await pool.query(query, [collectionId, limit, offset]); + + // Get total count + const countQuery = 'SELECT COUNT(*) FROM collection_users WHERE "collectionId" = $1'; + const countResult = await pool.query(countQuery, [collectionId]); + const totalCount = parseInt(countResult.rows[0].count); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + memberships: result.rows, + pagination: { + total: totalCount, + offset, + limit, + hasMore: offset + limit < totalCount, + }, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error listing collection memberships: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 10. ADD GROUP TO COLLECTION + { + name: 'add_group_to_collection', + description: 'Add a group to a collection with specific permissions.', + inputSchema: { + type: 'object', + properties: { + collectionId: { + type: 'string', + description: 'Collection ID (UUID)', + }, + groupId: { + type: 'string', + description: 'Group ID (UUID)', + }, + permission: { + type: 'string', + description: 'Permission level for the group', + enum: ['read', 'read_write', 'maintain'], + default: 'read_write', + }, + createdById: { + type: 'string', + description: 'User ID adding the group (UUID)', + }, + }, + required: ['collectionId', 'groupId', 'createdById'], + }, + handler: async (args: { + collectionId: string; + groupId: string; + permission?: string; + createdById: string; + }, pool: Pool): Promise => { + try { + const { collectionId, groupId, permission = 'read_write', createdById } = args; + + if (!isValidUUID(collectionId) || !isValidUUID(groupId) || !isValidUUID(createdById)) { + return { + content: [{ type: 'text', text: 'Invalid UUID format for collectionId, groupId, or createdById.' }], + isError: true, + }; + } + + const query = ` + INSERT INTO collection_groups ("collectionId", "groupId", permission, "createdById", "createdAt", "updatedAt") + VALUES ($1, $2, $3, $4, NOW(), NOW()) + ON CONFLICT ("collectionId", "groupId") + DO UPDATE SET permission = $3, "updatedAt" = NOW() + RETURNING + id, "collectionId", "groupId", permission, "createdById", "createdAt", "updatedAt" + `; + + const result = await pool.query(query, [collectionId, groupId, permission, createdById]); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + message: 'Group added to collection successfully', + membership: result.rows[0], + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error adding group to collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 11. REMOVE GROUP FROM COLLECTION + { + name: 'remove_group_from_collection', + description: 'Remove a group from a collection.', + inputSchema: { + type: 'object', + properties: { + collectionId: { + type: 'string', + description: 'Collection ID (UUID)', + }, + groupId: { + type: 'string', + description: 'Group ID (UUID)', + }, + }, + required: ['collectionId', 'groupId'], + }, + handler: async (args: { collectionId: string; groupId: string }, pool: Pool): Promise => { + try { + const { collectionId, groupId } = args; + + if (!isValidUUID(collectionId) || !isValidUUID(groupId)) { + return { + content: [{ type: 'text', text: 'Invalid UUID format for collectionId or groupId.' }], + isError: true, + }; + } + + const query = ` + DELETE FROM collection_groups + WHERE "collectionId" = $1 AND "groupId" = $2 + RETURNING id, "collectionId", "groupId" + `; + + const result = await pool.query(query, [collectionId, groupId]); + + if (result.rows.length === 0) { + return { + content: [{ type: 'text', text: 'Group membership not found in collection.' }], + isError: true, + }; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + message: 'Group removed from collection successfully', + membership: result.rows[0], + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error removing group from collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 12. LIST COLLECTION GROUP MEMBERSHIPS + { + name: 'list_collection_group_memberships', + description: 'List all group memberships for a collection.', + inputSchema: { + type: 'object', + properties: { + collectionId: { + type: 'string', + description: 'Collection ID (UUID)', + }, + offset: { + type: 'number', + description: 'Number of records to skip (default: 0)', + default: 0, + }, + limit: { + type: 'number', + description: 'Maximum number of records to return (default: 25, max: 100)', + default: 25, + }, + }, + required: ['collectionId'], + }, + handler: async (args: { collectionId: string; offset?: number; limit?: number }, pool: Pool): Promise => { + try { + const { collectionId, offset = 0, limit = 25 } = args; + + if (!isValidUUID(collectionId)) { + return { + content: [{ type: 'text', text: 'Invalid collectionId format. Must be a valid UUID.' }], + isError: true, + }; + } + + validatePagination(offset, limit); + + const query = ` + SELECT + cg.id, + cg."collectionId", + cg."groupId", + cg.permission, + cg."createdById", + cg."createdAt", + cg."updatedAt", + g.name as "groupName", + creator.name as "addedByName" + FROM collection_groups cg + LEFT JOIN groups g ON cg."groupId" = g.id + LEFT JOIN users creator ON cg."createdById" = creator.id + WHERE cg."collectionId" = $1 + ORDER BY cg."createdAt" DESC + LIMIT $2 OFFSET $3 + `; + + const result = await pool.query(query, [collectionId, limit, offset]); + + // Get total count + const countQuery = 'SELECT COUNT(*) FROM collection_groups WHERE "collectionId" = $1'; + const countResult = await pool.query(countQuery, [collectionId]); + const totalCount = parseInt(countResult.rows[0].count); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + memberships: result.rows, + pagination: { + total: totalCount, + offset, + limit, + hasMore: offset + limit < totalCount, + }, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error listing collection group memberships: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 13. EXPORT COLLECTION + { + name: 'export_collection', + description: 'Export all documents from a collection as markdown files.', + inputSchema: { + type: 'object', + properties: { + collectionId: { + type: 'string', + description: 'Collection ID (UUID)', + }, + includeArchived: { + type: 'boolean', + description: 'Include archived documents', + default: false, + }, + }, + required: ['collectionId'], + }, + handler: async (args: { collectionId: string; includeArchived?: boolean }, pool: Pool): Promise => { + try { + const { collectionId, includeArchived = false } = args; + + if (!isValidUUID(collectionId)) { + return { + content: [{ type: 'text', text: 'Invalid collectionId format. Must be a valid UUID.' }], + isError: true, + }; + } + + // Get collection info + const collectionQuery = ` + SELECT id, name, description + FROM collections + WHERE id = $1 AND "deletedAt" IS NULL + `; + const collectionResult = await pool.query(collectionQuery, [collectionId]); + + if (collectionResult.rows.length === 0) { + return { + content: [{ type: 'text', text: 'Collection not found or has been deleted.' }], + isError: true, + }; + } + + const collection = collectionResult.rows[0]; + + // Get documents + let documentsQuery = ` + SELECT + d.id, + d.title, + d.emoji, + d.text, + d."createdAt", + d."updatedAt", + d."publishedAt", + u.name as "authorName" + FROM documents d + LEFT JOIN users u ON d."createdById" = u.id + WHERE d."collectionId" = $1 AND d."deletedAt" IS NULL + `; + + if (!includeArchived) { + documentsQuery += ` AND d."archivedAt" IS NULL`; + } + + documentsQuery += ` ORDER BY d."createdAt" ASC`; + + const documentsResult = await pool.query(documentsQuery, [collectionId]); + + // Format export + const exports = documentsResult.rows.map(doc => { + const markdown = `--- +title: ${doc.title} +emoji: ${doc.emoji || ''} +author: ${doc.authorName} +created: ${doc.createdAt} +updated: ${doc.updatedAt} +published: ${doc.publishedAt || 'Not published'} +--- + +${doc.text || ''} +`; + return { + id: doc.id, + title: doc.title, + filename: `${doc.title.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.md`, + markdown, + }; + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + collection: { + id: collection.id, + name: collection.name, + description: collection.description, + }, + documentCount: exports.length, + documents: exports, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error exporting collection: ${error.message}` }], + isError: true, + }; + } + }, + }, + + // 14. EXPORT ALL COLLECTIONS + { + name: 'export_all_collections', + description: 'Export all collections and their documents from a team.', + inputSchema: { + type: 'object', + properties: { + teamId: { + type: 'string', + description: 'Team ID (UUID)', + }, + includeArchived: { + type: 'boolean', + description: 'Include archived documents', + default: false, + }, + }, + required: ['teamId'], + }, + handler: async (args: { teamId: string; includeArchived?: boolean }, pool: Pool): Promise => { + try { + const { teamId, includeArchived = false } = args; + + if (!isValidUUID(teamId)) { + return { + content: [{ type: 'text', text: 'Invalid teamId format. Must be a valid UUID.' }], + isError: true, + }; + } + + // Get all collections + const collectionsQuery = ` + SELECT id, name, description + FROM collections + WHERE "teamId" = $1 AND "deletedAt" IS NULL + ORDER BY index ASC, "createdAt" ASC + `; + const collectionsResult = await pool.query(collectionsQuery, [teamId]); + + const exports = []; + + for (const collection of collectionsResult.rows) { + // Get documents for each collection + let documentsQuery = ` + SELECT + d.id, + d.title, + d.emoji, + d.text, + d."createdAt", + d."updatedAt", + d."publishedAt", + u.name as "authorName" + FROM documents d + LEFT JOIN users u ON d."createdById" = u.id + WHERE d."collectionId" = $1 AND d."deletedAt" IS NULL + `; + + if (!includeArchived) { + documentsQuery += ` AND d."archivedAt" IS NULL`; + } + + documentsQuery += ` ORDER BY d."createdAt" ASC`; + + const documentsResult = await pool.query(documentsQuery, [collection.id]); + + const documents = documentsResult.rows.map(doc => { + const markdown = `--- +title: ${doc.title} +emoji: ${doc.emoji || ''} +author: ${doc.authorName} +created: ${doc.createdAt} +updated: ${doc.updatedAt} +published: ${doc.publishedAt || 'Not published'} +--- + +${doc.text || ''} +`; + return { + id: doc.id, + title: doc.title, + filename: `${doc.title.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.md`, + markdown, + }; + }); + + exports.push({ + collection: { + id: collection.id, + name: collection.name, + description: collection.description, + }, + documentCount: documents.length, + documents, + }); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + teamId, + collectionCount: exports.length, + totalDocuments: exports.reduce((sum, col) => sum + col.documentCount, 0), + collections: exports, + }, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error exporting all collections: ${error.message}` }], + isError: true, + }; + } + }, + }, +]; diff --git a/src/tools/comments.ts b/src/tools/comments.ts new file mode 100644 index 0000000..5bfa926 --- /dev/null +++ b/src/tools/comments.ts @@ -0,0 +1,480 @@ +/** + * MCP Outline PostgreSQL - Comments Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse, CommentArgs, GetCommentArgs, CreateCommentArgs, UpdateCommentArgs } from '../types/tools.js'; +import { validatePagination, isValidUUID } from '../utils/security.js'; + +/** + * comments.list - List comments with optional filters + */ +const listComments: BaseTool = { + name: 'outline_comments_list', + description: 'List comments with optional filtering by document or collection. Supports pagination.', + inputSchema: { + type: 'object', + properties: { + document_id: { + type: 'string', + description: 'Filter by document ID (UUID)', + }, + collection_id: { + type: 'string', + description: 'Filter by collection ID (UUID)', + }, + limit: { + type: 'number', + description: 'Maximum number of results (default: 25, max: 100)', + }, + offset: { + type: 'number', + description: 'Number of results to skip (default: 0)', + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { limit, offset } = validatePagination(args.limit, args.offset); + const conditions: string[] = []; + const params: any[] = []; + let paramIndex = 1; + + if (args.document_id) { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + conditions.push(`c."documentId" = $${paramIndex++}`); + params.push(args.document_id); + } + + if (args.collection_id) { + if (!isValidUUID(args.collection_id)) { + throw new Error('Invalid collection_id format'); + } + conditions.push(`d."collectionId" = $${paramIndex++}`); + params.push(args.collection_id); + } + + const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; + + const query = ` + SELECT + c.id, + c.data, + c."documentId", + c."parentCommentId", + c."createdById", + c."resolvedById", + c."resolvedAt", + c."createdAt", + c."updatedAt", + u.name as "createdByName", + u.email as "createdByEmail", + ru.name as "resolvedByName", + d.title as "documentTitle" + FROM comments c + LEFT JOIN users u ON c."createdById" = u.id + LEFT JOIN users ru ON c."resolvedById" = ru.id + LEFT JOIN documents d ON c."documentId" = d.id + ${whereClause} + ORDER BY c."createdAt" DESC + LIMIT $${paramIndex++} OFFSET $${paramIndex} + `; + + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows, + pagination: { + limit, + offset, + total: result.rows.length, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * comments.info - Get detailed information about a specific comment + */ +const getComment: BaseTool = { + name: 'outline_comments_info', + description: 'Get detailed information about a specific comment by ID.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Comment ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid comment ID format'); + } + + const query = ` + SELECT + c.id, + c.data, + c."documentId", + c."parentCommentId", + c."createdById", + c."resolvedById", + c."resolvedAt", + c."createdAt", + c."updatedAt", + u.name as "createdByName", + u.email as "createdByEmail", + ru.name as "resolvedByName", + d.title as "documentTitle", + d."collectionId" + FROM comments c + LEFT JOIN users u ON c."createdById" = u.id + LEFT JOIN users ru ON c."resolvedById" = ru.id + LEFT JOIN documents d ON c."documentId" = d.id + WHERE c.id = $1 + `; + + const result = await pgClient.query(query, [args.id]); + + if (result.rows.length === 0) { + throw new Error('Comment not found'); + } + + // Get replies if this is a parent comment + const repliesQuery = ` + SELECT + c.id, + c.data, + c."createdById", + c."createdAt", + u.name as "createdByName" + FROM comments c + LEFT JOIN users u ON c."createdById" = u.id + WHERE c."parentCommentId" = $1 + ORDER BY c."createdAt" ASC + `; + + const replies = await pgClient.query(repliesQuery, [args.id]); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + ...result.rows[0], + replies: replies.rows, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * comments.create - Create a new comment + */ +const createComment: BaseTool = { + name: 'outline_comments_create', + description: 'Create a new comment on a document. Can be a top-level comment or a reply to another comment.', + inputSchema: { + type: 'object', + properties: { + document_id: { + type: 'string', + description: 'Document ID (UUID)', + }, + data: { + type: 'object', + description: 'Comment data (JSON object with content)', + }, + parent_comment_id: { + type: 'string', + description: 'Parent comment ID for replies (UUID, optional)', + }, + }, + required: ['document_id', 'data'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + + if (args.parent_comment_id && !isValidUUID(args.parent_comment_id)) { + throw new Error('Invalid parent_comment_id format'); + } + + // Verify document exists + const docCheck = await pgClient.query( + 'SELECT id FROM documents WHERE id = $1 AND "deletedAt" IS NULL', + [args.document_id] + ); + + if (docCheck.rows.length === 0) { + throw new Error('Document not found or deleted'); + } + + // Verify parent comment exists if provided + if (args.parent_comment_id) { + const parentCheck = await pgClient.query( + 'SELECT id FROM comments WHERE id = $1 AND "documentId" = $2', + [args.parent_comment_id, args.document_id] + ); + + if (parentCheck.rows.length === 0) { + throw new Error('Parent comment not found or not on the same document'); + } + } + + // Note: In real implementation, createdById should come from authentication context + // For now, we'll get the first admin user + const userQuery = await pgClient.query( + 'SELECT id FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL LIMIT 1' + ); + + if (userQuery.rows.length === 0) { + throw new Error('No valid user found to create comment'); + } + + const createdById = userQuery.rows[0].id; + + const query = ` + INSERT INTO comments ( + "documentId", + "data", + "parentCommentId", + "createdById", + "createdAt", + "updatedAt" + ) VALUES ($1, $2, $3, $4, NOW(), NOW()) + RETURNING * + `; + + const result = await pgClient.query(query, [ + args.document_id, + JSON.stringify(args.data), + args.parent_comment_id || null, + createdById, + ]); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * comments.update - Update an existing comment + */ +const updateComment: BaseTool = { + name: 'outline_comments_update', + description: 'Update the content of an existing comment.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Comment ID (UUID)', + }, + data: { + type: 'object', + description: 'Updated comment data (JSON object)', + }, + }, + required: ['id', 'data'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid comment ID format'); + } + + const query = ` + UPDATE comments + SET + "data" = $1, + "updatedAt" = NOW() + WHERE id = $2 + RETURNING * + `; + + const result = await pgClient.query(query, [JSON.stringify(args.data), args.id]); + + if (result.rows.length === 0) { + throw new Error('Comment not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * comments.delete - Delete a comment + */ +const deleteComment: BaseTool = { + name: 'outline_comments_delete', + description: 'Delete a comment. This will also delete all replies to this comment.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Comment ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid comment ID format'); + } + + // Delete replies first + await pgClient.query('DELETE FROM comments WHERE "parentCommentId" = $1', [args.id]); + + // Delete the comment + const result = await pgClient.query('DELETE FROM comments WHERE id = $1 RETURNING id', [args.id]); + + if (result.rows.length === 0) { + throw new Error('Comment not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: 'Comment deleted successfully', + id: result.rows[0].id, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * comments.resolve - Mark a comment as resolved + */ +const resolveComment: BaseTool = { + name: 'outline_comments_resolve', + description: 'Mark a comment as resolved. Can also be used to unresolve a comment by setting resolved to false.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Comment ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid comment ID format'); + } + + // Get first admin user as resolver + const userQuery = await pgClient.query( + 'SELECT id FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL LIMIT 1' + ); + + if (userQuery.rows.length === 0) { + throw new Error('No valid user found to resolve comment'); + } + + const resolvedById = userQuery.rows[0].id; + + const query = ` + UPDATE comments + SET + "resolvedById" = $1, + "resolvedAt" = NOW(), + "updatedAt" = NOW() + WHERE id = $2 + RETURNING * + `; + + const result = await pgClient.query(query, [resolvedById, args.id]); + + if (result.rows.length === 0) { + throw new Error('Comment not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all comment tools +export const commentsTools: BaseTool[] = [ + listComments, + getComment, + createComment, + updateComment, + deleteComment, + resolveComment, +]; diff --git a/src/tools/documents.ts b/src/tools/documents.ts new file mode 100644 index 0000000..a601a01 --- /dev/null +++ b/src/tools/documents.ts @@ -0,0 +1,1342 @@ +/** + * MCP Outline PostgreSQL - Documents Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse, DocumentArgs, GetDocumentArgs, CreateDocumentArgs, UpdateDocumentArgs, SearchDocumentsArgs, MoveDocumentArgs } from '../types/tools.js'; +import { validatePagination, validateSortDirection, validateSortField, isValidUUID, sanitizeInput } from '../utils/security.js'; + +/** + * 1. list_documents - Lista documentos publicados e drafts com filtros e paginação + */ +const listDocuments: BaseTool = { + name: 'list_documents', + description: 'Lista documentos publicados e drafts com filtros e paginação. Suporta ordenação e filtros por collection, utilizador, templates e estado de publicação.', + inputSchema: { + type: 'object', + properties: { + collection_id: { type: 'string', description: 'UUID da collection para filtrar' }, + user_id: { type: 'string', description: 'UUID do utilizador (autor) para filtrar' }, + limit: { type: 'number', description: 'Máximo de resultados (default: 25, max: 100)' }, + offset: { type: 'number', description: 'Offset para paginação' }, + sort: { type: 'string', enum: ['updatedAt', 'createdAt', 'title', 'publishedAt'], description: 'Campo de ordenação' }, + direction: { type: 'string', enum: ['ASC', 'DESC'], description: 'Direcção de ordenação' }, + template: { type: 'boolean', description: 'Filtrar apenas templates' }, + archived: { type: 'boolean', description: 'Incluir documentos arquivados (default: false)' }, + published: { type: 'boolean', description: 'Filtrar apenas publicados (default: true)' } + } + }, + handler: async (args, pgClient): Promise => { + try { + const { limit, offset } = validatePagination(args.limit, args.offset); + const sort = validateSortField(args.sort, ['updatedAt', 'createdAt', 'title', 'publishedAt'], 'updatedAt'); + const direction = validateSortDirection(args.direction); + + let query = ` + SELECT d.id, d."urlId", d.title, d.text, d.emoji, + d."collectionId", d."parentDocumentId", d."createdById", d."lastModifiedById", + d."publishedAt", d."createdAt", d."updatedAt", d."archivedAt", + d.template, d."templateId", d."fullWidth", d.version, + c.name as "collectionName", c.color as "collectionColor", + u.name as "createdByName", u.email as "createdByEmail", + lm.name as "lastModifiedByName" + FROM documents d + LEFT JOIN collections c ON d."collectionId" = c.id + LEFT JOIN users u ON d."createdById" = u.id + LEFT JOIN users lm ON d."lastModifiedById" = lm.id + WHERE d."deletedAt" IS NULL + `; + + const params: any[] = []; + let paramIndex = 1; + + // Filtros + if (args.collection_id) { + if (!isValidUUID(args.collection_id)) throw new Error('collection_id inválido (deve ser UUID)'); + query += ` AND d."collectionId" = $${paramIndex++}`; + params.push(args.collection_id); + } + + if (args.user_id) { + if (!isValidUUID(args.user_id)) throw new Error('user_id inválido (deve ser UUID)'); + query += ` AND d."createdById" = $${paramIndex++}`; + params.push(args.user_id); + } + + if (args.template !== undefined) { + query += ` AND d.template = $${paramIndex++}`; + params.push(args.template); + } + + if (!args.archived) { + query += ` AND d."archivedAt" IS NULL`; + } + + if (args.published !== false) { + query += ` AND d."publishedAt" IS NOT NULL`; + } else if (args.published === false) { + query += ` AND d."publishedAt" IS NULL`; + } + + // Ordenação e paginação + query += ` ORDER BY d."${sort}" ${direction} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`; + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + documents: result.rows, + pagination: { limit, offset, count: result.rows.length } + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 2. get_document - Obter documento por ID, urlId ou shareId + */ +const getDocument: BaseTool = { + name: 'get_document', + description: 'Obter detalhes completos de um documento por ID (UUID), urlId ou shareId. Retorna texto completo, metadata e informações de colaboradores.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' }, + share_id: { type: 'string', description: 'ID de partilha pública (alternativa ao id)' } + } + }, + handler: async (args, pgClient): Promise => { + try { + if (!args.id && !args.share_id) { + throw new Error('É necessário fornecer id ou share_id'); + } + + let query = ` + SELECT d.id, d."urlId", d.title, d.text, d.emoji, + d."collectionId", d."parentDocumentId", d."createdById", d."lastModifiedById", + d."publishedAt", d."createdAt", d."updatedAt", d."archivedAt", d."deletedAt", + d.template, d."templateId", d."fullWidth", d.version, + c.name as "collectionName", c.color as "collectionColor", + u.name as "createdByName", u.email as "createdByEmail", + lm.name as "lastModifiedByName" + FROM documents d + LEFT JOIN collections c ON d."collectionId" = c.id + LEFT JOIN users u ON d."createdById" = u.id + LEFT JOIN users lm ON d."lastModifiedById" = lm.id + `; + + const params: any[] = []; + + if (args.id) { + if (!isValidUUID(args.id)) throw new Error('id inválido (deve ser UUID)'); + query += ` WHERE d.id = $1`; + params.push(args.id); + } else if (args.share_id) { + query += ` INNER JOIN shares s ON d.id = s."documentId" WHERE s.id = $1`; + params.push(args.share_id); + } + + const result = await pgClient.query(query, params); + + if (result.rows.length === 0) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: 'Documento não encontrado' }, null, 2) + }] + }; + } + + return { + content: [{ + type: 'text', + text: JSON.stringify({ document: result.rows[0] }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 3. create_document - Criar novo documento + */ +const createDocument: BaseTool = { + name: 'create_document', + description: 'Criar novo documento numa collection. Pode ser draft (não publicado) ou publicado imediatamente. Suporta criação de documentos hierárquicos (parent_document_id).', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string', description: 'Título do documento' }, + text: { type: 'string', description: 'Conteúdo em Markdown (opcional para drafts)' }, + collection_id: { type: 'string', description: 'UUID da collection' }, + parent_document_id: { type: 'string', description: 'UUID do documento pai (opcional, para hierarquia)' }, + template: { type: 'boolean', description: 'Marcar como template (default: false)' }, + publish: { type: 'boolean', description: 'Publicar imediatamente (default: false, cria draft)' } + }, + required: ['title', 'collection_id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.collection_id)) { + throw new Error('collection_id inválido (deve ser UUID)'); + } + + if (args.parent_document_id && !isValidUUID(args.parent_document_id)) { + throw new Error('parent_document_id inválido (deve ser UUID)'); + } + + // Verificar que a collection existe + const collectionCheck = await pgClient.query( + `SELECT id FROM collections WHERE id = $1 AND "deletedAt" IS NULL`, + [args.collection_id] + ); + + if (collectionCheck.rows.length === 0) { + throw new Error('Collection não encontrada ou foi eliminada'); + } + + // Obter primeiro utilizador activo como createdById (necessário) + const userResult = await pgClient.query( + `SELECT id FROM users WHERE "deletedAt" IS NULL AND "isSuspended" = false LIMIT 1` + ); + + if (userResult.rows.length === 0) { + throw new Error('Nenhum utilizador activo encontrado'); + } + + const userId = userResult.rows[0].id; + const title = sanitizeInput(args.title); + const text = args.text ? sanitizeInput(args.text) : ''; + const publishedAt = args.publish ? new Date().toISOString() : null; + + const query = ` + INSERT INTO documents ( + title, text, "collectionId", "parentDocumentId", "createdById", + "lastModifiedById", template, "publishedAt", "createdAt", "updatedAt", version + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW(), 1) + RETURNING id, title, "collectionId", "publishedAt", "createdAt" + `; + + const params = [ + title, + text, + args.collection_id, + args.parent_document_id || null, + userId, + userId, + args.template || false, + publishedAt + ]; + + const result = await pgClient.query(query, params); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + document: result.rows[0], + message: args.publish ? 'Documento criado e publicado' : 'Draft criado (não publicado)' + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 4. update_document - Actualizar documento existente + */ +const updateDocument: BaseTool = { + name: 'update_document', + description: 'Actualizar título e/ou conteúdo de um documento. Suporta substituição completa ou append ao texto existente.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' }, + title: { type: 'string', description: 'Novo título (opcional)' }, + text: { type: 'string', description: 'Novo conteúdo (opcional)' }, + done: { type: 'boolean', description: 'Marcar como concluído (actualiza publishedAt se draft)' }, + append: { type: 'boolean', description: 'Adicionar texto ao final em vez de substituir (default: false)' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + // Verificar que documento existe + const docCheck = await pgClient.query( + `SELECT id, text, "publishedAt" FROM documents WHERE id = $1 AND "deletedAt" IS NULL`, + [args.id] + ); + + if (docCheck.rows.length === 0) { + throw new Error('Documento não encontrado ou foi eliminado'); + } + + const updates: string[] = []; + const params: any[] = []; + let paramIndex = 1; + + if (args.title) { + updates.push(`title = $${paramIndex++}`); + params.push(sanitizeInput(args.title)); + } + + if (args.text !== undefined) { + if (args.append) { + const currentText = docCheck.rows[0].text || ''; + updates.push(`text = $${paramIndex++}`); + params.push(currentText + '\n\n' + sanitizeInput(args.text)); + } else { + updates.push(`text = $${paramIndex++}`); + params.push(sanitizeInput(args.text)); + } + } + + if (args.done && !docCheck.rows[0].publishedAt) { + updates.push(`"publishedAt" = $${paramIndex++}`); + params.push(new Date().toISOString()); + } + + if (updates.length === 0) { + throw new Error('Nenhum campo para actualizar fornecido'); + } + + updates.push(`"updatedAt" = NOW()`); + updates.push(`version = version + 1`); + + params.push(args.id); + + const query = ` + UPDATE documents + SET ${updates.join(', ')} + WHERE id = $${paramIndex} + RETURNING id, title, "updatedAt", version, "publishedAt" + `; + + const result = await pgClient.query(query, params); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + document: result.rows[0] + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 5. delete_document - Soft delete ou permanente + */ +const deleteDocument: BaseTool<{ id: string; permanent?: boolean }> = { + name: 'delete_document', + description: 'Eliminar documento (soft delete por default, permanente se especificado). Soft delete permite restaurar depois.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' }, + permanent: { type: 'boolean', description: 'Eliminação permanente (irreversível, default: false)' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + let query: string; + + if (args.permanent) { + query = `DELETE FROM documents WHERE id = $1 RETURNING id`; + } else { + query = `UPDATE documents SET "deletedAt" = NOW() WHERE id = $1 AND "deletedAt" IS NULL RETURNING id, "deletedAt"`; + } + + const result = await pgClient.query(query, [args.id]); + + if (result.rows.length === 0) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: 'Documento não encontrado ou já eliminado' }, null, 2) + }] + }; + } + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + message: args.permanent ? 'Documento eliminado permanentemente' : 'Documento eliminado (soft delete)', + id: result.rows[0].id + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 6. search_documents - Full-text search usando PostgreSQL tsvector + */ +const searchDocuments: BaseTool = { + name: 'search_documents', + description: 'Pesquisa full-text em documentos usando PostgreSQL tsvector. Pesquisa em título e conteúdo. Suporta filtros por collection e utilizador.', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Termo de pesquisa' }, + collection_id: { type: 'string', description: 'Filtrar por collection (opcional)' }, + user_id: { type: 'string', description: 'Filtrar por autor (opcional)' }, + include_archived: { type: 'boolean', description: 'Incluir arquivados (default: false)' }, + include_drafts: { type: 'boolean', description: 'Incluir drafts não publicados (default: false)' }, + limit: { type: 'number', description: 'Máximo de resultados (default: 25, max: 100)' }, + offset: { type: 'number', description: 'Offset para paginação' } + }, + required: ['query'] + }, + handler: async (args, pgClient): Promise => { + try { + const { limit, offset } = validatePagination(args.limit, args.offset); + const searchTerm = sanitizeInput(args.query); + + let query = ` + SELECT d.id, d.title, d.text, d."collectionId", d."publishedAt", d."createdAt", + c.name as "collectionName", + u.name as "createdByName", + ts_rank(to_tsvector('english', d.title || ' ' || d.text), plainto_tsquery('english', $1)) as rank + FROM documents d + LEFT JOIN collections c ON d."collectionId" = c.id + LEFT JOIN users u ON d."createdById" = u.id + WHERE d."deletedAt" IS NULL + AND to_tsvector('english', d.title || ' ' || d.text) @@ plainto_tsquery('english', $1) + `; + + const params: any[] = [searchTerm]; + let paramIndex = 2; + + if (args.collection_id) { + if (!isValidUUID(args.collection_id)) throw new Error('collection_id inválido'); + query += ` AND d."collectionId" = $${paramIndex++}`; + params.push(args.collection_id); + } + + if (args.user_id) { + if (!isValidUUID(args.user_id)) throw new Error('user_id inválido'); + query += ` AND d."createdById" = $${paramIndex++}`; + params.push(args.user_id); + } + + if (!args.include_archived) { + query += ` AND d."archivedAt" IS NULL`; + } + + if (!args.include_drafts) { + query += ` AND d."publishedAt" IS NOT NULL`; + } + + query += ` ORDER BY rank DESC, d."updatedAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`; + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + query: searchTerm, + results: result.rows, + pagination: { limit, offset, count: result.rows.length } + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 7. list_drafts - Listar drafts (documentos não publicados) + */ +const listDrafts: BaseTool<{ collection_id?: string; limit?: number; offset?: number }> = { + name: 'list_drafts', + description: 'Listar documentos em draft (não publicados, publishedAt IS NULL). Útil para ver trabalho em curso.', + inputSchema: { + type: 'object', + properties: { + collection_id: { type: 'string', description: 'Filtrar por collection (opcional)' }, + limit: { type: 'number', description: 'Máximo de resultados (default: 25)' }, + offset: { type: 'number', description: 'Offset para paginação' } + } + }, + handler: async (args, pgClient): Promise => { + try { + const { limit, offset } = validatePagination(args.limit, args.offset); + + let query = ` + SELECT d.id, d.title, d."collectionId", d."createdById", d."createdAt", d."updatedAt", + c.name as "collectionName", + u.name as "createdByName" + FROM documents d + LEFT JOIN collections c ON d."collectionId" = c.id + LEFT JOIN users u ON d."createdById" = u.id + WHERE d."deletedAt" IS NULL + AND d."publishedAt" IS NULL + AND d."archivedAt" IS NULL + `; + + const params: any[] = []; + let paramIndex = 1; + + if (args.collection_id) { + if (!isValidUUID(args.collection_id)) throw new Error('collection_id inválido'); + query += ` AND d."collectionId" = $${paramIndex++}`; + params.push(args.collection_id); + } + + query += ` ORDER BY d."updatedAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`; + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + drafts: result.rows, + pagination: { limit, offset, count: result.rows.length } + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 8. list_viewed_documents - Documentos visualizados (JOIN com views) + */ +const listViewedDocuments: BaseTool<{ user_id?: string; limit?: number; offset?: number }> = { + name: 'list_viewed_documents', + description: 'Listar documentos visualizados recentemente com estatísticas de views. Opcionalmente filtrar por utilizador.', + inputSchema: { + type: 'object', + properties: { + user_id: { type: 'string', description: 'UUID do utilizador (opcional, mostra views de todos se omitido)' }, + limit: { type: 'number', description: 'Máximo de resultados (default: 25)' }, + offset: { type: 'number', description: 'Offset para paginação' } + } + }, + handler: async (args, pgClient): Promise => { + try { + const { limit, offset } = validatePagination(args.limit, args.offset); + + let query = ` + SELECT d.id, d.title, d."collectionId", + c.name as "collectionName", + COUNT(v.id) as view_count, + MAX(v."updatedAt") as last_viewed + FROM views v + INNER JOIN documents d ON v."documentId" = d.id + LEFT JOIN collections c ON d."collectionId" = c.id + WHERE d."deletedAt" IS NULL + `; + + const params: any[] = []; + let paramIndex = 1; + + if (args.user_id) { + if (!isValidUUID(args.user_id)) throw new Error('user_id inválido'); + query += ` AND v."userId" = $${paramIndex++}`; + params.push(args.user_id); + } + + query += ` GROUP BY d.id, d.title, d."collectionId", c.name + ORDER BY last_viewed DESC + LIMIT $${paramIndex++} OFFSET $${paramIndex++}`; + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + viewed_documents: result.rows, + pagination: { limit, offset, count: result.rows.length } + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 9. archive_document - Arquivar documento + */ +const archiveDocument: BaseTool<{ id: string }> = { + name: 'archive_document', + description: 'Arquivar documento (define archivedAt). Documentos arquivados não aparecem em listagens normais mas podem ser restaurados.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + const result = await pgClient.query( + `UPDATE documents SET "archivedAt" = NOW(), "updatedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL AND "archivedAt" IS NULL + RETURNING id, title, "archivedAt"`, + [args.id] + ); + + if (result.rows.length === 0) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: 'Documento não encontrado, já arquivado ou eliminado' }, null, 2) + }] + }; + } + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + message: 'Documento arquivado', + document: result.rows[0] + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 10. restore_document - Restaurar documento arquivado ou eliminado + */ +const restoreDocument: BaseTool<{ id: string }> = { + name: 'restore_document', + description: 'Restaurar documento arquivado ou eliminado (soft delete). Limpa archivedAt e deletedAt.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + const result = await pgClient.query( + `UPDATE documents + SET "archivedAt" = NULL, "deletedAt" = NULL, "updatedAt" = NOW() + WHERE id = $1 AND ("archivedAt" IS NOT NULL OR "deletedAt" IS NOT NULL) + RETURNING id, title, "updatedAt"`, + [args.id] + ); + + if (result.rows.length === 0) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: 'Documento não encontrado ou não estava arquivado/eliminado' }, null, 2) + }] + }; + } + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + message: 'Documento restaurado', + document: result.rows[0] + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 11. move_document - Mover documento (mudar collection ou parent) + */ +const moveDocument: BaseTool = { + name: 'move_document', + description: 'Mover documento para outra collection ou mudar parent (hierarquia). Pelo menos um de collection_id ou parent_document_id deve ser fornecido.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento a mover' }, + collection_id: { type: 'string', description: 'Nova collection (opcional)' }, + parent_document_id: { type: 'string', description: 'Novo parent (opcional, null remove parent)' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + if (!args.collection_id && !args.parent_document_id) { + throw new Error('É necessário fornecer collection_id ou parent_document_id'); + } + + const updates: string[] = []; + const params: any[] = []; + let paramIndex = 1; + + if (args.collection_id) { + if (!isValidUUID(args.collection_id)) throw new Error('collection_id inválido'); + updates.push(`"collectionId" = $${paramIndex++}`); + params.push(args.collection_id); + } + + if (args.parent_document_id !== undefined) { + if (args.parent_document_id && !isValidUUID(args.parent_document_id)) { + throw new Error('parent_document_id inválido'); + } + updates.push(`"parentDocumentId" = $${paramIndex++}`); + params.push(args.parent_document_id || null); + } + + updates.push(`"updatedAt" = NOW()`); + params.push(args.id); + + const query = ` + UPDATE documents + SET ${updates.join(', ')} + WHERE id = $${paramIndex} AND "deletedAt" IS NULL + RETURNING id, title, "collectionId", "parentDocumentId", "updatedAt" + `; + + const result = await pgClient.query(query, params); + + if (result.rows.length === 0) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: 'Documento não encontrado ou foi eliminado' }, null, 2) + }] + }; + } + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + message: 'Documento movido', + document: result.rows[0] + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 12. unpublish_document - Despublicar documento (volta a draft) + */ +const unpublishDocument: BaseTool<{ id: string }> = { + name: 'unpublish_document', + description: 'Despublicar documento (limpa publishedAt, voltando a draft). Útil para retirar documentos de visualização pública.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + const result = await pgClient.query( + `UPDATE documents SET "publishedAt" = NULL, "updatedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL AND "publishedAt" IS NOT NULL + RETURNING id, title, "updatedAt"`, + [args.id] + ); + + if (result.rows.length === 0) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: 'Documento não encontrado, foi eliminado ou já era draft' }, null, 2) + }] + }; + } + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + message: 'Documento despublicado (agora é draft)', + document: result.rows[0] + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 13. templatize_document - Criar template a partir de documento + */ +const templatizeDocument: BaseTool<{ id: string }> = { + name: 'templatize_document', + description: 'Criar template a partir de documento existente. O documento original mantém-se, é criada uma cópia marcada como template.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento origem' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + // Obter documento origem + const docResult = await pgClient.query( + `SELECT * FROM documents WHERE id = $1 AND "deletedAt" IS NULL`, + [args.id] + ); + + if (docResult.rows.length === 0) { + throw new Error('Documento não encontrado ou foi eliminado'); + } + + const doc = docResult.rows[0]; + + // Criar template + const templateResult = await pgClient.query( + `INSERT INTO documents ( + title, text, "collectionId", "createdById", "lastModifiedById", + template, "templateId", "publishedAt", "createdAt", "updatedAt", version + ) + VALUES ($1, $2, $3, $4, $5, true, $6, NOW(), NOW(), NOW(), 1) + RETURNING id, title, template, "templateId"`, + [ + `${doc.title} (Template)`, + doc.text, + doc.collectionId, + doc.createdById, + doc.lastModifiedById, + args.id + ] + ); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + message: 'Template criado a partir do documento', + template: templateResult.rows[0], + original_document_id: args.id + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 14. export_document - Retornar texto markdown + */ +const exportDocument: BaseTool<{ id: string }> = { + name: 'export_document', + description: 'Exportar documento em formato Markdown. Retorna título, conteúdo e metadata.', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + const result = await pgClient.query( + `SELECT d.id, d.title, d.text, d."createdAt", d."updatedAt", d."publishedAt", + c.name as "collectionName", + u.name as "createdByName" + FROM documents d + LEFT JOIN collections c ON d."collectionId" = c.id + LEFT JOIN users u ON d."createdById" = u.id + WHERE d.id = $1 AND d."deletedAt" IS NULL`, + [args.id] + ); + + if (result.rows.length === 0) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: 'Documento não encontrado ou foi eliminado' }, null, 2) + }] + }; + } + + const doc = result.rows[0]; + + const markdown = `# ${doc.title} + +**Collection:** ${doc.collectionName || 'N/A'} +**Autor:** ${doc.createdByName || 'N/A'} +**Criado:** ${doc.createdAt} +**Actualizado:** ${doc.updatedAt} +${doc.publishedAt ? `**Publicado:** ${doc.publishedAt}` : '**Estado:** Draft'} + +--- + +${doc.text || ''} +`; + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + id: doc.id, + title: doc.title, + markdown, + metadata: { + collectionName: doc.collectionName, + createdByName: doc.createdByName, + createdAt: doc.createdAt, + updatedAt: doc.updatedAt, + publishedAt: doc.publishedAt + } + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 15. import_document - Criar documento a partir de texto/markdown + */ +const importDocument: BaseTool<{ title: string; text: string; collection_id: string; publish?: boolean }> = { + name: 'import_document', + description: 'Importar documento a partir de texto/Markdown. Cria novo documento numa collection.', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string', description: 'Título do documento' }, + text: { type: 'string', description: 'Conteúdo em Markdown' }, + collection_id: { type: 'string', description: 'UUID da collection destino' }, + publish: { type: 'boolean', description: 'Publicar imediatamente (default: true)' } + }, + required: ['title', 'text', 'collection_id'] + }, + handler: async (args, pgClient): Promise => { + // Reutilizar create_document + return createDocument.handler({ + title: args.title, + text: args.text, + collection_id: args.collection_id, + publish: args.publish !== false + }, pgClient); + } +}; + +/** + * 16. list_document_users - Utilizadores com acesso ao documento + */ +const listDocumentUsers: BaseTool<{ id: string }> = { + name: 'list_document_users', + description: 'Listar utilizadores com acesso a um documento (via collection permissions ou memberships directas).', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + // Obter utilizadores via collection_users + const result = await pgClient.query( + `SELECT DISTINCT u.id, u.name, u.email, cu.permission + FROM documents d + INNER JOIN collection_users cu ON d."collectionId" = cu."collectionId" + INNER JOIN users u ON cu."userId" = u.id + WHERE d.id = $1 AND d."deletedAt" IS NULL AND u."deletedAt" IS NULL + ORDER BY u.name`, + [args.id] + ); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + document_id: args.id, + users: result.rows + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 17. list_document_memberships - Memberships directas do documento + */ +const listDocumentMemberships: BaseTool<{ id: string }> = { + name: 'list_document_memberships', + description: 'Listar memberships directas do documento (via collection_users da collection do documento).', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' } + }, + required: ['id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + + const result = await pgClient.query( + `SELECT cu.id, cu."userId", cu."collectionId", cu.permission, cu."createdAt", + u.name as "userName", u.email as "userEmail", + c.name as "collectionName" + FROM documents d + INNER JOIN collection_users cu ON d."collectionId" = cu."collectionId" + INNER JOIN users u ON cu."userId" = u.id + INNER JOIN collections c ON cu."collectionId" = c.id + WHERE d.id = $1 AND d."deletedAt" IS NULL + ORDER BY cu."createdAt" DESC`, + [args.id] + ); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + document_id: args.id, + memberships: result.rows + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 18. add_user_to_document - Adicionar permissão de utilizador ao documento + */ +const addUserToDocument: BaseTool<{ id: string; user_id: string; permission?: 'read' | 'read_write' }> = { + name: 'add_user_to_document', + description: 'Adicionar utilizador com permissão ao documento (via collection_users da collection do documento).', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' }, + user_id: { type: 'string', description: 'UUID do utilizador' }, + permission: { type: 'string', enum: ['read', 'read_write'], description: 'Tipo de permissão (default: read)' } + }, + required: ['id', 'user_id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + if (!isValidUUID(args.user_id)) { + throw new Error('user_id inválido (deve ser UUID)'); + } + + // Obter collectionId do documento + const docResult = await pgClient.query( + `SELECT "collectionId" FROM documents WHERE id = $1 AND "deletedAt" IS NULL`, + [args.id] + ); + + if (docResult.rows.length === 0) { + throw new Error('Documento não encontrado ou foi eliminado'); + } + + const collectionId = docResult.rows[0].collectionId; + const permission = args.permission || 'read'; + + // Inserir ou actualizar membership + const result = await pgClient.query( + `INSERT INTO collection_users ("userId", "collectionId", permission, "createdAt", "updatedAt") + VALUES ($1, $2, $3, NOW(), NOW()) + ON CONFLICT ("userId", "collectionId") + DO UPDATE SET permission = $3, "updatedAt" = NOW() + RETURNING id, "userId", "collectionId", permission`, + [args.user_id, collectionId, permission] + ); + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + message: 'Utilizador adicionado ao documento (via collection)', + membership: result.rows[0] + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +/** + * 19. remove_user_from_document - Remover permissão de utilizador do documento + */ +const removeUserFromDocument: BaseTool<{ id: string; user_id: string }> = { + name: 'remove_user_from_document', + description: 'Remover utilizador do documento (via collection_users da collection do documento).', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'UUID do documento' }, + user_id: { type: 'string', description: 'UUID do utilizador' } + }, + required: ['id', 'user_id'] + }, + handler: async (args, pgClient): Promise => { + try { + if (!isValidUUID(args.id)) { + throw new Error('id inválido (deve ser UUID)'); + } + if (!isValidUUID(args.user_id)) { + throw new Error('user_id inválido (deve ser UUID)'); + } + + // Obter collectionId do documento + const docResult = await pgClient.query( + `SELECT "collectionId" FROM documents WHERE id = $1 AND "deletedAt" IS NULL`, + [args.id] + ); + + if (docResult.rows.length === 0) { + throw new Error('Documento não encontrado ou foi eliminado'); + } + + const collectionId = docResult.rows[0].collectionId; + + const result = await pgClient.query( + `DELETE FROM collection_users + WHERE "userId" = $1 AND "collectionId" = $2 + RETURNING id, "userId", "collectionId"`, + [args.user_id, collectionId] + ); + + if (result.rows.length === 0) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: 'Membership não encontrada' }, null, 2) + }] + }; + } + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + success: true, + message: 'Utilizador removido do documento (via collection)', + removed: result.rows[0] + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + }] + }; + } + } +}; + +// Export all document tools +export const documentsTools: BaseTool[] = [ + listDocuments, + getDocument, + createDocument, + updateDocument, + deleteDocument, + searchDocuments, + listDrafts, + listViewedDocuments, + archiveDocument, + restoreDocument, + moveDocument, + unpublishDocument, + templatizeDocument, + exportDocument, + importDocument, + listDocumentUsers, + listDocumentMemberships, + addUserToDocument, + removeUserFromDocument +]; diff --git a/src/tools/events.ts b/src/tools/events.ts new file mode 100644 index 0000000..5f79379 --- /dev/null +++ b/src/tools/events.ts @@ -0,0 +1,370 @@ +/** + * MCP Outline PostgreSQL - Events Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse, EventArgs } from '../types/tools.js'; +import { validatePagination, isValidUUID } from '../utils/security.js'; + +/** + * events.list - List events with optional filters + */ +const listEvents: BaseTool = { + name: 'outline_events_list', + description: 'List system events with optional filtering by name, actor, document, collection, or date range. Useful for audit logs and activity tracking.', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Filter by event name (e.g., "documents.create", "users.signin")', + }, + actor_id: { + type: 'string', + description: 'Filter by actor user ID (UUID)', + }, + document_id: { + type: 'string', + description: 'Filter by document ID (UUID)', + }, + collection_id: { + type: 'string', + description: 'Filter by collection ID (UUID)', + }, + date_from: { + type: 'string', + description: 'Filter events from this date (ISO 8601 format)', + }, + date_to: { + type: 'string', + description: 'Filter events until this date (ISO 8601 format)', + }, + audit_log: { + type: 'boolean', + description: 'Filter only audit log worthy events (default: false)', + }, + limit: { + type: 'number', + description: 'Maximum number of results (default: 25, max: 100)', + }, + offset: { + type: 'number', + description: 'Number of results to skip (default: 0)', + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { limit, offset } = validatePagination(args.limit, args.offset); + const conditions: string[] = []; + const params: any[] = []; + let paramIndex = 1; + + if (args.name) { + conditions.push(`e.name = $${paramIndex++}`); + params.push(args.name); + } + + if (args.actor_id) { + if (!isValidUUID(args.actor_id)) { + throw new Error('Invalid actor_id format'); + } + conditions.push(`e."actorId" = $${paramIndex++}`); + params.push(args.actor_id); + } + + if (args.document_id) { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + conditions.push(`e."documentId" = $${paramIndex++}`); + params.push(args.document_id); + } + + if (args.collection_id) { + if (!isValidUUID(args.collection_id)) { + throw new Error('Invalid collection_id format'); + } + conditions.push(`e."collectionId" = $${paramIndex++}`); + params.push(args.collection_id); + } + + if (args.date_from) { + conditions.push(`e."createdAt" >= $${paramIndex++}`); + params.push(args.date_from); + } + + if (args.date_to) { + conditions.push(`e."createdAt" <= $${paramIndex++}`); + params.push(args.date_to); + } + + // Audit log filter - common audit events + if (args.audit_log) { + conditions.push(`e.name IN ( + 'users.create', 'users.update', 'users.delete', 'users.signin', + 'documents.create', 'documents.update', 'documents.delete', 'documents.publish', + 'collections.create', 'collections.update', 'collections.delete', + 'groups.create', 'groups.update', 'groups.delete', + 'shares.create', 'shares.revoke' + )`); + } + + const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; + + const query = ` + SELECT + e.id, + e.name, + e."modelId", + e."actorId", + e."userId", + e."collectionId", + e."documentId", + e."teamId", + e.ip, + e.data, + e."createdAt", + actor.name as "actorName", + actor.email as "actorEmail", + u.name as "userName", + c.name as "collectionName", + d.title as "documentTitle" + FROM events e + LEFT JOIN users actor ON e."actorId" = actor.id + LEFT JOIN users u ON e."userId" = u.id + LEFT JOIN collections c ON e."collectionId" = c.id + LEFT JOIN documents d ON e."documentId" = d.id + ${whereClause} + ORDER BY e."createdAt" DESC + LIMIT $${paramIndex++} OFFSET $${paramIndex} + `; + + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows, + pagination: { + limit, + offset, + total: result.rows.length, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * events.info - Get detailed information about a specific event + */ +const getEvent: BaseTool<{ id: string }> = { + name: 'outline_events_info', + description: 'Get detailed information about a specific event by ID, including all associated metadata.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Event ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid event ID format'); + } + + const query = ` + SELECT + e.id, + e.name, + e."modelId", + e."actorId", + e."userId", + e."collectionId", + e."documentId", + e."teamId", + e.ip, + e.data, + e."createdAt", + actor.name as "actorName", + actor.email as "actorEmail", + actor."isAdmin" as "actorIsAdmin", + u.name as "userName", + u.email as "userEmail", + c.name as "collectionName", + d.title as "documentTitle", + t.name as "teamName" + FROM events e + LEFT JOIN users actor ON e."actorId" = actor.id + LEFT JOIN users u ON e."userId" = u.id + LEFT JOIN collections c ON e."collectionId" = c.id + LEFT JOIN documents d ON e."documentId" = d.id + LEFT JOIN teams t ON e."teamId" = t.id + WHERE e.id = $1 + `; + + const result = await pgClient.query(query, [args.id]); + + if (result.rows.length === 0) { + throw new Error('Event not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * events.stats - Get event statistics and summaries + */ +const getEventStats: BaseTool = { + name: 'outline_events_stats', + description: 'Get statistical analysis of events. Provides event counts by type, top actors, and activity trends.', + inputSchema: { + type: 'object', + properties: { + date_from: { + type: 'string', + description: 'Statistics from this date (ISO 8601 format)', + }, + date_to: { + type: 'string', + description: 'Statistics until this date (ISO 8601 format)', + }, + collection_id: { + type: 'string', + description: 'Filter statistics by collection ID (UUID)', + }, + }, + }, + handler: async (args, pgClient): Promise => { + const conditions: string[] = []; + const params: any[] = []; + let paramIndex = 1; + + if (args.date_from) { + conditions.push(`e."createdAt" >= $${paramIndex++}`); + params.push(args.date_from); + } + + if (args.date_to) { + conditions.push(`e."createdAt" <= $${paramIndex++}`); + params.push(args.date_to); + } + + if (args.collection_id) { + if (!isValidUUID(args.collection_id)) { + throw new Error('Invalid collection_id format'); + } + conditions.push(`e."collectionId" = $${paramIndex++}`); + params.push(args.collection_id); + } + + const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; + + // Event counts by type + const eventsByTypeQuery = await pgClient.query( + `SELECT + e.name, + COUNT(*) as count + FROM events e + ${whereClause} + GROUP BY e.name + ORDER BY count DESC + LIMIT 20`, + params + ); + + // Top actors + const topActorsQuery = await pgClient.query( + `SELECT + e."actorId", + u.name as "actorName", + u.email as "actorEmail", + COUNT(*) as "eventCount" + FROM events e + LEFT JOIN users u ON e."actorId" = u.id + ${whereClause} + GROUP BY e."actorId", u.name, u.email + ORDER BY "eventCount" DESC + LIMIT 10`, + params + ); + + // Activity by day (last 30 days or filtered range) + const activityByDayQuery = await pgClient.query( + `SELECT + DATE(e."createdAt") as date, + COUNT(*) as "eventCount" + FROM events e + ${whereClause} + GROUP BY DATE(e."createdAt") + ORDER BY date DESC + LIMIT 30`, + params + ); + + // Total statistics + const totalsQuery = await pgClient.query( + `SELECT + COUNT(*) as "totalEvents", + COUNT(DISTINCT e."actorId") as "uniqueActors", + COUNT(DISTINCT e."documentId") as "affectedDocuments", + COUNT(DISTINCT e."collectionId") as "affectedCollections" + FROM events e + ${whereClause}`, + params + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + totals: totalsQuery.rows[0], + eventsByType: eventsByTypeQuery.rows, + topActors: topActorsQuery.rows, + activityByDay: activityByDayQuery.rows, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all event tools +export const eventsTools: BaseTool[] = [ + listEvents, + getEvent, + getEventStats, +]; diff --git a/src/tools/file-operations.ts b/src/tools/file-operations.ts new file mode 100644 index 0000000..69e321d --- /dev/null +++ b/src/tools/file-operations.ts @@ -0,0 +1,303 @@ +/** + * MCP Outline PostgreSQL - File Operations Tools + * Handles background file operations (import/export tracking) + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { + BaseTool, + ToolResponse, + FileOperationArgs, + GetFileOperationArgs, +} from '../types/tools.js'; +import { FileOperation } from '../types/db.js'; + +/** + * file_operations.list - List background file operations + */ +const listFileOperations: BaseTool = { + name: 'outline_file_operations_list', + description: 'List background file operations (imports/exports) with optional filtering by type. Returns operation status, progress, and download URLs for completed exports.', + inputSchema: { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['import', 'export'], + description: 'Filter by operation type', + }, + limit: { + type: 'number', + description: 'Maximum number of results to return', + default: 25, + }, + offset: { + type: 'number', + description: 'Number of results to skip', + default: 0, + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { type, limit = 25, offset = 0 } = args; + + let query = ` + SELECT + fo.id, + fo.type, + fo.state, + fo.format, + fo.size, + fo.key, + fo.url, + fo.error, + fo."collectionId", + fo."userId", + fo."teamId", + fo."createdAt", + fo."updatedAt", + u.name as "userName", + u.email as "userEmail", + c.name as "collectionName" + FROM file_operations fo + LEFT JOIN users u ON fo."userId" = u.id + LEFT JOIN collections c ON fo."collectionId" = c.id + WHERE 1=1 + `; + + const params: any[] = []; + let paramIndex = 1; + + if (type) { + query += ` AND fo.type = $${paramIndex}`; + params.push(type); + paramIndex++; + } + + query += ` ORDER BY fo."createdAt" DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + const operations = result.rows.map((row) => ({ + id: row.id, + type: row.type, + state: row.state, + format: row.format, + size: row.size, + url: row.url, + error: row.error, + collectionId: row.collectionId, + collectionName: row.collectionName, + userId: row.userId, + userName: row.userName, + userEmail: row.userEmail, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + })); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: operations, + pagination: { + offset, + limit, + total: result.rowCount || 0, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * file_operations.info - Get file operation details + */ +const getFileOperation: BaseTool = { + name: 'outline_file_operations_info', + description: 'Get detailed information about a specific file operation by ID. Use this to track job status and get download URLs for completed exports.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'File operation UUID', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + const { id } = args; + + const result = await pgClient.query( + ` + SELECT + fo.*, + u.name as "userName", + u.email as "userEmail", + c.name as "collectionName" + FROM file_operations fo + LEFT JOIN users u ON fo."userId" = u.id + LEFT JOIN collections c ON fo."collectionId" = c.id + WHERE fo.id = $1 + `, + [id] + ); + + if (result.rows.length === 0) { + throw new Error(`File operation not found: ${id}`); + } + + const operation = result.rows[0]; + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: operation, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * file_operations.redirect - Get download URL for completed file operation + */ +const redirectFileOperation: BaseTool = { + name: 'outline_file_operations_redirect', + description: 'Get the download URL for a completed file operation. Returns the URL field from the operation if state is "complete".', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'File operation UUID', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + const { id } = args; + + const result = await pgClient.query( + ` + SELECT id, state, url, type, format + FROM file_operations + WHERE id = $1 + `, + [id] + ); + + if (result.rows.length === 0) { + throw new Error(`File operation not found: ${id}`); + } + + const operation = result.rows[0]; + + if (operation.state !== 'complete') { + throw new Error( + `File operation not complete. Current state: ${operation.state}` + ); + } + + if (!operation.url) { + throw new Error('Download URL not available for this operation'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + id: operation.id, + url: operation.url, + type: operation.type, + format: operation.format, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * file_operations.delete - Remove file operation record + */ +const deleteFileOperation: BaseTool = { + name: 'outline_file_operations_delete', + description: 'Delete a file operation record from the database. This removes the tracking record but does not delete the actual file if it was uploaded to storage.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'File operation UUID to delete', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + const { id } = args; + + const result = await pgClient.query( + ` + DELETE FROM file_operations + WHERE id = $1 + RETURNING id, type, state + `, + [id] + ); + + if (result.rows.length === 0) { + throw new Error(`File operation not found: ${id}`); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + success: true, + deleted: result.rows[0], + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all file operation tools +export const fileOperationsTools: BaseTool[] = [ + listFileOperations, + getFileOperation, + redirectFileOperation, + deleteFileOperation, +]; diff --git a/src/tools/groups.ts b/src/tools/groups.ts new file mode 100644 index 0000000..e3ee79a --- /dev/null +++ b/src/tools/groups.ts @@ -0,0 +1,564 @@ +/** + * MCP Outline PostgreSQL - Groups Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse, GroupArgs, GetGroupArgs, CreateGroupArgs, UpdateGroupArgs } from '../types/tools.js'; +import { validatePagination, isValidUUID, sanitizeInput } from '../utils/security.js'; + +/** + * groups.list - List all groups + */ +const listGroups: BaseTool = { + name: 'outline_list_groups', + description: 'List all groups with optional search query. Supports pagination and returns group details including member counts.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query to filter groups by name', + }, + limit: { + type: 'number', + description: 'Maximum number of results to return (max 100)', + default: 25, + }, + offset: { + type: 'number', + description: 'Number of results to skip for pagination', + default: 0, + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { limit, offset } = validatePagination(args.limit, args.offset); + const query = args.query ? sanitizeInput(args.query) : undefined; + + let whereConditions = ['g."deletedAt" IS NULL']; + const queryParams: any[] = [limit, offset]; + let paramIndex = 3; + + if (query) { + whereConditions.push(`LOWER(g.name) LIKE LOWER($${paramIndex})`); + queryParams.push(`%${query}%`); + paramIndex++; + } + + const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; + + const result = await pgClient.query( + ` + SELECT + g.id, + g.name, + g."teamId", + g."createdById", + g."createdAt", + g."updatedAt", + t.name as "teamName", + u.name as "createdByName", + (SELECT COUNT(*) FROM group_users WHERE "groupId" = g.id AND "deletedAt" IS NULL) as "memberCount", + (SELECT COUNT(*) FROM groups WHERE ${whereConditions.join(' AND ')}) as total + FROM groups g + LEFT JOIN teams t ON g."teamId" = t.id + LEFT JOIN users u ON g."createdById" = u.id + ${whereClause} + ORDER BY g."createdAt" DESC + LIMIT $1 OFFSET $2 + `, + queryParams + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + groups: result.rows, + total: result.rows.length > 0 ? parseInt(result.rows[0].total) : 0, + limit, + offset, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * groups.info - Get group details by ID + */ +const getGroup: BaseTool = { + name: 'outline_get_group', + description: 'Get detailed information about a specific group by its ID. Returns group details and member statistics.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Group UUID', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid group ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + SELECT + g.id, + g.name, + g."teamId", + g."createdById", + g."createdAt", + g."updatedAt", + t.name as "teamName", + u.name as "createdByName", + (SELECT COUNT(*) FROM group_users WHERE "groupId" = g.id AND "deletedAt" IS NULL) as "memberCount" + FROM groups g + LEFT JOIN teams t ON g."teamId" = t.id + LEFT JOIN users u ON g."createdById" = u.id + WHERE g.id = $1 AND g."deletedAt" IS NULL + `, + [args.id] + ); + + if (result.rows.length === 0) { + throw new Error('Group not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * groups.create - Create new group + */ +const createGroup: BaseTool = { + name: 'outline_create_group', + description: 'Create a new group with the specified name. Groups can be used to organize users and manage permissions.', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name of the group', + }, + }, + required: ['name'], + }, + handler: async (args, pgClient): Promise => { + const name = sanitizeInput(args.name); + + // Get team ID (assuming first team, adjust as needed) + const teamResult = await pgClient.query(`SELECT id FROM teams LIMIT 1`); + if (teamResult.rows.length === 0) { + throw new Error('No team found'); + } + const teamId = teamResult.rows[0].id; + + // Get first admin user as creator (adjust as needed) + const userResult = await pgClient.query( + `SELECT id FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL LIMIT 1` + ); + if (userResult.rows.length === 0) { + throw new Error('No admin user found'); + } + const createdById = userResult.rows[0].id; + + const result = await pgClient.query( + ` + INSERT INTO groups ( + id, name, "teamId", "createdById", + "createdAt", "updatedAt" + ) + VALUES ( + gen_random_uuid(), $1, $2, $3, + NOW(), NOW() + ) + RETURNING * + `, + [name, teamId, createdById] + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'Group created successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * groups.update - Update group details + */ +const updateGroup: BaseTool = { + name: 'outline_update_group', + description: 'Update the name of an existing group.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Group UUID', + }, + name: { + type: 'string', + description: 'New name for the group', + }, + }, + required: ['id', 'name'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid group ID format. Must be a valid UUID.'); + } + + const name = sanitizeInput(args.name); + + const result = await pgClient.query( + ` + UPDATE groups + SET name = $1, "updatedAt" = NOW() + WHERE id = $2 AND "deletedAt" IS NULL + RETURNING * + `, + [name, args.id] + ); + + if (result.rows.length === 0) { + throw new Error('Group not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'Group updated successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * groups.delete - Soft delete group + */ +const deleteGroup: BaseTool = { + name: 'outline_delete_group', + description: 'Soft delete a group. This marks the group as deleted but preserves the data for audit purposes.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Group UUID to delete', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid group ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + UPDATE groups + SET "deletedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL + RETURNING id, name + `, + [args.id] + ); + + if (result.rows.length === 0) { + throw new Error('Group not found or already deleted'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'Group deleted successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * groups.memberships - List group members + */ +const listGroupMembers: BaseTool = { + name: 'outline_list_group_members', + description: 'List all members of a specific group. Returns user details for each group member.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Group UUID', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid group ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + SELECT + gu.id as "membershipId", + gu."userId", + gu."groupId", + gu."createdById", + gu."createdAt", + u.name as "userName", + u.email as "userEmail", + u."isAdmin" as "userIsAdmin", + creator.name as "addedByName" + FROM group_users gu + JOIN users u ON gu."userId" = u.id + LEFT JOIN users creator ON gu."createdById" = creator.id + WHERE gu."groupId" = $1 AND gu."deletedAt" IS NULL AND u."deletedAt" IS NULL + ORDER BY gu."createdAt" DESC + `, + [args.id] + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + members: result.rows, + total: result.rows.length, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * groups.add_user - Add user to group + */ +const addUserToGroup: BaseTool<{ id: string; user_id: string }> = { + name: 'outline_add_user_to_group', + description: 'Add a user to a group. Creates a group membership relationship.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Group UUID', + }, + user_id: { + type: 'string', + description: 'User UUID to add to the group', + }, + }, + required: ['id', 'user_id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid group ID format. Must be a valid UUID.'); + } + if (!isValidUUID(args.user_id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + // Check if group exists + const groupCheck = await pgClient.query( + `SELECT id FROM groups WHERE id = $1 AND "deletedAt" IS NULL`, + [args.id] + ); + if (groupCheck.rows.length === 0) { + throw new Error('Group not found'); + } + + // Check if user exists + const userCheck = await pgClient.query( + `SELECT id FROM users WHERE id = $1 AND "deletedAt" IS NULL`, + [args.user_id] + ); + if (userCheck.rows.length === 0) { + throw new Error('User not found'); + } + + // Check if user is already in group + const existingMembership = await pgClient.query( + `SELECT id FROM group_users WHERE "groupId" = $1 AND "userId" = $2 AND "deletedAt" IS NULL`, + [args.id, args.user_id] + ); + if (existingMembership.rows.length > 0) { + throw new Error('User is already a member of this group'); + } + + // Get first admin user as creator (adjust as needed) + const creatorResult = await pgClient.query( + `SELECT id FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL LIMIT 1` + ); + const createdById = creatorResult.rows.length > 0 ? creatorResult.rows[0].id : args.user_id; + + const result = await pgClient.query( + ` + INSERT INTO group_users ( + id, "userId", "groupId", "createdById", + "createdAt", "updatedAt" + ) + VALUES ( + gen_random_uuid(), $1, $2, $3, + NOW(), NOW() + ) + RETURNING * + `, + [args.user_id, args.id, createdById] + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User added to group successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * groups.remove_user - Remove user from group + */ +const removeUserFromGroup: BaseTool<{ id: string; user_id: string }> = { + name: 'outline_remove_user_from_group', + description: 'Remove a user from a group. Soft deletes the group membership.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Group UUID', + }, + user_id: { + type: 'string', + description: 'User UUID to remove from the group', + }, + }, + required: ['id', 'user_id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid group ID format. Must be a valid UUID.'); + } + if (!isValidUUID(args.user_id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + UPDATE group_users + SET "deletedAt" = NOW() + WHERE "groupId" = $1 AND "userId" = $2 AND "deletedAt" IS NULL + RETURNING id, "userId", "groupId" + `, + [args.id, args.user_id] + ); + + if (result.rows.length === 0) { + throw new Error('User is not a member of this group'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User removed from group successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all group tools +export const groupsTools: BaseTool[] = [ + listGroups, + getGroup, + createGroup, + updateGroup, + deleteGroup, + listGroupMembers, + addUserToGroup, + removeUserFromGroup, +]; diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..699aec7 --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,41 @@ +/** + * MCP Outline PostgreSQL - Tools Index + * Central export for all MCP tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +// Document Tools - Core document management (to be implemented) +export { documentsTools } from './documents.js'; + +// Collection Tools - Collection management (to be implemented) +export { collectionsTools } from './collections.js'; + +// User Tools - User management (to be implemented) +export { usersTools } from './users.js'; + +// Group Tools - Group and team management (to be implemented) +export { groupsTools } from './groups.js'; + +// Comment Tools - Comment management (to be implemented) +export { commentsTools } from './comments.js'; + +// Share Tools - Document sharing and public links (to be implemented) +export { sharesTools } from './shares.js'; + +// Revision Tools - Document version history (to be implemented) +export { revisionsTools } from './revisions.js'; + +// Event Tools - Audit log and activity tracking (to be implemented) +export { eventsTools } from './events.js'; + +// Attachment Tools - File attachments (to be implemented) +export { attachmentsTools } from './attachments.js'; + +// File Operation Tools - Import/Export operations +export { fileOperationsTools } from './file-operations.js'; + +// OAuth Tools - OAuth client management +export { oauthTools } from './oauth.js'; + +// Auth Tools - Authentication and authorization +export { authTools } from './auth.js'; diff --git a/src/tools/oauth.ts b/src/tools/oauth.ts new file mode 100644 index 0000000..e6d099a --- /dev/null +++ b/src/tools/oauth.ts @@ -0,0 +1,546 @@ +/** + * MCP Outline PostgreSQL - OAuth Tools + * Manages OAuth applications and user authentications + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { + BaseTool, + ToolResponse, + OAuthClientArgs, + GetOAuthClientArgs, + CreateOAuthClientArgs, + UpdateOAuthClientArgs, + PaginationArgs, +} from '../types/tools.js'; + +interface OAuthClient { + id: string; + name: string; + secret: string; + redirectUris: string[]; + description?: string; + teamId: string; + createdById: string; + createdAt: Date; + updatedAt: Date; +} + +interface OAuthAuthentication { + id: string; + providerId: string; + userId: string; + teamId: string; + scopes: string[]; + createdAt: Date; + updatedAt: Date; +} + +/** + * oauth_clients.list - List OAuth applications + */ +const listOAuthClients: BaseTool = { + name: 'outline_oauth_clients_list', + description: 'List all registered OAuth applications/clients. Returns client details including name, redirect URIs, and creation info.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of results to return', + default: 25, + }, + offset: { + type: 'number', + description: 'Number of results to skip', + default: 0, + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { limit = 25, offset = 0 } = args; + + const result = await pgClient.query( + ` + SELECT + oc.*, + u.name as "createdByName", + u.email as "createdByEmail" + FROM oauth_clients oc + LEFT JOIN users u ON oc."createdById" = u.id + ORDER BY oc."createdAt" DESC + LIMIT $1 OFFSET $2 + `, + [limit, offset] + ); + + const clients = result.rows.map((row) => ({ + id: row.id, + name: row.name, + redirectUris: row.redirectUris, + description: row.description, + teamId: row.teamId, + createdById: row.createdById, + createdByName: row.createdByName, + createdByEmail: row.createdByEmail, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + // Secret omitted for security + })); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: clients, + pagination: { + offset, + limit, + total: result.rowCount || 0, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * oauth_clients.info - Get OAuth client details + */ +const getOAuthClient: BaseTool = { + name: 'outline_oauth_clients_info', + description: 'Get detailed information about a specific OAuth client/application by ID.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'OAuth client UUID', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + const { id } = args; + + const result = await pgClient.query( + ` + SELECT + oc.*, + u.name as "createdByName", + u.email as "createdByEmail" + FROM oauth_clients oc + LEFT JOIN users u ON oc."createdById" = u.id + WHERE oc.id = $1 + `, + [id] + ); + + if (result.rows.length === 0) { + throw new Error(`OAuth client not found: ${id}`); + } + + const client = result.rows[0]; + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: client, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * oauth_clients.create - Register new OAuth application + */ +const createOAuthClient: BaseTool = { + name: 'outline_oauth_clients_create', + description: 'Register a new OAuth application/client. Generates client credentials for OAuth flow integration.', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Application name', + }, + redirect_uris: { + type: 'array', + items: { type: 'string' }, + description: 'List of allowed redirect URIs for OAuth flow', + }, + description: { + type: 'string', + description: 'Optional application description', + }, + }, + required: ['name', 'redirect_uris'], + }, + handler: async (args, pgClient): Promise => { + const { name, redirect_uris, description } = args; + + // Generate random client secret (in production, use crypto.randomBytes) + const secret = `sk_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`; + + const result = await pgClient.query( + ` + INSERT INTO oauth_clients (name, secret, "redirectUris", description) + VALUES ($1, $2, $3, $4) + RETURNING * + `, + [name, secret, JSON.stringify(redirect_uris), description] + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'OAuth client created successfully. Store the secret securely - it will not be shown again.', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * oauth_clients.update - Modify OAuth application settings + */ +const updateOAuthClient: BaseTool = { + name: 'outline_oauth_clients_update', + description: 'Update OAuth client/application settings such as name, redirect URIs, or description.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'OAuth client UUID', + }, + name: { + type: 'string', + description: 'New application name', + }, + redirect_uris: { + type: 'array', + items: { type: 'string' }, + description: 'Updated list of allowed redirect URIs', + }, + description: { + type: 'string', + description: 'Updated description', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + const { id, name, redirect_uris, description } = args; + + const updates: string[] = []; + const values: any[] = []; + let paramIndex = 1; + + if (name !== undefined) { + updates.push(`name = $${paramIndex}`); + values.push(name); + paramIndex++; + } + + if (redirect_uris !== undefined) { + updates.push(`"redirectUris" = $${paramIndex}`); + values.push(JSON.stringify(redirect_uris)); + paramIndex++; + } + + if (description !== undefined) { + updates.push(`description = $${paramIndex}`); + values.push(description); + paramIndex++; + } + + if (updates.length === 0) { + throw new Error('No fields to update'); + } + + updates.push(`"updatedAt" = NOW()`); + values.push(id); + + const result = await pgClient.query( + ` + UPDATE oauth_clients + SET ${updates.join(', ')} + WHERE id = $${paramIndex} + RETURNING * + `, + values + ); + + if (result.rows.length === 0) { + throw new Error(`OAuth client not found: ${id}`); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * oauth_clients.rotate_secret - Generate new client secret + */ +const rotateOAuthClientSecret: BaseTool = { + name: 'outline_oauth_clients_rotate_secret', + description: 'Generate a new client secret for an OAuth application. The old secret will be invalidated.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'OAuth client UUID', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + const { id } = args; + + const newSecret = `sk_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`; + + const result = await pgClient.query( + ` + UPDATE oauth_clients + SET secret = $1, "updatedAt" = NOW() + WHERE id = $2 + RETURNING id, name, secret, "updatedAt" + `, + [newSecret, id] + ); + + if (result.rows.length === 0) { + throw new Error(`OAuth client not found: ${id}`); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'Secret rotated successfully. Update your application configuration with the new secret.', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * oauth_clients.delete - Remove OAuth application + */ +const deleteOAuthClient: BaseTool = { + name: 'outline_oauth_clients_delete', + description: 'Delete an OAuth client/application. This will revoke all active authentications using this client.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'OAuth client UUID to delete', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + const { id } = args; + + const result = await pgClient.query( + ` + DELETE FROM oauth_clients + WHERE id = $1 + RETURNING id, name + `, + [id] + ); + + if (result.rows.length === 0) { + throw new Error(`OAuth client not found: ${id}`); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + success: true, + deleted: result.rows[0], + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * oauth_authentications.list - List user OAuth authentications + */ +const listOAuthAuthentications: BaseTool = { + name: 'outline_oauth_authentications_list', + description: 'List all OAuth authentications (user authorizations). Shows which users have granted access to which providers.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of results to return', + default: 25, + }, + offset: { + type: 'number', + description: 'Number of results to skip', + default: 0, + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { limit = 25, offset = 0 } = args; + + const result = await pgClient.query( + ` + SELECT + oa.*, + u.name as "userName", + u.email as "userEmail" + FROM oauth_authentications oa + LEFT JOIN users u ON oa."userId" = u.id + ORDER BY oa."createdAt" DESC + LIMIT $1 OFFSET $2 + `, + [limit, offset] + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows, + pagination: { + offset, + limit, + total: result.rowCount || 0, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * oauth_authentications.delete - Revoke OAuth authentication + */ +const deleteOAuthAuthentication: BaseTool<{ id: string }> = { + name: 'outline_oauth_authentications_delete', + description: 'Revoke an OAuth authentication. This disconnects the user from the OAuth provider.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'OAuth authentication UUID to revoke', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + const { id } = args; + + const result = await pgClient.query( + ` + DELETE FROM oauth_authentications + WHERE id = $1 + RETURNING id, "providerId", "userId" + `, + [id] + ); + + if (result.rows.length === 0) { + throw new Error(`OAuth authentication not found: ${id}`); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + success: true, + revoked: result.rows[0], + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all OAuth tools +export const oauthTools: BaseTool[] = [ + listOAuthClients, + getOAuthClient, + createOAuthClient, + updateOAuthClient, + rotateOAuthClientSecret, + deleteOAuthClient, + listOAuthAuthentications, + deleteOAuthAuthentication, +]; diff --git a/src/tools/revisions.ts b/src/tools/revisions.ts new file mode 100644 index 0000000..1a9b421 --- /dev/null +++ b/src/tools/revisions.ts @@ -0,0 +1,335 @@ +/** + * MCP Outline PostgreSQL - Revisions Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse, RevisionArgs, GetRevisionArgs } from '../types/tools.js'; +import { validatePagination, isValidUUID } from '../utils/security.js'; + +/** + * revisions.list - List document revisions + */ +const listRevisions: BaseTool = { + name: 'outline_revisions_list', + description: 'List all revisions for a specific document. Revisions are ordered from newest to oldest.', + inputSchema: { + type: 'object', + properties: { + document_id: { + type: 'string', + description: 'Document ID (UUID)', + }, + limit: { + type: 'number', + description: 'Maximum number of results (default: 25, max: 100)', + }, + offset: { + type: 'number', + description: 'Number of results to skip (default: 0)', + }, + }, + required: ['document_id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + + const { limit, offset } = validatePagination(args.limit, args.offset); + + // Verify document exists + const docCheck = await pgClient.query( + 'SELECT id, title FROM documents WHERE id = $1', + [args.document_id] + ); + + if (docCheck.rows.length === 0) { + throw new Error('Document not found'); + } + + const query = ` + SELECT + r.id, + r.version, + r."editorVersion", + r.title, + r.emoji, + r."documentId", + r."userId", + r."createdAt", + u.name as "createdByName", + u.email as "createdByEmail", + LENGTH(r.text) as "textLength" + FROM revisions r + LEFT JOIN users u ON r."userId" = u.id + WHERE r."documentId" = $1 + ORDER BY r.version DESC + LIMIT $2 OFFSET $3 + `; + + const result = await pgClient.query(query, [args.document_id, limit, offset]); + + // Get total count + const countQuery = await pgClient.query( + 'SELECT COUNT(*) as total FROM revisions WHERE "documentId" = $1', + [args.document_id] + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows, + document: docCheck.rows[0], + pagination: { + limit, + offset, + total: parseInt(countQuery.rows[0].total), + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * revisions.info - Get detailed information about a specific revision + */ +const getRevision: BaseTool = { + name: 'outline_revisions_info', + description: 'Get detailed information about a specific revision, including full text content.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Revision ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid revision ID format'); + } + + const query = ` + SELECT + r.id, + r.version, + r."editorVersion", + r.title, + r.text, + r.emoji, + r."documentId", + r."userId", + r."createdAt", + u.name as "createdByName", + u.email as "createdByEmail", + d.title as "currentDocumentTitle" + FROM revisions r + LEFT JOIN users u ON r."userId" = u.id + LEFT JOIN documents d ON r."documentId" = d.id + WHERE r.id = $1 + `; + + const result = await pgClient.query(query, [args.id]); + + if (result.rows.length === 0) { + throw new Error('Revision not found'); + } + + // Get revision statistics + const statsQuery = await pgClient.query( + `SELECT + COUNT(*) as "totalRevisions", + MIN(version) as "firstVersion", + MAX(version) as "latestVersion" + FROM revisions + WHERE "documentId" = $1`, + [result.rows[0].documentId] + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + statistics: statsQuery.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * revisions.compare - Compare two revisions or a revision with current document + */ +const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = { + name: 'outline_revisions_compare', + description: 'Compare two document revisions or compare a revision with the current document version. Returns both versions for comparison.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'First revision ID (UUID)', + }, + compare_to: { + type: 'string', + description: 'Second revision ID to compare with (UUID). If not provided, compares with current document.', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid revision ID format'); + } + + if (args.compare_to && !isValidUUID(args.compare_to)) { + throw new Error('Invalid compare_to revision ID format'); + } + + // Get first revision + const revision1Query = await pgClient.query( + `SELECT + r.id, + r.version, + r.title, + r.text, + r.emoji, + r."documentId", + r."createdAt", + u.name as "createdByName" + FROM revisions r + LEFT JOIN users u ON r."userId" = u.id + WHERE r.id = $1`, + [args.id] + ); + + if (revision1Query.rows.length === 0) { + throw new Error('Revision not found'); + } + + const revision1 = revision1Query.rows[0]; + let revision2; + + if (args.compare_to) { + // Compare with another revision + const revision2Query = await pgClient.query( + `SELECT + r.id, + r.version, + r.title, + r.text, + r.emoji, + r."documentId", + r."createdAt", + u.name as "createdByName" + FROM revisions r + LEFT JOIN users u ON r."userId" = u.id + WHERE r.id = $1`, + [args.compare_to] + ); + + if (revision2Query.rows.length === 0) { + throw new Error('Comparison revision not found'); + } + + revision2 = revision2Query.rows[0]; + + // Verify both revisions are from the same document + if (revision1.documentId !== revision2.documentId) { + throw new Error('Revisions are from different documents'); + } + } else { + // Compare with current document + const currentDocQuery = await pgClient.query( + `SELECT + d.id, + d.title, + d.text, + d.emoji, + d."updatedAt" as "createdAt", + u.name as "createdByName" + FROM documents d + LEFT JOIN users u ON d."updatedById" = u.id + WHERE d.id = $1`, + [revision1.documentId] + ); + + if (currentDocQuery.rows.length === 0) { + throw new Error('Document not found'); + } + + revision2 = { + ...currentDocQuery.rows[0], + version: 'current', + }; + } + + // Calculate basic diff statistics + const textLengthDiff = revision2.text.length - revision1.text.length; + const titleChanged = revision1.title !== revision2.title; + const emojiChanged = revision1.emoji !== revision2.emoji; + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + revision1: { + id: revision1.id, + version: revision1.version, + title: revision1.title, + text: revision1.text, + emoji: revision1.emoji, + createdAt: revision1.createdAt, + createdByName: revision1.createdByName, + }, + revision2: { + id: revision2.id, + version: revision2.version, + title: revision2.title, + text: revision2.text, + emoji: revision2.emoji, + createdAt: revision2.createdAt, + createdByName: revision2.createdByName, + }, + comparison: { + titleChanged, + emojiChanged, + textLengthDiff, + textLengthDiffPercent: ((textLengthDiff / revision1.text.length) * 100).toFixed(2) + '%', + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all revision tools +export const revisionsTools: BaseTool[] = [ + listRevisions, + getRevision, + compareRevisions, +]; diff --git a/src/tools/shares.ts b/src/tools/shares.ts new file mode 100644 index 0000000..6d03743 --- /dev/null +++ b/src/tools/shares.ts @@ -0,0 +1,470 @@ +/** + * MCP Outline PostgreSQL - Shares Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse, ShareArgs, GetShareArgs, CreateShareArgs, UpdateShareArgs } from '../types/tools.js'; +import { validatePagination, isValidUUID, isValidUrlId } from '../utils/security.js'; + +/** + * shares.list - List document shares with optional filters + */ +const listShares: BaseTool = { + name: 'outline_shares_list', + description: 'List document shares with optional filtering. Supports pagination.', + inputSchema: { + type: 'object', + properties: { + document_id: { + type: 'string', + description: 'Filter by document ID (UUID)', + }, + limit: { + type: 'number', + description: 'Maximum number of results (default: 25, max: 100)', + }, + offset: { + type: 'number', + description: 'Number of results to skip (default: 0)', + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { limit, offset } = validatePagination(args.limit, args.offset); + const conditions: string[] = ['s."revokedAt" IS NULL']; + const params: any[] = []; + let paramIndex = 1; + + if (args.document_id) { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + conditions.push(`s."documentId" = $${paramIndex++}`); + params.push(args.document_id); + } + + const whereClause = `WHERE ${conditions.join(' AND ')}`; + + const query = ` + SELECT + s.id, + s."urlId", + s."documentId", + s."userId", + s."teamId", + s."includeChildDocuments", + s.published, + s.domain, + s."lastAccessedAt", + s.views, + s."createdAt", + s."updatedAt", + d.title as "documentTitle", + u.name as "createdByName", + u.email as "createdByEmail" + FROM shares s + LEFT JOIN documents d ON s."documentId" = d.id + LEFT JOIN users u ON s."userId" = u.id + ${whereClause} + ORDER BY s."createdAt" DESC + LIMIT $${paramIndex++} OFFSET $${paramIndex} + `; + + params.push(limit, offset); + + const result = await pgClient.query(query, params); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows, + pagination: { + limit, + offset, + total: result.rows.length, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * shares.info - Get detailed information about a specific share + */ +const getShare: BaseTool = { + name: 'outline_shares_info', + description: 'Get detailed information about a specific share by ID or document ID.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Share ID (UUID)', + }, + document_id: { + type: 'string', + description: 'Document ID to find share for (UUID)', + }, + }, + }, + handler: async (args, pgClient): Promise => { + let query: string; + let params: string[]; + + if (args.id) { + if (!isValidUUID(args.id)) { + throw new Error('Invalid share ID format'); + } + query = ` + SELECT + s.id, + s."urlId", + s."documentId", + s."userId", + s."teamId", + s."includeChildDocuments", + s.published, + s.domain, + s."lastAccessedAt", + s.views, + s."createdAt", + s."updatedAt", + s."revokedAt", + s."revokedById", + d.title as "documentTitle", + d.text as "documentText", + u.name as "createdByName", + u.email as "createdByEmail", + ru.name as "revokedByName" + FROM shares s + LEFT JOIN documents d ON s."documentId" = d.id + LEFT JOIN users u ON s."userId" = u.id + LEFT JOIN users ru ON s."revokedById" = ru.id + WHERE s.id = $1 + `; + params = [args.id]; + } else if (args.document_id) { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + query = ` + SELECT + s.id, + s."urlId", + s."documentId", + s."userId", + s."teamId", + s."includeChildDocuments", + s.published, + s.domain, + s."lastAccessedAt", + s.views, + s."createdAt", + s."updatedAt", + s."revokedAt", + s."revokedById", + d.title as "documentTitle", + u.name as "createdByName" + FROM shares s + LEFT JOIN documents d ON s."documentId" = d.id + LEFT JOIN users u ON s."userId" = u.id + WHERE s."documentId" = $1 AND s."revokedAt" IS NULL + ORDER BY s."createdAt" DESC + LIMIT 1 + `; + params = [args.document_id]; + } else { + throw new Error('Either id or document_id must be provided'); + } + + const result = await pgClient.query(query, params); + + if (result.rows.length === 0) { + throw new Error('Share not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * shares.create - Create a new document share + */ +const createShare: BaseTool = { + name: 'outline_shares_create', + description: 'Create a new share link for a document. Returns the share details including the URL ID.', + inputSchema: { + type: 'object', + properties: { + document_id: { + type: 'string', + description: 'Document ID to share (UUID)', + }, + published: { + type: 'boolean', + description: 'Whether the share is published (default: true)', + }, + include_child_documents: { + type: 'boolean', + description: 'Include child documents in the share (default: false)', + }, + url_id: { + type: 'string', + description: 'Custom URL ID (optional, will be auto-generated if not provided)', + }, + }, + required: ['document_id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.document_id)) { + throw new Error('Invalid document_id format'); + } + + if (args.url_id && !isValidUrlId(args.url_id)) { + throw new Error('Invalid url_id format. Use only alphanumeric characters, hyphens, and underscores.'); + } + + // Verify document exists + const docCheck = await pgClient.query( + 'SELECT id, "teamId" FROM documents WHERE id = $1 AND "deletedAt" IS NULL', + [args.document_id] + ); + + if (docCheck.rows.length === 0) { + throw new Error('Document not found or deleted'); + } + + const teamId = docCheck.rows[0].teamId; + + // Get first admin user as creator + const userQuery = await pgClient.query( + 'SELECT id FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL LIMIT 1' + ); + + if (userQuery.rows.length === 0) { + throw new Error('No valid user found to create share'); + } + + const userId = userQuery.rows[0].id; + + // Generate urlId if not provided + const urlId = args.url_id || `share-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + const query = ` + INSERT INTO shares ( + "urlId", + "documentId", + "userId", + "teamId", + "includeChildDocuments", + published, + views, + "createdAt", + "updatedAt" + ) VALUES ($1, $2, $3, $4, $5, $6, 0, NOW(), NOW()) + RETURNING * + `; + + const result = await pgClient.query(query, [ + urlId, + args.document_id, + userId, + teamId, + args.include_child_documents || false, + args.published !== false, // Default to true + ]); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * shares.update - Update an existing share + */ +const updateShare: BaseTool = { + name: 'outline_shares_update', + description: 'Update an existing share. Can change published status or include child documents setting.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Share ID (UUID)', + }, + published: { + type: 'boolean', + description: 'Whether the share is published', + }, + include_child_documents: { + type: 'boolean', + description: 'Include child documents in the share', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid share ID format'); + } + + const updates: string[] = []; + const params: any[] = []; + let paramIndex = 1; + + if (args.published !== undefined) { + updates.push(`published = $${paramIndex++}`); + params.push(args.published); + } + + if (args.include_child_documents !== undefined) { + updates.push(`"includeChildDocuments" = $${paramIndex++}`); + params.push(args.include_child_documents); + } + + if (updates.length === 0) { + throw new Error('No updates provided'); + } + + updates.push(`"updatedAt" = NOW()`); + params.push(args.id); + + const query = ` + UPDATE shares + SET ${updates.join(', ')} + WHERE id = $${paramIndex} AND "revokedAt" IS NULL + RETURNING * + `; + + const result = await pgClient.query(query, params); + + if (result.rows.length === 0) { + throw new Error('Share not found or already revoked'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * shares.revoke - Revoke a share link + */ +const revokeShare: BaseTool = { + name: 'outline_shares_revoke', + description: 'Revoke a share link, making it no longer accessible.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Share ID (UUID)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id!)) { + throw new Error('Invalid share ID format'); + } + + // Get first admin user as revoker + const userQuery = await pgClient.query( + 'SELECT id FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL LIMIT 1' + ); + + if (userQuery.rows.length === 0) { + throw new Error('No valid user found to revoke share'); + } + + const revokedById = userQuery.rows[0].id; + + const query = ` + UPDATE shares + SET + "revokedAt" = NOW(), + "revokedById" = $1, + "updatedAt" = NOW() + WHERE id = $2 AND "revokedAt" IS NULL + RETURNING * + `; + + const result = await pgClient.query(query, [revokedById, args.id]); + + if (result.rows.length === 0) { + throw new Error('Share not found or already revoked'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: 'Share revoked successfully', + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all share tools +export const sharesTools: BaseTool[] = [ + listShares, + getShare, + createShare, + updateShare, + revokeShare, +]; diff --git a/src/tools/users.ts b/src/tools/users.ts new file mode 100644 index 0000000..f66913a --- /dev/null +++ b/src/tools/users.ts @@ -0,0 +1,660 @@ +/** + * MCP Outline PostgreSQL - Users Tools + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; +import { BaseTool, ToolResponse, UserArgs, GetUserArgs, CreateUserArgs, UpdateUserArgs } from '../types/tools.js'; +import { validatePagination, isValidUUID, isValidEmail, sanitizeInput } from '../utils/security.js'; + +/** + * users.list - List users with filtering + */ +const listUsers: BaseTool = { + name: 'outline_list_users', + description: 'List users with optional filtering by query string, role, or status. Supports pagination and returns user profiles including roles and suspension status.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query to filter users by name or email', + }, + filter: { + type: 'string', + enum: ['all', 'admins', 'members', 'suspended', 'invited'], + description: 'Filter users by role or status', + }, + limit: { + type: 'number', + description: 'Maximum number of results to return (max 100)', + default: 25, + }, + offset: { + type: 'number', + description: 'Number of results to skip for pagination', + default: 0, + }, + }, + }, + handler: async (args, pgClient): Promise => { + const { limit, offset } = validatePagination(args.limit, args.offset); + const query = args.query ? sanitizeInput(args.query) : undefined; + const filter = args.filter || 'all'; + + let whereConditions = ['u."deletedAt" IS NULL']; + const queryParams: any[] = [limit, offset]; + let paramIndex = 3; + + // Add search query filter + if (query) { + whereConditions.push(`(LOWER(u.name) LIKE LOWER($${paramIndex}) OR LOWER(u.email) LIKE LOWER($${paramIndex}))`); + queryParams.push(`%${query}%`); + paramIndex++; + } + + // Add role/status filters + switch (filter) { + case 'admins': + whereConditions.push('u."isAdmin" = true'); + break; + case 'members': + whereConditions.push('u."isAdmin" = false AND u."isViewer" = false'); + break; + case 'suspended': + whereConditions.push('u."isSuspended" = true'); + break; + case 'invited': + whereConditions.push('u."lastSignedInAt" IS NULL'); + break; + } + + const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; + + const result = await pgClient.query( + ` + SELECT + u.id, + u.email, + u.username, + u.name, + u."avatarUrl", + u.language, + u.preferences, + u."notificationSettings", + u.timezone, + u."isAdmin", + u."isViewer", + u."isSuspended", + u."lastActiveAt", + u."lastSignedInAt", + u."suspendedAt", + u."suspendedById", + u."teamId", + u."createdAt", + u."updatedAt", + t.name as "teamName", + (SELECT COUNT(*) FROM users WHERE ${whereConditions.join(' AND ')}) as total + FROM users u + LEFT JOIN teams t ON u."teamId" = t.id + ${whereClause} + ORDER BY u."createdAt" DESC + LIMIT $1 OFFSET $2 + `, + queryParams + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: { + users: result.rows, + total: result.rows.length > 0 ? parseInt(result.rows[0].total) : 0, + limit, + offset, + }, + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * users.info - Get user details by ID + */ +const getUser: BaseTool = { + name: 'outline_get_user', + description: 'Get detailed information about a specific user by their ID. Returns full user profile including preferences, permissions, and activity.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'User UUID', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + SELECT + u.id, + u.email, + u.username, + u.name, + u."avatarUrl", + u.language, + u.preferences, + u."notificationSettings", + u.timezone, + u."isAdmin", + u."isViewer", + u."isSuspended", + u."lastActiveAt", + u."lastSignedInAt", + u."suspendedAt", + u."suspendedById", + u."teamId", + u."createdAt", + u."updatedAt", + t.name as "teamName", + suspender.name as "suspendedByName", + (SELECT COUNT(*) FROM documents WHERE "createdById" = u.id AND "deletedAt" IS NULL) as "documentCount", + (SELECT COUNT(*) FROM collections WHERE "createdById" = u.id AND "deletedAt" IS NULL) as "collectionCount" + FROM users u + LEFT JOIN teams t ON u."teamId" = t.id + LEFT JOIN users suspender ON u."suspendedById" = suspender.id + WHERE u.id = $1 AND u."deletedAt" IS NULL + `, + [args.id] + ); + + if (result.rows.length === 0) { + throw new Error('User not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * users.create - Create new user + */ +const createUser: BaseTool = { + name: 'outline_create_user', + description: 'Create a new user with specified name, email, and optional role. User will be added to the team associated with the database.', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Full name of the user', + }, + email: { + type: 'string', + description: 'Email address (must be unique)', + }, + role: { + type: 'string', + enum: ['admin', 'member', 'viewer'], + description: 'User role (default: member)', + default: 'member', + }, + }, + required: ['name', 'email'], + }, + handler: async (args, pgClient): Promise => { + const name = sanitizeInput(args.name); + const email = sanitizeInput(args.email); + const role = args.role || 'member'; + + if (!isValidEmail(email)) { + throw new Error('Invalid email format'); + } + + // Check if user already exists + const existingUser = await pgClient.query( + `SELECT id FROM users WHERE email = $1`, + [email] + ); + + if (existingUser.rows.length > 0) { + throw new Error('User with this email already exists'); + } + + // Get team ID (assuming first team, adjust as needed) + const teamResult = await pgClient.query(`SELECT id FROM teams LIMIT 1`); + if (teamResult.rows.length === 0) { + throw new Error('No team found'); + } + const teamId = teamResult.rows[0].id; + + const isAdmin = role === 'admin'; + const isViewer = role === 'viewer'; + + const result = await pgClient.query( + ` + INSERT INTO users ( + id, email, name, "teamId", "isAdmin", "isViewer", + "createdAt", "updatedAt" + ) + VALUES ( + gen_random_uuid(), $1, $2, $3, $4, $5, + NOW(), NOW() + ) + RETURNING * + `, + [email, name, teamId, isAdmin, isViewer] + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User created successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * users.update - Update user details + */ +const updateUser: BaseTool = { + name: 'outline_update_user', + description: 'Update user profile information such as name, avatar, or language preferences.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'User UUID', + }, + name: { + type: 'string', + description: 'Updated full name', + }, + avatar_url: { + type: 'string', + description: 'URL to avatar image', + }, + language: { + type: 'string', + description: 'Language code (e.g., en_US, pt_PT)', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + const updates: string[] = []; + const values: any[] = []; + let paramIndex = 1; + + if (args.name !== undefined) { + updates.push(`name = $${paramIndex++}`); + values.push(sanitizeInput(args.name)); + } + + if (args.avatar_url !== undefined) { + updates.push(`"avatarUrl" = $${paramIndex++}`); + values.push(sanitizeInput(args.avatar_url)); + } + + if (args.language !== undefined) { + updates.push(`language = $${paramIndex++}`); + values.push(sanitizeInput(args.language)); + } + + if (updates.length === 0) { + throw new Error('No updates provided'); + } + + updates.push(`"updatedAt" = NOW()`); + values.push(args.id); + + const result = await pgClient.query( + ` + UPDATE users + SET ${updates.join(', ')} + WHERE id = $${paramIndex} AND "deletedAt" IS NULL + RETURNING * + `, + values + ); + + if (result.rows.length === 0) { + throw new Error('User not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User updated successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * users.delete - Soft delete user + */ +const deleteUser: BaseTool = { + name: 'outline_delete_user', + description: 'Soft delete a user. This marks the user as deleted but preserves their data for audit purposes.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'User UUID to delete', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + UPDATE users + SET "deletedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL + RETURNING id, email, name + `, + [args.id] + ); + + if (result.rows.length === 0) { + throw new Error('User not found or already deleted'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User deleted successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * users.suspend - Suspend user account + */ +const suspendUser: BaseTool = { + name: 'outline_suspend_user', + description: 'Suspend a user account. Suspended users cannot access the system but their data is preserved.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'User UUID to suspend', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + UPDATE users + SET "isSuspended" = true, "suspendedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL + RETURNING id, email, name, "isSuspended", "suspendedAt" + `, + [args.id] + ); + + if (result.rows.length === 0) { + throw new Error('User not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User suspended successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * users.activate - Activate suspended user + */ +const activateUser: BaseTool = { + name: 'outline_activate_user', + description: 'Reactivate a suspended user account, restoring their access to the system.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'User UUID to activate', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + UPDATE users + SET "isSuspended" = false, "suspendedAt" = NULL, "suspendedById" = NULL + WHERE id = $1 AND "deletedAt" IS NULL + RETURNING id, email, name, "isSuspended" + `, + [args.id] + ); + + if (result.rows.length === 0) { + throw new Error('User not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User activated successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * users.promote - Promote user to admin + */ +const promoteUser: BaseTool = { + name: 'outline_promote_user', + description: 'Promote a user to admin role, granting them full administrative permissions.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'User UUID to promote', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + UPDATE users + SET "isAdmin" = true, "isViewer" = false, "updatedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL + RETURNING id, email, name, "isAdmin", "isViewer" + `, + [args.id] + ); + + if (result.rows.length === 0) { + throw new Error('User not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User promoted to admin successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +/** + * users.demote - Demote admin to member + */ +const demoteUser: BaseTool = { + name: 'outline_demote_user', + description: 'Demote an admin user to regular member role, removing administrative permissions.', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'User UUID to demote', + }, + }, + required: ['id'], + }, + handler: async (args, pgClient): Promise => { + if (!isValidUUID(args.id)) { + throw new Error('Invalid user ID format. Must be a valid UUID.'); + } + + const result = await pgClient.query( + ` + UPDATE users + SET "isAdmin" = false, "updatedAt" = NOW() + WHERE id = $1 AND "deletedAt" IS NULL + RETURNING id, email, name, "isAdmin", "isViewer" + `, + [args.id] + ); + + if (result.rows.length === 0) { + throw new Error('User not found'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + data: result.rows[0], + message: 'User demoted to member successfully', + }, + null, + 2 + ), + }, + ], + }; + }, +}; + +// Export all user tools +export const usersTools: BaseTool[] = [ + listUsers, + getUser, + createUser, + updateUser, + deleteUser, + suspendUser, + activateUser, + promoteUser, + demoteUser, +]; diff --git a/src/types/db.ts b/src/types/db.ts new file mode 100644 index 0000000..fbf104e --- /dev/null +++ b/src/types/db.ts @@ -0,0 +1,324 @@ +/** + * MCP Outline PostgreSQL - Database Types + * Based on Outline PostgreSQL schema + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +// Base row type for all database queries +export interface DatabaseRow { + [key: string]: unknown; +} + +// Document entity +export interface Document extends DatabaseRow { + id: string; + urlId: string; + title: string; + text: string; + emoji?: string; + collectionId: string; + parentDocumentId?: string; + createdById: string; + lastModifiedById?: string; + publishedAt?: Date; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; + archivedAt?: Date; + template: boolean; + templateId?: string; + fullWidth: boolean; + insightsEnabled: boolean; + sourceMetadata?: object; + version?: number; +} + +// Collection entity +export interface Collection extends DatabaseRow { + id: string; + urlId: string; + name: string; + description?: string; + icon?: string; + color?: string; + index?: string; + permission?: string; + maintainerApprovalRequired: boolean; + documentStructure?: object; + sharing: boolean; + sort?: object; + teamId: string; + createdById: string; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; + archivedAt?: Date; +} + +// User entity +export interface User extends DatabaseRow { + id: string; + email: string; + username?: string; + name: string; + avatarUrl?: string; + language?: string; + preferences?: object; + notificationSettings?: object; + timezone?: string; + isAdmin: boolean; + isViewer: boolean; + isSuspended: boolean; + lastActiveAt?: Date; + lastActiveIp?: string; + lastSignedInAt?: Date; + lastSignedInIp?: string; + lastSigninEmailSentAt?: Date; + suspendedAt?: Date; + suspendedById?: string; + teamId: string; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; +} + +// Team entity +export interface Team extends DatabaseRow { + id: string; + name: string; + subdomain?: string; + domain?: string; + defaultCollectionId?: string; + avatarUrl?: string; + sharing: boolean; + inviteRequired: boolean; + memberCollectionCreate: boolean; + guestSignin: boolean; + documentEmbeds: boolean; + collaborativeEditing: boolean; + defaultUserRole: string; + createdAt: Date; + updatedAt: Date; +} + +// Group entity +export interface Group extends DatabaseRow { + id: string; + name: string; + teamId: string; + createdById: string; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; +} + +// GroupUser entity +export interface GroupUser extends DatabaseRow { + id: string; + userId: string; + groupId: string; + createdById: string; + createdAt: Date; + updatedAt: Date; +} + +// Comment entity +export interface Comment extends DatabaseRow { + id: string; + data: object; + documentId: string; + parentCommentId?: string; + createdById: string; + resolvedById?: string; + resolvedAt?: Date; + createdAt: Date; + updatedAt: Date; +} + +// Share entity +export interface Share extends DatabaseRow { + id: string; + urlId: string; + documentId: string; + userId: string; + teamId: string; + includeChildDocuments: boolean; + published: boolean; + domain?: string; + lastAccessedAt?: Date; + views: number; + createdAt: Date; + updatedAt: Date; + revokedAt?: Date; + revokedById?: string; +} + +// Revision entity +export interface Revision extends DatabaseRow { + id: string; + version: number; + editorVersion?: string; + title: string; + text: string; + emoji?: string; + documentId: string; + userId: string; + createdAt: Date; +} + +// Event entity +export interface Event extends DatabaseRow { + id: string; + name: string; + modelId?: string; + actorId?: string; + userId?: string; + collectionId?: string; + documentId?: string; + teamId: string; + ip?: string; + data?: object; + createdAt: Date; +} + +// Attachment entity +export interface Attachment extends DatabaseRow { + id: string; + key: string; + url: string; + contentType: string; + size: number; + acl: string; + documentId?: string; + userId: string; + teamId: string; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; +} + +// CollectionUser entity (permissions) +export interface CollectionUser extends DatabaseRow { + id: string; + collectionId: string; + userId: string; + permission: string; + createdById: string; + createdAt: Date; + updatedAt: Date; +} + +// CollectionGroup entity (group permissions) +export interface CollectionGroup extends DatabaseRow { + id: string; + collectionId: string; + groupId: string; + permission: string; + createdById: string; + createdAt: Date; + updatedAt: Date; +} + +// Star entity +export interface Star extends DatabaseRow { + id: string; + index?: string; + documentId?: string; + collectionId?: string; + userId: string; + createdAt: Date; + updatedAt: Date; +} + +// Pin entity +export interface Pin extends DatabaseRow { + id: string; + index?: string; + documentId: string; + collectionId?: string; + teamId: string; + createdById: string; + createdAt: Date; + updatedAt: Date; +} + +// View entity +export interface View extends DatabaseRow { + id: string; + documentId: string; + userId: string; + count: number; + createdAt: Date; + updatedAt: Date; +} + +// ApiKey entity +export interface ApiKey extends DatabaseRow { + id: string; + name: string; + secret: string; + hash: string; + lastActiveAt?: Date; + expiresAt?: Date; + userId: string; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; +} + +// FileOperation entity +export interface FileOperation extends DatabaseRow { + id: string; + type: string; + state: string; + format?: string; + size?: number; + key?: string; + url?: string; + error?: string; + collectionId?: string; + userId: string; + teamId: string; + createdAt: Date; + updatedAt: Date; +} + +// WebhookSubscription entity +export interface WebhookSubscription extends DatabaseRow { + id: string; + name: string; + url: string; + enabled: boolean; + events: string[]; + secret?: string; + teamId: string; + createdById: string; + createdAt: Date; + updatedAt: Date; +} + +// SearchQuery entity +export interface SearchQuery extends DatabaseRow { + id: string; + query: string; + results: number; + source: string; + userId: string; + teamId: string; + createdAt: Date; +} + +// Integration entity +export interface Integration extends DatabaseRow { + id: string; + type: string; + service: string; + events: string[]; + settings?: object; + authentication?: object; + collectionId?: string; + teamId: string; + userId: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..46e8b3c --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,7 @@ +/** + * MCP Outline PostgreSQL - Types Index + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +export * from './tools.js'; +export * from './db.js'; diff --git a/src/types/tools.ts b/src/types/tools.ts new file mode 100644 index 0000000..a080d5f --- /dev/null +++ b/src/types/tools.ts @@ -0,0 +1,292 @@ +/** + * MCP Outline PostgreSQL - Tool Types + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +import { Pool } from 'pg'; + +// Base types for all tools +export interface ToolResponse { + content: Array<{ + type: 'text'; + text: string; + }>; + [key: string]: unknown; // Index signature for MCP SDK compatibility +} + +export interface BaseTool> { + name: string; + description: string; + inputSchema: { + type: string; + properties: Record; + required?: string[]; + }; + handler: (args: TArgs, pgClient: Pool) => Promise; +} + +// Common argument types +export interface PaginationArgs { + limit?: number; + offset?: number; +} + +export interface DateRangeArgs { + date_from?: string; + date_to?: string; +} + +export interface SortArgs { + sort?: string; + direction?: 'ASC' | 'DESC'; +} + +// Document specific types +export interface DocumentArgs extends PaginationArgs, SortArgs { + collection_id?: string; + user_id?: string; + template?: boolean; + published?: boolean; + archived?: boolean; +} + +export interface GetDocumentArgs { + id?: string; + share_id?: string; +} + +export interface CreateDocumentArgs { + title: string; + text?: string; + collection_id: string; + parent_document_id?: string; + template?: boolean; + publish?: boolean; +} + +export interface UpdateDocumentArgs { + id: string; + title?: string; + text?: string; + done?: boolean; + append?: boolean; +} + +export interface SearchDocumentsArgs extends PaginationArgs { + query: string; + collection_id?: string; + user_id?: string; + include_archived?: boolean; + include_drafts?: boolean; +} + +export interface MoveDocumentArgs { + id: string; + collection_id?: string; + parent_document_id?: string; +} + +// Collection specific types +export interface CollectionArgs extends PaginationArgs { + include_deleted?: boolean; +} + +export interface GetCollectionArgs { + id: string; +} + +export interface CreateCollectionArgs { + name: string; + description?: string; + color?: string; + permission?: 'read' | 'read_write'; + sharing?: boolean; + icon?: string; + sort?: { field: string; direction: 'asc' | 'desc' }; +} + +export interface UpdateCollectionArgs { + id: string; + name?: string; + description?: string; + color?: string; + permission?: 'read' | 'read_write'; + sharing?: boolean; + icon?: string; + sort?: { field: string; direction: 'asc' | 'desc' }; +} + +// User specific types +export interface UserArgs extends PaginationArgs { + query?: string; + filter?: 'all' | 'admins' | 'members' | 'suspended' | 'invited'; +} + +export interface GetUserArgs { + id: string; +} + +export interface CreateUserArgs { + name: string; + email: string; + role?: 'admin' | 'member' | 'viewer'; +} + +export interface UpdateUserArgs { + id: string; + name?: string; + avatar_url?: string; + language?: string; +} + +// Group specific types +export interface GroupArgs extends PaginationArgs { + query?: string; +} + +export interface GetGroupArgs { + id: string; +} + +export interface CreateGroupArgs { + name: string; +} + +export interface UpdateGroupArgs { + id: string; + name: string; +} + +// Comment specific types +export interface CommentArgs extends PaginationArgs { + document_id?: string; + collection_id?: string; +} + +export interface GetCommentArgs { + id: string; +} + +export interface CreateCommentArgs { + document_id: string; + data: object; + parent_comment_id?: string; +} + +export interface UpdateCommentArgs { + id: string; + data: object; +} + +// Share specific types +export interface ShareArgs extends PaginationArgs { + document_id?: string; +} + +export interface GetShareArgs { + id?: string; + document_id?: string; +} + +export interface CreateShareArgs { + document_id: string; + published?: boolean; + include_child_documents?: boolean; + url_id?: string; +} + +export interface UpdateShareArgs { + id: string; + published?: boolean; + include_child_documents?: boolean; +} + +// Revision specific types +export interface RevisionArgs extends PaginationArgs { + document_id: string; +} + +export interface GetRevisionArgs { + id: string; +} + +// Event specific types +export interface EventArgs extends PaginationArgs, DateRangeArgs { + name?: string; + actor_id?: string; + document_id?: string; + collection_id?: string; + audit_log?: boolean; +} + +// Attachment specific types +export interface CreateAttachmentArgs { + name: string; + document_id?: string; + content_type: string; + size: number; +} + +export interface GetAttachmentArgs { + id: string; +} + +// File Operation specific types +export interface FileOperationArgs extends PaginationArgs { + type?: 'import' | 'export'; +} + +export interface GetFileOperationArgs { + id: string; +} + +// OAuth Client specific types +export interface OAuthClientArgs extends PaginationArgs {} + +export interface GetOAuthClientArgs { + id: string; +} + +export interface CreateOAuthClientArgs { + name: string; + redirect_uris: string[]; + description?: string; +} + +export interface UpdateOAuthClientArgs { + id: string; + name?: string; + redirect_uris?: string[]; + description?: string; +} + +// Membership specific types +export interface MembershipArgs extends PaginationArgs { + query?: string; + permission?: 'read' | 'read_write' | 'admin'; +} + +export interface AddMemberArgs { + id: string; + user_id: string; + permission?: 'read' | 'read_write' | 'admin'; +} + +export interface RemoveMemberArgs { + id: string; + user_id: string; +} + +export interface AddGroupMemberArgs { + id: string; + group_id: string; + permission?: 'read' | 'read_write' | 'admin'; +} + +export interface RemoveGroupMemberArgs { + id: string; + group_id: string; +} + +// Helper type to ensure type safety +export type ToolHandler = (args: TArgs, pgClient: Pool) => Promise; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..70ac657 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,7 @@ +/** + * MCP Outline PostgreSQL - Utils Index + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +export * from './logger.js'; +export * from './security.js'; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..affeea6 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,90 @@ +/** + * MCP Outline PostgreSQL - Logger + * Optimized for MCP - reduce logs to not exhaust Claude context + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +type LogLevel = 'error' | 'warn' | 'info' | 'debug'; + +interface LogEntry { + timestamp: string; + level: LogLevel; + message: string; + data?: Record; +} + +const LOG_LEVELS: Record = { + error: 0, + warn: 1, + info: 2, + debug: 3 +}; + +class Logger { + private level: LogLevel; + + constructor() { + this.level = (process.env.LOG_LEVEL as LogLevel) || 'error'; + } + + private shouldLog(level: LogLevel): boolean { + return LOG_LEVELS[level] <= LOG_LEVELS[this.level]; + } + + private formatLog(level: LogLevel, message: string, data?: Record): string { + const entry: LogEntry = { + timestamp: new Date().toISOString(), + level, + message, + ...(data && { data }) + }; + return JSON.stringify(entry); + } + + private write(level: LogLevel, message: string, data?: Record): void { + if (!this.shouldLog(level)) return; + + const formatted = this.formatLog(level, message, data); + + // For MCP, send logs to stderr + if (process.env.MCP_MODE !== 'false') { + process.stderr.write(formatted + '\n'); + } else { + console.log(formatted); + } + } + + error(message: string, data?: Record): void { + this.write('error', message, data); + } + + warn(message: string, data?: Record): void { + this.write('warn', message, data); + } + + info(message: string, data?: Record): void { + this.write('info', message, data); + } + + debug(message: string, data?: Record): void { + this.write('debug', message, data); + } +} + +export const logger = new Logger(); + +// Log queries for auditing (if enabled) - OPTIMIZED +export function logQuery( + sql: string, + _params?: any[], + duration?: number, + _clientId?: string +): void { + // DISABLED by default to save Claude context + if (process.env.ENABLE_AUDIT_LOG === 'true' && process.env.NODE_ENV !== 'production') { + logger.debug('SQL', { + sql: sql.substring(0, 50), + duration + }); + } +} diff --git a/src/utils/security.ts b/src/utils/security.ts new file mode 100644 index 0000000..b06180f --- /dev/null +++ b/src/utils/security.ts @@ -0,0 +1,115 @@ +/** + * MCP Outline PostgreSQL - Security Utilities + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ + +// Rate limiting store +const rateLimitStore: Map = new Map(); + +// Rate limit configuration +const RATE_LIMIT_WINDOW = 60000; // 1 minute +const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '100', 10); + +/** + * Check if a request should be rate limited + */ +export function checkRateLimit(type: string, clientId: string): boolean { + const key = `${type}:${clientId}`; + const now = Date.now(); + const entry = rateLimitStore.get(key); + + if (!entry || now > entry.resetAt) { + rateLimitStore.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW }); + return true; + } + + if (entry.count >= RATE_LIMIT_MAX) { + return false; + } + + entry.count++; + return true; +} + +/** + * Sanitize SQL input to prevent injection + * Note: Always use parameterized queries, this is a secondary safety measure + */ +export function sanitizeInput(input: string): string { + if (typeof input !== 'string') return input; + + // Remove null bytes + let sanitized = input.replace(/\0/g, ''); + + // Trim whitespace + sanitized = sanitized.trim(); + + return sanitized; +} + +/** + * Validate UUID format + */ +export function isValidUUID(uuid: string): boolean { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} + +/** + * Validate URL ID format (Outline uses URL-safe IDs) + */ +export function isValidUrlId(urlId: string): boolean { + const urlIdRegex = /^[a-zA-Z0-9_-]+$/; + return urlIdRegex.test(urlId); +} + +/** + * Validate email format + */ +export function isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +/** + * Escape HTML entities for safe display + */ +export function escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, (char) => map[char]); +} + +/** + * Validate pagination parameters + */ +export function validatePagination(limit?: number, offset?: number): { limit: number; offset: number } { + const maxLimit = 100; + const defaultLimit = 25; + + return { + limit: Math.min(Math.max(1, limit || defaultLimit), maxLimit), + offset: Math.max(0, offset || 0) + }; +} + +/** + * Validate sort direction + */ +export function validateSortDirection(direction?: string): 'ASC' | 'DESC' { + const upper = (direction || 'DESC').toUpperCase(); + return upper === 'ASC' ? 'ASC' : 'DESC'; +} + +/** + * Validate sort field against allowed fields + */ +export function validateSortField(field: string | undefined, allowedFields: string[], defaultField: string): string { + if (!field) return defaultField; + return allowedFields.includes(field) ? field : defaultField; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9424ecd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +}