Commit
·
3d23b0f
1
Parent(s):
6b09394
first commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +9 -0
- .gitattributes +0 -35
- .gitignore +38 -0
- Dockerfile +42 -0
- README.md +584 -7
- docker-compose.yml +17 -0
- docs/ARCHITECTURE.md +67 -0
- package.json +32 -0
- pnpm-lock.yaml +1936 -0
- src/app.ts +99 -0
- src/config/env.ts +11 -0
- src/modules/atcoder/constants.ts +28 -0
- src/modules/atcoder/handlers.ts +48 -0
- src/modules/atcoder/index.ts +2 -0
- src/modules/atcoder/provider.ts +109 -0
- src/modules/atcoder/routes.ts +51 -0
- src/modules/atcoder/schemas.ts +143 -0
- src/modules/atcoder/service.ts +63 -0
- src/modules/atcoder/types.ts +95 -0
- src/modules/codechef/constants.ts +16 -0
- src/modules/codechef/handlers.ts +12 -0
- src/modules/codechef/index.ts +2 -0
- src/modules/codechef/provider.ts +40 -0
- src/modules/codechef/routes.ts +18 -0
- src/modules/codechef/schemas.ts +23 -0
- src/modules/codechef/service.ts +20 -0
- src/modules/codechef/types.ts +15 -0
- src/modules/codeforces/handlers.ts +160 -0
- src/modules/codeforces/index.ts +2 -0
- src/modules/codeforces/provider.ts +221 -0
- src/modules/codeforces/routes.ts +98 -0
- src/modules/codeforces/schemas.ts +347 -0
- src/modules/codeforces/service.ts +217 -0
- src/modules/codeforces/types.ts +189 -0
- src/modules/gfg/constants.ts +15 -0
- src/modules/gfg/handlers.ts +52 -0
- src/modules/gfg/index.ts +2 -0
- src/modules/gfg/provider.ts +95 -0
- src/modules/gfg/routes.ts +58 -0
- src/modules/gfg/schemas.ts +87 -0
- src/modules/gfg/service.ts +61 -0
- src/modules/gfg/types.ts +78 -0
- src/modules/leetcode/constants.ts +18 -0
- src/modules/leetcode/handlers/contest.ts +36 -0
- src/modules/leetcode/handlers/discussion.ts +30 -0
- src/modules/leetcode/handlers/index.ts +4 -0
- src/modules/leetcode/handlers/problem.ts +37 -0
- src/modules/leetcode/handlers/user.ts +123 -0
- src/modules/leetcode/index.ts +2 -0
- 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|