relfa commited on
Commit
ae9d2aa
·
0 Parent(s):

feat: initial LLM gateway proxy for Claude Code

Browse files
Files changed (14) hide show
  1. .dockerignore +10 -0
  2. .env.example +18 -0
  3. .gitignore +6 -0
  4. Dockerfile +39 -0
  5. README.md +158 -0
  6. docker-compose.yml +14 -0
  7. package-lock.json +1273 -0
  8. package.json +26 -0
  9. src/auth.ts +28 -0
  10. src/config.ts +49 -0
  11. src/index.ts +138 -0
  12. src/proxy.ts +133 -0
  13. src/types.ts +55 -0
  14. tsconfig.json +24 -0
.dockerignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.*
5
+ !.env.example
6
+ .git
7
+ .gitignore
8
+ *.md
9
+ .vscode
10
+ .idea
.env.example ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # === Required ===
2
+ ANTHROPIC_API_KEY=sk-ant-... # Real Anthropic API key (stays on the server)
3
+ PROXY_AUTH_TOKEN=your-secure-shared-secret # Token that Claude Code uses as ANTHROPIC_AUTH_TOKEN
4
+
5
+ # === Optional ===
6
+ PORT=7860 # Default: 7860 (Hugging Face Spaces default)
7
+ HOST=0.0.0.0
8
+ LOG_LEVEL=info # trace | debug | info | warn | error
9
+
10
+ # Security
11
+ RATE_LIMIT_MAX=100 # Requests per time window per IP
12
+ RATE_LIMIT_WINDOW_MS=60000 # Time window in ms
13
+ BODY_LIMIT=5242880 # Max body size in bytes (default: 5 MB)
14
+ CORS_ORIGIN= # Empty = disabled
15
+
16
+ # Upstream
17
+ ANTHROPIC_BASE_URL=https://api.anthropic.com # Overridable for testing
18
+ UPSTREAM_TIMEOUT_MS=300000 # Upstream request timeout (default: 5 min)
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.*
5
+ !.env.example
6
+ *.tsbuildinfo
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Stage 1: Build ────────────────────────────────────────────────────────────
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ COPY package.json package-lock.json* ./
7
+ RUN npm ci --ignore-scripts
8
+
9
+ COPY tsconfig.json ./
10
+ COPY src/ ./src/
11
+
12
+ RUN npm run build
13
+
14
+ # ── Stage 2: Run ──────────────────────────────────────────────────────────────
15
+ FROM node:20-alpine AS runner
16
+
17
+ WORKDIR /app
18
+
19
+ # Hugging Face Spaces runs containers as uid 1000
20
+ RUN addgroup -g 1000 appgroup && \
21
+ adduser -u 1000 -G appgroup -s /bin/sh -D appuser && \
22
+ mkdir -p /data && \
23
+ chown -R appuser:appgroup /data /app
24
+
25
+ COPY --from=builder --chown=appuser:appgroup /app/package.json /app/package-lock.json* ./
26
+ RUN npm ci --omit=dev --ignore-scripts
27
+
28
+ COPY --from=builder --chown=appuser:appgroup /app/dist/ ./dist/
29
+
30
+ USER appuser
31
+
32
+ # Hugging Face Spaces default port
33
+ ENV PORT=7860
34
+ ENV HOST=0.0.0.0
35
+ ENV NODE_ENV=production
36
+
37
+ EXPOSE 7860
38
+
39
+ CMD ["node", "dist/index.js"]
README.md ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI Proxy – LLM Gateway for Claude Code
2
+
3
+ A transparent relay proxy that forwards [Anthropic Messages API](https://docs.anthropic.com/en/api/messages) requests from **Claude Code CLI** through environments where `api.anthropic.com` is blocked (e.g. corporate firewalls). The proxy performs auth-swap, header hygiene, and supports SSE streaming.
4
+
5
+ ```
6
+ Claude Code CLI → AI Proxy (this server) → api.anthropic.com
7
+ ANTHROPIC_BASE_URL Upstream
8
+ ```
9
+
10
+ ## Features
11
+
12
+ - **1:1 transparent relay** – no request/response body modification
13
+ - **SSE streaming** – chunk-by-chunk forwarding, zero buffering
14
+ - **Auth swap** – client authenticates with a shared token; server injects the real API key
15
+ - **Header hygiene** – strips hop-by-hop headers, authorization, and client-sent API keys
16
+ - **Rate limiting** – per-IP, configurable window and max
17
+ - **Defensive headers** – `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`
18
+ - **Graceful shutdown** – finishes in-flight streams before exiting
19
+ - **Hugging Face Spaces ready** – Docker configuration pre-set for HF Spaces deployment
20
+
21
+ ## Quick Start (Local)
22
+
23
+ ```bash
24
+ # 1. Clone & install
25
+ git clone <your-repo-url> && cd ai-proxy
26
+ npm install
27
+
28
+ # 2. Configure – copy and edit
29
+ cp .env.example .env
30
+ # Set ANTHROPIC_API_KEY and PROXY_AUTH_TOKEN in .env
31
+
32
+ # 3. Run
33
+ npm run dev
34
+ ```
35
+
36
+ Health check: `curl http://localhost:7860/health`
37
+
38
+ ## Environment Variables
39
+
40
+ | Variable | Required | Default | Description |
41
+ |---|---|---|---|
42
+ | `ANTHROPIC_API_KEY` | ✅ | – | Real Anthropic API key (stays on the server) |
43
+ | `PROXY_AUTH_TOKEN` | ✅ | – | Shared secret for client authentication |
44
+ | `PORT` | – | `7860` | Server port |
45
+ | `HOST` | – | `0.0.0.0` | Server bind address |
46
+ | `LOG_LEVEL` | – | `info` | `trace` \| `debug` \| `info` \| `warn` \| `error` |
47
+ | `RATE_LIMIT_MAX` | – | `100` | Requests per time window per IP |
48
+ | `RATE_LIMIT_WINDOW_MS` | – | `60000` | Rate limit window (ms) |
49
+ | `BODY_LIMIT` | – | `5242880` | Max request body size (bytes, 5 MB) |
50
+ | `CORS_ORIGIN` | – | *(disabled)* | CORS origin (e.g. `*` or `https://example.com`) |
51
+ | `ANTHROPIC_BASE_URL` | – | `https://api.anthropic.com` | Upstream URL (override for testing) |
52
+ | `UPSTREAM_TIMEOUT_MS` | – | `300000` | Upstream request timeout (5 min) |
53
+
54
+ ## API Endpoints
55
+
56
+ | Method | Path | Auth | Description |
57
+ |---|---|---|---|
58
+ | `GET` | `/health` | No | Health check → `{"status":"ok"}` |
59
+ | `POST` | `/v1/messages` | Yes | Chat completions (relayed 1:1) |
60
+ | `POST` | `/v1/messages/count_tokens` | Yes | Token counting (relayed 1:1) |
61
+
62
+ All other routes return `404`. Non-POST methods on API routes return `405`.
63
+
64
+ ## Docker
65
+
66
+ ### Local (docker compose)
67
+
68
+ ```bash
69
+ cp .env.example .env
70
+ # Edit .env with your keys
71
+ docker compose up --build
72
+ ```
73
+
74
+ ### Hugging Face Spaces
75
+
76
+ 1. Create a new Space on [huggingface.co/new-space](https://huggingface.co/new-space):
77
+ - **SDK**: Docker
78
+ - **Visibility**: Private (recommended – this handles API keys)
79
+
80
+ 2. Push this repository to the Space:
81
+ ```bash
82
+ git remote add hf https://huggingface.co/spaces/<YOUR_USER>/<SPACE_NAME>
83
+ git push hf main
84
+ ```
85
+
86
+ 3. Configure **Secrets** in Space Settings → Repository secrets:
87
+ - `ANTHROPIC_API_KEY` = your real Anthropic key
88
+ - `PROXY_AUTH_TOKEN` = your chosen shared secret
89
+
90
+ 4. The Space will build and deploy automatically. Your proxy URL will be:
91
+ ```
92
+ https://<YOUR_USER>-<SPACE_NAME>.hf.space
93
+ ```
94
+
95
+ > **Note:** HF Spaces secrets become environment variables at runtime. The Dockerfile already defaults to port 7860 and runs as uid 1000 as required by the platform.
96
+
97
+ ## Claude Code Client Configuration
98
+
99
+ ### Option 1: Environment Variables
100
+
101
+ ```bash
102
+ export ANTHROPIC_BASE_URL=https://your-server.example.com
103
+ export ANTHROPIC_AUTH_TOKEN=your-proxy-auth-token
104
+ claude
105
+ ```
106
+
107
+ ### Option 2: Persistent (settings.json)
108
+
109
+ ```json
110
+ // ~/.claude/settings.json
111
+ {
112
+ "env": {
113
+ "ANTHROPIC_BASE_URL": "https://your-server.example.com",
114
+ "ANTHROPIC_AUTH_TOKEN": "your-proxy-auth-token"
115
+ }
116
+ }
117
+ ```
118
+
119
+ ### Option 3: Managed Settings (Enterprise)
120
+
121
+ ```json
122
+ // macOS: /Library/Application Support/ClaudeCode/managed-settings.json
123
+ // Linux: /etc/claude-code/managed-settings.json
124
+ {
125
+ "env": {
126
+ "ANTHROPIC_BASE_URL": "https://your-server.example.com"
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### Test the Connection
132
+
133
+ ```bash
134
+ # Health check
135
+ curl https://your-server.example.com/health
136
+
137
+ # Test message
138
+ curl -X POST https://your-server.example.com/v1/messages \
139
+ -H "Authorization: Bearer your-proxy-auth-token" \
140
+ -H "Content-Type: application/json" \
141
+ -H "anthropic-version: 2023-06-01" \
142
+ -d '{
143
+ "model": "claude-sonnet-4-20250514",
144
+ "max_tokens": 100,
145
+ "messages": [{"role": "user", "content": "Hi"}]
146
+ }'
147
+ ```
148
+
149
+ ## Tech Stack
150
+
151
+ - **Runtime:** Node.js ≥ 20
152
+ - **Framework:** [Fastify](https://fastify.dev/) 5
153
+ - **HTTP Client:** [undici](https://undici.nodejs.org/)
154
+ - **Language:** TypeScript (strict mode)
155
+
156
+ ## License
157
+
158
+ MIT
docker-compose.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ ai-proxy:
3
+ build: .
4
+ ports:
5
+ - "${PORT:-7860}:${PORT:-7860}"
6
+ env_file:
7
+ - .env
8
+ restart: unless-stopped
9
+ healthcheck:
10
+ test: [ "CMD", "wget", "--spider", "-q", "http://localhost:${PORT:-7860}/health" ]
11
+ interval: 30s
12
+ timeout: 5s
13
+ retries: 3
14
+ start_period: 10s
package-lock.json ADDED
@@ -0,0 +1,1273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ai-proxy",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "ai-proxy",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "@fastify/rate-limit": "^10.2.2",
12
+ "dotenv": "^16.5.0",
13
+ "fastify": "^5.3.3",
14
+ "undici": "^7.5.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.13.10",
18
+ "tsx": "^4.19.3",
19
+ "typescript": "^5.8.2"
20
+ },
21
+ "engines": {
22
+ "node": ">=20"
23
+ }
24
+ },
25
+ "node_modules/@esbuild/aix-ppc64": {
26
+ "version": "0.27.3",
27
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
28
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
29
+ "cpu": [
30
+ "ppc64"
31
+ ],
32
+ "dev": true,
33
+ "license": "MIT",
34
+ "optional": true,
35
+ "os": [
36
+ "aix"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18"
40
+ }
41
+ },
42
+ "node_modules/@esbuild/android-arm": {
43
+ "version": "0.27.3",
44
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
45
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
46
+ "cpu": [
47
+ "arm"
48
+ ],
49
+ "dev": true,
50
+ "license": "MIT",
51
+ "optional": true,
52
+ "os": [
53
+ "android"
54
+ ],
55
+ "engines": {
56
+ "node": ">=18"
57
+ }
58
+ },
59
+ "node_modules/@esbuild/android-arm64": {
60
+ "version": "0.27.3",
61
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
62
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
63
+ "cpu": [
64
+ "arm64"
65
+ ],
66
+ "dev": true,
67
+ "license": "MIT",
68
+ "optional": true,
69
+ "os": [
70
+ "android"
71
+ ],
72
+ "engines": {
73
+ "node": ">=18"
74
+ }
75
+ },
76
+ "node_modules/@esbuild/android-x64": {
77
+ "version": "0.27.3",
78
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
79
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
80
+ "cpu": [
81
+ "x64"
82
+ ],
83
+ "dev": true,
84
+ "license": "MIT",
85
+ "optional": true,
86
+ "os": [
87
+ "android"
88
+ ],
89
+ "engines": {
90
+ "node": ">=18"
91
+ }
92
+ },
93
+ "node_modules/@esbuild/darwin-arm64": {
94
+ "version": "0.27.3",
95
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
96
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
97
+ "cpu": [
98
+ "arm64"
99
+ ],
100
+ "dev": true,
101
+ "license": "MIT",
102
+ "optional": true,
103
+ "os": [
104
+ "darwin"
105
+ ],
106
+ "engines": {
107
+ "node": ">=18"
108
+ }
109
+ },
110
+ "node_modules/@esbuild/darwin-x64": {
111
+ "version": "0.27.3",
112
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
113
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
114
+ "cpu": [
115
+ "x64"
116
+ ],
117
+ "dev": true,
118
+ "license": "MIT",
119
+ "optional": true,
120
+ "os": [
121
+ "darwin"
122
+ ],
123
+ "engines": {
124
+ "node": ">=18"
125
+ }
126
+ },
127
+ "node_modules/@esbuild/freebsd-arm64": {
128
+ "version": "0.27.3",
129
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
130
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
131
+ "cpu": [
132
+ "arm64"
133
+ ],
134
+ "dev": true,
135
+ "license": "MIT",
136
+ "optional": true,
137
+ "os": [
138
+ "freebsd"
139
+ ],
140
+ "engines": {
141
+ "node": ">=18"
142
+ }
143
+ },
144
+ "node_modules/@esbuild/freebsd-x64": {
145
+ "version": "0.27.3",
146
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
147
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
148
+ "cpu": [
149
+ "x64"
150
+ ],
151
+ "dev": true,
152
+ "license": "MIT",
153
+ "optional": true,
154
+ "os": [
155
+ "freebsd"
156
+ ],
157
+ "engines": {
158
+ "node": ">=18"
159
+ }
160
+ },
161
+ "node_modules/@esbuild/linux-arm": {
162
+ "version": "0.27.3",
163
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
164
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
165
+ "cpu": [
166
+ "arm"
167
+ ],
168
+ "dev": true,
169
+ "license": "MIT",
170
+ "optional": true,
171
+ "os": [
172
+ "linux"
173
+ ],
174
+ "engines": {
175
+ "node": ">=18"
176
+ }
177
+ },
178
+ "node_modules/@esbuild/linux-arm64": {
179
+ "version": "0.27.3",
180
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
181
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
182
+ "cpu": [
183
+ "arm64"
184
+ ],
185
+ "dev": true,
186
+ "license": "MIT",
187
+ "optional": true,
188
+ "os": [
189
+ "linux"
190
+ ],
191
+ "engines": {
192
+ "node": ">=18"
193
+ }
194
+ },
195
+ "node_modules/@esbuild/linux-ia32": {
196
+ "version": "0.27.3",
197
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
198
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
199
+ "cpu": [
200
+ "ia32"
201
+ ],
202
+ "dev": true,
203
+ "license": "MIT",
204
+ "optional": true,
205
+ "os": [
206
+ "linux"
207
+ ],
208
+ "engines": {
209
+ "node": ">=18"
210
+ }
211
+ },
212
+ "node_modules/@esbuild/linux-loong64": {
213
+ "version": "0.27.3",
214
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
215
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
216
+ "cpu": [
217
+ "loong64"
218
+ ],
219
+ "dev": true,
220
+ "license": "MIT",
221
+ "optional": true,
222
+ "os": [
223
+ "linux"
224
+ ],
225
+ "engines": {
226
+ "node": ">=18"
227
+ }
228
+ },
229
+ "node_modules/@esbuild/linux-mips64el": {
230
+ "version": "0.27.3",
231
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
232
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
233
+ "cpu": [
234
+ "mips64el"
235
+ ],
236
+ "dev": true,
237
+ "license": "MIT",
238
+ "optional": true,
239
+ "os": [
240
+ "linux"
241
+ ],
242
+ "engines": {
243
+ "node": ">=18"
244
+ }
245
+ },
246
+ "node_modules/@esbuild/linux-ppc64": {
247
+ "version": "0.27.3",
248
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
249
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
250
+ "cpu": [
251
+ "ppc64"
252
+ ],
253
+ "dev": true,
254
+ "license": "MIT",
255
+ "optional": true,
256
+ "os": [
257
+ "linux"
258
+ ],
259
+ "engines": {
260
+ "node": ">=18"
261
+ }
262
+ },
263
+ "node_modules/@esbuild/linux-riscv64": {
264
+ "version": "0.27.3",
265
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
266
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
267
+ "cpu": [
268
+ "riscv64"
269
+ ],
270
+ "dev": true,
271
+ "license": "MIT",
272
+ "optional": true,
273
+ "os": [
274
+ "linux"
275
+ ],
276
+ "engines": {
277
+ "node": ">=18"
278
+ }
279
+ },
280
+ "node_modules/@esbuild/linux-s390x": {
281
+ "version": "0.27.3",
282
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
283
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
284
+ "cpu": [
285
+ "s390x"
286
+ ],
287
+ "dev": true,
288
+ "license": "MIT",
289
+ "optional": true,
290
+ "os": [
291
+ "linux"
292
+ ],
293
+ "engines": {
294
+ "node": ">=18"
295
+ }
296
+ },
297
+ "node_modules/@esbuild/linux-x64": {
298
+ "version": "0.27.3",
299
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
300
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
301
+ "cpu": [
302
+ "x64"
303
+ ],
304
+ "dev": true,
305
+ "license": "MIT",
306
+ "optional": true,
307
+ "os": [
308
+ "linux"
309
+ ],
310
+ "engines": {
311
+ "node": ">=18"
312
+ }
313
+ },
314
+ "node_modules/@esbuild/netbsd-arm64": {
315
+ "version": "0.27.3",
316
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
317
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
318
+ "cpu": [
319
+ "arm64"
320
+ ],
321
+ "dev": true,
322
+ "license": "MIT",
323
+ "optional": true,
324
+ "os": [
325
+ "netbsd"
326
+ ],
327
+ "engines": {
328
+ "node": ">=18"
329
+ }
330
+ },
331
+ "node_modules/@esbuild/netbsd-x64": {
332
+ "version": "0.27.3",
333
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
334
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
335
+ "cpu": [
336
+ "x64"
337
+ ],
338
+ "dev": true,
339
+ "license": "MIT",
340
+ "optional": true,
341
+ "os": [
342
+ "netbsd"
343
+ ],
344
+ "engines": {
345
+ "node": ">=18"
346
+ }
347
+ },
348
+ "node_modules/@esbuild/openbsd-arm64": {
349
+ "version": "0.27.3",
350
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
351
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
352
+ "cpu": [
353
+ "arm64"
354
+ ],
355
+ "dev": true,
356
+ "license": "MIT",
357
+ "optional": true,
358
+ "os": [
359
+ "openbsd"
360
+ ],
361
+ "engines": {
362
+ "node": ">=18"
363
+ }
364
+ },
365
+ "node_modules/@esbuild/openbsd-x64": {
366
+ "version": "0.27.3",
367
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
368
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
369
+ "cpu": [
370
+ "x64"
371
+ ],
372
+ "dev": true,
373
+ "license": "MIT",
374
+ "optional": true,
375
+ "os": [
376
+ "openbsd"
377
+ ],
378
+ "engines": {
379
+ "node": ">=18"
380
+ }
381
+ },
382
+ "node_modules/@esbuild/openharmony-arm64": {
383
+ "version": "0.27.3",
384
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
385
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
386
+ "cpu": [
387
+ "arm64"
388
+ ],
389
+ "dev": true,
390
+ "license": "MIT",
391
+ "optional": true,
392
+ "os": [
393
+ "openharmony"
394
+ ],
395
+ "engines": {
396
+ "node": ">=18"
397
+ }
398
+ },
399
+ "node_modules/@esbuild/sunos-x64": {
400
+ "version": "0.27.3",
401
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
402
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
403
+ "cpu": [
404
+ "x64"
405
+ ],
406
+ "dev": true,
407
+ "license": "MIT",
408
+ "optional": true,
409
+ "os": [
410
+ "sunos"
411
+ ],
412
+ "engines": {
413
+ "node": ">=18"
414
+ }
415
+ },
416
+ "node_modules/@esbuild/win32-arm64": {
417
+ "version": "0.27.3",
418
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
419
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
420
+ "cpu": [
421
+ "arm64"
422
+ ],
423
+ "dev": true,
424
+ "license": "MIT",
425
+ "optional": true,
426
+ "os": [
427
+ "win32"
428
+ ],
429
+ "engines": {
430
+ "node": ">=18"
431
+ }
432
+ },
433
+ "node_modules/@esbuild/win32-ia32": {
434
+ "version": "0.27.3",
435
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
436
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
437
+ "cpu": [
438
+ "ia32"
439
+ ],
440
+ "dev": true,
441
+ "license": "MIT",
442
+ "optional": true,
443
+ "os": [
444
+ "win32"
445
+ ],
446
+ "engines": {
447
+ "node": ">=18"
448
+ }
449
+ },
450
+ "node_modules/@esbuild/win32-x64": {
451
+ "version": "0.27.3",
452
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
453
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
454
+ "cpu": [
455
+ "x64"
456
+ ],
457
+ "dev": true,
458
+ "license": "MIT",
459
+ "optional": true,
460
+ "os": [
461
+ "win32"
462
+ ],
463
+ "engines": {
464
+ "node": ">=18"
465
+ }
466
+ },
467
+ "node_modules/@fastify/ajv-compiler": {
468
+ "version": "4.0.5",
469
+ "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz",
470
+ "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==",
471
+ "funding": [
472
+ {
473
+ "type": "github",
474
+ "url": "https://github.com/sponsors/fastify"
475
+ },
476
+ {
477
+ "type": "opencollective",
478
+ "url": "https://opencollective.com/fastify"
479
+ }
480
+ ],
481
+ "license": "MIT",
482
+ "dependencies": {
483
+ "ajv": "^8.12.0",
484
+ "ajv-formats": "^3.0.1",
485
+ "fast-uri": "^3.0.0"
486
+ }
487
+ },
488
+ "node_modules/@fastify/error": {
489
+ "version": "4.2.0",
490
+ "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
491
+ "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==",
492
+ "funding": [
493
+ {
494
+ "type": "github",
495
+ "url": "https://github.com/sponsors/fastify"
496
+ },
497
+ {
498
+ "type": "opencollective",
499
+ "url": "https://opencollective.com/fastify"
500
+ }
501
+ ],
502
+ "license": "MIT"
503
+ },
504
+ "node_modules/@fastify/fast-json-stringify-compiler": {
505
+ "version": "5.0.3",
506
+ "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz",
507
+ "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==",
508
+ "funding": [
509
+ {
510
+ "type": "github",
511
+ "url": "https://github.com/sponsors/fastify"
512
+ },
513
+ {
514
+ "type": "opencollective",
515
+ "url": "https://opencollective.com/fastify"
516
+ }
517
+ ],
518
+ "license": "MIT",
519
+ "dependencies": {
520
+ "fast-json-stringify": "^6.0.0"
521
+ }
522
+ },
523
+ "node_modules/@fastify/forwarded": {
524
+ "version": "3.0.1",
525
+ "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz",
526
+ "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==",
527
+ "funding": [
528
+ {
529
+ "type": "github",
530
+ "url": "https://github.com/sponsors/fastify"
531
+ },
532
+ {
533
+ "type": "opencollective",
534
+ "url": "https://opencollective.com/fastify"
535
+ }
536
+ ],
537
+ "license": "MIT"
538
+ },
539
+ "node_modules/@fastify/merge-json-schemas": {
540
+ "version": "0.2.1",
541
+ "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
542
+ "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==",
543
+ "funding": [
544
+ {
545
+ "type": "github",
546
+ "url": "https://github.com/sponsors/fastify"
547
+ },
548
+ {
549
+ "type": "opencollective",
550
+ "url": "https://opencollective.com/fastify"
551
+ }
552
+ ],
553
+ "license": "MIT",
554
+ "dependencies": {
555
+ "dequal": "^2.0.3"
556
+ }
557
+ },
558
+ "node_modules/@fastify/proxy-addr": {
559
+ "version": "5.1.0",
560
+ "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
561
+ "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==",
562
+ "funding": [
563
+ {
564
+ "type": "github",
565
+ "url": "https://github.com/sponsors/fastify"
566
+ },
567
+ {
568
+ "type": "opencollective",
569
+ "url": "https://opencollective.com/fastify"
570
+ }
571
+ ],
572
+ "license": "MIT",
573
+ "dependencies": {
574
+ "@fastify/forwarded": "^3.0.0",
575
+ "ipaddr.js": "^2.1.0"
576
+ }
577
+ },
578
+ "node_modules/@fastify/rate-limit": {
579
+ "version": "10.3.0",
580
+ "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz",
581
+ "integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==",
582
+ "funding": [
583
+ {
584
+ "type": "github",
585
+ "url": "https://github.com/sponsors/fastify"
586
+ },
587
+ {
588
+ "type": "opencollective",
589
+ "url": "https://opencollective.com/fastify"
590
+ }
591
+ ],
592
+ "license": "MIT",
593
+ "dependencies": {
594
+ "@lukeed/ms": "^2.0.2",
595
+ "fastify-plugin": "^5.0.0",
596
+ "toad-cache": "^3.7.0"
597
+ }
598
+ },
599
+ "node_modules/@lukeed/ms": {
600
+ "version": "2.0.2",
601
+ "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
602
+ "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
603
+ "license": "MIT",
604
+ "engines": {
605
+ "node": ">=8"
606
+ }
607
+ },
608
+ "node_modules/@pinojs/redact": {
609
+ "version": "0.4.0",
610
+ "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
611
+ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
612
+ "license": "MIT"
613
+ },
614
+ "node_modules/@types/node": {
615
+ "version": "22.19.15",
616
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
617
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
618
+ "dev": true,
619
+ "license": "MIT",
620
+ "dependencies": {
621
+ "undici-types": "~6.21.0"
622
+ }
623
+ },
624
+ "node_modules/abstract-logging": {
625
+ "version": "2.0.1",
626
+ "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
627
+ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
628
+ "license": "MIT"
629
+ },
630
+ "node_modules/ajv": {
631
+ "version": "8.18.0",
632
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
633
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
634
+ "license": "MIT",
635
+ "dependencies": {
636
+ "fast-deep-equal": "^3.1.3",
637
+ "fast-uri": "^3.0.1",
638
+ "json-schema-traverse": "^1.0.0",
639
+ "require-from-string": "^2.0.2"
640
+ },
641
+ "funding": {
642
+ "type": "github",
643
+ "url": "https://github.com/sponsors/epoberezkin"
644
+ }
645
+ },
646
+ "node_modules/ajv-formats": {
647
+ "version": "3.0.1",
648
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
649
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
650
+ "license": "MIT",
651
+ "dependencies": {
652
+ "ajv": "^8.0.0"
653
+ },
654
+ "peerDependencies": {
655
+ "ajv": "^8.0.0"
656
+ },
657
+ "peerDependenciesMeta": {
658
+ "ajv": {
659
+ "optional": true
660
+ }
661
+ }
662
+ },
663
+ "node_modules/atomic-sleep": {
664
+ "version": "1.0.0",
665
+ "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
666
+ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
667
+ "license": "MIT",
668
+ "engines": {
669
+ "node": ">=8.0.0"
670
+ }
671
+ },
672
+ "node_modules/avvio": {
673
+ "version": "9.2.0",
674
+ "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz",
675
+ "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==",
676
+ "funding": [
677
+ {
678
+ "type": "github",
679
+ "url": "https://github.com/sponsors/fastify"
680
+ },
681
+ {
682
+ "type": "opencollective",
683
+ "url": "https://opencollective.com/fastify"
684
+ }
685
+ ],
686
+ "license": "MIT",
687
+ "dependencies": {
688
+ "@fastify/error": "^4.0.0",
689
+ "fastq": "^1.17.1"
690
+ }
691
+ },
692
+ "node_modules/cookie": {
693
+ "version": "1.1.1",
694
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
695
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
696
+ "license": "MIT",
697
+ "engines": {
698
+ "node": ">=18"
699
+ },
700
+ "funding": {
701
+ "type": "opencollective",
702
+ "url": "https://opencollective.com/express"
703
+ }
704
+ },
705
+ "node_modules/dequal": {
706
+ "version": "2.0.3",
707
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
708
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
709
+ "license": "MIT",
710
+ "engines": {
711
+ "node": ">=6"
712
+ }
713
+ },
714
+ "node_modules/dotenv": {
715
+ "version": "16.6.1",
716
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
717
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
718
+ "license": "BSD-2-Clause",
719
+ "engines": {
720
+ "node": ">=12"
721
+ },
722
+ "funding": {
723
+ "url": "https://dotenvx.com"
724
+ }
725
+ },
726
+ "node_modules/esbuild": {
727
+ "version": "0.27.3",
728
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
729
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
730
+ "dev": true,
731
+ "hasInstallScript": true,
732
+ "license": "MIT",
733
+ "bin": {
734
+ "esbuild": "bin/esbuild"
735
+ },
736
+ "engines": {
737
+ "node": ">=18"
738
+ },
739
+ "optionalDependencies": {
740
+ "@esbuild/aix-ppc64": "0.27.3",
741
+ "@esbuild/android-arm": "0.27.3",
742
+ "@esbuild/android-arm64": "0.27.3",
743
+ "@esbuild/android-x64": "0.27.3",
744
+ "@esbuild/darwin-arm64": "0.27.3",
745
+ "@esbuild/darwin-x64": "0.27.3",
746
+ "@esbuild/freebsd-arm64": "0.27.3",
747
+ "@esbuild/freebsd-x64": "0.27.3",
748
+ "@esbuild/linux-arm": "0.27.3",
749
+ "@esbuild/linux-arm64": "0.27.3",
750
+ "@esbuild/linux-ia32": "0.27.3",
751
+ "@esbuild/linux-loong64": "0.27.3",
752
+ "@esbuild/linux-mips64el": "0.27.3",
753
+ "@esbuild/linux-ppc64": "0.27.3",
754
+ "@esbuild/linux-riscv64": "0.27.3",
755
+ "@esbuild/linux-s390x": "0.27.3",
756
+ "@esbuild/linux-x64": "0.27.3",
757
+ "@esbuild/netbsd-arm64": "0.27.3",
758
+ "@esbuild/netbsd-x64": "0.27.3",
759
+ "@esbuild/openbsd-arm64": "0.27.3",
760
+ "@esbuild/openbsd-x64": "0.27.3",
761
+ "@esbuild/openharmony-arm64": "0.27.3",
762
+ "@esbuild/sunos-x64": "0.27.3",
763
+ "@esbuild/win32-arm64": "0.27.3",
764
+ "@esbuild/win32-ia32": "0.27.3",
765
+ "@esbuild/win32-x64": "0.27.3"
766
+ }
767
+ },
768
+ "node_modules/fast-decode-uri-component": {
769
+ "version": "1.0.1",
770
+ "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
771
+ "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
772
+ "license": "MIT"
773
+ },
774
+ "node_modules/fast-deep-equal": {
775
+ "version": "3.1.3",
776
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
777
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
778
+ "license": "MIT"
779
+ },
780
+ "node_modules/fast-json-stringify": {
781
+ "version": "6.3.0",
782
+ "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz",
783
+ "integrity": "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==",
784
+ "funding": [
785
+ {
786
+ "type": "github",
787
+ "url": "https://github.com/sponsors/fastify"
788
+ },
789
+ {
790
+ "type": "opencollective",
791
+ "url": "https://opencollective.com/fastify"
792
+ }
793
+ ],
794
+ "license": "MIT",
795
+ "dependencies": {
796
+ "@fastify/merge-json-schemas": "^0.2.0",
797
+ "ajv": "^8.12.0",
798
+ "ajv-formats": "^3.0.1",
799
+ "fast-uri": "^3.0.0",
800
+ "json-schema-ref-resolver": "^3.0.0",
801
+ "rfdc": "^1.2.0"
802
+ }
803
+ },
804
+ "node_modules/fast-querystring": {
805
+ "version": "1.1.2",
806
+ "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
807
+ "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
808
+ "license": "MIT",
809
+ "dependencies": {
810
+ "fast-decode-uri-component": "^1.0.1"
811
+ }
812
+ },
813
+ "node_modules/fast-uri": {
814
+ "version": "3.1.0",
815
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
816
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
817
+ "funding": [
818
+ {
819
+ "type": "github",
820
+ "url": "https://github.com/sponsors/fastify"
821
+ },
822
+ {
823
+ "type": "opencollective",
824
+ "url": "https://opencollective.com/fastify"
825
+ }
826
+ ],
827
+ "license": "BSD-3-Clause"
828
+ },
829
+ "node_modules/fastify": {
830
+ "version": "5.8.1",
831
+ "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.1.tgz",
832
+ "integrity": "sha512-y0kicFvvn7CYWoPOVLOcvn4YyKQz03DIY7UxmyOy21/J8eXm09R+tmb+tVDBW5h+pja30cHI5dqUcSlvY86V2A==",
833
+ "funding": [
834
+ {
835
+ "type": "github",
836
+ "url": "https://github.com/sponsors/fastify"
837
+ },
838
+ {
839
+ "type": "opencollective",
840
+ "url": "https://opencollective.com/fastify"
841
+ }
842
+ ],
843
+ "license": "MIT",
844
+ "dependencies": {
845
+ "@fastify/ajv-compiler": "^4.0.5",
846
+ "@fastify/error": "^4.0.0",
847
+ "@fastify/fast-json-stringify-compiler": "^5.0.0",
848
+ "@fastify/proxy-addr": "^5.0.0",
849
+ "abstract-logging": "^2.0.1",
850
+ "avvio": "^9.0.0",
851
+ "fast-json-stringify": "^6.0.0",
852
+ "find-my-way": "^9.0.0",
853
+ "light-my-request": "^6.0.0",
854
+ "pino": "^9.14.0 || ^10.1.0",
855
+ "process-warning": "^5.0.0",
856
+ "rfdc": "^1.3.1",
857
+ "secure-json-parse": "^4.0.0",
858
+ "semver": "^7.6.0",
859
+ "toad-cache": "^3.7.0"
860
+ }
861
+ },
862
+ "node_modules/fastify-plugin": {
863
+ "version": "5.1.0",
864
+ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
865
+ "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
866
+ "funding": [
867
+ {
868
+ "type": "github",
869
+ "url": "https://github.com/sponsors/fastify"
870
+ },
871
+ {
872
+ "type": "opencollective",
873
+ "url": "https://opencollective.com/fastify"
874
+ }
875
+ ],
876
+ "license": "MIT"
877
+ },
878
+ "node_modules/fastq": {
879
+ "version": "1.20.1",
880
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
881
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
882
+ "license": "ISC",
883
+ "dependencies": {
884
+ "reusify": "^1.0.4"
885
+ }
886
+ },
887
+ "node_modules/find-my-way": {
888
+ "version": "9.5.0",
889
+ "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz",
890
+ "integrity": "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==",
891
+ "license": "MIT",
892
+ "dependencies": {
893
+ "fast-deep-equal": "^3.1.3",
894
+ "fast-querystring": "^1.0.0",
895
+ "safe-regex2": "^5.0.0"
896
+ },
897
+ "engines": {
898
+ "node": ">=20"
899
+ }
900
+ },
901
+ "node_modules/fsevents": {
902
+ "version": "2.3.3",
903
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
904
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
905
+ "dev": true,
906
+ "hasInstallScript": true,
907
+ "license": "MIT",
908
+ "optional": true,
909
+ "os": [
910
+ "darwin"
911
+ ],
912
+ "engines": {
913
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
914
+ }
915
+ },
916
+ "node_modules/get-tsconfig": {
917
+ "version": "4.13.6",
918
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
919
+ "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
920
+ "dev": true,
921
+ "license": "MIT",
922
+ "dependencies": {
923
+ "resolve-pkg-maps": "^1.0.0"
924
+ },
925
+ "funding": {
926
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
927
+ }
928
+ },
929
+ "node_modules/ipaddr.js": {
930
+ "version": "2.3.0",
931
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
932
+ "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
933
+ "license": "MIT",
934
+ "engines": {
935
+ "node": ">= 10"
936
+ }
937
+ },
938
+ "node_modules/json-schema-ref-resolver": {
939
+ "version": "3.0.0",
940
+ "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz",
941
+ "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==",
942
+ "funding": [
943
+ {
944
+ "type": "github",
945
+ "url": "https://github.com/sponsors/fastify"
946
+ },
947
+ {
948
+ "type": "opencollective",
949
+ "url": "https://opencollective.com/fastify"
950
+ }
951
+ ],
952
+ "license": "MIT",
953
+ "dependencies": {
954
+ "dequal": "^2.0.3"
955
+ }
956
+ },
957
+ "node_modules/json-schema-traverse": {
958
+ "version": "1.0.0",
959
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
960
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
961
+ "license": "MIT"
962
+ },
963
+ "node_modules/light-my-request": {
964
+ "version": "6.6.0",
965
+ "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
966
+ "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==",
967
+ "funding": [
968
+ {
969
+ "type": "github",
970
+ "url": "https://github.com/sponsors/fastify"
971
+ },
972
+ {
973
+ "type": "opencollective",
974
+ "url": "https://opencollective.com/fastify"
975
+ }
976
+ ],
977
+ "license": "BSD-3-Clause",
978
+ "dependencies": {
979
+ "cookie": "^1.0.1",
980
+ "process-warning": "^4.0.0",
981
+ "set-cookie-parser": "^2.6.0"
982
+ }
983
+ },
984
+ "node_modules/light-my-request/node_modules/process-warning": {
985
+ "version": "4.0.1",
986
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
987
+ "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
988
+ "funding": [
989
+ {
990
+ "type": "github",
991
+ "url": "https://github.com/sponsors/fastify"
992
+ },
993
+ {
994
+ "type": "opencollective",
995
+ "url": "https://opencollective.com/fastify"
996
+ }
997
+ ],
998
+ "license": "MIT"
999
+ },
1000
+ "node_modules/on-exit-leak-free": {
1001
+ "version": "2.1.2",
1002
+ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
1003
+ "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
1004
+ "license": "MIT",
1005
+ "engines": {
1006
+ "node": ">=14.0.0"
1007
+ }
1008
+ },
1009
+ "node_modules/pino": {
1010
+ "version": "10.3.1",
1011
+ "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
1012
+ "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
1013
+ "license": "MIT",
1014
+ "dependencies": {
1015
+ "@pinojs/redact": "^0.4.0",
1016
+ "atomic-sleep": "^1.0.0",
1017
+ "on-exit-leak-free": "^2.1.0",
1018
+ "pino-abstract-transport": "^3.0.0",
1019
+ "pino-std-serializers": "^7.0.0",
1020
+ "process-warning": "^5.0.0",
1021
+ "quick-format-unescaped": "^4.0.3",
1022
+ "real-require": "^0.2.0",
1023
+ "safe-stable-stringify": "^2.3.1",
1024
+ "sonic-boom": "^4.0.1",
1025
+ "thread-stream": "^4.0.0"
1026
+ },
1027
+ "bin": {
1028
+ "pino": "bin.js"
1029
+ }
1030
+ },
1031
+ "node_modules/pino-abstract-transport": {
1032
+ "version": "3.0.0",
1033
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
1034
+ "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
1035
+ "license": "MIT",
1036
+ "dependencies": {
1037
+ "split2": "^4.0.0"
1038
+ }
1039
+ },
1040
+ "node_modules/pino-std-serializers": {
1041
+ "version": "7.1.0",
1042
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
1043
+ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
1044
+ "license": "MIT"
1045
+ },
1046
+ "node_modules/process-warning": {
1047
+ "version": "5.0.0",
1048
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
1049
+ "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
1050
+ "funding": [
1051
+ {
1052
+ "type": "github",
1053
+ "url": "https://github.com/sponsors/fastify"
1054
+ },
1055
+ {
1056
+ "type": "opencollective",
1057
+ "url": "https://opencollective.com/fastify"
1058
+ }
1059
+ ],
1060
+ "license": "MIT"
1061
+ },
1062
+ "node_modules/quick-format-unescaped": {
1063
+ "version": "4.0.4",
1064
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
1065
+ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
1066
+ "license": "MIT"
1067
+ },
1068
+ "node_modules/real-require": {
1069
+ "version": "0.2.0",
1070
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
1071
+ "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
1072
+ "license": "MIT",
1073
+ "engines": {
1074
+ "node": ">= 12.13.0"
1075
+ }
1076
+ },
1077
+ "node_modules/require-from-string": {
1078
+ "version": "2.0.2",
1079
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
1080
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
1081
+ "license": "MIT",
1082
+ "engines": {
1083
+ "node": ">=0.10.0"
1084
+ }
1085
+ },
1086
+ "node_modules/resolve-pkg-maps": {
1087
+ "version": "1.0.0",
1088
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
1089
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
1090
+ "dev": true,
1091
+ "license": "MIT",
1092
+ "funding": {
1093
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
1094
+ }
1095
+ },
1096
+ "node_modules/ret": {
1097
+ "version": "0.5.0",
1098
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
1099
+ "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
1100
+ "license": "MIT",
1101
+ "engines": {
1102
+ "node": ">=10"
1103
+ }
1104
+ },
1105
+ "node_modules/reusify": {
1106
+ "version": "1.1.0",
1107
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
1108
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
1109
+ "license": "MIT",
1110
+ "engines": {
1111
+ "iojs": ">=1.0.0",
1112
+ "node": ">=0.10.0"
1113
+ }
1114
+ },
1115
+ "node_modules/rfdc": {
1116
+ "version": "1.4.1",
1117
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
1118
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
1119
+ "license": "MIT"
1120
+ },
1121
+ "node_modules/safe-regex2": {
1122
+ "version": "5.0.0",
1123
+ "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz",
1124
+ "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==",
1125
+ "funding": [
1126
+ {
1127
+ "type": "github",
1128
+ "url": "https://github.com/sponsors/fastify"
1129
+ },
1130
+ {
1131
+ "type": "opencollective",
1132
+ "url": "https://opencollective.com/fastify"
1133
+ }
1134
+ ],
1135
+ "license": "MIT",
1136
+ "dependencies": {
1137
+ "ret": "~0.5.0"
1138
+ }
1139
+ },
1140
+ "node_modules/safe-stable-stringify": {
1141
+ "version": "2.5.0",
1142
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
1143
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
1144
+ "license": "MIT",
1145
+ "engines": {
1146
+ "node": ">=10"
1147
+ }
1148
+ },
1149
+ "node_modules/secure-json-parse": {
1150
+ "version": "4.1.0",
1151
+ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
1152
+ "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
1153
+ "funding": [
1154
+ {
1155
+ "type": "github",
1156
+ "url": "https://github.com/sponsors/fastify"
1157
+ },
1158
+ {
1159
+ "type": "opencollective",
1160
+ "url": "https://opencollective.com/fastify"
1161
+ }
1162
+ ],
1163
+ "license": "BSD-3-Clause"
1164
+ },
1165
+ "node_modules/semver": {
1166
+ "version": "7.7.4",
1167
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
1168
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1169
+ "license": "ISC",
1170
+ "bin": {
1171
+ "semver": "bin/semver.js"
1172
+ },
1173
+ "engines": {
1174
+ "node": ">=10"
1175
+ }
1176
+ },
1177
+ "node_modules/set-cookie-parser": {
1178
+ "version": "2.7.2",
1179
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
1180
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
1181
+ "license": "MIT"
1182
+ },
1183
+ "node_modules/sonic-boom": {
1184
+ "version": "4.2.1",
1185
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
1186
+ "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
1187
+ "license": "MIT",
1188
+ "dependencies": {
1189
+ "atomic-sleep": "^1.0.0"
1190
+ }
1191
+ },
1192
+ "node_modules/split2": {
1193
+ "version": "4.2.0",
1194
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
1195
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
1196
+ "license": "ISC",
1197
+ "engines": {
1198
+ "node": ">= 10.x"
1199
+ }
1200
+ },
1201
+ "node_modules/thread-stream": {
1202
+ "version": "4.0.0",
1203
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
1204
+ "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
1205
+ "license": "MIT",
1206
+ "dependencies": {
1207
+ "real-require": "^0.2.0"
1208
+ },
1209
+ "engines": {
1210
+ "node": ">=20"
1211
+ }
1212
+ },
1213
+ "node_modules/toad-cache": {
1214
+ "version": "3.7.0",
1215
+ "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
1216
+ "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
1217
+ "license": "MIT",
1218
+ "engines": {
1219
+ "node": ">=12"
1220
+ }
1221
+ },
1222
+ "node_modules/tsx": {
1223
+ "version": "4.21.0",
1224
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
1225
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
1226
+ "dev": true,
1227
+ "license": "MIT",
1228
+ "dependencies": {
1229
+ "esbuild": "~0.27.0",
1230
+ "get-tsconfig": "^4.7.5"
1231
+ },
1232
+ "bin": {
1233
+ "tsx": "dist/cli.mjs"
1234
+ },
1235
+ "engines": {
1236
+ "node": ">=18.0.0"
1237
+ },
1238
+ "optionalDependencies": {
1239
+ "fsevents": "~2.3.3"
1240
+ }
1241
+ },
1242
+ "node_modules/typescript": {
1243
+ "version": "5.9.3",
1244
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1245
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1246
+ "dev": true,
1247
+ "license": "Apache-2.0",
1248
+ "bin": {
1249
+ "tsc": "bin/tsc",
1250
+ "tsserver": "bin/tsserver"
1251
+ },
1252
+ "engines": {
1253
+ "node": ">=14.17"
1254
+ }
1255
+ },
1256
+ "node_modules/undici": {
1257
+ "version": "7.22.0",
1258
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
1259
+ "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
1260
+ "license": "MIT",
1261
+ "engines": {
1262
+ "node": ">=20.18.1"
1263
+ }
1264
+ },
1265
+ "node_modules/undici-types": {
1266
+ "version": "6.21.0",
1267
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1268
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1269
+ "dev": true,
1270
+ "license": "MIT"
1271
+ }
1272
+ }
1273
+ }
package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ai-proxy",
3
+ "version": "1.0.0",
4
+ "description": "LLM Gateway Proxy for Claude Code – transparent Anthropic API relay with auth-swap",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "dev": "tsx --watch src/index.ts",
9
+ "build": "tsc",
10
+ "start": "node dist/index.js"
11
+ },
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "dependencies": {
16
+ "fastify": "^5.3.3",
17
+ "@fastify/rate-limit": "^10.2.2",
18
+ "dotenv": "^16.5.0",
19
+ "undici": "^7.5.0"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.8.2",
23
+ "@types/node": "^22.13.10",
24
+ "tsx": "^4.19.3"
25
+ }
26
+ }
src/auth.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2
+
3
+ /**
4
+ * Creates a Fastify onRequest hook that validates the Authorization header
5
+ * against the configured proxy auth token.
6
+ *
7
+ * Rejects with 401 if the header is missing or the token doesn't match.
8
+ */
9
+ export function createAuthHook(proxyAuthToken: string): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
10
+ return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
11
+ const authHeader = request.headers['authorization'];
12
+
13
+ if (!authHeader) {
14
+ reply.code(401).send({ error: 'Missing Authorization header' });
15
+ return;
16
+ }
17
+
18
+ // Support "Bearer <token>" format
19
+ const token = authHeader.startsWith('Bearer ')
20
+ ? authHeader.slice(7)
21
+ : authHeader;
22
+
23
+ if (token !== proxyAuthToken) {
24
+ reply.code(401).send({ error: 'Invalid authorization token' });
25
+ return;
26
+ }
27
+ };
28
+ }
src/config.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'dotenv/config';
2
+ import type { ProxyConfig } from './types.js';
3
+
4
+ /**
5
+ * Reads a required environment variable or aborts the process.
6
+ */
7
+ function requireEnv(name: string): string {
8
+ const value = process.env[name]?.trim();
9
+ if (!value) {
10
+ console.error(`[FATAL] Missing required environment variable: ${name}`);
11
+ process.exit(1);
12
+ }
13
+ return value;
14
+ }
15
+
16
+ /**
17
+ * Reads an optional environment variable with a fallback default.
18
+ */
19
+ function optionalEnv(name: string, fallback: string): string {
20
+ return process.env[name]?.trim() || fallback;
21
+ }
22
+
23
+ /**
24
+ * Parses and validates all environment variables into a typed, frozen config object.
25
+ * Aborts the process immediately if required variables are missing.
26
+ */
27
+ export function loadConfig(): ProxyConfig {
28
+ const config: ProxyConfig = {
29
+ anthropicApiKey: requireEnv('ANTHROPIC_API_KEY'),
30
+ proxyAuthToken: requireEnv('PROXY_AUTH_TOKEN'),
31
+ port: parseInt(optionalEnv('PORT', '7860'), 10),
32
+ host: optionalEnv('HOST', '0.0.0.0'),
33
+ logLevel: optionalEnv('LOG_LEVEL', 'info'),
34
+ rateLimitMax: parseInt(optionalEnv('RATE_LIMIT_MAX', '100'), 10),
35
+ rateLimitWindowMs: parseInt(optionalEnv('RATE_LIMIT_WINDOW_MS', '60000'), 10),
36
+ bodyLimit: parseInt(optionalEnv('BODY_LIMIT', '5242880'), 10),
37
+ corsOrigin: optionalEnv('CORS_ORIGIN', ''),
38
+ anthropicBaseUrl: optionalEnv('ANTHROPIC_BASE_URL', 'https://api.anthropic.com'),
39
+ upstreamTimeoutMs: parseInt(optionalEnv('UPSTREAM_TIMEOUT_MS', '300000'), 10),
40
+ };
41
+
42
+ // Validate numeric values
43
+ if (isNaN(config.port) || config.port < 1 || config.port > 65535) {
44
+ console.error(`[FATAL] Invalid PORT value: ${process.env['PORT']}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ return Object.freeze(config);
49
+ }
src/index.ts ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Fastify, { type FastifyError } from 'fastify';
2
+ import rateLimit from '@fastify/rate-limit';
3
+ import { loadConfig } from './config.js';
4
+ import { createAuthHook } from './auth.js';
5
+ import { forwardRequest } from './proxy.js';
6
+ import { PROXY_ROUTES } from './types.js';
7
+
8
+ const config = loadConfig();
9
+
10
+ const app = Fastify({
11
+ logger: {
12
+ level: config.logLevel,
13
+ // Redact sensitive fields from logs
14
+ redact: ['req.headers.authorization', 'req.headers["x-api-key"]'],
15
+ },
16
+ bodyLimit: config.bodyLimit,
17
+ trustProxy: true,
18
+ });
19
+
20
+ /** Register rate limiting. */
21
+ await app.register(rateLimit, {
22
+ max: config.rateLimitMax,
23
+ timeWindow: config.rateLimitWindowMs,
24
+ addHeadersOnExceeding: { 'x-ratelimit-limit': true, 'x-ratelimit-remaining': true, 'x-ratelimit-reset': true },
25
+ addHeaders: { 'x-ratelimit-limit': true, 'x-ratelimit-remaining': true, 'x-ratelimit-reset': true, 'retry-after': true },
26
+ });
27
+
28
+ /** Optional CORS support – @fastify/cors must be installed separately. */
29
+ if (config.corsOrigin) {
30
+ try {
31
+ // Dynamic import: @fastify/cors is an optional dependency
32
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
33
+ const corsPlugin = await import(/* webpackIgnore: true */ '@fastify/cors' + '');
34
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
35
+ await app.register(corsPlugin.default ?? corsPlugin, { origin: config.corsOrigin });
36
+ } catch {
37
+ app.log.warn('CORS_ORIGIN is set but @fastify/cors is not installed. Run: npm install @fastify/cors');
38
+ }
39
+ }
40
+
41
+ /** Add defensive response headers to every response. */
42
+ app.addHook('onSend', async (_request, reply) => {
43
+ reply.header('X-Content-Type-Options', 'nosniff');
44
+ reply.header('X-Frame-Options', 'DENY');
45
+ });
46
+
47
+ /** Auth hook instance. */
48
+ const authHook = createAuthHook(config.proxyAuthToken);
49
+
50
+ /**
51
+ * Content-Type validation hook for API routes.
52
+ * Rejects requests that don't send application/json.
53
+ */
54
+ async function validateContentType(
55
+ request: Parameters<typeof authHook>[0],
56
+ reply: Parameters<typeof authHook>[1],
57
+ ): Promise<void> {
58
+ const ct = request.headers['content-type'];
59
+ if (!ct || !ct.includes('application/json')) {
60
+ reply.code(415).send({ error: 'Unsupported Media Type. Expected application/json.' });
61
+ }
62
+ }
63
+
64
+ // ── Routes ──────────────────────────────────────────────────────────────────
65
+
66
+ /** Health check – no auth required. */
67
+ app.get('/health', async (_request, reply) => {
68
+ reply.send({ status: 'ok' });
69
+ });
70
+
71
+ /** Register proxy routes. */
72
+ for (const route of PROXY_ROUTES) {
73
+ app.post(route, {
74
+ onRequest: [authHook, validateContentType],
75
+ }, async (request, reply) => {
76
+ await forwardRequest(request, reply, route, config, app.log);
77
+ });
78
+ }
79
+
80
+ /** Catch-all 404 for unregistered routes. */
81
+ app.setNotFoundHandler((_request, reply) => {
82
+ reply.code(404).send({ error: 'Not found' });
83
+ });
84
+
85
+ /** Method not allowed – not needed since Fastify handles it,
86
+ * but we customize the response format. */
87
+ app.setErrorHandler((error: FastifyError, _request, reply) => {
88
+ const statusCode = error.statusCode ?? 500;
89
+
90
+ if (statusCode === 405) {
91
+ reply.code(405).send({ error: 'Method not allowed' });
92
+ return;
93
+ }
94
+ if (statusCode === 429) {
95
+ // Rate limit exceeded – forward Fastify's rate-limit response
96
+ reply.code(429).send({ error: 'Too many requests. Please retry later.' });
97
+ return;
98
+ }
99
+
100
+ app.log.error({ err: error }, 'Unhandled error');
101
+ reply.code(statusCode).send({ error: 'Internal server error' });
102
+ });
103
+
104
+ // ── Server Start ────────────────────────────────────────────────────────────
105
+
106
+ const start = async (): Promise<void> => {
107
+ try {
108
+ await app.listen({ port: config.port, host: config.host });
109
+ app.log.info(`Proxy listening on ${config.host}:${config.port}`);
110
+ } catch (err) {
111
+ app.log.fatal({ err }, 'Failed to start server');
112
+ process.exit(1);
113
+ }
114
+ };
115
+
116
+ // ── Graceful Shutdown ───────────────────────────────────────────────────────
117
+
118
+ const shutdown = async (signal: string): Promise<void> => {
119
+ app.log.info(`Received ${signal}, shutting down gracefully…`);
120
+ try {
121
+ await app.close();
122
+ app.log.info('Server closed.');
123
+ process.exit(0);
124
+ } catch (err) {
125
+ app.log.error({ err }, 'Error during shutdown');
126
+ process.exit(1);
127
+ }
128
+ };
129
+
130
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
131
+ process.on('SIGINT', () => void shutdown('SIGINT'));
132
+
133
+ // Catch unhandled rejections to keep the server stable
134
+ process.on('unhandledRejection', (reason) => {
135
+ app.log.error({ reason }, 'Unhandled promise rejection');
136
+ });
137
+
138
+ await start();
src/proxy.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { request as undiciRequest } from 'undici';
2
+ import type { FastifyRequest, FastifyReply, FastifyBaseLogger } from 'fastify';
3
+ import type { ProxyConfig } from './types.js';
4
+ import { FORWARDED_HEADERS, HOP_BY_HOP_HEADERS } from './types.js';
5
+
6
+ /**
7
+ * Builds the sanitized header set for the upstream request.
8
+ *
9
+ * - Forwards only explicitly allowed headers from the client
10
+ * - Strips authorization and x-api-key (client must not set the real key)
11
+ * - Strips hop-by-hop headers
12
+ * - Injects the real Anthropic API key as x-api-key
13
+ */
14
+ function buildUpstreamHeaders(
15
+ incomingHeaders: Record<string, string | string[] | undefined>,
16
+ anthropicApiKey: string,
17
+ ): Record<string, string> {
18
+ const headers: Record<string, string> = {};
19
+
20
+ for (const name of FORWARDED_HEADERS) {
21
+ const value = incomingHeaders[name];
22
+ if (value !== undefined) {
23
+ headers[name] = Array.isArray(value) ? value.join(', ') : value;
24
+ }
25
+ }
26
+
27
+ // Inject the real API key
28
+ headers['x-api-key'] = anthropicApiKey;
29
+
30
+ return headers;
31
+ }
32
+
33
+ /**
34
+ * Checks whether a response header should be forwarded back to the client.
35
+ * Strips hop-by-hop and internal headers.
36
+ */
37
+ function shouldForwardResponseHeader(name: string): boolean {
38
+ const lower = name.toLowerCase();
39
+ if (HOP_BY_HOP_HEADERS.includes(lower)) return false;
40
+ if (lower === 'x-api-key') return false;
41
+ return true;
42
+ }
43
+
44
+ /**
45
+ * Forwards an incoming request to the upstream Anthropic API and streams
46
+ * or relays the response back to the client.
47
+ *
48
+ * - Streaming: if upstream responds with `text/event-stream`, the body is
49
+ * piped directly to the client without buffering.
50
+ * - Non-streaming: the full response body is read and sent back.
51
+ * - Errors from upstream are forwarded with their original status code and body.
52
+ */
53
+ export async function forwardRequest(
54
+ request: FastifyRequest,
55
+ reply: FastifyReply,
56
+ targetPath: string,
57
+ config: ProxyConfig,
58
+ logger: FastifyBaseLogger,
59
+ ): Promise<void> {
60
+ const upstreamUrl = `${config.anthropicBaseUrl}${targetPath}`;
61
+
62
+ const upstreamHeaders = buildUpstreamHeaders(
63
+ request.headers as Record<string, string | string[] | undefined>,
64
+ config.anthropicApiKey,
65
+ );
66
+
67
+ logger.debug({ upstreamUrl, headers: Object.keys(upstreamHeaders) }, 'Forwarding request upstream');
68
+
69
+ let upstreamResponse: Awaited<ReturnType<typeof undiciRequest>>;
70
+ try {
71
+ upstreamResponse = await undiciRequest(upstreamUrl, {
72
+ method: 'POST',
73
+ headers: upstreamHeaders,
74
+ body: JSON.stringify(request.body),
75
+ headersTimeout: config.upstreamTimeoutMs,
76
+ bodyTimeout: config.upstreamTimeoutMs,
77
+ });
78
+ } catch (err: unknown) {
79
+ const message = err instanceof Error ? err.message : 'Unknown upstream error';
80
+ logger.error({ err }, 'Upstream request failed');
81
+
82
+ // Distinguish timeout from other errors
83
+ if (message.includes('timeout') || message.includes('Timeout')) {
84
+ reply.code(504).send({ error: 'Upstream request timed out' });
85
+ } else {
86
+ reply.code(502).send({ error: 'Failed to connect to upstream API' });
87
+ }
88
+ return;
89
+ }
90
+
91
+ const { statusCode, headers: responseHeaders, body: responseBody } = upstreamResponse;
92
+
93
+ // Forward response headers (filtered)
94
+ const forwardedHeaders: Record<string, string | string[]> = {};
95
+ for (const [name, value] of Object.entries(responseHeaders)) {
96
+ if (value !== undefined && shouldForwardResponseHeader(name)) {
97
+ forwardedHeaders[name] = value;
98
+ }
99
+ }
100
+
101
+ const contentType = responseHeaders['content-type'];
102
+ const isStreaming =
103
+ typeof contentType === 'string' && contentType.includes('text/event-stream');
104
+
105
+ if (isStreaming) {
106
+ // Streaming: pipe upstream SSE body directly to the client without buffering
107
+ logger.debug('Streaming SSE response to client');
108
+
109
+ reply.raw.writeHead(statusCode, forwardedHeaders as Record<string, string>);
110
+
111
+ for await (const chunk of responseBody) {
112
+ if (!reply.raw.write(chunk)) {
113
+ // Back-pressure: wait for drain
114
+ await new Promise<void>((resolve) => reply.raw.once('drain', resolve));
115
+ }
116
+ }
117
+
118
+ reply.raw.end();
119
+ // Mark the reply as sent so Fastify doesn't try to send again
120
+ reply.hijack();
121
+ } else {
122
+ // Non-streaming: read full body and forward
123
+ const bodyChunks: Buffer[] = [];
124
+ for await (const chunk of responseBody) {
125
+ bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
126
+ }
127
+ const fullBody = Buffer.concat(bodyChunks);
128
+
129
+ logger.debug({ statusCode, bodyLength: fullBody.length }, 'Forwarding non-streaming response');
130
+
131
+ reply.code(statusCode).headers(forwardedHeaders).send(fullBody);
132
+ }
133
+ }
src/types.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** Shared TypeScript types for the proxy server. */
2
+
3
+ /** Typed server configuration derived from environment variables. */
4
+ export interface ProxyConfig {
5
+ /** Anthropic API key used for upstream requests. */
6
+ readonly anthropicApiKey: string;
7
+ /** Shared secret token for client authentication. */
8
+ readonly proxyAuthToken: string;
9
+ /** Server listen port. */
10
+ readonly port: number;
11
+ /** Server listen host. */
12
+ readonly host: string;
13
+ /** Pino log level. */
14
+ readonly logLevel: string;
15
+ /** Max requests per rate-limit window per IP. */
16
+ readonly rateLimitMax: number;
17
+ /** Rate-limit window duration in ms. */
18
+ readonly rateLimitWindowMs: number;
19
+ /** Max request body size in bytes. */
20
+ readonly bodyLimit: number;
21
+ /** CORS origin (empty string = disabled). */
22
+ readonly corsOrigin: string;
23
+ /** Upstream Anthropic base URL. */
24
+ readonly anthropicBaseUrl: string;
25
+ /** Upstream request timeout in ms. */
26
+ readonly upstreamTimeoutMs: number;
27
+ }
28
+
29
+ /** Headers that must be forwarded to the upstream API. */
30
+ export const FORWARDED_HEADERS: ReadonlyArray<string> = [
31
+ 'anthropic-beta',
32
+ 'anthropic-version',
33
+ 'content-type',
34
+ 'accept',
35
+ 'x-request-id',
36
+ ] as const;
37
+
38
+ /** Hop-by-hop headers that must be stripped before forwarding. */
39
+ export const HOP_BY_HOP_HEADERS: ReadonlyArray<string> = [
40
+ 'connection', // Controls if the network connection stays open after the current transaction.
41
+ 'keep-alive', // Settings for persistent connections.
42
+ 'transfer-encoding', // Specifies the form of encoding used to safely transfer the payload.
43
+ 'upgrade', // Mechanism for switching to a different protocol (e.g., WebSocket).
44
+ 'proxy-authorization', // Credentials for authenticating the client with a proxy.
45
+ 'te', // Specifies the transfer encodings the user agent is willing to accept.
46
+ 'trailer', // Indicates that specific headers will be present in the trailer of a chunked message.
47
+ ] as const;
48
+
49
+ /** Allowed proxy route paths. */
50
+ export const PROXY_ROUTES = [
51
+ '/v1/messages',
52
+ '/v1/messages/count_tokens',
53
+ ] as const;
54
+
55
+ export type ProxyRoute = (typeof PROXY_ROUTES)[number];
tsconfig.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": [
18
+ "src/**/*"
19
+ ],
20
+ "exclude": [
21
+ "node_modules",
22
+ "dist"
23
+ ]
24
+ }