anujjoshi3105 commited on
Commit
3d23b0f
·
1 Parent(s): 6b09394

first commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +9 -0
  2. .gitattributes +0 -35
  3. .gitignore +38 -0
  4. Dockerfile +42 -0
  5. README.md +584 -7
  6. docker-compose.yml +17 -0
  7. docs/ARCHITECTURE.md +67 -0
  8. package.json +32 -0
  9. pnpm-lock.yaml +1936 -0
  10. src/app.ts +99 -0
  11. src/config/env.ts +11 -0
  12. src/modules/atcoder/constants.ts +28 -0
  13. src/modules/atcoder/handlers.ts +48 -0
  14. src/modules/atcoder/index.ts +2 -0
  15. src/modules/atcoder/provider.ts +109 -0
  16. src/modules/atcoder/routes.ts +51 -0
  17. src/modules/atcoder/schemas.ts +143 -0
  18. src/modules/atcoder/service.ts +63 -0
  19. src/modules/atcoder/types.ts +95 -0
  20. src/modules/codechef/constants.ts +16 -0
  21. src/modules/codechef/handlers.ts +12 -0
  22. src/modules/codechef/index.ts +2 -0
  23. src/modules/codechef/provider.ts +40 -0
  24. src/modules/codechef/routes.ts +18 -0
  25. src/modules/codechef/schemas.ts +23 -0
  26. src/modules/codechef/service.ts +20 -0
  27. src/modules/codechef/types.ts +15 -0
  28. src/modules/codeforces/handlers.ts +160 -0
  29. src/modules/codeforces/index.ts +2 -0
  30. src/modules/codeforces/provider.ts +221 -0
  31. src/modules/codeforces/routes.ts +98 -0
  32. src/modules/codeforces/schemas.ts +347 -0
  33. src/modules/codeforces/service.ts +217 -0
  34. src/modules/codeforces/types.ts +189 -0
  35. src/modules/gfg/constants.ts +15 -0
  36. src/modules/gfg/handlers.ts +52 -0
  37. src/modules/gfg/index.ts +2 -0
  38. src/modules/gfg/provider.ts +95 -0
  39. src/modules/gfg/routes.ts +58 -0
  40. src/modules/gfg/schemas.ts +87 -0
  41. src/modules/gfg/service.ts +61 -0
  42. src/modules/gfg/types.ts +78 -0
  43. src/modules/leetcode/constants.ts +18 -0
  44. src/modules/leetcode/handlers/contest.ts +36 -0
  45. src/modules/leetcode/handlers/discussion.ts +30 -0
  46. src/modules/leetcode/handlers/index.ts +4 -0
  47. src/modules/leetcode/handlers/problem.ts +37 -0
  48. src/modules/leetcode/handlers/user.ts +123 -0
  49. src/modules/leetcode/index.ts +2 -0
  50. src/modules/leetcode/provider.ts +177 -0
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .git
4
+ .env
5
+ .gitignore
6
+ .dockerignore
7
+ logs
8
+ tmp
9
+ coverage
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+ .yarn/install-state.gz
8
+
9
+ # testing
10
+ /coverage
11
+
12
+ # next.js
13
+ /.next/
14
+ /out/
15
+
16
+ # production
17
+ /build
18
+ /dist
19
+
20
+ # misc
21
+ .DS_Store
22
+ *.pem
23
+
24
+ # debug
25
+ npm-debug.log*
26
+ yarn-debug.log*
27
+ yarn-error.log*
28
+ pnpm-debug.log*
29
+
30
+ # local env files
31
+ .env*.local
32
+ .env*
33
+ # vercel
34
+ .vercel
35
+
36
+ # typescript
37
+ *.tsbuildinfo
38
+ next-env.d.ts
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build
2
+ FROM node:20-alpine AS builder
3
+
4
+ # Install pnpm
5
+ RUN npm install -g pnpm
6
+
7
+ WORKDIR /app
8
+
9
+ # Copy package files
10
+ COPY package.json pnpm-lock.yaml ./
11
+
12
+ # Install dependencies
13
+ RUN pnpm install --frozen-lockfile
14
+
15
+ # Copy source code
16
+ COPY . .
17
+
18
+ # Build the application
19
+ RUN pnpm run build
20
+
21
+ # Stage 2: Production
22
+ FROM node:20-alpine
23
+
24
+ # Install pnpm
25
+ RUN npm install -g pnpm
26
+
27
+ WORKDIR /app
28
+
29
+ # Copy package files
30
+ COPY package.json pnpm-lock.yaml ./
31
+
32
+ # Install only production dependencies
33
+ RUN pnpm install --prod --frozen-lockfile
34
+
35
+ # Copy built artifacts from builder stage
36
+ COPY --from=builder /app/dist ./dist
37
+
38
+ # Expose the application port
39
+ EXPOSE 3000
40
+
41
+ # Start the application
42
+ CMD ["pnpm", "start"]
README.md CHANGED
@@ -1,10 +1,587 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Vortex
3
- emoji: 🐠
4
- colorFrom: blue
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Vortex CP
2
+
3
+ A high-speed, vertical-slice aggregator for competitive programming metrics. Vortex CP unifies data from LeetCode, Codeforces, CodeChef, AtCoder, and GeeksforGeeks into a single, modular API layer. Built for modern web applications and AI agents via the **Model Context Protocol (MCP)**.
4
+
5
+ ---
6
+
7
+ ## Architecture
8
+
9
+ Vortex follows a **strict vertical slice architecture**. Each competitive programming platform is an isolated, self-contained plugin that flows through a consistent request pipeline.
10
+
11
+ ### Vortex Flow Diagram
12
+
13
+ ```mermaid
14
+ graph TD
15
+ A[HTTP Request] --> B[Fastify Router]
16
+ B --> C{Route Match}
17
+
18
+ C -->|/api/v1/leetcode| D1[LeetCode Handler]
19
+ C -->|/api/v1/codeforces| D2[Codeforces Handler]
20
+ C -->|/api/v1/codechef| D3[CodeChef Handler]
21
+ C -->|/api/v1/atcoder| D4[AtCoder Handler]
22
+ C -->|/api/v1/gfg| D5[GFG Handler]
23
+ C -->|/api/v1/ratings| D6[Aggregator Handler]
24
+
25
+ D1 --> E1[LeetCode Service]
26
+ D2 --> E2[Codeforces Service]
27
+ D3 --> E3[CodeChef Service]
28
+ D4 --> E4[AtCoder Service]
29
+ D5 --> E5[GFG Service]
30
+ D6 --> E6[Aggregator Service]
31
+
32
+ E1 --> F1[LeetCode Provider]
33
+ E2 --> F2[Codeforces Provider]
34
+ E3 --> F3[CodeChef Provider]
35
+ E4 --> F4[AtCoder Provider]
36
+ E5 --> F5[GFG Provider]
37
+ E6 --> F6[Multi-Platform Provider]
38
+
39
+ F1 --> G1[LeetCode GraphQL API]
40
+ F2 --> G2[Codeforces REST API]
41
+ F3 --> G3[CodeChef REST API]
42
+ F4 --> G4[AtCoder REST API]
43
+ F5 --> G5[GFG Scraper]
44
+
45
+ G1 --> H[Response]
46
+ G2 --> H
47
+ G3 --> H
48
+ G4 --> H
49
+ G5 --> H
50
+
51
+ H --> I[JSON Response]
52
+
53
+ style D1 fill:#e3f2fd
54
+ style D2 fill:#e3f2fd
55
+ style D3 fill:#e3f2fd
56
+ style D4 fill:#e3f2fd
57
+ style D5 fill:#e3f2fd
58
+ style D6 fill:#fff3e0
59
+ style E6 fill:#fff3e0
60
+ style F6 fill:#fff3e0
61
+ ```
62
+
63
+ ### Vertical Slice Philosophy
64
+
65
+ Each platform module (`src/modules/{platform}/`) is a **complete vertical slice**:
66
+
67
+ | Layer | Responsibility | Dependencies |
68
+ |-------|---------------|--------------|
69
+ | **routes.ts** | Fastify plugin registration, OpenAPI schemas, endpoint definitions | None (Entry Point) |
70
+ | **handlers.ts** | HTTP request/response handling, parameter extraction, validation | Service layer |
71
+ | **service.ts** | Business logic, data transformation, error handling | Provider layer |
72
+ | **provider.ts** | External API integration, HTTP client configuration | External APIs only |
73
+ | **types.ts** | TypeScript interfaces for domain models | None |
74
+ | **schemas.ts** | JSON Schema for request/response validation | None |
75
+
76
+ **Key Constraint:** Each layer can only depend on layers below it. No cross-module dependencies are allowed except through the aggregator service.
77
+
78
  ---
79
+
80
+ ## Project Structure
81
+
82
+ ```
83
+ src/
84
+ ├── app.ts # Fastify app configuration & plugin registration
85
+ ├── server.ts # HTTP server bootstrap
86
+ ├── config/
87
+ │ └── env.ts # Environment variable validation
88
+ ├── shared/
89
+ │ ├── middlewares/
90
+ │ │ └── validate.ts # Global validation middleware
91
+ │ └── utils/
92
+ │ ├── http-client.ts # Axios instance with timeout & retry
93
+ │ └── timeout.ts # Request timeout utilities
94
+ ├── types/
95
+ │ ├── api.ts # Common API response types
96
+ │ └── fastify.ts # Fastify type augmentation
97
+ └── modules/
98
+ ├── leetcode/ # LeetCode vertical slice
99
+ ├── codeforces/ # Codeforces vertical slice
100
+ ├── codechef/ # CodeChef vertical slice
101
+ ├── atcoder/ # AtCoder vertical slice
102
+ ├── gfg/ # GeeksforGeeks vertical slice
103
+ ├── ratings/ # Cross-platform aggregator
104
+ └── mcp/ # Model Context Protocol server
105
+ ```
106
+
107
  ---
108
 
109
+ ## Installation
110
+
111
+ ### Prerequisites
112
+
113
+ - **Node.js** 18+ (LTS recommended)
114
+ - **pnpm** 8+ (install via `npm i -g pnpm`)
115
+
116
+ ### Setup
117
+
118
+ ```bash
119
+ # Clone repository
120
+ git clone https://github.com/Anujjoshi3105/vortex.git
121
+ cd vortex
122
+
123
+ # Install dependencies
124
+ pnpm install
125
+
126
+ # Configure environment
127
+ cp .env.example .env
128
+ # Edit .env with your configuration
129
+ ```
130
+
131
+ #### `.env.example`
132
+
133
+ ```env
134
+ # Server Configuration
135
+ PORT=3000
136
+ HOST=0.0.0.0
137
+ NODE_ENV=development
138
+
139
+ # Security (optional)
140
+ AUTH_SECRET=your_auth_secret_here
141
+ ```
142
+
143
+ ### Development
144
+
145
+ ```bash
146
+ # Start dev server with hot-reload
147
+ pnpm dev
148
+ ```
149
+
150
+ Server starts at `http://localhost:3000`
151
+
152
+ API documentation: `http://localhost:3000/docs`
153
+
154
+ ### Production Build
155
+
156
+ ```bash
157
+ # Compile TypeScript
158
+ pnpm build
159
+
160
+ # Start production server
161
+ pnpm start
162
+ ```
163
+
164
+ ### MCP Inspector (AI Tool Testing)
165
+
166
+ ```bash
167
+ # Launch MCP Inspector UI
168
+ pnpm inspect
169
+ ```
170
+
171
+ Opens at `http://localhost:6274` for interactive MCP tool testing.
172
+
173
+ ---
174
+
175
+ ## API Reference
176
+
177
+ ### Platform-Specific Endpoints
178
+
179
+ | Platform | Endpoint | Description |
180
+ |----------|----------|-------------|
181
+ | **LeetCode** | `GET /api/v1/leetcode/rating?username={user}` | User contest rating & ranking |
182
+ | | `GET /api/v1/leetcode/contest-ranking?username={user}` | Contest participation history |
183
+ | | `GET /api/v1/leetcode/daily-problem` | Today's daily challenge |
184
+ | | `GET /api/v1/leetcode/contests` | Upcoming contests |
185
+ | **Codeforces** | `GET /api/v1/codeforces/rating?username={user}` | Current rating & rank |
186
+ | | `GET /api/v1/codeforces/contest-history?username={user}` | Rating change graph |
187
+ | | `GET /api/v1/codeforces/status?username={user}&from=1&count=10` | Recent submissions |
188
+ | | `GET /api/v1/codeforces/solved-problems?username={user}` | Unique solved problems |
189
+ | **CodeChef** | `GET /api/v1/codechef/rating?username={user}` | Current rating & stars |
190
+ | **AtCoder** | `GET /api/v1/atcoder/rating?username={user}` | Current rating & color |
191
+ | **GeeksforGeeks** | `GET /api/v1/gfg/rating?username={user}` | Overall score & rank |
192
+
193
+ ### Aggregator Endpoint
194
+
195
+ ```http
196
+ GET /api/v1/ratings?username={user}
197
+ ```
198
+
199
+ Returns unified ratings from **all platforms** in a single response:
200
+
201
+ ```json
202
+ {
203
+ "username": "tourist",
204
+ "platforms": {
205
+ "leetcode": { "rating": 3200, "rank": "Knight", ... },
206
+ "codeforces": { "rating": 3821, "rank": "Legendary Grandmaster", ... },
207
+ "codechef": { "rating": 2800, "stars": "7★", ... },
208
+ "atcoder": { "rating": 3817, "color": "red", ... },
209
+ "gfg": { "score": 9500, "rank": 1, ... }
210
+ }
211
+ }
212
+ ```
213
+
214
+ ### Health Check
215
+
216
+ ```http
217
+ GET /health
218
+ ```
219
+
220
+ Returns `{ "status": "ok" }` for uptime monitoring.
221
+
222
+ ---
223
+
224
+ ## Model Context Protocol (MCP)
225
+
226
+ Vortex exposes its functionality as **MCP tools** for AI agents (e.g., Claude Desktop, LangChain agents).
227
+
228
+ ### Connecting to Claude Desktop
229
+
230
+ Add to your `claude_desktop_config.json`:
231
+
232
+ ```json
233
+ {
234
+ "mcpServers": {
235
+ "vortex-cp": {
236
+ "command": "node",
237
+ "args": [
238
+ "path/to/vortex/dist/server.js"
239
+ ],
240
+ "env": {
241
+ "PORT": "3000"
242
+ }
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ Restart Claude Desktop.
249
+
250
+ ### MCP Endpoints (HTTP/SSE)
251
+
252
+ - **SSE Transport**: `GET /mcp/sse`
253
+ - **Message Handler**: `POST /mcp/messages?sessionId={id}`
254
+
255
+ Use the MCP Inspector (`pnpm inspect`) to test tools interactively before integrating with agents.
256
+
257
+ ---
258
+
259
+ ## Contributors Guide
260
+
261
+ ### Adding a New Platform
262
+
263
+ To maintain **vertical slice integrity**, follow this checklist when adding a new platform (e.g., HackerRank):
264
+
265
+ #### 1. Create Module Directory
266
+
267
+ ```bash
268
+ mkdir -p src/modules/hackerrank
269
+ cd src/modules/hackerrank
270
+ touch index.ts routes.ts handlers.ts service.ts provider.ts types.ts schemas.ts
271
+ ```
272
+
273
+ #### 2. Define Types (`types.ts`)
274
+
275
+ ```typescript
276
+ export interface HackerRankRating {
277
+ username: string;
278
+ rating: number;
279
+ rank: string;
280
+ solvedCount: number;
281
+ }
282
+ ```
283
+
284
+ #### 3. Implement Provider (`provider.ts`)
285
+
286
+ **Responsibility:** External API integration only. No business logic.
287
+
288
+ ```typescript
289
+ import axios from 'axios';
290
+ import { HackerRankRating } from './types';
291
+
292
+ export async function fetchUserRating(username: string): Promise<HackerRankRating> {
293
+ const { data } = await axios.get(`https://api.hackerrank.com/users/${username}`);
294
+ return {
295
+ username: data.username,
296
+ rating: data.rating,
297
+ rank: data.rank,
298
+ solvedCount: data.challenges_solved
299
+ };
300
+ }
301
+ ```
302
+
303
+ #### 4. Implement Service (`service.ts`)
304
+
305
+ **Responsibility:** Business logic, data transformation, error handling.
306
+
307
+ ```typescript
308
+ import * as provider from './provider';
309
+ import { HackerRankRating } from './types';
310
+
311
+ export async function getUserRating(username: string): Promise<HackerRankRating> {
312
+ if (!username || username.length < 3) {
313
+ throw new Error('Invalid username');
314
+ }
315
+
316
+ const data = await provider.fetchUserRating(username);
317
+
318
+ // Apply business rules (e.g., normalize rank)
319
+ return {
320
+ ...data,
321
+ rank: data.rank.toUpperCase()
322
+ };
323
+ }
324
+ ```
325
+
326
+ #### 5. Implement Handlers (`handlers.ts`)
327
+
328
+ **Responsibility:** HTTP request/response handling, parameter extraction.
329
+
330
+ ```typescript
331
+ import { FastifyRequest, FastifyReply } from 'fastify';
332
+ import * as service from './service';
333
+
334
+ interface RatingQuery {
335
+ username: string;
336
+ }
337
+
338
+ export async function getUserRatingHandler(
339
+ request: FastifyRequest<{ Querystring: RatingQuery }>,
340
+ reply: FastifyReply
341
+ ) {
342
+ const { username } = request.query;
343
+
344
+ try {
345
+ const data = await service.getUserRating(username);
346
+ reply.send({ success: true, data });
347
+ } catch (error) {
348
+ reply.status(500).send({ success: false, error: error.message });
349
+ }
350
+ }
351
+ ```
352
+
353
+ #### 6. Define Schemas (`schemas.ts`)
354
+
355
+ **Responsibility:** OpenAPI/JSON Schema for validation and documentation.
356
+
357
+ ```typescript
358
+ export const ratingQuerySchema = {
359
+ type: 'object',
360
+ required: ['username'],
361
+ properties: {
362
+ username: { type: 'string', minLength: 3 }
363
+ }
364
+ };
365
+
366
+ export const ratingResponseSchema = {
367
+ type: 'object',
368
+ properties: {
369
+ success: { type: 'boolean' },
370
+ data: {
371
+ type: 'object',
372
+ properties: {
373
+ username: { type: 'string' },
374
+ rating: { type: 'number' },
375
+ rank: { type: 'string' },
376
+ solvedCount: { type: 'number' }
377
+ }
378
+ }
379
+ }
380
+ };
381
+ ```
382
+
383
+ #### 7. Create Routes Plugin (`routes.ts`)
384
+
385
+ **Responsibility:** Fastify plugin registration, OpenAPI tags.
386
+
387
+ ```typescript
388
+ import { FastifyPluginAsync } from 'fastify';
389
+ import * as handlers from './handlers';
390
+ import * as schemas from './schemas';
391
+
392
+ export const hackerrankPlugin: FastifyPluginAsync = async (fastify) => {
393
+ fastify.get('/rating', {
394
+ schema: {
395
+ tags: ['HackerRank'],
396
+ description: 'Get HackerRank user rating',
397
+ querystring: schemas.ratingQuerySchema,
398
+ response: { 200: schemas.ratingResponseSchema }
399
+ }
400
+ }, handlers.getUserRatingHandler);
401
+ };
402
+
403
+ export default hackerrankPlugin;
404
+ ```
405
+
406
+ #### 8. Barrel Export (`index.ts`)
407
+
408
+ ```typescript
409
+ export { hackerrankPlugin } from './routes';
410
+ export * from './types';
411
+ ```
412
+
413
+ #### 9. Register in App (`src/app.ts`)
414
+
415
+ ```typescript
416
+ import { hackerrankPlugin } from './modules/hackerrank';
417
+
418
+ // Inside buildApp()
419
+ await fastify.register(hackerrankPlugin, { prefix: '/api/v1/hackerrank' });
420
+ ```
421
+
422
+ #### 10. Update OpenAPI Tags (`src/app.ts`)
423
+
424
+ ```typescript
425
+ tags: [
426
+ // ... existing tags
427
+ { name: 'HackerRank', description: 'HackerRank platform integration' }
428
+ ]
429
+ ```
430
+
431
+ **Result:** Your new platform is now a self-contained vertical slice with zero coupling to other modules.
432
+
433
+ ---
434
+
435
+ ## System Auditor Review: Future Resilience Checklist
436
+
437
+ The following improvements are recommended for production-grade deployments at scale.
438
+
439
+ ### Resiliency
440
+
441
+ | Feature | Status | Priority | Effort |
442
+ |---------|--------|----------|--------|
443
+ | **Circuit Breaker** | ❌ Not Implemented | High | Medium |
444
+ | └─ Prevent cascading failures when external APIs are down | | | |
445
+ | └─ Recommended: [opossum](https://www.npmjs.com/package/opossum) | | | |
446
+ | **Retry Logic** | ⚠️ Partial (Axios defaults) | Medium | Low |
447
+ | └─ Exponential backoff for transient failures | | | |
448
+ | └─ Recommended: [axios-retry](https://www.npmjs.com/package/axios-retry) | | | |
449
+ | **Fallback Responses** | ❌ Not Implemented | Medium | Low |
450
+ | └─ Return cached/stale data when APIs are unreachable | | | |
451
+
452
+ ### Performance
453
+
454
+ | Feature | Status | Priority | Effort |
455
+ |---------|--------|----------|--------|
456
+ | **Redis Caching** | ❌ Not Implemented | High | Medium |
457
+ | └─ Cache platform APIs (TTL: 5-15 minutes) | | | |
458
+ | └─ Recommended: [@fastify/redis](https://github.com/fastify/fastify-redis) | | | |
459
+ | **Response Compression** | ❌ Not Implemented | Medium | Low |
460
+ | └─ Compress JSON responses > 1KB | | | |
461
+ | └─ Recommended: [@fastify/compress](https://github.com/fastify/fastify-compress) | | | |
462
+ | **Request Timeouts** | ✅ Implemented | - | - |
463
+ | └─ Per-provider timeout configuration exists | | | |
464
+
465
+ ### Security
466
+
467
+ | Feature | Status | Priority | Effort |
468
+ |---------|--------|----------|--------|
469
+ | **Rate Limiting** | ❌ Not Implemented | High | Low |
470
+ | └─ Prevent abuse (e.g., 100 req/min per IP) | | | |
471
+ | └─ Recommended: [@fastify/rate-limit](https://github.com/fastify/fastify-rate-limit) | | | |
472
+ | **API Key Middleware** | ⚠️ Partial (AUTH_SECRET unused) | Medium | Low |
473
+ | └─ Optional authentication for public deployments | | | |
474
+ | **Input Sanitization** | ✅ Implemented | - | - |
475
+ | └─ JSON Schema validation active on all endpoints | | | |
476
+ | **CORS Restrictions** | ⚠️ Too Permissive | Medium | Low |
477
+ | └─ Currently allows `origin: '*'` - restrict in production | | | |
478
+
479
+ ### Observability
480
+
481
+ | Feature | Status | Priority | Effort |
482
+ |---------|--------|----------|--------|
483
+ | **Structured Logging** | ⚠️ Basic (Fastify Logger) | High | Medium |
484
+ | └─ Migrate to [Pino](https://github.com/pinojs/pino) with JSON output | | | |
485
+ | └─ Add request IDs, trace context | | | |
486
+ | **OpenTelemetry** | ❌ Not Implemented | Medium | High |
487
+ | └─ Distributed tracing for multi-provider requests | | | |
488
+ | └─ Recommended: [@opentelemetry/auto-instrumentations-node](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) | | | |
489
+ | **Metrics Endpoint** | ❌ Not Implemented | Medium | Medium |
490
+ | └─ Expose Prometheus metrics (`/metrics`) | | | |
491
+ | └─ Track: request latency, error rates, cache hit rates | | | |
492
+
493
+ ### Implementation Roadmap
494
+
495
+ **Phase 1: Resiliency** (Sprint 1-2)
496
+ 1. Add circuit breaker pattern to all providers
497
+ 2. Implement Redis caching layer
498
+ 3. Add rate limiting middleware
499
+
500
+ **Phase 2: Security** (Sprint 3)
501
+ 1. Restrict CORS to whitelisted origins
502
+ 2. Implement API key authentication (optional)
503
+ 3. Add request/response sanitization audit
504
+
505
+ **Phase 3: Observability** (Sprint 4-5)
506
+ 1. Migrate to Pino structured logging
507
+ 2. Add OpenTelemetry instrumentation
508
+ 3. Create Prometheus metrics endpoint
509
+ 4. Build Grafana dashboards
510
+
511
+ ---
512
+
513
+ ## Testing
514
+
515
+ ### Manual Testing
516
+
517
+ ```bash
518
+ # Test single platform
519
+ curl "http://localhost:3000/api/v1/leetcode/rating?username=tourist"
520
+
521
+ # Test aggregator
522
+ curl "http://localhost:3000/api/v1/ratings?username=tourist"
523
+ ```
524
+
525
+ ### MCP Tool Testing
526
+
527
+ ```bash
528
+ # Launch MCP Inspector
529
+ pnpm inspect
530
+
531
+ # Test in MCP Inspector UI at http://localhost:6274
532
+ # Select tool: get_leetcode_user_rating
533
+ # Input: { "username": "tourist" }
534
+ ```
535
+
536
+ ---
537
+
538
+ ## Technology Stack
539
+
540
+ | Category | Package | Purpose |
541
+ |----------|---------|---------|
542
+ | **Runtime** | Node.js 18+ | JavaScript runtime |
543
+ | **Framework** | Fastify 5.x | High-performance HTTP server |
544
+ | **Language** | TypeScript 5.x | Type-safe development |
545
+ | **HTTP Client** | Axios 1.x | External API requests |
546
+ | **Validation** | Zod 4.x | Runtime type validation |
547
+ | **Scraping** | Cheerio 1.x | HTML parsing (GFG) |
548
+ | **Documentation** | @fastify/swagger | OpenAPI generation |
549
+ | **AI Protocol** | @modelcontextprotocol/sdk | MCP server implementation |
550
+ | **Package Manager** | pnpm 8+ | Fast, disk-efficient installs |
551
+
552
+ ---
553
+
554
+ ## Contributing
555
+
556
+ ### Guidelines
557
+
558
+ 1. **Maintain Vertical Slices**: New features must follow the handler → service → provider pattern.
559
+ 2. **Type Everything**: All functions must have explicit TypeScript types.
560
+ 3. **Schema Validation**: Add JSON Schema for all new endpoints.
561
+ 4. **No Cross-Module Imports**: Modules cannot import from other platform modules (use the aggregator pattern).
562
+ 5. **Update OpenAPI**: Add tags and descriptions for all new routes.
563
+
564
+ ### Pull Request Checklist
565
+
566
+ - [ ] New vertical slice follows project structure
567
+ - [ ] TypeScript types defined in `types.ts`
568
+ - [ ] JSON Schema validation in `schemas.ts`
569
+ - [ ] OpenAPI documentation updated
570
+ - [ ] No cross-module dependencies
571
+ - [ ] Tested via Swagger UI and MCP Inspector
572
+
573
+ ---
574
+
575
+ ## License
576
+
577
+ **ISC** - See [LICENSE](LICENSE) for details.
578
+
579
+ ---
580
+
581
+ ## Project Status
582
+
583
+ **Current Version:** 1.0.0
584
+ **Deployment:** Development (not production-ready - see Auditor Review)
585
+ **Maintained By:** [@Anujjoshi3105](https://github.com/Anujjoshi3105)
586
+
587
+ For issues or feature requests, open a GitHub issue at [Anujjoshi3105/vortex](https://github.com/Anujjoshi3105/vortex).
docker-compose.yml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ api:
5
+ build: .
6
+ ports:
7
+ - "${PORT:-3000}:${PORT:-3000}"
8
+ env_file:
9
+ - .env
10
+ environment:
11
+ - NODE_ENV=production
12
+ restart: unless-stopped
13
+ healthcheck:
14
+ test: [ "CMD", "wget", "-qO-", "http://localhost:${PORT:-3000}/health" ]
15
+ interval: 30s
16
+ timeout: 10s
17
+ retries: 3
docs/ARCHITECTURE.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture Design
2
+
3
+ ## Overview
4
+
5
+ Vortex is built using a **Feature-Based Plugin Architecture** with **Vertical Slices**. This design ensures that each platform integration (LeetCode, Codeforces, etc.) is self-contained, highly modular, and easy to maintain.
6
+
7
+ ## Modular Structure
8
+
9
+ Each platform and the AI interface is implemented as a Fastify plugin located in `src/modules/{module_name}/`. This allows for:
10
+ - **Isolation**: Changes in one platform don't affect others.
11
+ - **Scalability**: Easy to add or remove platforms.
12
+ - **Consistency**: Every module follows the same structure and design patterns.
13
+ - **AI Integration**: Dedicated MCP module for real-time tool access for AI agents.
14
+
15
+
16
+ ## Vertical Slices
17
+
18
+ Unlike traditional horizontal layering (where all controllers are in one folder, all services in another), we use vertical slices. All code related to a specific feature (like LeetCode) lives in one directory.
19
+
20
+ ### Layered Responsibilities
21
+
22
+ Inside each module, we follow a strict layering pattern:
23
+
24
+ 1. **Plugin Entry (`index.ts`)**: Exports the Fastify plugin.
25
+ 2. **Routes (`routes.ts`)**: Defines endpoints, registers schemas, and assigns handlers.
26
+ 3. **Handlers (`handlers/`)**: Extracts data from requests (params, query, body), calls services, and sends responses.
27
+ 4. **Services (`services/`)**: Orchestrates business logic. Pure TypeScript, no Fastify dependencies.
28
+ 5. **Provider (`provider.ts`)**: Handles external communication (HTTP, GraphQL, Scraping).
29
+ 6. **Schemas (`schemas.ts`)**: JSON schemas for request/response validation (also used for Swagger).
30
+ 7. **Types (`types.ts`)**: TypeScript interfaces for data structures.
31
+
32
+ ### MCP Module Structure
33
+
34
+ The `src/modules/mcp/` module follows a slightly different structure to support the Model Context Protocol:
35
+ - **`tools/`**: Implementation of AI-callable tools.
36
+ - **`prompts/`**: Pre-defined prompts for LLM guidance.
37
+ - **`resources/`**: Static or dynamic data resources exposed to agents.
38
+ - **`index.ts`**: Plugin registration using `fastify-mcp`.
39
+
40
+
41
+
42
+ ## Data Flow
43
+
44
+ ```mermaid
45
+ graph TD
46
+ User([User]) -->|HTTP Request| Server[Fastify Server]
47
+ Server -->|Routes| Handler[Handler Layer]
48
+ Handler -->|Params/Query| Service[Service Layer]
49
+ Service -->|Business Logic| Provider[Provider Layer]
50
+ Provider -->|Fetch| ExternalAPI[(External API / Web)]
51
+ ExternalAPI -->|Data| Provider
52
+ Provider -->|Raw Data| Service
53
+ Service -->|Model/Format| Handler
54
+ Handler -->|JSON Response| User
55
+ ```
56
+
57
+ ## Shared Infrastructure
58
+
59
+ - **`src/shared/`**: Common utilities, midlewares, and HTTP clients used across modules.
60
+ - **`src/config/`**: Environment variable management using `fastify-env`.
61
+ - **`src/types/`**: Global type definitions and Fastify type augmentation.
62
+
63
+ ## Performance & Validation
64
+
65
+ - **JSON Schema**: Every route uses high-performance JSON schema validation.
66
+ - **Serialization**: Fastify uses `fast-json-stringify` for lightning-fast responses based on our schemas.
67
+ - **TypeScript**: Full end-to-end type safety from provider to handler.
package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "vortex",
3
+ "version": "1.0.0",
4
+ "main": "dist/server.js",
5
+ "scripts": {
6
+ "dev": "nodemon src/server.ts",
7
+ "start": "node dist/server.js",
8
+ "build": "tsc",
9
+ "inspect": "npx @modelcontextprotocol/inspector"
10
+ },
11
+ "author": "",
12
+ "license": "ISC",
13
+ "description": "API for contest-related operations",
14
+ "dependencies": {
15
+ "@fastify/cors": "^11.2.0",
16
+ "@fastify/swagger": "^9.6.1",
17
+ "@fastify/swagger-ui": "^5.2.4",
18
+ "@modelcontextprotocol/sdk": "^1.25.3",
19
+ "axios": "^1.7.9",
20
+ "cheerio": "^1.0.0",
21
+ "dotenv": "^16.4.7",
22
+ "fastify": "^5.7.2",
23
+ "fastify-mcp": "^2.1.0",
24
+ "zod": "^4.3.6"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^25.0.10",
28
+ "nodemon": "^3.1.7",
29
+ "ts-node": "^10.9.2",
30
+ "typescript": "^5.9.3"
31
+ }
32
+ }
pnpm-lock.yaml ADDED
@@ -0,0 +1,1936 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ dependencies:
11
+ '@fastify/cors':
12
+ specifier: ^11.2.0
13
+ version: 11.2.0
14
+ '@fastify/env':
15
+ specifier: ^5.0.3
16
+ version: 5.0.3
17
+ '@fastify/formbody':
18
+ specifier: ^8.0.2
19
+ version: 8.0.2
20
+ '@fastify/swagger':
21
+ specifier: ^9.6.1
22
+ version: 9.6.1
23
+ '@fastify/swagger-ui':
24
+ specifier: ^5.2.4
25
+ version: 5.2.4
26
+ '@modelcontextprotocol/sdk':
27
+ specifier: ^1.25.3
28
+ version: 1.25.3(hono@4.11.6)(zod@4.3.6)
29
+ axios:
30
+ specifier: ^1.7.9
31
+ version: 1.13.3
32
+ cheerio:
33
+ specifier: ^1.0.0
34
+ version: 1.2.0
35
+ dotenv:
36
+ specifier: ^16.4.7
37
+ version: 16.6.1
38
+ fastify:
39
+ specifier: ^5.7.2
40
+ version: 5.7.2
41
+ fastify-mcp:
42
+ specifier: ^2.1.0
43
+ version: 2.1.0(hono@4.11.6)(zod@4.3.6)
44
+ zod:
45
+ specifier: ^4.3.6
46
+ version: 4.3.6
47
+ devDependencies:
48
+ '@types/node':
49
+ specifier: ^25.0.10
50
+ version: 25.0.10
51
+ nodemon:
52
+ specifier: ^3.1.7
53
+ version: 3.1.11
54
+ ts-node:
55
+ specifier: ^10.9.2
56
+ version: 10.9.2(@types/node@25.0.10)(typescript@5.9.3)
57
+ typescript:
58
+ specifier: ^5.9.3
59
+ version: 5.9.3
60
+
61
+ packages:
62
+
63
+ '@cspotcode/source-map-support@0.8.1':
64
+ resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
65
+ engines: {node: '>=12'}
66
+
67
+ '@fastify/accept-negotiator@2.0.1':
68
+ resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==}
69
+
70
+ '@fastify/ajv-compiler@4.0.5':
71
+ resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==}
72
+
73
+ '@fastify/cors@11.2.0':
74
+ resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==}
75
+
76
+ '@fastify/env@5.0.3':
77
+ resolution: {integrity: sha512-VqXKcw+keaZaCry9dDtphDQy6l+B1UOodk4q57NdIK/tjZsPMYEBTXjEDiZCAiD9KaGJXbJOMgYdgejU1iD0jA==}
78
+
79
+ '@fastify/error@4.2.0':
80
+ resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
81
+
82
+ '@fastify/fast-json-stringify-compiler@5.0.3':
83
+ resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==}
84
+
85
+ '@fastify/formbody@8.0.2':
86
+ resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==}
87
+
88
+ '@fastify/forwarded@3.0.1':
89
+ resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==}
90
+
91
+ '@fastify/merge-json-schemas@0.2.1':
92
+ resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
93
+
94
+ '@fastify/proxy-addr@5.1.0':
95
+ resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
96
+
97
+ '@fastify/send@4.1.0':
98
+ resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==}
99
+
100
+ '@fastify/static@9.0.0':
101
+ resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==}
102
+
103
+ '@fastify/swagger-ui@5.2.4':
104
+ resolution: {integrity: sha512-Maw8OYPUDxlOzKQd3VMv7T/fmjf2h6BWR3XHkhk3dD3rIfzO7C/UPnzGuTpOGMqw1HCUnctADBbeTNAzAwzUqA==}
105
+
106
+ '@fastify/swagger@9.6.1':
107
+ resolution: {integrity: sha512-fKlpJqFMWoi4H3EdUkDaMteEYRCfQMEkK0HJJ0eaf4aRlKd8cbq0pVkOfXDXmtvMTXYcnx3E+l023eFDBsA1HA==}
108
+
109
+ '@hono/node-server@1.19.9':
110
+ resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
111
+ engines: {node: '>=18.14.1'}
112
+ peerDependencies:
113
+ hono: ^4
114
+
115
+ '@isaacs/balanced-match@4.0.1':
116
+ resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
117
+ engines: {node: 20 || >=22}
118
+
119
+ '@isaacs/brace-expansion@5.0.0':
120
+ resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
121
+ engines: {node: 20 || >=22}
122
+
123
+ '@jridgewell/resolve-uri@3.1.2':
124
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
125
+ engines: {node: '>=6.0.0'}
126
+
127
+ '@jridgewell/sourcemap-codec@1.5.5':
128
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
129
+
130
+ '@jridgewell/trace-mapping@0.3.9':
131
+ resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
132
+
133
+ '@lukeed/ms@2.0.2':
134
+ resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
135
+ engines: {node: '>=8'}
136
+
137
+ '@modelcontextprotocol/sdk@1.25.3':
138
+ resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==}
139
+ engines: {node: '>=18'}
140
+ peerDependencies:
141
+ '@cfworker/json-schema': ^4.1.1
142
+ zod: ^3.25 || ^4.0
143
+ peerDependenciesMeta:
144
+ '@cfworker/json-schema':
145
+ optional: true
146
+
147
+ '@pinojs/redact@0.4.0':
148
+ resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
149
+
150
+ '@tsconfig/node10@1.0.12':
151
+ resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==}
152
+
153
+ '@tsconfig/node12@1.0.11':
154
+ resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
155
+
156
+ '@tsconfig/node14@1.0.3':
157
+ resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
158
+
159
+ '@tsconfig/node16@1.0.4':
160
+ resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
161
+
162
+ '@types/node@25.0.10':
163
+ resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==}
164
+
165
+ abstract-logging@2.0.1:
166
+ resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
167
+
168
+ accepts@2.0.0:
169
+ resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
170
+ engines: {node: '>= 0.6'}
171
+
172
+ acorn-walk@8.3.4:
173
+ resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
174
+ engines: {node: '>=0.4.0'}
175
+
176
+ acorn@8.15.0:
177
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
178
+ engines: {node: '>=0.4.0'}
179
+ hasBin: true
180
+
181
+ ajv-formats@3.0.1:
182
+ resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
183
+ peerDependencies:
184
+ ajv: ^8.0.0
185
+ peerDependenciesMeta:
186
+ ajv:
187
+ optional: true
188
+
189
+ ajv@8.17.1:
190
+ resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
191
+
192
+ anymatch@3.1.3:
193
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
194
+ engines: {node: '>= 8'}
195
+
196
+ arg@4.1.3:
197
+ resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
198
+
199
+ asynckit@0.4.0:
200
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
201
+
202
+ atomic-sleep@1.0.0:
203
+ resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
204
+ engines: {node: '>=8.0.0'}
205
+
206
+ avvio@9.1.0:
207
+ resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==}
208
+
209
+ axios@1.13.3:
210
+ resolution: {integrity: sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==}
211
+
212
+ balanced-match@1.0.2:
213
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
214
+
215
+ binary-extensions@2.3.0:
216
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
217
+ engines: {node: '>=8'}
218
+
219
+ body-parser@2.2.2:
220
+ resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
221
+ engines: {node: '>=18'}
222
+
223
+ boolbase@1.0.0:
224
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
225
+
226
+ brace-expansion@1.1.12:
227
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
228
+
229
+ braces@3.0.3:
230
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
231
+ engines: {node: '>=8'}
232
+
233
+ bytes@3.1.2:
234
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
235
+ engines: {node: '>= 0.8'}
236
+
237
+ call-bind-apply-helpers@1.0.2:
238
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
239
+ engines: {node: '>= 0.4'}
240
+
241
+ call-bound@1.0.4:
242
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
243
+ engines: {node: '>= 0.4'}
244
+
245
+ cheerio-select@2.1.0:
246
+ resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
247
+
248
+ cheerio@1.2.0:
249
+ resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
250
+ engines: {node: '>=20.18.1'}
251
+
252
+ chokidar@3.6.0:
253
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
254
+ engines: {node: '>= 8.10.0'}
255
+
256
+ combined-stream@1.0.8:
257
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
258
+ engines: {node: '>= 0.8'}
259
+
260
+ concat-map@0.0.1:
261
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
262
+
263
+ content-disposition@1.0.1:
264
+ resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
265
+ engines: {node: '>=18'}
266
+
267
+ content-type@1.0.5:
268
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
269
+ engines: {node: '>= 0.6'}
270
+
271
+ cookie-signature@1.2.2:
272
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
273
+ engines: {node: '>=6.6.0'}
274
+
275
+ cookie@0.7.2:
276
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
277
+ engines: {node: '>= 0.6'}
278
+
279
+ cookie@1.1.1:
280
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
281
+ engines: {node: '>=18'}
282
+
283
+ cors@2.8.6:
284
+ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
285
+ engines: {node: '>= 0.10'}
286
+
287
+ create-require@1.1.1:
288
+ resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
289
+
290
+ cross-spawn@7.0.6:
291
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
292
+ engines: {node: '>= 8'}
293
+
294
+ css-select@5.2.2:
295
+ resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
296
+
297
+ css-what@6.2.2:
298
+ resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
299
+ engines: {node: '>= 6'}
300
+
301
+ debug@4.4.3:
302
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
303
+ engines: {node: '>=6.0'}
304
+ peerDependencies:
305
+ supports-color: '*'
306
+ peerDependenciesMeta:
307
+ supports-color:
308
+ optional: true
309
+
310
+ delayed-stream@1.0.0:
311
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
312
+ engines: {node: '>=0.4.0'}
313
+
314
+ depd@2.0.0:
315
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
316
+ engines: {node: '>= 0.8'}
317
+
318
+ dequal@2.0.3:
319
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
320
+ engines: {node: '>=6'}
321
+
322
+ diff@4.0.4:
323
+ resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
324
+ engines: {node: '>=0.3.1'}
325
+
326
+ dom-serializer@2.0.0:
327
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
328
+
329
+ domelementtype@2.3.0:
330
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
331
+
332
+ domhandler@5.0.3:
333
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
334
+ engines: {node: '>= 4'}
335
+
336
+ domutils@3.2.2:
337
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
338
+
339
+ dotenv-expand@10.0.0:
340
+ resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
341
+ engines: {node: '>=12'}
342
+
343
+ dotenv@16.6.1:
344
+ resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
345
+ engines: {node: '>=12'}
346
+
347
+ dotenv@17.2.3:
348
+ resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
349
+ engines: {node: '>=12'}
350
+
351
+ dunder-proto@1.0.1:
352
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
353
+ engines: {node: '>= 0.4'}
354
+
355
+ ee-first@1.1.1:
356
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
357
+
358
+ encodeurl@2.0.0:
359
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
360
+ engines: {node: '>= 0.8'}
361
+
362
+ encoding-sniffer@0.2.1:
363
+ resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==}
364
+
365
+ entities@4.5.0:
366
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
367
+ engines: {node: '>=0.12'}
368
+
369
+ entities@6.0.1:
370
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
371
+ engines: {node: '>=0.12'}
372
+
373
+ entities@7.0.1:
374
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
375
+ engines: {node: '>=0.12'}
376
+
377
+ env-schema@6.1.0:
378
+ resolution: {integrity: sha512-TWtYV2jKe7bd/19kzvNGa8GRRrSwmIMarhcWBzuZYPbHtdlUdjYhnaFvxrO4+GvcwF10sEeVGzf9b/wqLIyf9A==}
379
+
380
+ es-define-property@1.0.1:
381
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
382
+ engines: {node: '>= 0.4'}
383
+
384
+ es-errors@1.3.0:
385
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
386
+ engines: {node: '>= 0.4'}
387
+
388
+ es-object-atoms@1.1.1:
389
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
390
+ engines: {node: '>= 0.4'}
391
+
392
+ es-set-tostringtag@2.1.0:
393
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
394
+ engines: {node: '>= 0.4'}
395
+
396
+ escape-html@1.0.3:
397
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
398
+
399
+ etag@1.8.1:
400
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
401
+ engines: {node: '>= 0.6'}
402
+
403
+ eventsource-parser@3.0.6:
404
+ resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
405
+ engines: {node: '>=18.0.0'}
406
+
407
+ eventsource@3.0.7:
408
+ resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
409
+ engines: {node: '>=18.0.0'}
410
+
411
+ express-rate-limit@7.5.1:
412
+ resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==}
413
+ engines: {node: '>= 16'}
414
+ peerDependencies:
415
+ express: '>= 4.11'
416
+
417
+ express@5.2.1:
418
+ resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
419
+ engines: {node: '>= 18'}
420
+
421
+ fast-decode-uri-component@1.0.1:
422
+ resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
423
+
424
+ fast-deep-equal@3.1.3:
425
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
426
+
427
+ fast-json-stringify@6.2.0:
428
+ resolution: {integrity: sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw==}
429
+
430
+ fast-querystring@1.1.2:
431
+ resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
432
+
433
+ fast-uri@3.1.0:
434
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
435
+
436
+ fastify-mcp@2.1.0:
437
+ resolution: {integrity: sha512-nx1Es7kEqzYe3COWwqdzQbtjgGJVKXFow6pd6rKKsByyXANdJAkEXAXeIJ+ox/vhGD26X7yX6pDDGmaezkBy6g==}
438
+
439
+ fastify-plugin@5.1.0:
440
+ resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==}
441
+
442
+ fastify@5.7.2:
443
+ resolution: {integrity: sha512-dBJolW+hm6N/yJVf6J5E1BxOBNkuXNl405nrfeR8SpvGWG3aCC2XDHyiFBdow8Win1kj7sjawQc257JlYY6M/A==}
444
+
445
+ fastq@1.20.1:
446
+ resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
447
+
448
+ fill-range@7.1.1:
449
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
450
+ engines: {node: '>=8'}
451
+
452
+ finalhandler@2.1.1:
453
+ resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
454
+ engines: {node: '>= 18.0.0'}
455
+
456
+ find-my-way@9.4.0:
457
+ resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==}
458
+ engines: {node: '>=20'}
459
+
460
+ follow-redirects@1.15.11:
461
+ resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
462
+ engines: {node: '>=4.0'}
463
+ peerDependencies:
464
+ debug: '*'
465
+ peerDependenciesMeta:
466
+ debug:
467
+ optional: true
468
+
469
+ form-data@4.0.5:
470
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
471
+ engines: {node: '>= 6'}
472
+
473
+ forwarded@0.2.0:
474
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
475
+ engines: {node: '>= 0.6'}
476
+
477
+ fresh@2.0.0:
478
+ resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
479
+ engines: {node: '>= 0.8'}
480
+
481
+ fsevents@2.3.3:
482
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
483
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
484
+ os: [darwin]
485
+
486
+ function-bind@1.1.2:
487
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
488
+
489
+ get-intrinsic@1.3.0:
490
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
491
+ engines: {node: '>= 0.4'}
492
+
493
+ get-proto@1.0.1:
494
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
495
+ engines: {node: '>= 0.4'}
496
+
497
+ glob-parent@5.1.2:
498
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
499
+ engines: {node: '>= 6'}
500
+
501
+ glob@13.0.0:
502
+ resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
503
+ engines: {node: 20 || >=22}
504
+
505
+ gopd@1.2.0:
506
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
507
+ engines: {node: '>= 0.4'}
508
+
509
+ has-flag@3.0.0:
510
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
511
+ engines: {node: '>=4'}
512
+
513
+ has-symbols@1.1.0:
514
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
515
+ engines: {node: '>= 0.4'}
516
+
517
+ has-tostringtag@1.0.2:
518
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
519
+ engines: {node: '>= 0.4'}
520
+
521
+ hasown@2.0.2:
522
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
523
+ engines: {node: '>= 0.4'}
524
+
525
+ hono@4.11.6:
526
+ resolution: {integrity: sha512-ofIiiHyl34SV6AuhE3YT2mhO5HRWokce+eUYE82TsP6z0/H3JeJcjVWEMSIAiw2QkjDOEpES/lYsg8eEbsLtdw==}
527
+ engines: {node: '>=16.9.0'}
528
+
529
+ htmlparser2@10.1.0:
530
+ resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
531
+
532
+ http-errors@2.0.1:
533
+ resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
534
+ engines: {node: '>= 0.8'}
535
+
536
+ iconv-lite@0.6.3:
537
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
538
+ engines: {node: '>=0.10.0'}
539
+
540
+ iconv-lite@0.7.2:
541
+ resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
542
+ engines: {node: '>=0.10.0'}
543
+
544
+ ignore-by-default@1.0.1:
545
+ resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
546
+
547
+ inherits@2.0.4:
548
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
549
+
550
+ ipaddr.js@1.9.1:
551
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
552
+ engines: {node: '>= 0.10'}
553
+
554
+ ipaddr.js@2.3.0:
555
+ resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
556
+ engines: {node: '>= 10'}
557
+
558
+ is-binary-path@2.1.0:
559
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
560
+ engines: {node: '>=8'}
561
+
562
+ is-extglob@2.1.1:
563
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
564
+ engines: {node: '>=0.10.0'}
565
+
566
+ is-glob@4.0.3:
567
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
568
+ engines: {node: '>=0.10.0'}
569
+
570
+ is-number@7.0.0:
571
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
572
+ engines: {node: '>=0.12.0'}
573
+
574
+ is-promise@4.0.0:
575
+ resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
576
+
577
+ isexe@2.0.0:
578
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
579
+
580
+ jose@6.1.3:
581
+ resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
582
+
583
+ json-schema-ref-resolver@3.0.0:
584
+ resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
585
+
586
+ json-schema-resolver@3.0.0:
587
+ resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==}
588
+ engines: {node: '>=20'}
589
+
590
+ json-schema-traverse@1.0.0:
591
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
592
+
593
+ json-schema-typed@8.0.2:
594
+ resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
595
+
596
+ light-my-request@6.6.0:
597
+ resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
598
+
599
+ lru-cache@11.2.5:
600
+ resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==}
601
+ engines: {node: 20 || >=22}
602
+
603
+ make-error@1.3.6:
604
+ resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
605
+
606
+ math-intrinsics@1.1.0:
607
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
608
+ engines: {node: '>= 0.4'}
609
+
610
+ media-typer@1.1.0:
611
+ resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
612
+ engines: {node: '>= 0.8'}
613
+
614
+ merge-descriptors@2.0.0:
615
+ resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
616
+ engines: {node: '>=18'}
617
+
618
+ mime-db@1.52.0:
619
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
620
+ engines: {node: '>= 0.6'}
621
+
622
+ mime-db@1.54.0:
623
+ resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
624
+ engines: {node: '>= 0.6'}
625
+
626
+ mime-types@2.1.35:
627
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
628
+ engines: {node: '>= 0.6'}
629
+
630
+ mime-types@3.0.2:
631
+ resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
632
+ engines: {node: '>=18'}
633
+
634
+ mime@3.0.0:
635
+ resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
636
+ engines: {node: '>=10.0.0'}
637
+ hasBin: true
638
+
639
+ minimatch@10.1.1:
640
+ resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
641
+ engines: {node: 20 || >=22}
642
+
643
+ minimatch@3.1.2:
644
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
645
+
646
+ minipass@7.1.2:
647
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
648
+ engines: {node: '>=16 || 14 >=14.17'}
649
+
650
+ ms@2.1.3:
651
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
652
+
653
+ negotiator@1.0.0:
654
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
655
+ engines: {node: '>= 0.6'}
656
+
657
+ nodemon@3.1.11:
658
+ resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
659
+ engines: {node: '>=10'}
660
+ hasBin: true
661
+
662
+ normalize-path@3.0.0:
663
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
664
+ engines: {node: '>=0.10.0'}
665
+
666
+ nth-check@2.1.1:
667
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
668
+
669
+ object-assign@4.1.1:
670
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
671
+ engines: {node: '>=0.10.0'}
672
+
673
+ object-inspect@1.13.4:
674
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
675
+ engines: {node: '>= 0.4'}
676
+
677
+ on-exit-leak-free@2.1.2:
678
+ resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
679
+ engines: {node: '>=14.0.0'}
680
+
681
+ on-finished@2.4.1:
682
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
683
+ engines: {node: '>= 0.8'}
684
+
685
+ once@1.4.0:
686
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
687
+
688
+ openapi-types@12.1.3:
689
+ resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
690
+
691
+ parse5-htmlparser2-tree-adapter@7.1.0:
692
+ resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
693
+
694
+ parse5-parser-stream@7.1.2:
695
+ resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
696
+
697
+ parse5@7.3.0:
698
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
699
+
700
+ parseurl@1.3.3:
701
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
702
+ engines: {node: '>= 0.8'}
703
+
704
+ path-key@3.1.1:
705
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
706
+ engines: {node: '>=8'}
707
+
708
+ path-scurry@2.0.1:
709
+ resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
710
+ engines: {node: 20 || >=22}
711
+
712
+ path-to-regexp@8.3.0:
713
+ resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
714
+
715
+ picomatch@2.3.1:
716
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
717
+ engines: {node: '>=8.6'}
718
+
719
+ pino-abstract-transport@3.0.0:
720
+ resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
721
+
722
+ pino-std-serializers@7.1.0:
723
+ resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
724
+
725
+ pino@10.3.0:
726
+ resolution: {integrity: sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA==}
727
+ hasBin: true
728
+
729
+ pkce-challenge@5.0.1:
730
+ resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
731
+ engines: {node: '>=16.20.0'}
732
+
733
+ process-warning@4.0.1:
734
+ resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
735
+
736
+ process-warning@5.0.0:
737
+ resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
738
+
739
+ proxy-addr@2.0.7:
740
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
741
+ engines: {node: '>= 0.10'}
742
+
743
+ proxy-from-env@1.1.0:
744
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
745
+
746
+ pstree.remy@1.1.8:
747
+ resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
748
+
749
+ qs@6.14.1:
750
+ resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
751
+ engines: {node: '>=0.6'}
752
+
753
+ quick-format-unescaped@4.0.4:
754
+ resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
755
+
756
+ range-parser@1.2.1:
757
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
758
+ engines: {node: '>= 0.6'}
759
+
760
+ raw-body@3.0.2:
761
+ resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
762
+ engines: {node: '>= 0.10'}
763
+
764
+ readdirp@3.6.0:
765
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
766
+ engines: {node: '>=8.10.0'}
767
+
768
+ real-require@0.2.0:
769
+ resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
770
+ engines: {node: '>= 12.13.0'}
771
+
772
+ require-from-string@2.0.2:
773
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
774
+ engines: {node: '>=0.10.0'}
775
+
776
+ ret@0.5.0:
777
+ resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==}
778
+ engines: {node: '>=10'}
779
+
780
+ reusify@1.1.0:
781
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
782
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
783
+
784
+ rfdc@1.4.1:
785
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
786
+
787
+ router@2.2.0:
788
+ resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
789
+ engines: {node: '>= 18'}
790
+
791
+ safe-regex2@5.0.0:
792
+ resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==}
793
+
794
+ safe-stable-stringify@2.5.0:
795
+ resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
796
+ engines: {node: '>=10'}
797
+
798
+ safer-buffer@2.1.2:
799
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
800
+
801
+ secure-json-parse@4.1.0:
802
+ resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
803
+
804
+ semver@7.7.3:
805
+ resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
806
+ engines: {node: '>=10'}
807
+ hasBin: true
808
+
809
+ send@1.2.1:
810
+ resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
811
+ engines: {node: '>= 18'}
812
+
813
+ serve-static@2.2.1:
814
+ resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
815
+ engines: {node: '>= 18'}
816
+
817
+ set-cookie-parser@2.7.2:
818
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
819
+
820
+ setprototypeof@1.2.0:
821
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
822
+
823
+ shebang-command@2.0.0:
824
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
825
+ engines: {node: '>=8'}
826
+
827
+ shebang-regex@3.0.0:
828
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
829
+ engines: {node: '>=8'}
830
+
831
+ side-channel-list@1.0.0:
832
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
833
+ engines: {node: '>= 0.4'}
834
+
835
+ side-channel-map@1.0.1:
836
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
837
+ engines: {node: '>= 0.4'}
838
+
839
+ side-channel-weakmap@1.0.2:
840
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
841
+ engines: {node: '>= 0.4'}
842
+
843
+ side-channel@1.1.0:
844
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
845
+ engines: {node: '>= 0.4'}
846
+
847
+ simple-update-notifier@2.0.0:
848
+ resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
849
+ engines: {node: '>=10'}
850
+
851
+ sonic-boom@4.2.0:
852
+ resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
853
+
854
+ split2@4.2.0:
855
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
856
+ engines: {node: '>= 10.x'}
857
+
858
+ statuses@2.0.2:
859
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
860
+ engines: {node: '>= 0.8'}
861
+
862
+ supports-color@5.5.0:
863
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
864
+ engines: {node: '>=4'}
865
+
866
+ thread-stream@4.0.0:
867
+ resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
868
+ engines: {node: '>=20'}
869
+
870
+ to-regex-range@5.0.1:
871
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
872
+ engines: {node: '>=8.0'}
873
+
874
+ toad-cache@3.7.0:
875
+ resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
876
+ engines: {node: '>=12'}
877
+
878
+ toidentifier@1.0.1:
879
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
880
+ engines: {node: '>=0.6'}
881
+
882
+ touch@3.1.1:
883
+ resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
884
+ hasBin: true
885
+
886
+ ts-node@10.9.2:
887
+ resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
888
+ hasBin: true
889
+ peerDependencies:
890
+ '@swc/core': '>=1.2.50'
891
+ '@swc/wasm': '>=1.2.50'
892
+ '@types/node': '*'
893
+ typescript: '>=2.7'
894
+ peerDependenciesMeta:
895
+ '@swc/core':
896
+ optional: true
897
+ '@swc/wasm':
898
+ optional: true
899
+
900
+ type-is@2.0.1:
901
+ resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
902
+ engines: {node: '>= 0.6'}
903
+
904
+ typescript@5.9.3:
905
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
906
+ engines: {node: '>=14.17'}
907
+ hasBin: true
908
+
909
+ undefsafe@2.0.5:
910
+ resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
911
+
912
+ undici-types@7.16.0:
913
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
914
+
915
+ undici@7.19.1:
916
+ resolution: {integrity: sha512-Gpq0iNm5M6cQWlyHQv9MV+uOj1jWk7LpkoE5vSp/7zjb4zMdAcUD+VL5y0nH4p9EbUklq00eVIIX/XcDHzu5xg==}
917
+ engines: {node: '>=20.18.1'}
918
+
919
+ unpipe@1.0.0:
920
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
921
+ engines: {node: '>= 0.8'}
922
+
923
+ v8-compile-cache-lib@3.0.1:
924
+ resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
925
+
926
+ vary@1.1.2:
927
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
928
+ engines: {node: '>= 0.8'}
929
+
930
+ whatwg-encoding@3.1.1:
931
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
932
+ engines: {node: '>=18'}
933
+ deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
934
+
935
+ whatwg-mimetype@4.0.0:
936
+ resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
937
+ engines: {node: '>=18'}
938
+
939
+ which@2.0.2:
940
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
941
+ engines: {node: '>= 8'}
942
+ hasBin: true
943
+
944
+ wrappy@1.0.2:
945
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
946
+
947
+ yaml@2.8.2:
948
+ resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
949
+ engines: {node: '>= 14.6'}
950
+ hasBin: true
951
+
952
+ yn@3.1.1:
953
+ resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
954
+ engines: {node: '>=6'}
955
+
956
+ zod-to-json-schema@3.25.1:
957
+ resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
958
+ peerDependencies:
959
+ zod: ^3.25 || ^4
960
+
961
+ zod@4.3.6:
962
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
963
+
964
+ snapshots:
965
+
966
+ '@cspotcode/source-map-support@0.8.1':
967
+ dependencies:
968
+ '@jridgewell/trace-mapping': 0.3.9
969
+
970
+ '@fastify/accept-negotiator@2.0.1': {}
971
+
972
+ '@fastify/ajv-compiler@4.0.5':
973
+ dependencies:
974
+ ajv: 8.17.1
975
+ ajv-formats: 3.0.1(ajv@8.17.1)
976
+ fast-uri: 3.1.0
977
+
978
+ '@fastify/cors@11.2.0':
979
+ dependencies:
980
+ fastify-plugin: 5.1.0
981
+ toad-cache: 3.7.0
982
+
983
+ '@fastify/env@5.0.3':
984
+ dependencies:
985
+ env-schema: 6.1.0
986
+ fastify-plugin: 5.1.0
987
+
988
+ '@fastify/error@4.2.0': {}
989
+
990
+ '@fastify/fast-json-stringify-compiler@5.0.3':
991
+ dependencies:
992
+ fast-json-stringify: 6.2.0
993
+
994
+ '@fastify/formbody@8.0.2':
995
+ dependencies:
996
+ fast-querystring: 1.1.2
997
+ fastify-plugin: 5.1.0
998
+
999
+ '@fastify/forwarded@3.0.1': {}
1000
+
1001
+ '@fastify/merge-json-schemas@0.2.1':
1002
+ dependencies:
1003
+ dequal: 2.0.3
1004
+
1005
+ '@fastify/proxy-addr@5.1.0':
1006
+ dependencies:
1007
+ '@fastify/forwarded': 3.0.1
1008
+ ipaddr.js: 2.3.0
1009
+
1010
+ '@fastify/send@4.1.0':
1011
+ dependencies:
1012
+ '@lukeed/ms': 2.0.2
1013
+ escape-html: 1.0.3
1014
+ fast-decode-uri-component: 1.0.1
1015
+ http-errors: 2.0.1
1016
+ mime: 3.0.0
1017
+
1018
+ '@fastify/static@9.0.0':
1019
+ dependencies:
1020
+ '@fastify/accept-negotiator': 2.0.1
1021
+ '@fastify/send': 4.1.0
1022
+ content-disposition: 1.0.1
1023
+ fastify-plugin: 5.1.0
1024
+ fastq: 1.20.1
1025
+ glob: 13.0.0
1026
+
1027
+ '@fastify/swagger-ui@5.2.4':
1028
+ dependencies:
1029
+ '@fastify/static': 9.0.0
1030
+ fastify-plugin: 5.1.0
1031
+ openapi-types: 12.1.3
1032
+ rfdc: 1.4.1
1033
+ yaml: 2.8.2
1034
+
1035
+ '@fastify/swagger@9.6.1':
1036
+ dependencies:
1037
+ fastify-plugin: 5.1.0
1038
+ json-schema-resolver: 3.0.0
1039
+ openapi-types: 12.1.3
1040
+ rfdc: 1.4.1
1041
+ yaml: 2.8.2
1042
+ transitivePeerDependencies:
1043
+ - supports-color
1044
+
1045
+ '@hono/node-server@1.19.9(hono@4.11.6)':
1046
+ dependencies:
1047
+ hono: 4.11.6
1048
+
1049
+ '@isaacs/balanced-match@4.0.1': {}
1050
+
1051
+ '@isaacs/brace-expansion@5.0.0':
1052
+ dependencies:
1053
+ '@isaacs/balanced-match': 4.0.1
1054
+
1055
+ '@jridgewell/resolve-uri@3.1.2': {}
1056
+
1057
+ '@jridgewell/sourcemap-codec@1.5.5': {}
1058
+
1059
+ '@jridgewell/trace-mapping@0.3.9':
1060
+ dependencies:
1061
+ '@jridgewell/resolve-uri': 3.1.2
1062
+ '@jridgewell/sourcemap-codec': 1.5.5
1063
+
1064
+ '@lukeed/ms@2.0.2': {}
1065
+
1066
+ '@modelcontextprotocol/sdk@1.25.3(hono@4.11.6)(zod@4.3.6)':
1067
+ dependencies:
1068
+ '@hono/node-server': 1.19.9(hono@4.11.6)
1069
+ ajv: 8.17.1
1070
+ ajv-formats: 3.0.1(ajv@8.17.1)
1071
+ content-type: 1.0.5
1072
+ cors: 2.8.6
1073
+ cross-spawn: 7.0.6
1074
+ eventsource: 3.0.7
1075
+ eventsource-parser: 3.0.6
1076
+ express: 5.2.1
1077
+ express-rate-limit: 7.5.1(express@5.2.1)
1078
+ jose: 6.1.3
1079
+ json-schema-typed: 8.0.2
1080
+ pkce-challenge: 5.0.1
1081
+ raw-body: 3.0.2
1082
+ zod: 4.3.6
1083
+ zod-to-json-schema: 3.25.1(zod@4.3.6)
1084
+ transitivePeerDependencies:
1085
+ - hono
1086
+ - supports-color
1087
+
1088
+ '@pinojs/redact@0.4.0': {}
1089
+
1090
+ '@tsconfig/node10@1.0.12': {}
1091
+
1092
+ '@tsconfig/node12@1.0.11': {}
1093
+
1094
+ '@tsconfig/node14@1.0.3': {}
1095
+
1096
+ '@tsconfig/node16@1.0.4': {}
1097
+
1098
+ '@types/node@25.0.10':
1099
+ dependencies:
1100
+ undici-types: 7.16.0
1101
+
1102
+ abstract-logging@2.0.1: {}
1103
+
1104
+ accepts@2.0.0:
1105
+ dependencies:
1106
+ mime-types: 3.0.2
1107
+ negotiator: 1.0.0
1108
+
1109
+ acorn-walk@8.3.4:
1110
+ dependencies:
1111
+ acorn: 8.15.0
1112
+
1113
+ acorn@8.15.0: {}
1114
+
1115
+ ajv-formats@3.0.1(ajv@8.17.1):
1116
+ optionalDependencies:
1117
+ ajv: 8.17.1
1118
+
1119
+ ajv@8.17.1:
1120
+ dependencies:
1121
+ fast-deep-equal: 3.1.3
1122
+ fast-uri: 3.1.0
1123
+ json-schema-traverse: 1.0.0
1124
+ require-from-string: 2.0.2
1125
+
1126
+ anymatch@3.1.3:
1127
+ dependencies:
1128
+ normalize-path: 3.0.0
1129
+ picomatch: 2.3.1
1130
+
1131
+ arg@4.1.3: {}
1132
+
1133
+ asynckit@0.4.0: {}
1134
+
1135
+ atomic-sleep@1.0.0: {}
1136
+
1137
+ avvio@9.1.0:
1138
+ dependencies:
1139
+ '@fastify/error': 4.2.0
1140
+ fastq: 1.20.1
1141
+
1142
+ axios@1.13.3:
1143
+ dependencies:
1144
+ follow-redirects: 1.15.11
1145
+ form-data: 4.0.5
1146
+ proxy-from-env: 1.1.0
1147
+ transitivePeerDependencies:
1148
+ - debug
1149
+
1150
+ balanced-match@1.0.2: {}
1151
+
1152
+ binary-extensions@2.3.0: {}
1153
+
1154
+ body-parser@2.2.2:
1155
+ dependencies:
1156
+ bytes: 3.1.2
1157
+ content-type: 1.0.5
1158
+ debug: 4.4.3(supports-color@5.5.0)
1159
+ http-errors: 2.0.1
1160
+ iconv-lite: 0.7.2
1161
+ on-finished: 2.4.1
1162
+ qs: 6.14.1
1163
+ raw-body: 3.0.2
1164
+ type-is: 2.0.1
1165
+ transitivePeerDependencies:
1166
+ - supports-color
1167
+
1168
+ boolbase@1.0.0: {}
1169
+
1170
+ brace-expansion@1.1.12:
1171
+ dependencies:
1172
+ balanced-match: 1.0.2
1173
+ concat-map: 0.0.1
1174
+
1175
+ braces@3.0.3:
1176
+ dependencies:
1177
+ fill-range: 7.1.1
1178
+
1179
+ bytes@3.1.2: {}
1180
+
1181
+ call-bind-apply-helpers@1.0.2:
1182
+ dependencies:
1183
+ es-errors: 1.3.0
1184
+ function-bind: 1.1.2
1185
+
1186
+ call-bound@1.0.4:
1187
+ dependencies:
1188
+ call-bind-apply-helpers: 1.0.2
1189
+ get-intrinsic: 1.3.0
1190
+
1191
+ cheerio-select@2.1.0:
1192
+ dependencies:
1193
+ boolbase: 1.0.0
1194
+ css-select: 5.2.2
1195
+ css-what: 6.2.2
1196
+ domelementtype: 2.3.0
1197
+ domhandler: 5.0.3
1198
+ domutils: 3.2.2
1199
+
1200
+ cheerio@1.2.0:
1201
+ dependencies:
1202
+ cheerio-select: 2.1.0
1203
+ dom-serializer: 2.0.0
1204
+ domhandler: 5.0.3
1205
+ domutils: 3.2.2
1206
+ encoding-sniffer: 0.2.1
1207
+ htmlparser2: 10.1.0
1208
+ parse5: 7.3.0
1209
+ parse5-htmlparser2-tree-adapter: 7.1.0
1210
+ parse5-parser-stream: 7.1.2
1211
+ undici: 7.19.1
1212
+ whatwg-mimetype: 4.0.0
1213
+
1214
+ chokidar@3.6.0:
1215
+ dependencies:
1216
+ anymatch: 3.1.3
1217
+ braces: 3.0.3
1218
+ glob-parent: 5.1.2
1219
+ is-binary-path: 2.1.0
1220
+ is-glob: 4.0.3
1221
+ normalize-path: 3.0.0
1222
+ readdirp: 3.6.0
1223
+ optionalDependencies:
1224
+ fsevents: 2.3.3
1225
+
1226
+ combined-stream@1.0.8:
1227
+ dependencies:
1228
+ delayed-stream: 1.0.0
1229
+
1230
+ concat-map@0.0.1: {}
1231
+
1232
+ content-disposition@1.0.1: {}
1233
+
1234
+ content-type@1.0.5: {}
1235
+
1236
+ cookie-signature@1.2.2: {}
1237
+
1238
+ cookie@0.7.2: {}
1239
+
1240
+ cookie@1.1.1: {}
1241
+
1242
+ cors@2.8.6:
1243
+ dependencies:
1244
+ object-assign: 4.1.1
1245
+ vary: 1.1.2
1246
+
1247
+ create-require@1.1.1: {}
1248
+
1249
+ cross-spawn@7.0.6:
1250
+ dependencies:
1251
+ path-key: 3.1.1
1252
+ shebang-command: 2.0.0
1253
+ which: 2.0.2
1254
+
1255
+ css-select@5.2.2:
1256
+ dependencies:
1257
+ boolbase: 1.0.0
1258
+ css-what: 6.2.2
1259
+ domhandler: 5.0.3
1260
+ domutils: 3.2.2
1261
+ nth-check: 2.1.1
1262
+
1263
+ css-what@6.2.2: {}
1264
+
1265
+ debug@4.4.3(supports-color@5.5.0):
1266
+ dependencies:
1267
+ ms: 2.1.3
1268
+ optionalDependencies:
1269
+ supports-color: 5.5.0
1270
+
1271
+ delayed-stream@1.0.0: {}
1272
+
1273
+ depd@2.0.0: {}
1274
+
1275
+ dequal@2.0.3: {}
1276
+
1277
+ diff@4.0.4: {}
1278
+
1279
+ dom-serializer@2.0.0:
1280
+ dependencies:
1281
+ domelementtype: 2.3.0
1282
+ domhandler: 5.0.3
1283
+ entities: 4.5.0
1284
+
1285
+ domelementtype@2.3.0: {}
1286
+
1287
+ domhandler@5.0.3:
1288
+ dependencies:
1289
+ domelementtype: 2.3.0
1290
+
1291
+ domutils@3.2.2:
1292
+ dependencies:
1293
+ dom-serializer: 2.0.0
1294
+ domelementtype: 2.3.0
1295
+ domhandler: 5.0.3
1296
+
1297
+ dotenv-expand@10.0.0: {}
1298
+
1299
+ dotenv@16.6.1: {}
1300
+
1301
+ dotenv@17.2.3: {}
1302
+
1303
+ dunder-proto@1.0.1:
1304
+ dependencies:
1305
+ call-bind-apply-helpers: 1.0.2
1306
+ es-errors: 1.3.0
1307
+ gopd: 1.2.0
1308
+
1309
+ ee-first@1.1.1: {}
1310
+
1311
+ encodeurl@2.0.0: {}
1312
+
1313
+ encoding-sniffer@0.2.1:
1314
+ dependencies:
1315
+ iconv-lite: 0.6.3
1316
+ whatwg-encoding: 3.1.1
1317
+
1318
+ entities@4.5.0: {}
1319
+
1320
+ entities@6.0.1: {}
1321
+
1322
+ entities@7.0.1: {}
1323
+
1324
+ env-schema@6.1.0:
1325
+ dependencies:
1326
+ ajv: 8.17.1
1327
+ dotenv: 17.2.3
1328
+ dotenv-expand: 10.0.0
1329
+
1330
+ es-define-property@1.0.1: {}
1331
+
1332
+ es-errors@1.3.0: {}
1333
+
1334
+ es-object-atoms@1.1.1:
1335
+ dependencies:
1336
+ es-errors: 1.3.0
1337
+
1338
+ es-set-tostringtag@2.1.0:
1339
+ dependencies:
1340
+ es-errors: 1.3.0
1341
+ get-intrinsic: 1.3.0
1342
+ has-tostringtag: 1.0.2
1343
+ hasown: 2.0.2
1344
+
1345
+ escape-html@1.0.3: {}
1346
+
1347
+ etag@1.8.1: {}
1348
+
1349
+ eventsource-parser@3.0.6: {}
1350
+
1351
+ eventsource@3.0.7:
1352
+ dependencies:
1353
+ eventsource-parser: 3.0.6
1354
+
1355
+ express-rate-limit@7.5.1(express@5.2.1):
1356
+ dependencies:
1357
+ express: 5.2.1
1358
+
1359
+ express@5.2.1:
1360
+ dependencies:
1361
+ accepts: 2.0.0
1362
+ body-parser: 2.2.2
1363
+ content-disposition: 1.0.1
1364
+ content-type: 1.0.5
1365
+ cookie: 0.7.2
1366
+ cookie-signature: 1.2.2
1367
+ debug: 4.4.3(supports-color@5.5.0)
1368
+ depd: 2.0.0
1369
+ encodeurl: 2.0.0
1370
+ escape-html: 1.0.3
1371
+ etag: 1.8.1
1372
+ finalhandler: 2.1.1
1373
+ fresh: 2.0.0
1374
+ http-errors: 2.0.1
1375
+ merge-descriptors: 2.0.0
1376
+ mime-types: 3.0.2
1377
+ on-finished: 2.4.1
1378
+ once: 1.4.0
1379
+ parseurl: 1.3.3
1380
+ proxy-addr: 2.0.7
1381
+ qs: 6.14.1
1382
+ range-parser: 1.2.1
1383
+ router: 2.2.0
1384
+ send: 1.2.1
1385
+ serve-static: 2.2.1
1386
+ statuses: 2.0.2
1387
+ type-is: 2.0.1
1388
+ vary: 1.1.2
1389
+ transitivePeerDependencies:
1390
+ - supports-color
1391
+
1392
+ fast-decode-uri-component@1.0.1: {}
1393
+
1394
+ fast-deep-equal@3.1.3: {}
1395
+
1396
+ fast-json-stringify@6.2.0:
1397
+ dependencies:
1398
+ '@fastify/merge-json-schemas': 0.2.1
1399
+ ajv: 8.17.1
1400
+ ajv-formats: 3.0.1(ajv@8.17.1)
1401
+ fast-uri: 3.1.0
1402
+ json-schema-ref-resolver: 3.0.0
1403
+ rfdc: 1.4.1
1404
+
1405
+ fast-querystring@1.1.2:
1406
+ dependencies:
1407
+ fast-decode-uri-component: 1.0.1
1408
+
1409
+ fast-uri@3.1.0: {}
1410
+
1411
+ fastify-mcp@2.1.0(hono@4.11.6)(zod@4.3.6):
1412
+ dependencies:
1413
+ '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.6)(zod@4.3.6)
1414
+ fastify: 5.7.2
1415
+ transitivePeerDependencies:
1416
+ - '@cfworker/json-schema'
1417
+ - hono
1418
+ - supports-color
1419
+ - zod
1420
+
1421
+ fastify-plugin@5.1.0: {}
1422
+
1423
+ fastify@5.7.2:
1424
+ dependencies:
1425
+ '@fastify/ajv-compiler': 4.0.5
1426
+ '@fastify/error': 4.2.0
1427
+ '@fastify/fast-json-stringify-compiler': 5.0.3
1428
+ '@fastify/proxy-addr': 5.1.0
1429
+ abstract-logging: 2.0.1
1430
+ avvio: 9.1.0
1431
+ fast-json-stringify: 6.2.0
1432
+ find-my-way: 9.4.0
1433
+ light-my-request: 6.6.0
1434
+ pino: 10.3.0
1435
+ process-warning: 5.0.0
1436
+ rfdc: 1.4.1
1437
+ secure-json-parse: 4.1.0
1438
+ semver: 7.7.3
1439
+ toad-cache: 3.7.0
1440
+
1441
+ fastq@1.20.1:
1442
+ dependencies:
1443
+ reusify: 1.1.0
1444
+
1445
+ fill-range@7.1.1:
1446
+ dependencies:
1447
+ to-regex-range: 5.0.1
1448
+
1449
+ finalhandler@2.1.1:
1450
+ dependencies:
1451
+ debug: 4.4.3(supports-color@5.5.0)
1452
+ encodeurl: 2.0.0
1453
+ escape-html: 1.0.3
1454
+ on-finished: 2.4.1
1455
+ parseurl: 1.3.3
1456
+ statuses: 2.0.2
1457
+ transitivePeerDependencies:
1458
+ - supports-color
1459
+
1460
+ find-my-way@9.4.0:
1461
+ dependencies:
1462
+ fast-deep-equal: 3.1.3
1463
+ fast-querystring: 1.1.2
1464
+ safe-regex2: 5.0.0
1465
+
1466
+ follow-redirects@1.15.11: {}
1467
+
1468
+ form-data@4.0.5:
1469
+ dependencies:
1470
+ asynckit: 0.4.0
1471
+ combined-stream: 1.0.8
1472
+ es-set-tostringtag: 2.1.0
1473
+ hasown: 2.0.2
1474
+ mime-types: 2.1.35
1475
+
1476
+ forwarded@0.2.0: {}
1477
+
1478
+ fresh@2.0.0: {}
1479
+
1480
+ fsevents@2.3.3:
1481
+ optional: true
1482
+
1483
+ function-bind@1.1.2: {}
1484
+
1485
+ get-intrinsic@1.3.0:
1486
+ dependencies:
1487
+ call-bind-apply-helpers: 1.0.2
1488
+ es-define-property: 1.0.1
1489
+ es-errors: 1.3.0
1490
+ es-object-atoms: 1.1.1
1491
+ function-bind: 1.1.2
1492
+ get-proto: 1.0.1
1493
+ gopd: 1.2.0
1494
+ has-symbols: 1.1.0
1495
+ hasown: 2.0.2
1496
+ math-intrinsics: 1.1.0
1497
+
1498
+ get-proto@1.0.1:
1499
+ dependencies:
1500
+ dunder-proto: 1.0.1
1501
+ es-object-atoms: 1.1.1
1502
+
1503
+ glob-parent@5.1.2:
1504
+ dependencies:
1505
+ is-glob: 4.0.3
1506
+
1507
+ glob@13.0.0:
1508
+ dependencies:
1509
+ minimatch: 10.1.1
1510
+ minipass: 7.1.2
1511
+ path-scurry: 2.0.1
1512
+
1513
+ gopd@1.2.0: {}
1514
+
1515
+ has-flag@3.0.0: {}
1516
+
1517
+ has-symbols@1.1.0: {}
1518
+
1519
+ has-tostringtag@1.0.2:
1520
+ dependencies:
1521
+ has-symbols: 1.1.0
1522
+
1523
+ hasown@2.0.2:
1524
+ dependencies:
1525
+ function-bind: 1.1.2
1526
+
1527
+ hono@4.11.6: {}
1528
+
1529
+ htmlparser2@10.1.0:
1530
+ dependencies:
1531
+ domelementtype: 2.3.0
1532
+ domhandler: 5.0.3
1533
+ domutils: 3.2.2
1534
+ entities: 7.0.1
1535
+
1536
+ http-errors@2.0.1:
1537
+ dependencies:
1538
+ depd: 2.0.0
1539
+ inherits: 2.0.4
1540
+ setprototypeof: 1.2.0
1541
+ statuses: 2.0.2
1542
+ toidentifier: 1.0.1
1543
+
1544
+ iconv-lite@0.6.3:
1545
+ dependencies:
1546
+ safer-buffer: 2.1.2
1547
+
1548
+ iconv-lite@0.7.2:
1549
+ dependencies:
1550
+ safer-buffer: 2.1.2
1551
+
1552
+ ignore-by-default@1.0.1: {}
1553
+
1554
+ inherits@2.0.4: {}
1555
+
1556
+ ipaddr.js@1.9.1: {}
1557
+
1558
+ ipaddr.js@2.3.0: {}
1559
+
1560
+ is-binary-path@2.1.0:
1561
+ dependencies:
1562
+ binary-extensions: 2.3.0
1563
+
1564
+ is-extglob@2.1.1: {}
1565
+
1566
+ is-glob@4.0.3:
1567
+ dependencies:
1568
+ is-extglob: 2.1.1
1569
+
1570
+ is-number@7.0.0: {}
1571
+
1572
+ is-promise@4.0.0: {}
1573
+
1574
+ isexe@2.0.0: {}
1575
+
1576
+ jose@6.1.3: {}
1577
+
1578
+ json-schema-ref-resolver@3.0.0:
1579
+ dependencies:
1580
+ dequal: 2.0.3
1581
+
1582
+ json-schema-resolver@3.0.0:
1583
+ dependencies:
1584
+ debug: 4.4.3(supports-color@5.5.0)
1585
+ fast-uri: 3.1.0
1586
+ rfdc: 1.4.1
1587
+ transitivePeerDependencies:
1588
+ - supports-color
1589
+
1590
+ json-schema-traverse@1.0.0: {}
1591
+
1592
+ json-schema-typed@8.0.2: {}
1593
+
1594
+ light-my-request@6.6.0:
1595
+ dependencies:
1596
+ cookie: 1.1.1
1597
+ process-warning: 4.0.1
1598
+ set-cookie-parser: 2.7.2
1599
+
1600
+ lru-cache@11.2.5: {}
1601
+
1602
+ make-error@1.3.6: {}
1603
+
1604
+ math-intrinsics@1.1.0: {}
1605
+
1606
+ media-typer@1.1.0: {}
1607
+
1608
+ merge-descriptors@2.0.0: {}
1609
+
1610
+ mime-db@1.52.0: {}
1611
+
1612
+ mime-db@1.54.0: {}
1613
+
1614
+ mime-types@2.1.35:
1615
+ dependencies:
1616
+ mime-db: 1.52.0
1617
+
1618
+ mime-types@3.0.2:
1619
+ dependencies:
1620
+ mime-db: 1.54.0
1621
+
1622
+ mime@3.0.0: {}
1623
+
1624
+ minimatch@10.1.1:
1625
+ dependencies:
1626
+ '@isaacs/brace-expansion': 5.0.0
1627
+
1628
+ minimatch@3.1.2:
1629
+ dependencies:
1630
+ brace-expansion: 1.1.12
1631
+
1632
+ minipass@7.1.2: {}
1633
+
1634
+ ms@2.1.3: {}
1635
+
1636
+ negotiator@1.0.0: {}
1637
+
1638
+ nodemon@3.1.11:
1639
+ dependencies:
1640
+ chokidar: 3.6.0
1641
+ debug: 4.4.3(supports-color@5.5.0)
1642
+ ignore-by-default: 1.0.1
1643
+ minimatch: 3.1.2
1644
+ pstree.remy: 1.1.8
1645
+ semver: 7.7.3
1646
+ simple-update-notifier: 2.0.0
1647
+ supports-color: 5.5.0
1648
+ touch: 3.1.1
1649
+ undefsafe: 2.0.5
1650
+
1651
+ normalize-path@3.0.0: {}
1652
+
1653
+ nth-check@2.1.1:
1654
+ dependencies:
1655
+ boolbase: 1.0.0
1656
+
1657
+ object-assign@4.1.1: {}
1658
+
1659
+ object-inspect@1.13.4: {}
1660
+
1661
+ on-exit-leak-free@2.1.2: {}
1662
+
1663
+ on-finished@2.4.1:
1664
+ dependencies:
1665
+ ee-first: 1.1.1
1666
+
1667
+ once@1.4.0:
1668
+ dependencies:
1669
+ wrappy: 1.0.2
1670
+
1671
+ openapi-types@12.1.3: {}
1672
+
1673
+ parse5-htmlparser2-tree-adapter@7.1.0:
1674
+ dependencies:
1675
+ domhandler: 5.0.3
1676
+ parse5: 7.3.0
1677
+
1678
+ parse5-parser-stream@7.1.2:
1679
+ dependencies:
1680
+ parse5: 7.3.0
1681
+
1682
+ parse5@7.3.0:
1683
+ dependencies:
1684
+ entities: 6.0.1
1685
+
1686
+ parseurl@1.3.3: {}
1687
+
1688
+ path-key@3.1.1: {}
1689
+
1690
+ path-scurry@2.0.1:
1691
+ dependencies:
1692
+ lru-cache: 11.2.5
1693
+ minipass: 7.1.2
1694
+
1695
+ path-to-regexp@8.3.0: {}
1696
+
1697
+ picomatch@2.3.1: {}
1698
+
1699
+ pino-abstract-transport@3.0.0:
1700
+ dependencies:
1701
+ split2: 4.2.0
1702
+
1703
+ pino-std-serializers@7.1.0: {}
1704
+
1705
+ pino@10.3.0:
1706
+ dependencies:
1707
+ '@pinojs/redact': 0.4.0
1708
+ atomic-sleep: 1.0.0
1709
+ on-exit-leak-free: 2.1.2
1710
+ pino-abstract-transport: 3.0.0
1711
+ pino-std-serializers: 7.1.0
1712
+ process-warning: 5.0.0
1713
+ quick-format-unescaped: 4.0.4
1714
+ real-require: 0.2.0
1715
+ safe-stable-stringify: 2.5.0
1716
+ sonic-boom: 4.2.0
1717
+ thread-stream: 4.0.0
1718
+
1719
+ pkce-challenge@5.0.1: {}
1720
+
1721
+ process-warning@4.0.1: {}
1722
+
1723
+ process-warning@5.0.0: {}
1724
+
1725
+ proxy-addr@2.0.7:
1726
+ dependencies:
1727
+ forwarded: 0.2.0
1728
+ ipaddr.js: 1.9.1
1729
+
1730
+ proxy-from-env@1.1.0: {}
1731
+
1732
+ pstree.remy@1.1.8: {}
1733
+
1734
+ qs@6.14.1:
1735
+ dependencies:
1736
+ side-channel: 1.1.0
1737
+
1738
+ quick-format-unescaped@4.0.4: {}
1739
+
1740
+ range-parser@1.2.1: {}
1741
+
1742
+ raw-body@3.0.2:
1743
+ dependencies:
1744
+ bytes: 3.1.2
1745
+ http-errors: 2.0.1
1746
+ iconv-lite: 0.7.2
1747
+ unpipe: 1.0.0
1748
+
1749
+ readdirp@3.6.0:
1750
+ dependencies:
1751
+ picomatch: 2.3.1
1752
+
1753
+ real-require@0.2.0: {}
1754
+
1755
+ require-from-string@2.0.2: {}
1756
+
1757
+ ret@0.5.0: {}
1758
+
1759
+ reusify@1.1.0: {}
1760
+
1761
+ rfdc@1.4.1: {}
1762
+
1763
+ router@2.2.0:
1764
+ dependencies:
1765
+ debug: 4.4.3(supports-color@5.5.0)
1766
+ depd: 2.0.0
1767
+ is-promise: 4.0.0
1768
+ parseurl: 1.3.3
1769
+ path-to-regexp: 8.3.0
1770
+ transitivePeerDependencies:
1771
+ - supports-color
1772
+
1773
+ safe-regex2@5.0.0:
1774
+ dependencies:
1775
+ ret: 0.5.0
1776
+
1777
+ safe-stable-stringify@2.5.0: {}
1778
+
1779
+ safer-buffer@2.1.2: {}
1780
+
1781
+ secure-json-parse@4.1.0: {}
1782
+
1783
+ semver@7.7.3: {}
1784
+
1785
+ send@1.2.1:
1786
+ dependencies:
1787
+ debug: 4.4.3(supports-color@5.5.0)
1788
+ encodeurl: 2.0.0
1789
+ escape-html: 1.0.3
1790
+ etag: 1.8.1
1791
+ fresh: 2.0.0
1792
+ http-errors: 2.0.1
1793
+ mime-types: 3.0.2
1794
+ ms: 2.1.3
1795
+ on-finished: 2.4.1
1796
+ range-parser: 1.2.1
1797
+ statuses: 2.0.2
1798
+ transitivePeerDependencies:
1799
+ - supports-color
1800
+
1801
+ serve-static@2.2.1:
1802
+ dependencies:
1803
+ encodeurl: 2.0.0
1804
+ escape-html: 1.0.3
1805
+ parseurl: 1.3.3
1806
+ send: 1.2.1
1807
+ transitivePeerDependencies:
1808
+ - supports-color
1809
+
1810
+ set-cookie-parser@2.7.2: {}
1811
+
1812
+ setprototypeof@1.2.0: {}
1813
+
1814
+ shebang-command@2.0.0:
1815
+ dependencies:
1816
+ shebang-regex: 3.0.0
1817
+
1818
+ shebang-regex@3.0.0: {}
1819
+
1820
+ side-channel-list@1.0.0:
1821
+ dependencies:
1822
+ es-errors: 1.3.0
1823
+ object-inspect: 1.13.4
1824
+
1825
+ side-channel-map@1.0.1:
1826
+ dependencies:
1827
+ call-bound: 1.0.4
1828
+ es-errors: 1.3.0
1829
+ get-intrinsic: 1.3.0
1830
+ object-inspect: 1.13.4
1831
+
1832
+ side-channel-weakmap@1.0.2:
1833
+ dependencies:
1834
+ call-bound: 1.0.4
1835
+ es-errors: 1.3.0
1836
+ get-intrinsic: 1.3.0
1837
+ object-inspect: 1.13.4
1838
+ side-channel-map: 1.0.1
1839
+
1840
+ side-channel@1.1.0:
1841
+ dependencies:
1842
+ es-errors: 1.3.0
1843
+ object-inspect: 1.13.4
1844
+ side-channel-list: 1.0.0
1845
+ side-channel-map: 1.0.1
1846
+ side-channel-weakmap: 1.0.2
1847
+
1848
+ simple-update-notifier@2.0.0:
1849
+ dependencies:
1850
+ semver: 7.7.3
1851
+
1852
+ sonic-boom@4.2.0:
1853
+ dependencies:
1854
+ atomic-sleep: 1.0.0
1855
+
1856
+ split2@4.2.0: {}
1857
+
1858
+ statuses@2.0.2: {}
1859
+
1860
+ supports-color@5.5.0:
1861
+ dependencies:
1862
+ has-flag: 3.0.0
1863
+
1864
+ thread-stream@4.0.0:
1865
+ dependencies:
1866
+ real-require: 0.2.0
1867
+
1868
+ to-regex-range@5.0.1:
1869
+ dependencies:
1870
+ is-number: 7.0.0
1871
+
1872
+ toad-cache@3.7.0: {}
1873
+
1874
+ toidentifier@1.0.1: {}
1875
+
1876
+ touch@3.1.1: {}
1877
+
1878
+ ts-node@10.9.2(@types/node@25.0.10)(typescript@5.9.3):
1879
+ dependencies:
1880
+ '@cspotcode/source-map-support': 0.8.1
1881
+ '@tsconfig/node10': 1.0.12
1882
+ '@tsconfig/node12': 1.0.11
1883
+ '@tsconfig/node14': 1.0.3
1884
+ '@tsconfig/node16': 1.0.4
1885
+ '@types/node': 25.0.10
1886
+ acorn: 8.15.0
1887
+ acorn-walk: 8.3.4
1888
+ arg: 4.1.3
1889
+ create-require: 1.1.1
1890
+ diff: 4.0.4
1891
+ make-error: 1.3.6
1892
+ typescript: 5.9.3
1893
+ v8-compile-cache-lib: 3.0.1
1894
+ yn: 3.1.1
1895
+
1896
+ type-is@2.0.1:
1897
+ dependencies:
1898
+ content-type: 1.0.5
1899
+ media-typer: 1.1.0
1900
+ mime-types: 3.0.2
1901
+
1902
+ typescript@5.9.3: {}
1903
+
1904
+ undefsafe@2.0.5: {}
1905
+
1906
+ undici-types@7.16.0: {}
1907
+
1908
+ undici@7.19.1: {}
1909
+
1910
+ unpipe@1.0.0: {}
1911
+
1912
+ v8-compile-cache-lib@3.0.1: {}
1913
+
1914
+ vary@1.1.2: {}
1915
+
1916
+ whatwg-encoding@3.1.1:
1917
+ dependencies:
1918
+ iconv-lite: 0.6.3
1919
+
1920
+ whatwg-mimetype@4.0.0: {}
1921
+
1922
+ which@2.0.2:
1923
+ dependencies:
1924
+ isexe: 2.0.0
1925
+
1926
+ wrappy@1.0.2: {}
1927
+
1928
+ yaml@2.8.2: {}
1929
+
1930
+ yn@3.1.1: {}
1931
+
1932
+ zod-to-json-schema@3.25.1(zod@4.3.6):
1933
+ dependencies:
1934
+ zod: 4.3.6
1935
+
1936
+ zod@4.3.6: {}
src/app.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Fastify, { FastifyRequest, FastifyReply } from "fastify";
2
+ import cors from "@fastify/cors";
3
+ import swagger from "@fastify/swagger";
4
+ import swaggerUi from "@fastify/swagger-ui";
5
+ import config from "./config/env";
6
+
7
+ import { leetcodePlugin } from "./modules/leetcode";
8
+ import { codeforcesPlugin } from "./modules/codeforces";
9
+ import { codechefPlugin } from "./modules/codechef";
10
+ import { atcoderPlugin } from "./modules/atcoder";
11
+ import { gfgPlugin } from "./modules/gfg";
12
+ import { ratingsPlugin } from "./modules/ratings";
13
+ import { mcpPlugin } from "./modules/mcp";
14
+
15
+ export async function buildApp() {
16
+ const fastify = Fastify({
17
+ logger: true,
18
+ });
19
+
20
+ fastify.setErrorHandler((error: any, request, reply) => {
21
+ fastify.log.error(error);
22
+ const statusCode = error.statusCode || 500;
23
+ reply.status(statusCode).send({
24
+ success: false,
25
+ error: error.name || 'InternalServerError',
26
+ message: error.message || 'An unexpected error occurred',
27
+ });
28
+ });
29
+
30
+ await fastify.register(cors, {
31
+ exposedHeaders: ['WWW-Authenticate', 'Mcp-Session-Id', 'Last-Event-Id', 'Mcp-Protocol-Version'],
32
+ origin: '*',
33
+ });
34
+
35
+ await fastify.register(swagger, {
36
+ openapi: {
37
+ info: {
38
+ title: "Vortex",
39
+ description: "A high-performance modular API to fetch competitive programming contest ratings and user statistics from platforms like LeetCode, Codeforces, CodeChef, and more. Built with Fastify and TypeScript.",
40
+ version: "1.0.0",
41
+ contact: {
42
+ name: "GitHub",
43
+ url: "https://github.com/Anujjoshi3105/vortex",
44
+ },
45
+ license: {
46
+ name: "ISC",
47
+ url: "https://opensource.org/licenses/ISC",
48
+ },
49
+ },
50
+ servers: [
51
+ {
52
+ url: `http://localhost:${config.port}`,
53
+ description: 'Development server',
54
+ },
55
+ ],
56
+ tags: [
57
+ { name: 'Default', description: 'General server health and infrastructure endpoints' },
58
+ { name: 'MCP', description: 'Model Context Protocol endpoints for AI agent integration' },
59
+ { name: 'Ratings', description: 'Cross-platform rating aggregation and comparison' },
60
+ { name: 'LeetCode - User', description: 'Fetch user profiles, badges, solved statistics, and submission history' },
61
+ { name: 'LeetCode - Contests', description: 'Access contest rankings, history, and upcoming competition data' },
62
+ { name: 'LeetCode - Problems', description: 'Retrieve daily challenges, problem details, and official solutions' },
63
+ { name: 'LeetCode - Discussion', description: 'Explore trending topics and community comments' },
64
+ { name: 'Codeforces - User', description: 'Fetch user profiles, ratings, contest history, and blogs' },
65
+ { name: 'Codeforces - Contests', description: 'Access contest standings, hacks, and rating changes' },
66
+ { name: 'Codeforces - Problems', description: 'Retrieve problemset and recent platform submissions' },
67
+ { name: 'Codeforces - Blog', description: 'Explore blog entries and community comments' },
68
+ { name: 'CodeChef', description: 'CodeChef platform integration' },
69
+ { name: 'AtCoder', description: 'AtCoder platform integration' },
70
+ { name: 'GFG', description: 'GeeksforGeeks platform integration for user profiles, submissions, posts, and contest leaderboards' },
71
+ ],
72
+ },
73
+ });
74
+
75
+ await fastify.register(swaggerUi, {
76
+ routePrefix: "/docs",
77
+ uiConfig: {
78
+ docExpansion: 'list',
79
+ deepLinking: true,
80
+ filter: true,
81
+ },
82
+ staticCSP: true,
83
+ transformStaticCSP: (header) => header,
84
+ });
85
+
86
+ fastify.get("/health", { schema: { tags: ['Default'] } }, async (request: FastifyRequest, reply: FastifyReply) => {
87
+ return { status: "ok" };
88
+ });
89
+ await fastify.register(mcpPlugin);
90
+ await fastify.register(ratingsPlugin, { prefix: "/api/v1/ratings" });
91
+ await fastify.register(leetcodePlugin, { prefix: "/api/v1/leetcode" });
92
+ await fastify.register(codeforcesPlugin, { prefix: "/api/v1/codeforces" });
93
+ await fastify.register(codechefPlugin, { prefix: "/api/v1/codechef" });
94
+ await fastify.register(atcoderPlugin, { prefix: "/api/v1/atcoder" });
95
+ await fastify.register(gfgPlugin, { prefix: "/api/v1/gfg" });
96
+
97
+
98
+ return fastify;
99
+ }
src/config/env.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dotenv from 'dotenv';
2
+
3
+ dotenv.config();
4
+
5
+ export const config = {
6
+ port: Number(process.env.PORT) || 3000,
7
+ host: process.env.HOST || '0.0.0.0',
8
+ nodeEnv: process.env.NODE_ENV || 'development',
9
+ } as const;
10
+
11
+ export default config;
src/modules/atcoder/constants.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function mapRating(rating: number): string {
2
+ if (rating >= 2800) return 'Red';
3
+ if (rating >= 2400) return 'Orange';
4
+ if (rating >= 2000) return 'Yellow';
5
+ if (rating >= 1600) return 'Blue';
6
+ if (rating >= 1200) return 'Cyan';
7
+ if (rating >= 800) return 'Green';
8
+ if (rating >= 400) return 'Brown';
9
+ return 'Gray';
10
+ }
11
+
12
+ export const ATCODER_BASE_URL = 'https://atcoder.jp';
13
+
14
+ export const ATCODER_SELECTORS = {
15
+ AVATAR: '.avatar',
16
+ USERNAME: '.username',
17
+ KYU: 'h3 b',
18
+ } as const;
19
+
20
+ export const ATCODER_LABELS = {
21
+ RATING: 'Rating',
22
+ MAX_RATING: 'Highest Rating',
23
+ RANK: 'Rank',
24
+ RATED_MATCHES: 'Rated Matches',
25
+ LAST_COMPETED: 'Last Competed',
26
+ COUNTRY: 'Country/Region',
27
+ BIRTH_YEAR: 'Birth Year',
28
+ } as const;
src/modules/atcoder/handlers.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import * as service from './service';
3
+ import type { UserQuery, ContestQuery } from './types';
4
+
5
+ export async function getUserRatingHandler(
6
+ request: FastifyRequest<{ Querystring: UserQuery }>,
7
+ reply: FastifyReply
8
+ ) {
9
+ const { username } = request.query;
10
+ const data = await service.getUserRating(username);
11
+ return reply.send(data);
12
+ }
13
+
14
+ export async function getUserHistoryHandler(
15
+ request: FastifyRequest<{ Querystring: UserQuery }>,
16
+ reply: FastifyReply
17
+ ) {
18
+ const { username } = request.query;
19
+ const data = await service.getUserHistory(username);
20
+ return reply.send(data);
21
+ }
22
+
23
+ export async function getContestStandingsHandler(
24
+ request: FastifyRequest<{ Querystring: ContestQuery & { extended?: boolean } }>,
25
+ reply: FastifyReply
26
+ ) {
27
+ const { contestId, extended } = request.query;
28
+ const data = await service.getContestStandings(contestId, extended);
29
+ return reply.send(data);
30
+ }
31
+
32
+ export async function getContestResultsHandler(
33
+ request: FastifyRequest<{ Querystring: ContestQuery }>,
34
+ reply: FastifyReply
35
+ ) {
36
+ const { contestId } = request.query;
37
+ const data = await service.getContestResults(contestId);
38
+ return reply.send(data);
39
+ }
40
+
41
+ export async function getVirtualStandingsHandler(
42
+ request: FastifyRequest<{ Querystring: ContestQuery & { showGhost?: boolean } }>,
43
+ reply: FastifyReply
44
+ ) {
45
+ const { contestId, showGhost } = request.query;
46
+ const data = await service.getVirtualStandings(contestId, showGhost);
47
+ return reply.send(data);
48
+ }
src/modules/atcoder/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { default as atcoderPlugin } from './routes';
2
+ export * from './types';
src/modules/atcoder/provider.ts ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { httpClient } from '../../shared/utils/http-client';
2
+ import * as cheerio from 'cheerio';
3
+ import { UserHistory, ContestStandings, AtCoderUserRating, ContestResult } from './types';
4
+ import { ATCODER_BASE_URL, ATCODER_SELECTORS, ATCODER_LABELS } from './constants';
5
+
6
+ export async function fetchUserRating(username: string): Promise<AtCoderUserRating> {
7
+ const url = `${ATCODER_BASE_URL}/users/${username}`;
8
+ try {
9
+ const { data } = await httpClient.get(url);
10
+ const $ = cheerio.load(data);
11
+
12
+ // Check for 404 or "User not found"
13
+ if ($('title').text().includes('404') || $('body').text().includes('User not found')) {
14
+ throw new Error(`User '${username}' not found on AtCoder`);
15
+ }
16
+
17
+ const extractText = (label: string) => {
18
+ const th = $(`th:contains('${label}')`);
19
+ if (th.length === 0) return null;
20
+ return th
21
+ .next('td')
22
+ .text()
23
+ .replace(/\s+/g, ' ')
24
+ .trim();
25
+ };
26
+
27
+ const rawRating = extractText(ATCODER_LABELS.RATING);
28
+ if (rawRating === null) {
29
+ throw new Error('AtCoder schema change detected: Rating label not found');
30
+ }
31
+
32
+ const rating = parseInt(rawRating.split(' ')[0]) || 0;
33
+ const max_rating = parseInt(extractText(ATCODER_LABELS.MAX_RATING)?.split(' ')[0] || '0') || 0;
34
+ const rank = extractText(ATCODER_LABELS.RANK) || 'N/A';
35
+ const contests_participated = parseInt(extractText(ATCODER_LABELS.RATED_MATCHES) || '0') || 0;
36
+ const last_competed = extractText(ATCODER_LABELS.LAST_COMPETED) || 'N/A';
37
+ const country = extractText(ATCODER_LABELS.COUNTRY) || 'N/A';
38
+ const birth_year = extractText(ATCODER_LABELS.BIRTH_YEAR) || 'N/A';
39
+
40
+ const avatarAttr = $(ATCODER_SELECTORS.AVATAR).attr('src');
41
+ const avatar = avatarAttr
42
+ ? avatarAttr.startsWith('//') ? 'https:' + avatarAttr : avatarAttr
43
+ : '';
44
+ const display_name = $(ATCODER_SELECTORS.USERNAME).first().text().trim();
45
+ const kyu = $(ATCODER_SELECTORS.KYU).first().text().trim();
46
+
47
+ // Fetch rating history from the direct JSON endpoint
48
+ let rating_history: UserHistory[] = [];
49
+ try {
50
+ rating_history = await fetchUserHistory(username);
51
+ } catch (e) {
52
+ // Fallback to scraping if JSON endpoint fails
53
+ $('script').each((i, script) => {
54
+ const content = $(script).text();
55
+ if (content.includes('var rating_history =')) {
56
+ const match = content.match(/var rating_history\s*=\s*(\[.*?\]);/);
57
+ if (match) {
58
+ try {
59
+ rating_history = JSON.parse(match[1]);
60
+ } catch (err) { }
61
+ }
62
+ }
63
+ });
64
+ }
65
+
66
+ return {
67
+ rating,
68
+ max_rating,
69
+ rank,
70
+ contests_participated,
71
+ last_competed,
72
+ country,
73
+ birth_year,
74
+ avatar,
75
+ display_name: display_name || username,
76
+ kyu,
77
+ rating_history,
78
+ };
79
+ } catch (error: any) {
80
+ if (error.response?.status === 404) {
81
+ throw new Error(`User '${username}' not found on AtCoder`);
82
+ }
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ export async function fetchUserHistory(username: string): Promise<UserHistory[]> {
88
+ const url = `https://atcoder.jp/users/${username}/history/json`;
89
+ const { data } = await httpClient.get(url);
90
+ return data;
91
+ }
92
+
93
+ export async function fetchContestStandings(contestId: string, extended: boolean = false): Promise<ContestStandings> {
94
+ const url = `https://atcoder.jp/contests/${contestId}/standings/${extended ? 'extended/' : ''}json`;
95
+ const { data } = await httpClient.get(url);
96
+ return data;
97
+ }
98
+
99
+ export async function fetchContestResults(contestId: string): Promise<ContestResult[]> {
100
+ const url = `https://atcoder.jp/contests/${contestId}/results/json`;
101
+ const { data } = await httpClient.get(url);
102
+ return data;
103
+ }
104
+
105
+ export async function fetchVirtualStandings(contestId: string, showGhost: boolean = true): Promise<ContestStandings> {
106
+ const url = `https://atcoder.jp/contests/${contestId}/standings/virtual/json?showGhost=${showGhost}`;
107
+ const { data } = await httpClient.get(url);
108
+ return data;
109
+ }
src/modules/atcoder/routes.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import * as handlers from './handlers';
3
+ import * as schemas from './schemas';
4
+ import validateUsername from '../../shared/middlewares/validate';
5
+ import type { UserQuery, ContestQuery } from './types';
6
+
7
+ const atcoderRoutes: FastifyPluginAsync = async (fastify) => {
8
+ fastify.get<{ Querystring: UserQuery }>(
9
+ '/rating',
10
+ {
11
+ preHandler: [validateUsername],
12
+ schema: schemas.userRatingSchema,
13
+ },
14
+ handlers.getUserRatingHandler
15
+ );
16
+
17
+ fastify.get<{ Querystring: UserQuery }>(
18
+ '/history',
19
+ {
20
+ preHandler: [validateUsername],
21
+ schema: schemas.userHistorySchema,
22
+ },
23
+ handlers.getUserHistoryHandler
24
+ );
25
+
26
+ fastify.get<{ Querystring: ContestQuery & { extended?: boolean } }>(
27
+ '/standings',
28
+ {
29
+ schema: schemas.contestStandingsSchema,
30
+ },
31
+ handlers.getContestStandingsHandler
32
+ );
33
+
34
+ fastify.get<{ Querystring: ContestQuery }>(
35
+ '/results',
36
+ {
37
+ schema: schemas.contestResultsSchema,
38
+ },
39
+ handlers.getContestResultsHandler
40
+ );
41
+
42
+ fastify.get<{ Querystring: ContestQuery & { showGhost?: boolean } }>(
43
+ '/virtual-standings',
44
+ {
45
+ schema: schemas.virtualStandingsSchema,
46
+ },
47
+ handlers.getVirtualStandingsHandler
48
+ );
49
+ };
50
+
51
+ export default atcoderRoutes;
src/modules/atcoder/schemas.ts ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const userRatingSchema = {
2
+ summary: 'Get User Rating',
3
+ description: 'Fetches AtCoder user rating, rank, and platform details',
4
+ tags: ['AtCoder'],
5
+ querystring: {
6
+ type: 'object',
7
+ properties: {
8
+ username: { type: 'string', description: 'AtCoder username' },
9
+ },
10
+ required: ['username'],
11
+ },
12
+ response: {
13
+ 200: {
14
+ type: 'object',
15
+ properties: {
16
+ username: { type: 'string' },
17
+ display_name: { type: 'string' },
18
+ platform: { type: 'string' },
19
+ rating: { type: 'number' },
20
+ max_rating: { type: 'number' },
21
+ level: { type: 'string' },
22
+ rank: { type: 'string' },
23
+ contests_participated: { type: 'number' },
24
+ last_competed: { type: 'string' },
25
+ kyu: { type: 'string' },
26
+ country: { type: 'string' },
27
+ birth_year: { type: 'string' },
28
+ avatar: { type: 'string' },
29
+ rating_history: {
30
+ type: 'array',
31
+ items: {
32
+ type: 'object',
33
+ properties: {
34
+ IsRated: { type: 'boolean' },
35
+ Place: { type: 'number' },
36
+ OldRating: { type: 'number' },
37
+ NewRating: { type: 'number' },
38
+ Performance: { type: 'number' },
39
+ InnerPerformance: { type: 'number' },
40
+ ContestScreenName: { type: 'string' },
41
+ ContestName: { type: 'string' },
42
+ ContestNameEn: { type: 'string' },
43
+ EndTime: { type: 'string' },
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ };
51
+
52
+ export const userHistorySchema = {
53
+ summary: 'Get User History',
54
+ description: 'Fetches AtCoder user rating history directly from the JSON endpoint',
55
+ tags: ['AtCoder'],
56
+ querystring: {
57
+ type: 'object',
58
+ properties: {
59
+ username: { type: 'string', description: 'AtCoder username' },
60
+ },
61
+ required: ['username'],
62
+ },
63
+ response: {
64
+ 200: {
65
+ type: 'array',
66
+ items: {
67
+ type: 'object',
68
+ properties: {
69
+ IsRated: { type: 'boolean' },
70
+ Place: { type: 'number' },
71
+ OldRating: { type: 'number' },
72
+ NewRating: { type: 'number' },
73
+ Performance: { type: 'number' },
74
+ InnerPerformance: { type: 'number' },
75
+ ContestScreenName: { type: 'string' },
76
+ ContestName: { type: 'string' },
77
+ ContestNameEn: { type: 'string' },
78
+ EndTime: { type: 'string' },
79
+ }
80
+ }
81
+ }
82
+ }
83
+ };
84
+
85
+ export const contestStandingsSchema = {
86
+ summary: 'Get Contest Standings',
87
+ description: 'Fetches AtCoder contest standings in JSON format',
88
+ tags: ['AtCoder'],
89
+ querystring: {
90
+ type: 'object',
91
+ properties: {
92
+ contestId: { type: 'string', description: 'AtCoder contest ID (e.g., abc300)' },
93
+ extended: { type: 'boolean', description: 'Whether to fetch extended standings' },
94
+ },
95
+ required: ['contestId'],
96
+ },
97
+ response: {
98
+ 200: {
99
+ type: 'object',
100
+ additionalProperties: true
101
+ }
102
+ }
103
+ };
104
+
105
+ export const contestResultsSchema = {
106
+ summary: 'Get Contest Results',
107
+ description: 'Fetches AtCoder contest results in JSON format',
108
+ tags: ['AtCoder'],
109
+ querystring: {
110
+ type: 'object',
111
+ properties: {
112
+ contestId: { type: 'string', description: 'AtCoder contest ID' },
113
+ },
114
+ required: ['contestId'],
115
+ },
116
+ response: {
117
+ 200: {
118
+ type: 'object',
119
+ additionalProperties: true
120
+ }
121
+ }
122
+ };
123
+
124
+ export const virtualStandingsSchema = {
125
+ summary: 'Get Virtual Standings',
126
+ description: 'Fetches AtCoder virtual standings in JSON format',
127
+ tags: ['AtCoder'],
128
+ querystring: {
129
+ type: 'object',
130
+ properties: {
131
+ contestId: { type: 'string', description: 'AtCoder contest ID' },
132
+ showGhost: { type: 'boolean', description: 'Whether to show ghost entries' },
133
+ },
134
+ required: ['contestId'],
135
+ },
136
+ response: {
137
+ 200: {
138
+ type: 'object',
139
+ additionalProperties: true
140
+ }
141
+ }
142
+ };
143
+
src/modules/atcoder/service.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { mapRating } from './constants';
2
+ import * as provider from './provider';
3
+ import type { AtCoderRating, UserHistory, ContestStandings } from './types';
4
+
5
+ export async function getUserRating(username: string): Promise<AtCoderRating> {
6
+ try {
7
+ const data = await provider.fetchUserRating(username);
8
+ return {
9
+ username,
10
+ display_name: data.display_name,
11
+ platform: 'atcoder',
12
+ rating: data.rating,
13
+ max_rating: data.max_rating,
14
+ level: mapRating(data.rating),
15
+ rank: data.rank,
16
+ contests_participated: data.contests_participated,
17
+ last_competed: data.last_competed,
18
+ kyu: data.kyu,
19
+ country: data.country,
20
+ birth_year: data.birth_year,
21
+ avatar: data.avatar,
22
+ };
23
+ } catch (error: any) {
24
+ console.error(`AtCoder Error for ${username}:`, error.message);
25
+ throw new Error('Failed to fetch AtCoder user data');
26
+ }
27
+ }
28
+
29
+ export async function getUserHistory(username: string): Promise<UserHistory[]> {
30
+ try {
31
+ return await provider.fetchUserHistory(username);
32
+ } catch (error: any) {
33
+ console.error(`AtCoder History Error for ${username}:`, error.message);
34
+ throw new Error('Failed to fetch AtCoder user history');
35
+ }
36
+ }
37
+
38
+ export async function getContestStandings(contestId: string, extended: boolean = false): Promise<ContestStandings> {
39
+ try {
40
+ return await provider.fetchContestStandings(contestId, extended);
41
+ } catch (error: any) {
42
+ console.error(`AtCoder Standings Error for ${contestId}:`, error.message);
43
+ throw new Error('Failed to fetch AtCoder contest standings');
44
+ }
45
+ }
46
+
47
+ export async function getContestResults(contestId: string): Promise<any> {
48
+ try {
49
+ return await provider.fetchContestResults(contestId);
50
+ } catch (error: any) {
51
+ console.error(`AtCoder Results Error for ${contestId}:`, error.message);
52
+ throw new Error('Failed to fetch AtCoder contest results');
53
+ }
54
+ }
55
+
56
+ export async function getVirtualStandings(contestId: string, showGhost: boolean = true): Promise<any> {
57
+ try {
58
+ return await provider.fetchVirtualStandings(contestId, showGhost);
59
+ } catch (error: any) {
60
+ console.error(`AtCoder Virtual Standings Error for ${contestId}:`, error.message);
61
+ throw new Error('Failed to fetch AtCoder virtual standings');
62
+ }
63
+ }
src/modules/atcoder/types.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface UserHistory {
2
+ IsRated: boolean;
3
+ Place: number;
4
+ OldRating: number;
5
+ NewRating: number;
6
+ Performance: number;
7
+ InnerPerformance: number;
8
+ ContestScreenName: string;
9
+ ContestName: string;
10
+ ContestNameEn: string;
11
+ EndTime: string;
12
+ }
13
+
14
+ export interface AtCoderRating {
15
+ username: string;
16
+ display_name: string;
17
+ platform: string;
18
+ rating: number;
19
+ max_rating: number;
20
+ level: string;
21
+ rank: string;
22
+ contests_participated: number;
23
+ last_competed: string;
24
+ kyu: string;
25
+ country: string;
26
+ birth_year: string;
27
+ avatar: string;
28
+ }
29
+
30
+ export interface UserQuery {
31
+ username: string;
32
+ }
33
+
34
+ export interface ContestQuery {
35
+ contestId: string;
36
+ }
37
+
38
+ export interface AtCoderUserRating {
39
+ rating: number;
40
+ max_rating: number;
41
+ rank: string;
42
+ contests_participated: number;
43
+ last_competed: string;
44
+ country: string;
45
+ birth_year: string;
46
+ avatar: string;
47
+ display_name: string;
48
+ kyu: string;
49
+ rating_history: UserHistory[];
50
+ }
51
+
52
+ export interface ContestResult {
53
+ IsRated: boolean;
54
+ Place: number;
55
+ OldRating: number;
56
+ NewRating: number;
57
+ Performance: number;
58
+ InnerPerformance: number;
59
+ ContestScreenName: string;
60
+ ContestName: string;
61
+ ContestNameEn: string;
62
+ EndTime: string;
63
+ }
64
+
65
+ export interface StandingData {
66
+ Rank: number;
67
+ Additional: any;
68
+ UserScreenName: string;
69
+ UserDisplayName: string;
70
+ IsRated: boolean;
71
+ Rating: number;
72
+ OldRating: number;
73
+ TotalResult: {
74
+ Count: number;
75
+ Score: number;
76
+ Elapsed: number;
77
+ Penalty: number;
78
+ };
79
+ TaskResults: {
80
+ [key: string]: {
81
+ Count: number;
82
+ Score: number;
83
+ Elapsed: number;
84
+ Status: number;
85
+ Pending: boolean;
86
+ };
87
+ };
88
+ }
89
+
90
+ export interface ContestStandings {
91
+ Fixed: boolean;
92
+ AdditionalColumns: any[];
93
+ TaskInfo: any[];
94
+ StandingsData: StandingData[];
95
+ }
src/modules/codechef/constants.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function mapRating(rating: number): string {
2
+ if (rating >= 2500) return '7 star';
3
+ if (rating >= 2200) return '6 star';
4
+ if (rating >= 2000) return '5 star';
5
+ if (rating >= 1800) return '4 star';
6
+ if (rating >= 1600) return '3 star';
7
+ if (rating >= 1400) return '2 star';
8
+ return '1 star';
9
+ }
10
+
11
+ export const CODECHEF_BASE_URL = 'https://www.codechef.com/users/';
12
+
13
+ export const CODECHEF_SELECTORS = {
14
+ RATING: '.rating-number',
15
+ MAX_RATING: '.rating-header small',
16
+ } as const;
src/modules/codechef/handlers.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import * as service from './service';
3
+ import type { UserQuery } from './types';
4
+
5
+ export async function getUserRatingHandler(
6
+ request: FastifyRequest<{ Querystring: UserQuery }>,
7
+ reply: FastifyReply
8
+ ) {
9
+ const { username } = request.query;
10
+ const data = await service.getUserRating(username);
11
+ return reply.send(data);
12
+ }
src/modules/codechef/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { default as codechefPlugin } from './routes';
2
+ export * from './types';
src/modules/codechef/provider.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { httpClient } from '../../shared/utils/http-client';
2
+ import * as cheerio from 'cheerio';
3
+ import { CodeChefUserRating } from './types';
4
+ import { CODECHEF_BASE_URL, CODECHEF_SELECTORS } from './constants';
5
+
6
+ export async function fetchUserRating(username: string): Promise<CodeChefUserRating> {
7
+ const url = `${CODECHEF_BASE_URL}${username}`;
8
+ try {
9
+ const { data } = await httpClient.get(url);
10
+ const $ = cheerio.load(data);
11
+
12
+ // Check if user exists on page (CodeChef usually shows a specific page for missing users or 404s)
13
+ if ($('body').text().includes('not found') || $('title').text().includes('404')) {
14
+ throw new Error(`User '${username}' not found on CodeChef`);
15
+ }
16
+
17
+ const ratingElement = $(CODECHEF_SELECTORS.RATING).first();
18
+ if (ratingElement.length === 0) {
19
+ throw new Error('CodeChef schema change detected: Rating selector not found');
20
+ }
21
+
22
+ const ratingText = ratingElement.text().trim();
23
+ const rating = parseInt(ratingText);
24
+
25
+ const maxRatingElement = $(CODECHEF_SELECTORS.MAX_RATING).first();
26
+ const maxRatingText = maxRatingElement.text().match(/\d+/)?.[0];
27
+ const max_rating = maxRatingText ? parseInt(maxRatingText) : undefined;
28
+
29
+ if (isNaN(rating)) {
30
+ throw new Error('Could not parse CodeChef rating. Schema might have changed.');
31
+ }
32
+
33
+ return { rating, max_rating };
34
+ } catch (error: any) {
35
+ if (error.response?.status === 404) {
36
+ throw new Error(`User '${username}' not found on CodeChef`);
37
+ }
38
+ throw error;
39
+ }
40
+ }
src/modules/codechef/routes.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import * as handlers from './handlers';
3
+ import * as schemas from './schemas';
4
+ import validateUsername from '../../shared/middlewares/validate';
5
+ import type { UserQuery } from './types';
6
+
7
+ const codechefRoutes: FastifyPluginAsync = async (fastify) => {
8
+ fastify.get<{ Querystring: UserQuery }>(
9
+ '/rating',
10
+ {
11
+ preHandler: [validateUsername],
12
+ schema: schemas.userRatingSchema,
13
+ },
14
+ handlers.getUserRatingHandler
15
+ );
16
+ };
17
+
18
+ export default codechefRoutes;
src/modules/codechef/schemas.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const userRatingSchema = {
2
+ summary: 'Get User Rating',
3
+ description: 'Fetches CodeChef user rating and platform details',
4
+ tags: ['CodeChef'],
5
+ querystring: {
6
+ type: 'object',
7
+ properties: {
8
+ username: { type: 'string', description: 'CodeChef username' },
9
+ },
10
+ required: ['username'],
11
+ },
12
+ response: {
13
+ 200: {
14
+ type: 'object',
15
+ properties: {
16
+ username: { type: 'string' },
17
+ platform: { type: 'string' },
18
+ rating: { type: 'number' }
19
+ }
20
+ }
21
+ }
22
+ };
23
+
src/modules/codechef/service.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { mapRating } from './constants';
2
+ import * as provider from './provider';
3
+ import type { CodeChefRating } from './types';
4
+
5
+ export async function getUserRating(username: string): Promise<CodeChefRating> {
6
+ try {
7
+ const data = await provider.fetchUserRating(username);
8
+
9
+ return {
10
+ username,
11
+ platform: 'codechef',
12
+ rating: data.rating,
13
+ level: mapRating(data.rating),
14
+ max_rating: data.max_rating,
15
+ };
16
+ } catch (error: any) {
17
+ console.error(`CodeChef Error for ${username}:`, error.message);
18
+ throw new Error('Error fetching CodeChef rating');
19
+ }
20
+ }
src/modules/codechef/types.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface CodeChefRating {
2
+ username: string;
3
+ platform: string;
4
+ rating: number | string;
5
+ level: string;
6
+ max_rating?: number | string;
7
+ }
8
+
9
+ export interface UserQuery {
10
+ username: string;
11
+ }
12
+ export interface CodeChefUserRating {
13
+ rating: number;
14
+ max_rating?: number;
15
+ }
src/modules/codeforces/handlers.ts ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import * as service from './service';
3
+ import type {
4
+ UserQuery,
5
+ StatusQuery,
6
+ ProblemsetQuery,
7
+ RecentActionsQuery,
8
+ ContestQuery,
9
+ StandingsQuery,
10
+ ContestStatusQuery,
11
+ BlogEntryQuery,
12
+ RecentStatusQuery
13
+ } from './types';
14
+
15
+ export async function getUserRatingHandler(
16
+ request: FastifyRequest<{ Querystring: UserQuery }>,
17
+ reply: FastifyReply
18
+ ) {
19
+ const { username } = request.query;
20
+ const data = await service.getUserRating(username);
21
+ return reply.send(data);
22
+ }
23
+
24
+ export async function getContestHistoryHandler(
25
+ request: FastifyRequest<{ Querystring: UserQuery }>,
26
+ reply: FastifyReply
27
+ ) {
28
+ const { username } = request.query;
29
+ const data = await service.getContestHistory(username);
30
+ return reply.send(data);
31
+ }
32
+
33
+ export async function getUserStatusHandler(
34
+ request: FastifyRequest<{ Querystring: StatusQuery }>,
35
+ reply: FastifyReply
36
+ ) {
37
+ const { username, from = 1, count = 10 } = request.query;
38
+ const data = await service.getUserStatus(username, Number(from), Number(count));
39
+ return reply.send(data);
40
+ }
41
+
42
+ export async function getUserBlogsHandler(
43
+ request: FastifyRequest<{ Querystring: UserQuery }>,
44
+ reply: FastifyReply
45
+ ) {
46
+ const { username } = request.query;
47
+ const data = await service.getUserBlogs(username);
48
+ return reply.send(data);
49
+ }
50
+
51
+ export async function getSolvedProblemsHandler(
52
+ request: FastifyRequest<{ Querystring: UserQuery }>,
53
+ reply: FastifyReply
54
+ ) {
55
+ const { username } = request.query;
56
+ const data = await service.getSolvedProblems(username);
57
+ return reply.send(data);
58
+ }
59
+
60
+ export async function getContestsHandler(
61
+ request: FastifyRequest<{ Querystring: { gym?: boolean } }>,
62
+ reply: FastifyReply
63
+ ) {
64
+ const { gym = false } = request.query;
65
+ const data = await service.getContests(gym);
66
+ return reply.send(data);
67
+ }
68
+
69
+ export async function getRecentActionsHandler(
70
+ request: FastifyRequest<{ Querystring: RecentActionsQuery }>,
71
+ reply: FastifyReply
72
+ ) {
73
+ const { maxCount = 20 } = request.query;
74
+ const data = await service.getRecentActions(Number(maxCount));
75
+ return reply.send(data);
76
+ }
77
+
78
+ export async function getProblemsHandler(
79
+ request: FastifyRequest<{ Querystring: ProblemsetQuery }>,
80
+ reply: FastifyReply
81
+ ) {
82
+ const { tags } = request.query;
83
+ const data = await service.getProblems(tags);
84
+ return reply.send(data);
85
+ }
86
+
87
+ export async function getContestStandingsHandler(
88
+ request: FastifyRequest<{ Querystring: StandingsQuery }>,
89
+ reply: FastifyReply
90
+ ) {
91
+ const { contestId, from, count, handles, room, showUnofficial } = request.query;
92
+ const data = await service.getContestStandings(
93
+ Number(contestId),
94
+ from ? Number(from) : undefined,
95
+ count ? Number(count) : undefined,
96
+ handles,
97
+ room ? Number(room) : undefined,
98
+ showUnofficial
99
+ );
100
+ return reply.send(data);
101
+ }
102
+
103
+ export async function getContestRatingChangesHandler(
104
+ request: FastifyRequest<{ Querystring: ContestQuery }>,
105
+ reply: FastifyReply
106
+ ) {
107
+ const { contestId } = request.query;
108
+ const data = await service.getContestRatingChanges(Number(contestId));
109
+ return reply.send(data);
110
+ }
111
+
112
+ export async function getContestHacksHandler(
113
+ request: FastifyRequest<{ Querystring: ContestQuery }>,
114
+ reply: FastifyReply
115
+ ) {
116
+ const { contestId } = request.query;
117
+ const data = await service.getContestHacks(Number(contestId));
118
+ return reply.send(data);
119
+ }
120
+
121
+ export async function getContestStatusHandler(
122
+ request: FastifyRequest<{ Querystring: ContestStatusQuery }>,
123
+ reply: FastifyReply
124
+ ) {
125
+ const { contestId, handle, from, count } = request.query;
126
+ const data = await service.getContestStatus(
127
+ Number(contestId),
128
+ handle,
129
+ from ? Number(from) : undefined,
130
+ count ? Number(count) : undefined
131
+ );
132
+ return reply.send(data);
133
+ }
134
+
135
+ export async function getProblemsetRecentStatusHandler(
136
+ request: FastifyRequest<{ Querystring: RecentStatusQuery }>,
137
+ reply: FastifyReply
138
+ ) {
139
+ const { count = 10 } = request.query;
140
+ const data = await service.getProblemsetRecentStatus(Number(count));
141
+ return reply.send(data);
142
+ }
143
+
144
+ export async function getBlogEntryHandler(
145
+ request: FastifyRequest<{ Querystring: BlogEntryQuery }>,
146
+ reply: FastifyReply
147
+ ) {
148
+ const { blogEntryId } = request.query;
149
+ const data = await service.getBlogEntry(Number(blogEntryId));
150
+ return reply.send(data);
151
+ }
152
+
153
+ export async function getBlogCommentsHandler(
154
+ request: FastifyRequest<{ Querystring: BlogEntryQuery }>,
155
+ reply: FastifyReply
156
+ ) {
157
+ const { blogEntryId } = request.query;
158
+ const data = await service.getBlogComments(Number(blogEntryId));
159
+ return reply.send(data);
160
+ }
src/modules/codeforces/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { default as codeforcesPlugin } from './routes';
2
+ export * from './types';
src/modules/codeforces/provider.ts ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { httpClient } from '../../shared/utils/http-client';
2
+ import type {
3
+ CodeforcesUser,
4
+ CodeforcesContestHistory,
5
+ CodeforcesSubmission,
6
+ BlogEntry,
7
+ CodeforcesContest,
8
+ RecentAction,
9
+ CodeforcesProblem,
10
+ ContestStandings,
11
+ RatingChange,
12
+ Hack,
13
+ BlogEntryView,
14
+ BlogComment,
15
+ } from './types';
16
+
17
+ const CODEFORCES_API_BASE = 'https://codeforces.com/api';
18
+
19
+ // Fetch user information
20
+ export async function fetchUserInfo(username: string): Promise<CodeforcesUser> {
21
+ const url = `${CODEFORCES_API_BASE}/user.info?handles=${username}`;
22
+ const { data } = await httpClient.get(url);
23
+
24
+ if (data.status !== 'OK') {
25
+ throw new Error(data.comment || 'Error fetching user info');
26
+ }
27
+
28
+ return data.result[0];
29
+ }
30
+
31
+ // Fetch user's contest rating history
32
+ export async function fetchContestHistory(
33
+ username: string
34
+ ): Promise<CodeforcesContestHistory[]> {
35
+ const url = `${CODEFORCES_API_BASE}/user.rating?handle=${username}`;
36
+ const { data } = await httpClient.get(url);
37
+
38
+ if (data.status !== 'OK') {
39
+ throw new Error(data.comment || 'Error fetching contest history');
40
+ }
41
+
42
+ return data.result;
43
+ }
44
+
45
+ const MAX_CF_LIMIT = 1000;
46
+
47
+ // Fetch user's submission status
48
+ export async function fetchUserStatus(
49
+ username: string,
50
+ from: number = 1,
51
+ count: number = 10
52
+ ): Promise<CodeforcesSubmission[]> {
53
+ const safeCount = Math.min(count, MAX_CF_LIMIT);
54
+ const url = `${CODEFORCES_API_BASE}/user.status?handle=${username}&from=${from}&count=${safeCount}`;
55
+ const { data } = await httpClient.get(url);
56
+
57
+ if (data.status !== 'OK') {
58
+ throw new Error(data.comment || 'Error fetching user status');
59
+ }
60
+
61
+ return data.result;
62
+ }
63
+
64
+ // Fetch user's blog entries
65
+ export async function fetchBlogEntries(username: string): Promise<BlogEntry[]> {
66
+ const url = `${CODEFORCES_API_BASE}/user.blogEntries?handle=${username}`;
67
+ const { data } = await httpClient.get(url);
68
+
69
+ if (data.status !== 'OK') {
70
+ throw new Error(data.comment || 'Error fetching blog entries');
71
+ }
72
+
73
+ return data.result;
74
+ }
75
+
76
+ // Fetch user submissions with limit
77
+ export async function fetchAllSubmissions(
78
+ username: string,
79
+ from: number = 1,
80
+ count: number = 100
81
+ ): Promise<CodeforcesSubmission[]> {
82
+ const safeCount = Math.min(count, MAX_CF_LIMIT);
83
+ const url = `${CODEFORCES_API_BASE}/user.status?handle=${username}&from=${from}&count=${safeCount}`;
84
+ const { data } = await httpClient.get(url);
85
+
86
+ if (data.status !== 'OK') {
87
+ throw new Error(data.comment || 'Error fetching submissions');
88
+ }
89
+
90
+ return data.result;
91
+ }
92
+
93
+ // Fetch contests
94
+ export async function fetchContests(gym: boolean = false): Promise<CodeforcesContest[]> {
95
+ const url = `${CODEFORCES_API_BASE}/contest.list?gym=${gym}`;
96
+ const { data } = await httpClient.get(url);
97
+
98
+ if (data.status !== 'OK') {
99
+ throw new Error(data.comment || 'Error fetching contest list');
100
+ }
101
+
102
+ return data.result;
103
+ }
104
+
105
+ // Fetch recent actions
106
+ export async function fetchRecentActions(maxCount: number = 20): Promise<RecentAction[]> {
107
+ const url = `${CODEFORCES_API_BASE}/recentActions?maxCount=${maxCount}`;
108
+ const { data } = await httpClient.get(url);
109
+
110
+ if (data.status !== 'OK') {
111
+ throw new Error(data.comment || 'Error fetching recent actions');
112
+ }
113
+
114
+ return data.result;
115
+ }
116
+
117
+ // Fetch problemset problems
118
+ export async function fetchProblems(tags?: string): Promise<{ problems: CodeforcesProblem[] }> {
119
+ const url = tags
120
+ ? `${CODEFORCES_API_BASE}/problemset.problems?tags=${tags}`
121
+ : `${CODEFORCES_API_BASE}/problemset.problems`;
122
+ const { data } = await httpClient.get(url);
123
+
124
+ if (data.status !== 'OK') {
125
+ throw new Error(data.comment || 'Error fetching problemset');
126
+ }
127
+
128
+ return data.result;
129
+ }
130
+
131
+ // Fetch contest standings
132
+ export async function fetchContestStandings(
133
+ contestId: number,
134
+ from?: number,
135
+ count?: number,
136
+ handles?: string,
137
+ room?: number,
138
+ showUnofficial?: boolean
139
+ ): Promise<ContestStandings> {
140
+ let url = `${CODEFORCES_API_BASE}/contest.standings?contestId=${contestId}`;
141
+ if (from) url += `&from=${from}`;
142
+ if (count) url += `&count=${count}`;
143
+ if (handles) url += `&handles=${handles}`;
144
+ if (room) url += `&room=${room}`;
145
+ if (showUnofficial !== undefined) url += `&showUnofficial=${showUnofficial}`;
146
+
147
+ const { data } = await httpClient.get(url);
148
+ if (data.status !== 'OK') {
149
+ throw new Error(data.comment || 'Error fetching contest standings');
150
+ }
151
+ return data.result;
152
+ }
153
+
154
+ // Fetch contest rating changes
155
+ export async function fetchContestRatingChanges(contestId: number): Promise<RatingChange[]> {
156
+ const url = `${CODEFORCES_API_BASE}/contest.ratingChanges?contestId=${contestId}`;
157
+ const { data } = await httpClient.get(url);
158
+ if (data.status !== 'OK') {
159
+ throw new Error(data.comment || 'Error fetching rating changes');
160
+ }
161
+ return data.result;
162
+ }
163
+
164
+ // Fetch contest hacks
165
+ export async function fetchContestHacks(contestId: number): Promise<Hack[]> {
166
+ const url = `${CODEFORCES_API_BASE}/contest.hacks?contestId=${contestId}`;
167
+ const { data } = await httpClient.get(url);
168
+ if (data.status !== 'OK') {
169
+ throw new Error(data.comment || 'Error fetching contest hacks');
170
+ }
171
+ return data.result;
172
+ }
173
+
174
+ // Fetch contest status (submissions)
175
+ export async function fetchContestStatus(
176
+ contestId: number,
177
+ handle?: string,
178
+ from?: number,
179
+ count?: number
180
+ ): Promise<CodeforcesSubmission[]> {
181
+ let url = `${CODEFORCES_API_BASE}/contest.status?contestId=${contestId}`;
182
+ if (handle) url += `&handle=${handle}`;
183
+ if (from) url += `&from=${from}`;
184
+ if (count) url += `&count=${count}`;
185
+
186
+ const { data } = await httpClient.get(url);
187
+ if (data.status !== 'OK') {
188
+ throw new Error(data.comment || 'Error fetching contest status');
189
+ }
190
+ return data.result;
191
+ }
192
+
193
+ // Fetch problemset recent status
194
+ export async function fetchProblemsetRecentStatus(count: number): Promise<CodeforcesSubmission[]> {
195
+ const url = `${CODEFORCES_API_BASE}/problemset.recentStatus?count=${count}`;
196
+ const { data } = await httpClient.get(url);
197
+ if (data.status !== 'OK') {
198
+ throw new Error(data.comment || 'Error fetching recent status');
199
+ }
200
+ return data.result;
201
+ }
202
+
203
+ // Fetch blog entry content
204
+ export async function fetchBlogEntry(blogEntryId: number): Promise<BlogEntryView> {
205
+ const url = `${CODEFORCES_API_BASE}/blogEntry.view?blogEntryId=${blogEntryId}`;
206
+ const { data } = await httpClient.get(url);
207
+ if (data.status !== 'OK') {
208
+ throw new Error(data.comment || 'Error fetching blog entry');
209
+ }
210
+ return data.result;
211
+ }
212
+
213
+ // Fetch blog entry comments
214
+ export async function fetchBlogComments(blogEntryId: number): Promise<BlogComment[]> {
215
+ const url = `${CODEFORCES_API_BASE}/blogEntry.comments?blogEntryId=${blogEntryId}`;
216
+ const { data } = await httpClient.get(url);
217
+ if (data.status !== 'OK') {
218
+ throw new Error(data.comment || 'Error fetching blog comments');
219
+ }
220
+ return data.result;
221
+ }
src/modules/codeforces/routes.ts ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import * as handlers from './handlers';
3
+ import * as schemas from './schemas';
4
+ import validateUsername from '../../shared/middlewares/validate';
5
+ import type {
6
+ StatusQuery,
7
+ UserQuery,
8
+ ContestQuery,
9
+ StandingsQuery,
10
+ ContestStatusQuery,
11
+ BlogEntryQuery,
12
+ RecentStatusQuery
13
+ } from './types';
14
+
15
+ const codeforcesRoutes: FastifyPluginAsync = async (fastify) => {
16
+ // User routes (with username validation)
17
+ fastify.get<{ Querystring: UserQuery }>(
18
+ '/rating',
19
+ { preHandler: [validateUsername], schema: schemas.userRatingSchema },
20
+ handlers.getUserRatingHandler
21
+ );
22
+
23
+ fastify.get<{ Querystring: UserQuery }>(
24
+ '/contest-history',
25
+ { preHandler: [validateUsername], schema: schemas.contestHistorySchema },
26
+ handlers.getContestHistoryHandler
27
+ );
28
+
29
+ fastify.get<{ Querystring: StatusQuery }>(
30
+ '/status',
31
+ { preHandler: [validateUsername], schema: schemas.userStatusSchema },
32
+ handlers.getUserStatusHandler
33
+ );
34
+
35
+ fastify.get<{ Querystring: UserQuery }>(
36
+ '/blogs',
37
+ { preHandler: [validateUsername], schema: schemas.userBlogsSchema },
38
+ handlers.getUserBlogsHandler
39
+ );
40
+
41
+ fastify.get<{ Querystring: UserQuery }>(
42
+ '/solved-problems',
43
+ { preHandler: [validateUsername], schema: schemas.solvedProblemsSchema },
44
+ handlers.getSolvedProblemsHandler
45
+ );
46
+
47
+ // General platform routes
48
+ fastify.get('/contests', { schema: schemas.contestsSchema }, handlers.getContestsHandler);
49
+ fastify.get('/recent-actions', { schema: schemas.recentActionsSchema }, handlers.getRecentActionsHandler);
50
+ fastify.get('/problems', { schema: schemas.problemsSchema }, handlers.getProblemsHandler);
51
+
52
+ // Contest specific routes
53
+ fastify.get<{ Querystring: StandingsQuery }>(
54
+ '/contest/standings',
55
+ { schema: schemas.contestStandingsSchema },
56
+ handlers.getContestStandingsHandler
57
+ );
58
+
59
+ fastify.get<{ Querystring: ContestQuery }>(
60
+ '/contest/rating-changes',
61
+ { schema: schemas.contestRatingChangesSchema },
62
+ handlers.getContestRatingChangesHandler
63
+ );
64
+
65
+ fastify.get<{ Querystring: ContestQuery }>(
66
+ '/contest/hacks',
67
+ { schema: schemas.contestHacksSchema },
68
+ handlers.getContestHacksHandler
69
+ );
70
+
71
+ fastify.get<{ Querystring: ContestStatusQuery }>(
72
+ '/contest/status',
73
+ { schema: schemas.contestStatusSchema },
74
+ handlers.getContestStatusHandler
75
+ );
76
+
77
+ // Problemset routes
78
+ fastify.get<{ Querystring: RecentStatusQuery }>(
79
+ '/problemset/recent-status',
80
+ { schema: schemas.problemsetRecentStatusSchema },
81
+ handlers.getProblemsetRecentStatusHandler
82
+ );
83
+
84
+ // Blog routes
85
+ fastify.get<{ Querystring: BlogEntryQuery }>(
86
+ '/blog/view',
87
+ { schema: schemas.blogEntrySchema },
88
+ handlers.getBlogEntryHandler
89
+ );
90
+
91
+ fastify.get<{ Querystring: BlogEntryQuery }>(
92
+ '/blog/comments',
93
+ { schema: schemas.blogCommentsSchema },
94
+ handlers.getBlogCommentsHandler
95
+ );
96
+ };
97
+
98
+ export default codeforcesRoutes;
src/modules/codeforces/schemas.ts ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const userRatingSchema = {
2
+ summary: 'Get User Rating',
3
+ description: 'Fetches Codeforces user rating and rank details',
4
+ tags: ['Codeforces - User'],
5
+ querystring: {
6
+ type: 'object',
7
+ properties: {
8
+ username: { type: 'string', description: 'Codeforces handle' },
9
+ },
10
+ required: ['username'],
11
+ },
12
+ response: {
13
+ 200: {
14
+ type: 'object',
15
+ properties: {
16
+ username: { type: 'string' },
17
+ platform: { type: 'string' },
18
+ rating: { type: ['number', 'string'] },
19
+ level: { type: 'string' },
20
+ max_rating: { type: ['number', 'string'] },
21
+ max_level: { type: 'string' },
22
+ contribution: { type: 'number' },
23
+ friendOfCount: { type: 'number' },
24
+ avatar: { type: 'string' }
25
+ }
26
+ }
27
+ }
28
+ };
29
+
30
+ export const contestHistorySchema = {
31
+ summary: 'Get Contest History',
32
+ description: 'Fetches Codeforces contest participation history',
33
+ tags: ['Codeforces - User'],
34
+ querystring: {
35
+ type: 'object',
36
+ properties: {
37
+ username: { type: 'string', description: 'Codeforces handle' },
38
+ },
39
+ required: ['username'],
40
+ },
41
+ response: {
42
+ 200: {
43
+ type: 'array',
44
+ items: {
45
+ type: 'object',
46
+ properties: {
47
+ contestId: { type: 'number' },
48
+ contestName: { type: 'string' },
49
+ handle: { type: 'string' },
50
+ rank: { type: 'number' },
51
+ ratingUpdateTimeSeconds: { type: 'number' },
52
+ oldRating: { type: 'number' },
53
+ newRating: { type: 'number' }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ };
59
+
60
+ export const userStatusSchema = {
61
+ summary: 'Get User Status',
62
+ description: 'Fetches Codeforces user submission status/history',
63
+ tags: ['Codeforces - User'],
64
+ querystring: {
65
+ type: 'object',
66
+ properties: {
67
+ username: { type: 'string', description: 'Codeforces handle' },
68
+ from: { type: 'number', description: 'Starting index (1-based)', default: 1 },
69
+ count: { type: 'number', description: 'Number of submissions to fetch', default: 10 },
70
+ },
71
+ required: ['username'],
72
+ },
73
+ response: {
74
+ 200: {
75
+ type: 'array',
76
+ items: {
77
+ type: 'object',
78
+ properties: {
79
+ id: { type: 'number' },
80
+ contestId: { type: 'number' },
81
+ problem: {
82
+ type: 'object',
83
+ properties: {
84
+ contestId: { type: 'number' },
85
+ index: { type: 'string' },
86
+ name: { type: 'string' },
87
+ rating: { type: 'number' },
88
+ tags: { type: 'array', items: { type: 'string' } }
89
+ }
90
+ },
91
+ verdict: { type: 'string' },
92
+ programmingLanguage: { type: 'string' },
93
+ creationTimeSeconds: { type: 'number' }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ };
99
+
100
+ export const userBlogsSchema = {
101
+ summary: 'Get User Blogs',
102
+ description: 'Fetches blog posts written by a Codeforces user',
103
+ tags: ['Codeforces - User'],
104
+ querystring: {
105
+ type: 'object',
106
+ properties: {
107
+ username: { type: 'string', description: 'Codeforces handle' },
108
+ },
109
+ required: ['username'],
110
+ },
111
+ response: {
112
+ 200: {
113
+ type: 'array',
114
+ items: {
115
+ type: 'object',
116
+ properties: {
117
+ id: { type: 'number' },
118
+ title: { type: 'string' },
119
+ creationTimeSeconds: { type: 'number' },
120
+ rating: { type: 'number' }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ };
126
+
127
+ export const solvedProblemsSchema = {
128
+ summary: 'Get Solved Problems',
129
+ description: 'Fetches a list of problems solved by a Codeforces user',
130
+ tags: ['Codeforces - User'],
131
+ querystring: {
132
+ type: 'object',
133
+ properties: {
134
+ username: { type: 'string', description: 'Codeforces handle' },
135
+ },
136
+ required: ['username'],
137
+ },
138
+ response: {
139
+ 200: {
140
+ type: 'array',
141
+ items: {
142
+ type: 'object',
143
+ properties: {
144
+ id: { type: 'string' },
145
+ name: { type: 'string' },
146
+ rating: { type: 'number' },
147
+ tags: { type: 'array', items: { type: 'string' } },
148
+ link: { type: 'string' }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ };
154
+
155
+ export const contestsSchema = {
156
+ summary: 'Get Contests',
157
+ description: 'Fetches a list of Codeforces contests',
158
+ tags: ['Codeforces - Contests'],
159
+ querystring: {
160
+ type: 'object',
161
+ properties: {
162
+ gym: { type: 'boolean', description: 'Whether to include gym contests', default: false }
163
+ }
164
+ },
165
+ response: {
166
+ 200: {
167
+ type: 'array',
168
+ items: {
169
+ type: 'object',
170
+ properties: {
171
+ id: { type: 'number' },
172
+ name: { type: 'string' },
173
+ type: { type: 'string' },
174
+ phase: { type: 'string' },
175
+ frozen: { type: 'boolean' },
176
+ durationSeconds: { type: 'number' },
177
+ startTimeSeconds: { type: 'number' },
178
+ relativeTimeSeconds: { type: 'number' }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ };
184
+
185
+ export const recentActionsSchema = {
186
+ summary: 'Get Recent Actions',
187
+ description: 'Fetches recent actions on Codeforces (blogs, comments, etc.)',
188
+ tags: ['Codeforces - Blog'],
189
+ querystring: {
190
+ type: 'object',
191
+ properties: {
192
+ maxCount: { type: 'number', description: 'Maximum number of actions to fetch', default: 20 }
193
+ }
194
+ },
195
+ response: {
196
+ 200: {
197
+ type: 'array',
198
+ items: {
199
+ type: 'object',
200
+ properties: {
201
+ timeSeconds: { type: 'number' },
202
+ blogEntry: {
203
+ type: 'object',
204
+ properties: {
205
+ id: { type: 'number' },
206
+ title: { type: 'string' }
207
+ }
208
+ },
209
+ comment: {
210
+ type: 'object',
211
+ properties: {
212
+ id: { type: 'number' },
213
+ text: { type: 'string' },
214
+ commentatorHandle: { type: 'string' }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ };
222
+
223
+ export const problemsSchema = {
224
+ summary: 'Get Problemset Problems',
225
+ description: 'Fetches problems from the Codeforces problemset',
226
+ tags: ['Codeforces - Problems'],
227
+ querystring: {
228
+ type: 'object',
229
+ properties: {
230
+ tags: { type: 'string', description: 'Semicolon-separated list of tags' }
231
+ }
232
+ },
233
+ response: {
234
+ 200: {
235
+ type: 'array',
236
+ items: {
237
+ type: 'object',
238
+ properties: {
239
+ contestId: { type: 'number' },
240
+ index: { type: 'string' },
241
+ name: { type: 'string' },
242
+ rating: { type: 'number' },
243
+ tags: { type: 'array', items: { type: 'string' } }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ };
249
+
250
+ export const contestStandingsSchema = {
251
+ summary: 'Get Contest Standings',
252
+ description: 'Fetches the scoreboard of a specific contest',
253
+ tags: ['Codeforces - Contests'],
254
+ querystring: {
255
+ type: 'object',
256
+ properties: {
257
+ contestId: { type: 'number', description: 'ID of the contest' },
258
+ from: { type: 'number', description: 'Starting index (1-based)', default: 1 },
259
+ count: { type: 'number', description: 'Number of rows to fetch', default: 10 },
260
+ handles: { type: 'string', description: 'Semicolon-separated list of handles' },
261
+ room: { type: 'number', description: 'Room number' },
262
+ showUnofficial: { type: 'boolean', description: 'Whether to show unofficial results', default: false }
263
+ },
264
+ required: ['contestId']
265
+ }
266
+ };
267
+
268
+ export const contestRatingChangesSchema = {
269
+ summary: 'Get Contest Rating Changes',
270
+ description: 'Fetches rating changes for all participants after a contest',
271
+ tags: ['Codeforces - Contests'],
272
+ querystring: {
273
+ type: 'object',
274
+ properties: {
275
+ contestId: { type: 'number', description: 'ID of the contest' }
276
+ },
277
+ required: ['contestId']
278
+ }
279
+ };
280
+
281
+ export const contestHacksSchema = {
282
+ summary: 'Get Contest Hacks',
283
+ description: 'Fetches a list of all hacks in a contest',
284
+ tags: ['Codeforces - Contests'],
285
+ querystring: {
286
+ type: 'object',
287
+ properties: {
288
+ contestId: { type: 'number', description: 'ID of the contest' }
289
+ },
290
+ required: ['contestId']
291
+ }
292
+ };
293
+
294
+ export const contestStatusSchema = {
295
+ summary: 'Get Contest Status',
296
+ description: 'Fetches submissions for a specific contest',
297
+ tags: ['Codeforces - Contests'],
298
+ querystring: {
299
+ type: 'object',
300
+ properties: {
301
+ contestId: { type: 'number', description: 'ID of the contest' },
302
+ handle: { type: 'string', description: 'Codeforces handle' },
303
+ from: { type: 'number', description: 'Starting index (1-based)', default: 1 },
304
+ count: { type: 'number', description: 'Number of submissions to fetch', default: 10 }
305
+ },
306
+ required: ['contestId']
307
+ }
308
+ };
309
+
310
+ export const problemsetRecentStatusSchema = {
311
+ summary: 'Get Problemset Recent Status',
312
+ description: 'Fetches recent submissions across the platform',
313
+ tags: ['Codeforces - Problems'],
314
+ querystring: {
315
+ type: 'object',
316
+ properties: {
317
+ count: { type: 'number', description: 'Number of submissions to fetch', default: 10 }
318
+ },
319
+ required: ['count']
320
+ }
321
+ };
322
+
323
+ export const blogEntrySchema = {
324
+ summary: 'Get Blog Entry',
325
+ description: 'Fetches a specific blog entry',
326
+ tags: ['Codeforces - Blog'],
327
+ querystring: {
328
+ type: 'object',
329
+ properties: {
330
+ blogEntryId: { type: 'number', description: 'ID of the blog entry' }
331
+ },
332
+ required: ['blogEntryId']
333
+ }
334
+ };
335
+
336
+ export const blogCommentsSchema = {
337
+ summary: 'Get Blog Comments',
338
+ description: 'Fetches comments for a specific blog entry',
339
+ tags: ['Codeforces - Blog'],
340
+ querystring: {
341
+ type: 'object',
342
+ properties: {
343
+ blogEntryId: { type: 'number', description: 'ID of the blog entry' }
344
+ },
345
+ required: ['blogEntryId']
346
+ }
347
+ };
src/modules/codeforces/service.ts ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as provider from './provider';
2
+ import type {
3
+ CodeforcesRatingResponse,
4
+ CodeforcesContestHistory,
5
+ CodeforcesSubmission,
6
+ SolvedProblem,
7
+ BlogEntry,
8
+ CodeforcesContest,
9
+ RecentAction,
10
+ CodeforcesProblem,
11
+ ContestStandings,
12
+ RatingChange,
13
+ Hack,
14
+ } from './types';
15
+
16
+ // Get user's rating
17
+ export async function getUserRating(username: string): Promise<CodeforcesRatingResponse> {
18
+ try {
19
+ const user = await provider.fetchUserInfo(username);
20
+
21
+ return {
22
+ username,
23
+ platform: 'codeforces',
24
+ rating: user.rating || 'Unrated',
25
+ level: user.rank || 'Unrated',
26
+ max_rating: user.maxRating || 'Unrated',
27
+ max_level: user.maxRank || 'Unrated',
28
+ contribution: user.contribution,
29
+ friendOfCount: user.friendOfCount,
30
+ avatar: user.avatar,
31
+ };
32
+ } catch (error: any) {
33
+ console.error(`Codeforces Error for ${username}:`, error.message);
34
+ throw new Error('Error fetching Codeforces rating');
35
+ }
36
+ }
37
+
38
+ // Get user's contest history
39
+ export async function getContestHistory(
40
+ username: string
41
+ ): Promise<CodeforcesContestHistory[]> {
42
+ try {
43
+ return await provider.fetchContestHistory(username);
44
+ } catch (error: any) {
45
+ console.error(`Codeforces Error for ${username}:`, error.message);
46
+ throw new Error('Error fetching Codeforces contest history');
47
+ }
48
+ }
49
+
50
+ // Get user's submission status
51
+ export async function getUserStatus(
52
+ username: string,
53
+ from: number = 1,
54
+ count: number = 10
55
+ ): Promise<CodeforcesSubmission[]> {
56
+ try {
57
+ return await provider.fetchUserStatus(username, from, count);
58
+ } catch (error: any) {
59
+ console.error(`Codeforces Error for ${username}:`, error.message);
60
+ throw new Error('Error fetching Codeforces user status');
61
+ }
62
+ }
63
+
64
+ // Get user's blog entries
65
+ export async function getUserBlogs(username: string): Promise<BlogEntry[]> {
66
+ try {
67
+ return await provider.fetchBlogEntries(username);
68
+ } catch (error: any) {
69
+ console.error(`Codeforces Error for ${username}:`, error.message);
70
+ throw new Error('Error fetching Codeforces user blog entries');
71
+ }
72
+ }
73
+
74
+ // Get user's solved problems
75
+ export async function getSolvedProblems(username: string): Promise<SolvedProblem[]> {
76
+ try {
77
+ const submissions = await provider.fetchAllSubmissions(username, 1, 1000);
78
+
79
+ const solvedProblemsSet = new Set<string>();
80
+ const solvedProblems: SolvedProblem[] = [];
81
+
82
+ submissions.forEach((submission) => {
83
+ if (submission.verdict === 'OK') {
84
+ const problemId = `${submission.problem.contestId}${submission.problem.index}`;
85
+
86
+ if (!solvedProblemsSet.has(problemId)) {
87
+ solvedProblemsSet.add(problemId);
88
+ solvedProblems.push({
89
+ id: problemId,
90
+ name: submission.problem.name,
91
+ rating: submission.problem.rating,
92
+ tags: submission.problem.tags,
93
+ link: `https://codeforces.com/contest/${submission.problem.contestId}/problem/${submission.problem.index}`,
94
+ });
95
+ }
96
+ }
97
+ });
98
+
99
+ return solvedProblems;
100
+ } catch (error: any) {
101
+ console.error(`Codeforces Error for ${username}:`, error.message);
102
+ throw new Error('Error fetching Codeforces solved problems');
103
+ }
104
+ }
105
+
106
+ // Get contests
107
+ export async function getContests(gym: boolean = false): Promise<CodeforcesContest[]> {
108
+ try {
109
+ return await provider.fetchContests(gym);
110
+ } catch (error: any) {
111
+ console.error('Codeforces Error:', error.message);
112
+ throw new Error('Error fetching Codeforces contests');
113
+ }
114
+ }
115
+
116
+ // Get recent actions
117
+ export async function getRecentActions(maxCount: number = 20): Promise<RecentAction[]> {
118
+ try {
119
+ return await provider.fetchRecentActions(maxCount);
120
+ } catch (error: any) {
121
+ console.error('Codeforces Error:', error.message);
122
+ throw new Error('Error fetching Codeforces recent actions');
123
+ }
124
+ }
125
+
126
+ // Get problemset problems
127
+ export async function getProblems(tags?: string): Promise<CodeforcesProblem[]> {
128
+ try {
129
+ const result = await provider.fetchProblems(tags);
130
+ return result.problems;
131
+ } catch (error: any) {
132
+ console.error('Codeforces Error:', error.message);
133
+ throw new Error('Error fetching Codeforces problemset');
134
+ }
135
+ }
136
+
137
+ // Get contest standings
138
+ export async function getContestStandings(
139
+ contestId: number,
140
+ from?: number,
141
+ count?: number,
142
+ handles?: string,
143
+ room?: number,
144
+ showUnofficial?: boolean
145
+ ): Promise<ContestStandings> {
146
+ try {
147
+ return await provider.fetchContestStandings(contestId, from, count, handles, room, showUnofficial);
148
+ } catch (error: any) {
149
+ console.error('Codeforces Error:', error.message);
150
+ throw new Error('Error fetching Codeforces contest standings');
151
+ }
152
+ }
153
+
154
+ // Get rating changes
155
+ export async function getContestRatingChanges(contestId: number): Promise<RatingChange[]> {
156
+ try {
157
+ return await provider.fetchContestRatingChanges(contestId);
158
+ } catch (error: any) {
159
+ console.error('Codeforces Error:', error.message);
160
+ throw new Error('Error fetching Codeforces contest rating changes');
161
+ }
162
+ }
163
+
164
+ // Get contest hacks
165
+ export async function getContestHacks(contestId: number): Promise<Hack[]> {
166
+ try {
167
+ return await provider.fetchContestHacks(contestId);
168
+ } catch (error: any) {
169
+ console.error('Codeforces Error:', error.message);
170
+ throw new Error('Error fetching Codeforces contest hacks');
171
+ }
172
+ }
173
+
174
+ // Get contest status
175
+ export async function getContestStatus(
176
+ contestId: number,
177
+ handle?: string,
178
+ from?: number,
179
+ count?: number
180
+ ): Promise<CodeforcesSubmission[]> {
181
+ try {
182
+ return await provider.fetchContestStatus(contestId, handle, from, count);
183
+ } catch (error: any) {
184
+ console.error('Codeforces Error:', error.message);
185
+ throw new Error('Error fetching Codeforces contest status');
186
+ }
187
+ }
188
+
189
+ // Get problemset recent status
190
+ export async function getProblemsetRecentStatus(count: number): Promise<CodeforcesSubmission[]> {
191
+ try {
192
+ return await provider.fetchProblemsetRecentStatus(count);
193
+ } catch (error: any) {
194
+ console.error('Codeforces Error:', error.message);
195
+ throw new Error('Error fetching Codeforces problemset recent status');
196
+ }
197
+ }
198
+
199
+ // Get blog entry
200
+ export async function getBlogEntry(blogEntryId: number): Promise<any> {
201
+ try {
202
+ return await provider.fetchBlogEntry(blogEntryId);
203
+ } catch (error: any) {
204
+ console.error('Codeforces Error:', error.message);
205
+ throw new Error('Error fetching Codeforces blog entry');
206
+ }
207
+ }
208
+
209
+ // Get blog comments
210
+ export async function getBlogComments(blogEntryId: number): Promise<any[]> {
211
+ try {
212
+ return await provider.fetchBlogComments(blogEntryId);
213
+ } catch (error: any) {
214
+ console.error('Codeforces Error:', error.message);
215
+ throw new Error('Error fetching Codeforces blog comments');
216
+ }
217
+ }
src/modules/codeforces/types.ts ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Codeforces-specific TypeScript types
2
+
3
+ export interface CodeforcesUser {
4
+ handle: string;
5
+ rating?: number;
6
+ rank?: string;
7
+ maxRating?: number;
8
+ maxRank?: string;
9
+ contribution?: number;
10
+ friendOfCount?: number;
11
+ avatar?: string;
12
+ titlePhoto?: string;
13
+ lastOnlineTimeSeconds?: number;
14
+ registrationTimeSeconds?: number;
15
+ }
16
+
17
+ export interface CodeforcesRatingResponse {
18
+ username: string;
19
+ platform: string;
20
+ rating: number | string;
21
+ level: string;
22
+ max_rating?: number | string;
23
+ max_level?: string;
24
+ contribution?: number;
25
+ friendOfCount?: number;
26
+ avatar?: string;
27
+ }
28
+
29
+ export interface CodeforcesContestHistory {
30
+ contestId: number;
31
+ contestName: string;
32
+ handle: string;
33
+ rank: number;
34
+ ratingUpdateTimeSeconds: number;
35
+ oldRating: number;
36
+ newRating: number;
37
+ }
38
+
39
+ export interface CodeforcesProblem {
40
+ contestId: number;
41
+ index: string;
42
+ name: string;
43
+ rating?: number;
44
+ tags: string[];
45
+ }
46
+
47
+ export interface CodeforcesSubmission {
48
+ id: number;
49
+ contestId?: number;
50
+ problem: CodeforcesProblem;
51
+ verdict: string;
52
+ programmingLanguage: string;
53
+ creationTimeSeconds: number;
54
+ }
55
+
56
+ export interface SolvedProblem {
57
+ id: string;
58
+ name: string;
59
+ rating?: number;
60
+ tags: string[];
61
+ link: string;
62
+ }
63
+
64
+ export interface BlogEntry {
65
+ id: number;
66
+ title: string;
67
+ creationTimeSeconds: number;
68
+ rating: number;
69
+ }
70
+
71
+ export interface UserQuery {
72
+ username: string;
73
+ }
74
+
75
+ export interface StatusQuery {
76
+ username: string;
77
+ from?: number;
78
+ count?: number;
79
+ }
80
+
81
+ export interface CodeforcesContest {
82
+ id: number;
83
+ name: string;
84
+ type: string;
85
+ phase: string;
86
+ frozen: boolean;
87
+ durationSeconds: number;
88
+ startTimeSeconds?: number;
89
+ relativeTimeSeconds?: number;
90
+ }
91
+
92
+ export interface RecentAction {
93
+ timeSeconds: number;
94
+ blogEntry?: BlogEntry;
95
+ comment?: {
96
+ id: number;
97
+ creationTimeSeconds: number;
98
+ commentatorHandle: string;
99
+ text: string;
100
+ };
101
+ }
102
+
103
+ export interface RankingRow {
104
+ party: any;
105
+ rank: number;
106
+ points: number;
107
+ penalty: number;
108
+ successfulHackCount: number;
109
+ unsuccessfulHackCount: number;
110
+ problemResults: any[];
111
+ }
112
+
113
+ export interface ContestStandings {
114
+ contest: CodeforcesContest;
115
+ problems: CodeforcesProblem[];
116
+ rows: RankingRow[];
117
+ }
118
+
119
+ export interface RatingChange {
120
+ contestId: number;
121
+ contestName: string;
122
+ handle: string;
123
+ rank: number;
124
+ ratingUpdateTimeSeconds: number;
125
+ oldRating: number;
126
+ newRating: number;
127
+ }
128
+
129
+ export interface Hack {
130
+ id: number;
131
+ creationTimeSeconds: number;
132
+ hacker: any;
133
+ defender: any;
134
+ verdict?: string;
135
+ problem: CodeforcesProblem;
136
+ }
137
+
138
+ export interface ProblemsetQuery {
139
+ tags?: string;
140
+ }
141
+
142
+ export interface RecentActionsQuery {
143
+ maxCount: number;
144
+ }
145
+
146
+ export interface ContestQuery {
147
+ contestId: number;
148
+ }
149
+
150
+ export interface StandingsQuery {
151
+ contestId: number;
152
+ from?: number;
153
+ count?: number;
154
+ handles?: string;
155
+ room?: number;
156
+ showUnofficial?: boolean;
157
+ }
158
+
159
+ export interface ContestStatusQuery {
160
+ contestId: number;
161
+ handle?: string;
162
+ from?: number;
163
+ count?: number;
164
+ }
165
+
166
+ export interface BlogEntryQuery {
167
+ blogEntryId: number;
168
+ }
169
+
170
+ export interface RecentStatusQuery {
171
+ count: number;
172
+ }
173
+
174
+ export interface BlogEntryView {
175
+ id: number;
176
+ title: string;
177
+ content: string;
178
+ creationTimeSeconds: number;
179
+ rating: number;
180
+ authorHandle: string;
181
+ }
182
+
183
+ export interface BlogComment {
184
+ id: number;
185
+ creationTimeSeconds: number;
186
+ commentatorHandle: string;
187
+ text: string;
188
+ rating: number;
189
+ }
src/modules/gfg/constants.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const GFG_BASE_URL = 'https://www.geeksforgeeks.org/user/';
2
+
3
+ export const GFG_SELECTORS = {
4
+ NEXT_DATA: 'script#__NEXT_DATA__',
5
+ } as const;
6
+
7
+ export function mapRating(rating: number): string {
8
+ if (rating >= 2500) return '7 star';
9
+ if (rating >= 2200) return '6 star';
10
+ if (rating >= 2000) return '5 star';
11
+ if (rating >= 1800) return '4 star';
12
+ if (rating >= 1600) return '3 star';
13
+ if (rating >= 1400) return '2 star';
14
+ return '1 star';
15
+ }
src/modules/gfg/handlers.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import * as service from './service';
3
+ import type {
4
+ UserQuery,
5
+ SubmissionsQuery,
6
+ UserPostsQuery,
7
+ LeaderboardQuery,
8
+ PromotionalEventsQuery
9
+ } from './types';
10
+
11
+ export async function getUserRatingHandler(
12
+ request: FastifyRequest<{ Querystring: UserQuery }>,
13
+ reply: FastifyReply
14
+ ) {
15
+ const { username } = request.query;
16
+ const data = await service.getUserRating(username);
17
+ return reply.send(data);
18
+ }
19
+
20
+ export async function getUserSubmissionsHandler(
21
+ request: FastifyRequest<{ Body: SubmissionsQuery }>,
22
+ reply: FastifyReply
23
+ ) {
24
+ const data = await service.getUserSubmissions(request.body);
25
+ return reply.send(data);
26
+ }
27
+
28
+ export async function getUserPostsHandler(
29
+ request: FastifyRequest<{ Params: { username: string }, Querystring: Omit<UserPostsQuery, 'username'> }>,
30
+ reply: FastifyReply
31
+ ) {
32
+ const { username } = request.params;
33
+ const { fetch_type, page } = request.query;
34
+ const data = await service.getUserPosts({ username, fetch_type, page });
35
+ return reply.send(data);
36
+ }
37
+
38
+ export async function getPromotionalEventsHandler(
39
+ request: FastifyRequest<{ Querystring: PromotionalEventsQuery }>,
40
+ reply: FastifyReply
41
+ ) {
42
+ const data = await service.getPromotionalEvents(request.query);
43
+ return reply.send(data);
44
+ }
45
+
46
+ export async function getContestLeaderboardHandler(
47
+ request: FastifyRequest<{ Querystring: LeaderboardQuery }>,
48
+ reply: FastifyReply
49
+ ) {
50
+ const data = await service.getContestLeaderboard(request.query);
51
+ return reply.send(data);
52
+ }
src/modules/gfg/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { default as gfgPlugin } from './routes';
2
+ export * from './types';
src/modules/gfg/provider.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { httpClient } from '../../shared/utils/http-client';
2
+ import * as cheerio from 'cheerio';
3
+ import type {
4
+ GFGSubmissionsResponse,
5
+ SubmissionsQuery,
6
+ UserPostsQuery,
7
+ LeaderboardQuery,
8
+ PromotionalEventsQuery,
9
+ GFGUserRating,
10
+ GFGPost,
11
+ GFGPromotionalEvent,
12
+ GFGLeaderboard,
13
+ } from './types';
14
+ import { GFG_BASE_URL, GFG_SELECTORS } from './constants';
15
+
16
+ export async function fetchUserRating(username: string): Promise<GFGUserRating> {
17
+ const url = `${GFG_BASE_URL}${username}/`;
18
+ try {
19
+ const { data } = await httpClient.get(url);
20
+ const $ = cheerio.load(data);
21
+
22
+ // Check for 404 or "User not found"
23
+ if ($('title').text().includes('404') || $('body').text().includes('User not found')) {
24
+ throw new Error(`User '${username}' not found on GeeksforGeeks`);
25
+ }
26
+
27
+ const scriptContent = $(GFG_SELECTORS.NEXT_DATA).html();
28
+ if (!scriptContent) {
29
+ throw new Error('GeeksforGeeks schema change detected: NEXT_DATA script not found');
30
+ }
31
+
32
+ const jsonData = JSON.parse(scriptContent);
33
+ const userData = jsonData?.props?.pageProps?.contestData?.user_contest_data;
34
+ const stars = jsonData?.props?.pageProps?.contestData?.user_stars;
35
+
36
+ if (!userData && !jsonData?.props?.pageProps?.contestData) {
37
+ throw new Error(`User '${username}' contest data not found. Profile might be private or schema changed.`);
38
+ }
39
+
40
+ return {
41
+ rating: userData?.current_rating || 'Unrated',
42
+ stars: stars ? `${stars} star` : 'Unrated',
43
+ };
44
+ } catch (error: any) {
45
+ if (error.response?.status === 404) {
46
+ throw new Error(`User '${username}' not found on GeeksforGeeks`);
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ export async function fetchUserSubmissions(query: SubmissionsQuery): Promise<GFGSubmissionsResponse> {
53
+ const url = 'https://practiceapi.geeksforgeeks.org/api/v1/user/problems/submissions/';
54
+ const { data } = await httpClient.post(url, {
55
+ handle: query.handle,
56
+ requestType: query.requestType || "",
57
+ year: query.year || "",
58
+ month: query.month || ""
59
+ });
60
+ return data;
61
+ }
62
+
63
+ export async function fetchUserPosts(query: UserPostsQuery): Promise<GFGPost[]> {
64
+ const url = `https://communityapi.geeksforgeeks.org/post/user/${query.username}/`;
65
+ const { data } = await httpClient.get(url, {
66
+ params: {
67
+ fetch_type: query.fetch_type || 'posts',
68
+ page: query.page || 1
69
+ }
70
+ });
71
+ return data;
72
+ }
73
+
74
+ export async function fetchPromotionalEvents(query: PromotionalEventsQuery): Promise<GFGPromotionalEvent[]> {
75
+ const url = 'https://practiceapi.geeksforgeeks.org/api/vr/events/promotional/';
76
+ const { data } = await httpClient.get(url, {
77
+ params: {
78
+ page_source: query.page_source,
79
+ user_country_code: query.user_country_code || 'IN'
80
+ }
81
+ });
82
+ return data;
83
+ }
84
+
85
+ export async function fetchContestLeaderboard(query: LeaderboardQuery): Promise<GFGLeaderboard> {
86
+ const url = 'https://practiceapi.geeksforgeeks.org/api/latest/events/recurring/gfg-weekly-coding-contest/leaderboard/';
87
+ const { data } = await httpClient.get(url, {
88
+ params: {
89
+ leaderboard_type: query.leaderboard_type || 0,
90
+ page: query.page || 1,
91
+ year_month: query.year_month || ""
92
+ }
93
+ });
94
+ return data;
95
+ }
src/modules/gfg/routes.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import * as handlers from './handlers';
3
+ import * as schemas from './schemas';
4
+ import validateUsername from '../../shared/middlewares/validate';
5
+ import type {
6
+ UserQuery,
7
+ SubmissionsQuery,
8
+ UserPostsQuery,
9
+ LeaderboardQuery,
10
+ PromotionalEventsQuery
11
+ } from './types';
12
+
13
+ const gfgRoutes: FastifyPluginAsync = async (fastify) => {
14
+ // Legacy mapping (GET for consistency with other platforms)
15
+ fastify.get<{ Querystring: UserQuery }>(
16
+ '/rating',
17
+ {
18
+ preHandler: [validateUsername],
19
+ schema: schemas.userRatingSchema,
20
+ },
21
+ handlers.getUserRatingHandler
22
+ );
23
+
24
+ // New APIs
25
+ fastify.post<{ Body: SubmissionsQuery }>(
26
+ '/submissions',
27
+ {
28
+ schema: schemas.userSubmissionsSchema,
29
+ },
30
+ handlers.getUserSubmissionsHandler
31
+ );
32
+
33
+ fastify.get<{ Params: { username: string }, Querystring: Omit<UserPostsQuery, 'username'> }>(
34
+ '/posts/:username',
35
+ {
36
+ schema: schemas.userPostsSchema,
37
+ },
38
+ handlers.getUserPostsHandler
39
+ );
40
+
41
+ fastify.get<{ Querystring: PromotionalEventsQuery }>(
42
+ '/events/promotional',
43
+ {
44
+ schema: schemas.promotionalEventsSchema,
45
+ },
46
+ handlers.getPromotionalEventsHandler
47
+ );
48
+
49
+ fastify.get<{ Querystring: LeaderboardQuery }>(
50
+ '/leaderboard',
51
+ {
52
+ schema: schemas.contestLeaderboardSchema,
53
+ },
54
+ handlers.getContestLeaderboardHandler
55
+ );
56
+ };
57
+
58
+ export default gfgRoutes;
src/modules/gfg/schemas.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const userRatingSchema = {
2
+ summary: 'Get User Rating',
3
+ description: 'Fetches GeeksforGeeks user rating and platform details',
4
+ tags: ['GFG'],
5
+ querystring: {
6
+ type: 'object',
7
+ properties: {
8
+ username: { type: 'string', description: 'GFG username' },
9
+ },
10
+ required: ['username'],
11
+ },
12
+ response: {
13
+ 200: {
14
+ type: 'object',
15
+ properties: {
16
+ username: { type: 'string' },
17
+ platform: { type: 'string' },
18
+ rating: { type: ['number', 'string'] },
19
+ level: { type: 'string' }
20
+ }
21
+ }
22
+ }
23
+ };
24
+
25
+ export const userSubmissionsSchema = {
26
+ summary: 'Get User Submissions',
27
+ description: 'Fetches problems solved by a GFG user',
28
+ tags: ['GFG'],
29
+ body: {
30
+ type: 'object',
31
+ properties: {
32
+ handle: { type: 'string', description: 'GFG handle' },
33
+ requestType: { type: 'string', default: "" },
34
+ year: { type: 'string', default: "" },
35
+ month: { type: 'string', default: "" }
36
+ },
37
+ required: ['handle']
38
+ }
39
+ };
40
+
41
+ export const userPostsSchema = {
42
+ summary: 'Get User Posts',
43
+ description: 'Fetches articles and posts written by a GFG user',
44
+ tags: ['GFG'],
45
+ params: {
46
+ type: 'object',
47
+ properties: {
48
+ username: { type: 'string' }
49
+ },
50
+ required: ['username']
51
+ },
52
+ querystring: {
53
+ type: 'object',
54
+ properties: {
55
+ fetch_type: { type: 'string', default: 'posts' },
56
+ page: { type: 'number', default: 1 }
57
+ }
58
+ }
59
+ };
60
+
61
+ export const promotionalEventsSchema = {
62
+ summary: 'Get Promotional Events',
63
+ description: 'Fetches promotional events from GFG',
64
+ tags: ['GFG'],
65
+ querystring: {
66
+ type: 'object',
67
+ properties: {
68
+ page_source: { type: 'string' },
69
+ user_country_code: { type: 'string', default: 'IN' }
70
+ },
71
+ required: ['page_source']
72
+ }
73
+ };
74
+
75
+ export const contestLeaderboardSchema = {
76
+ summary: 'Get Contest Leaderboard',
77
+ description: 'Fetches the leaderboard for GFG weekly coding contests',
78
+ tags: ['GFG'],
79
+ querystring: {
80
+ type: 'object',
81
+ properties: {
82
+ leaderboard_type: { type: 'number', default: 0 },
83
+ page: { type: 'number', default: 1 },
84
+ year_month: { type: 'string', default: "" }
85
+ }
86
+ }
87
+ };
src/modules/gfg/service.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as provider from './provider';
2
+ import type {
3
+ GFGRating,
4
+ SubmissionsQuery,
5
+ GFGSubmissionsResponse,
6
+ UserPostsQuery,
7
+ LeaderboardQuery,
8
+ PromotionalEventsQuery
9
+ } from './types';
10
+
11
+ export async function getUserRating(username: string): Promise<GFGRating> {
12
+ try {
13
+ const data = await provider.fetchUserRating(username);
14
+
15
+ return {
16
+ username,
17
+ platform: 'gfg',
18
+ rating: data.rating,
19
+ level: data.stars,
20
+ };
21
+ } catch (error: any) {
22
+ console.error(`GFG Error for ${username}:`, error.message);
23
+ throw new Error('Error fetching GFG user data');
24
+ }
25
+ }
26
+
27
+ export async function getUserSubmissions(query: SubmissionsQuery): Promise<GFGSubmissionsResponse> {
28
+ try {
29
+ return await provider.fetchUserSubmissions(query);
30
+ } catch (error: any) {
31
+ console.error(`GFG Submissions Error for ${query.handle}:`, error.message);
32
+ throw new Error('Error fetching GFG submissions');
33
+ }
34
+ }
35
+
36
+ export async function getUserPosts(query: UserPostsQuery): Promise<any> {
37
+ try {
38
+ return await provider.fetchUserPosts(query);
39
+ } catch (error: any) {
40
+ console.error(`GFG Posts Error for ${query.username}:`, error.message);
41
+ throw new Error('Error fetching GFG user posts');
42
+ }
43
+ }
44
+
45
+ export async function getPromotionalEvents(query: PromotionalEventsQuery): Promise<any> {
46
+ try {
47
+ return await provider.fetchPromotionalEvents(query);
48
+ } catch (error: any) {
49
+ console.error('GFG Promotional Events Error:', error.message);
50
+ throw new Error('Error fetching GFG promotional events');
51
+ }
52
+ }
53
+
54
+ export async function getContestLeaderboard(query: LeaderboardQuery): Promise<any> {
55
+ try {
56
+ return await provider.fetchContestLeaderboard(query);
57
+ } catch (error: any) {
58
+ console.error('GFG Leaderboard Error:', error.message);
59
+ throw new Error('Error fetching GFG contest leaderboard');
60
+ }
61
+ }
src/modules/gfg/types.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface GFGRating {
2
+ username: string;
3
+ platform: string;
4
+ rating: number | string;
5
+ level: string;
6
+ }
7
+
8
+ export interface UserQuery {
9
+ username: string;
10
+ }
11
+
12
+ export interface SubmissionsQuery {
13
+ handle: string;
14
+ requestType?: string;
15
+ year?: string;
16
+ month?: string;
17
+ }
18
+
19
+ export interface GFGSubmission {
20
+ slug: string;
21
+ pname: string;
22
+ lang: string;
23
+ }
24
+
25
+ export interface GFGSubmissionsResponse {
26
+ status: string;
27
+ message: string;
28
+ result: {
29
+ [difficulty: string]: {
30
+ [id: string]: GFGSubmission;
31
+ };
32
+ };
33
+ count: number;
34
+ }
35
+
36
+ export interface UserPostsQuery {
37
+ username: string;
38
+ fetch_type?: string;
39
+ page?: number;
40
+ }
41
+
42
+ export interface LeaderboardQuery {
43
+ leaderboard_type?: number;
44
+ page?: number;
45
+ year_month?: string;
46
+ }
47
+
48
+ export interface PromotionalEventsQuery {
49
+ page_source: string;
50
+ user_country_code?: string;
51
+ }
52
+ export interface GFGUserRating {
53
+ rating: number | string;
54
+ stars: string;
55
+ }
56
+
57
+ export interface GFGPost {
58
+ id: number;
59
+ title: string;
60
+ content: string;
61
+ creationDate: string;
62
+ author: string;
63
+ }
64
+
65
+ export interface GFGPromotionalEvent {
66
+ id: number;
67
+ link: string;
68
+ title: string;
69
+ image: string;
70
+ }
71
+
72
+ export interface GFGLeaderboard {
73
+ users: {
74
+ handle: string;
75
+ rank: number;
76
+ score: number;
77
+ }[];
78
+ }
src/modules/leetcode/constants.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const LEETCODE_API_URL = 'https://leetcode.com/graphql';
2
+
3
+ export const LEETCODE_HEADERS = {
4
+ 'authority': 'leetcode.com',
5
+ 'accept': '*/*',
6
+ 'accept-language': 'en-US,en;q=0.9',
7
+ 'content-type': 'application/json',
8
+ 'origin': 'https://leetcode.com',
9
+ 'referer': 'https://leetcode.com/',
10
+ 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
11
+ 'sec-ch-ua-mobile': '?0',
12
+ 'sec-ch-ua-platform': '"Windows"',
13
+ 'sec-fetch-dest': 'empty',
14
+ 'sec-fetch-mode': 'cors',
15
+ 'sec-fetch-site': 'same-origin',
16
+ 'user-agent':
17
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
18
+ };
src/modules/leetcode/handlers/contest.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import * as service from '../services';
3
+ import type { ContestRankingQuery } from '../types';
4
+
5
+ export async function getContestRankingHandler(
6
+ request: FastifyRequest<{ Querystring: ContestRankingQuery }>,
7
+ reply: FastifyReply
8
+ ) {
9
+ const { username } = request.query;
10
+ const data = await service.getContestRankingInfo(username);
11
+ return reply.send(data);
12
+ }
13
+
14
+ export async function getContestHistogramHandler(
15
+ request: FastifyRequest,
16
+ reply: FastifyReply
17
+ ) {
18
+ const data = await service.getContestHistogram();
19
+ return reply.send(data);
20
+ }
21
+
22
+ export async function getAllContestsHandler(
23
+ request: FastifyRequest,
24
+ reply: FastifyReply
25
+ ) {
26
+ const data = await service.getAllContests();
27
+ return reply.send(data);
28
+ }
29
+
30
+ export async function getUpcomingContestsHandler(
31
+ request: FastifyRequest,
32
+ reply: FastifyReply
33
+ ) {
34
+ const data = await service.getUpcomingContests();
35
+ return reply.send(data);
36
+ }
src/modules/leetcode/handlers/discussion.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import * as service from '../services';
3
+
4
+ export async function getTrendingDiscussHandler(
5
+ request: FastifyRequest<{ Querystring: { first?: string } }>,
6
+ reply: FastifyReply
7
+ ) {
8
+ const first = parseInt(request.query.first || '20', 10);
9
+ const data = await service.getTrendingDiscuss(first);
10
+ return reply.send(data);
11
+ }
12
+
13
+ export async function getDiscussTopicHandler(
14
+ request: FastifyRequest<{ Params: { topicId: string } }>,
15
+ reply: FastifyReply
16
+ ) {
17
+ const topicId = parseInt(request.params.topicId, 10);
18
+ const data = await service.getDiscussTopic(topicId);
19
+ return reply.send(data);
20
+ }
21
+
22
+ export async function getDiscussCommentsHandler(
23
+ request: FastifyRequest<{ Params: { topicId: string }; Querystring: any }>,
24
+ reply: FastifyReply
25
+ ) {
26
+ const topicId = parseInt(request.params.topicId, 10);
27
+ const query = (request.query || {}) as Record<string, any>;
28
+ const data = await service.getDiscussComments({ topicId, ...query });
29
+ return reply.send(data);
30
+ }
src/modules/leetcode/handlers/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export * from './contest';
2
+ export * from './user';
3
+ export * from './problem';
4
+ export * from './discussion';
src/modules/leetcode/handlers/problem.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import * as service from '../services';
3
+
4
+ export async function getDailyProblemHandler(request: FastifyRequest, reply: FastifyReply) {
5
+ const raw = (request.query as any).raw === 'true';
6
+ const data = await service.getDailyProblem(raw);
7
+ return reply.send(data);
8
+ }
9
+
10
+ export async function getSelectProblemHandler(
11
+ request: FastifyRequest<{ Querystring: { titleSlug: string; raw?: string } }>,
12
+ reply: FastifyReply
13
+ ) {
14
+ const { titleSlug, raw } = request.query;
15
+ if (!titleSlug) {
16
+ return reply.status(400).send({ error: 'Missing titleSlug query parameter' });
17
+ }
18
+ const data = await service.getSelectProblem(titleSlug, raw === 'true');
19
+ return reply.send(data);
20
+ }
21
+
22
+ export async function getProblemsHandler(request: FastifyRequest, reply: FastifyReply) {
23
+ const data = await service.getProblems(request.query);
24
+ return reply.send(data);
25
+ }
26
+
27
+ export async function getOfficialSolutionHandler(
28
+ request: FastifyRequest<{ Querystring: { titleSlug: string } }>,
29
+ reply: FastifyReply
30
+ ) {
31
+ const { titleSlug } = request.query;
32
+ if (!titleSlug) {
33
+ return reply.status(400).send({ error: 'Missing titleSlug query parameter' });
34
+ }
35
+ const data = await service.getOfficialSolution(titleSlug);
36
+ return reply.send(data);
37
+ }
src/modules/leetcode/handlers/user.ts ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import * as service from '../services';
3
+ import type { ContestRankingQuery } from '../types';
4
+
5
+ export async function getUserRatingHandler(
6
+ request: FastifyRequest<{ Querystring: ContestRankingQuery }>,
7
+ reply: FastifyReply
8
+ ) {
9
+ const { username } = request.query;
10
+ const data = await service.getUserRating(username);
11
+ return reply.send(data);
12
+ }
13
+
14
+ export async function getUserProfileHandler(
15
+ request: FastifyRequest<{ Params: { username: string } }>,
16
+ reply: FastifyReply
17
+ ) {
18
+ const { username } = request.params;
19
+ const data = await service.getUserProfile(username);
20
+ return reply.send(data);
21
+ }
22
+
23
+ export async function getUserDetailsHandler(
24
+ request: FastifyRequest<{ Params: { username: string } }>,
25
+ reply: FastifyReply
26
+ ) {
27
+ const { username } = request.params;
28
+ const data = await service.getUserDetails(username);
29
+ return reply.send(data);
30
+ }
31
+
32
+ export async function getUserBadgesHandler(
33
+ request: FastifyRequest<{ Params: { username: string } }>,
34
+ reply: FastifyReply
35
+ ) {
36
+ const { username } = request.params;
37
+ const data = await service.getUserBadges(username);
38
+ return reply.send(data);
39
+ }
40
+
41
+ export async function getUserSolvedHandler(
42
+ request: FastifyRequest<{ Params: { username: string } }>,
43
+ reply: FastifyReply
44
+ ) {
45
+ const { username } = request.params;
46
+ const data = await service.getUserSolved(username);
47
+ return reply.send(data);
48
+ }
49
+
50
+ export async function getUserContestHandler(
51
+ request: FastifyRequest<{ Params: { username: string } }>,
52
+ reply: FastifyReply
53
+ ) {
54
+ const { username } = request.params;
55
+ const data = await service.getUserContest(username);
56
+ return reply.send(data);
57
+ }
58
+
59
+ export async function getUserContestHistoryHandler(
60
+ request: FastifyRequest<{ Params: { username: string } }>,
61
+ reply: FastifyReply
62
+ ) {
63
+ const { username } = request.params;
64
+ const data = await service.getUserContestHistory(username);
65
+ return reply.send(data);
66
+ }
67
+
68
+ export async function getUserSubmissionHandler(
69
+ request: FastifyRequest<{ Params: { username: string }; Querystring: { limit?: string } }>,
70
+ reply: FastifyReply
71
+ ) {
72
+ const { username } = request.params;
73
+ const limit = parseInt(request.query.limit || '20', 10);
74
+ const data = await service.getUserSubmission(username, limit);
75
+ return reply.send(data);
76
+ }
77
+
78
+ export async function getUserAcSubmissionHandler(
79
+ request: FastifyRequest<{ Params: { username: string }; Querystring: { limit?: string } }>,
80
+ reply: FastifyReply
81
+ ) {
82
+ const { username } = request.params;
83
+ const limit = parseInt(request.query.limit || '20', 10);
84
+ const data = await service.getUserAcSubmission(username, limit);
85
+ return reply.send(data);
86
+ }
87
+
88
+ export async function getUserCalendarHandler(
89
+ request: FastifyRequest<{ Params: { username: string }; Querystring: { year?: string } }>,
90
+ reply: FastifyReply
91
+ ) {
92
+ const { username } = request.params;
93
+ const year = parseInt(request.query.year || '0', 10);
94
+ const data = await service.getUserCalendar(username, year);
95
+ return reply.send(data);
96
+ }
97
+
98
+ export async function getUserSkillHandler(
99
+ request: FastifyRequest<{ Params: { username: string } }>,
100
+ reply: FastifyReply
101
+ ) {
102
+ const { username } = request.params;
103
+ const data = await service.getUserSkill(username);
104
+ return reply.send(data);
105
+ }
106
+
107
+ export async function getUserLanguageHandler(
108
+ request: FastifyRequest<{ Params: { username: string } }>,
109
+ reply: FastifyReply
110
+ ) {
111
+ const { username } = request.params;
112
+ const data = await service.getUserLanguage(username);
113
+ return reply.send(data);
114
+ }
115
+
116
+ export async function getUserProgressHandler(
117
+ request: FastifyRequest<{ Params: { username: string } }>,
118
+ reply: FastifyReply
119
+ ) {
120
+ const { username } = request.params;
121
+ const data = await service.getUserProgress(username);
122
+ return reply.send(data);
123
+ }
src/modules/leetcode/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { default as leetcodePlugin } from './routes';
2
+ export * from './types';
src/modules/leetcode/provider.ts ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { httpClient } from '../../shared/utils/http-client';
2
+ import {
3
+ USER_CONTEST_RANKING_QUERY,
4
+ USER_RATING_QUERY,
5
+ CONTEST_HISTOGRAM_QUERY,
6
+ ALL_CONTESTS_QUERY,
7
+ DAILY_PROBLEM_QUERY,
8
+ SELECT_PROBLEM_QUERY,
9
+ PROBLEM_LIST_QUERY,
10
+ OFFICIAL_SOLUTION_QUERY,
11
+ TRENDING_DISCUSS_QUERY,
12
+ DISCUSS_TOPIC_QUERY,
13
+ DISCUSS_COMMENTS_QUERY,
14
+ USER_PROFILE_QUERY,
15
+ USER_PROFILE_CALENDAR_QUERY,
16
+ USER_QUESTION_PROGRESS_QUERY,
17
+ SKILL_STATS_QUERY,
18
+ LANGUAGE_STATS_QUERY,
19
+ AC_SUBMISSION_QUERY,
20
+ SUBMISSION_QUERY,
21
+ GET_USER_PROFILE_QUERY,
22
+ CONTEST_QUERY,
23
+ USER_BADGES_QUERY,
24
+ USER_SOLVED_QUERY,
25
+ } from './utils/queries';
26
+ import type {
27
+ ContestRankingResponse,
28
+ ContestHistogramResponse,
29
+ Contest,
30
+ UserData,
31
+ DailyProblemData,
32
+ SelectProblemData,
33
+ ProblemSetQuestionListData,
34
+ UserProfileResponse,
35
+ UserRatingData,
36
+ OfficialSolutionData,
37
+ TrendingDiscussData,
38
+ DiscussTopicData,
39
+ DiscussCommentsData,
40
+ UserCalendarData,
41
+ UserQuestionProgressData,
42
+ SkillStatsData,
43
+ LanguageStatsData,
44
+ AcSubmissionsData,
45
+ SubmissionsData,
46
+ ContestData,
47
+ } from './types';
48
+
49
+ import { LEETCODE_API_URL, LEETCODE_HEADERS } from './constants';
50
+
51
+ async function leetcodeRequest<T>(query: string, variables: object = {}): Promise<T> {
52
+ const payload = { query, variables };
53
+ const response = await httpClient.post(LEETCODE_API_URL, payload, {
54
+ headers: LEETCODE_HEADERS,
55
+ });
56
+
57
+ if (response.status !== 200) {
58
+ throw new Error(`LeetCode API returned status ${response.status}`);
59
+ }
60
+
61
+ if (response.data.errors) {
62
+ throw new Error(response.data.errors[0].message);
63
+ }
64
+
65
+ return response.data.data;
66
+ }
67
+
68
+ // Existing functions
69
+ export async function fetchUserContestRanking(username: string): Promise<ContestRankingResponse> {
70
+ return await leetcodeRequest<ContestRankingResponse>(USER_CONTEST_RANKING_QUERY, { username });
71
+ }
72
+
73
+ export async function fetchUserRating(username: string): Promise<UserRatingData> {
74
+ return await leetcodeRequest<UserRatingData>(USER_RATING_QUERY, { username });
75
+ }
76
+
77
+ export async function fetchContestHistogram(): Promise<ContestHistogramResponse> {
78
+ return await leetcodeRequest<ContestHistogramResponse>(CONTEST_HISTOGRAM_QUERY);
79
+ }
80
+
81
+ export async function fetchAllContests(): Promise<Contest[]> {
82
+ const data = await leetcodeRequest<{ allContests: Contest[] }>(ALL_CONTESTS_QUERY);
83
+ return data.allContests;
84
+ }
85
+
86
+ export async function fetchDailyProblem(): Promise<DailyProblemData> {
87
+ return await leetcodeRequest<DailyProblemData>(DAILY_PROBLEM_QUERY);
88
+ }
89
+
90
+ export async function fetchSelectProblem(titleSlug: string): Promise<SelectProblemData> {
91
+ return await leetcodeRequest<SelectProblemData>(SELECT_PROBLEM_QUERY, { titleSlug });
92
+ }
93
+
94
+ const MAX_LEETCODE_LIMIT = 100;
95
+
96
+ export async function fetchProblems(filters: {
97
+ categorySlug?: string;
98
+ limit?: number;
99
+ skip?: number;
100
+ filters?: any;
101
+ }): Promise<ProblemSetQuestionListData> {
102
+ const { categorySlug, limit = 20, skip = 0, filters: questionFilters } = filters;
103
+ const safeLimit = Math.min(limit, MAX_LEETCODE_LIMIT);
104
+
105
+ return await leetcodeRequest<ProblemSetQuestionListData>(PROBLEM_LIST_QUERY, {
106
+ categorySlug,
107
+ limit: safeLimit,
108
+ skip,
109
+ filters: questionFilters,
110
+ });
111
+ }
112
+
113
+ export async function fetchOfficialSolution(titleSlug: string): Promise<OfficialSolutionData> {
114
+ return await leetcodeRequest<OfficialSolutionData>(OFFICIAL_SOLUTION_QUERY, { titleSlug });
115
+ }
116
+
117
+ export async function fetchTrendingDiscuss(first: number): Promise<TrendingDiscussData> {
118
+ return await leetcodeRequest<TrendingDiscussData>(TRENDING_DISCUSS_QUERY, { first });
119
+ }
120
+
121
+ export async function fetchDiscussTopic(topicId: number): Promise<DiscussTopicData> {
122
+ return await leetcodeRequest<DiscussTopicData>(DISCUSS_TOPIC_QUERY, { topicId });
123
+ }
124
+
125
+ export async function fetchDiscussComments(params: {
126
+ topicId: number;
127
+ orderBy?: string;
128
+ pageNo?: number;
129
+ numPerPage?: number;
130
+ }): Promise<DiscussCommentsData> {
131
+ return await leetcodeRequest<DiscussCommentsData>(DISCUSS_COMMENTS_QUERY, params);
132
+ }
133
+
134
+ export async function fetchUserProfile(username: string): Promise<UserProfileResponse> {
135
+ return await leetcodeRequest<UserProfileResponse>(GET_USER_PROFILE_QUERY, { username });
136
+ }
137
+
138
+ export async function fetchUserData(username: string): Promise<UserData> {
139
+ // This query is very large and might need optimization or splitting
140
+ return await leetcodeRequest<UserData>(USER_PROFILE_QUERY, { username });
141
+ }
142
+
143
+ export async function fetchUserCalendar(username: string, year: number): Promise<UserCalendarData> {
144
+ return await leetcodeRequest<UserCalendarData>(USER_PROFILE_CALENDAR_QUERY, { username, year });
145
+ }
146
+
147
+ export async function fetchUserQuestionProgress(username: string): Promise<UserQuestionProgressData> {
148
+ return await leetcodeRequest<UserQuestionProgressData>(USER_QUESTION_PROGRESS_QUERY, { username });
149
+ }
150
+
151
+ export async function fetchSkillStats(username: string): Promise<SkillStatsData> {
152
+ return await leetcodeRequest<SkillStatsData>(SKILL_STATS_QUERY, { username });
153
+ }
154
+
155
+ export async function fetchLanguageStats(username: string): Promise<LanguageStatsData> {
156
+ return await leetcodeRequest<LanguageStatsData>(LANGUAGE_STATS_QUERY, { username });
157
+ }
158
+
159
+ export async function fetchAcSubmissions(username: string, limit: number): Promise<AcSubmissionsData> {
160
+ return await leetcodeRequest<AcSubmissionsData>(AC_SUBMISSION_QUERY, { username, limit });
161
+ }
162
+
163
+ export async function fetchSubmissions(username: string, limit: number): Promise<SubmissionsData> {
164
+ return await leetcodeRequest<SubmissionsData>(SUBMISSION_QUERY, { username, limit });
165
+ }
166
+
167
+ export async function fetchContestData(username: string): Promise<ContestData> {
168
+ return await leetcodeRequest<ContestData>(CONTEST_QUERY, { username });
169
+ }
170
+
171
+ export async function fetchUserBadges(username: string): Promise<any> {
172
+ return await leetcodeRequest<any>(USER_BADGES_QUERY, { username });
173
+ }
174
+
175
+ export async function fetchUserSolved(username: string): Promise<any> {
176
+ return await leetcodeRequest<any>(USER_SOLVED_QUERY, { username });
177
+ }