woiceatus commited on
Commit
e43a4a9
·
0 Parent(s):
.env.example ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ OPENAI_API_KEY=your_openai_api_key
2
+ OPENAI_BASE_URL=https://api.openai.com/v1
3
+ PORT=3000
4
+ MEDIA_TTL_SECONDS=3600
5
+ REQUEST_TIMEOUT_MS=60000
6
+ JSON_LIMIT=50mb
7
+ MAX_AUDIO_DOWNLOAD_MB=25
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .env
3
+ dist/
4
+ coverage/
5
+ tmp/
6
+ .tmp/
7
+ *.log
8
+ npm-debug.log*
9
+ .DS_Store
doc/agent.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent Task Log
2
+
3
+ ## Current Task
4
+
5
+ - Start time: 2026-03-01T00:59:31.8639973+08:00
6
+ - End time: 2026-03-01T01:12:48.8144187+08:00
7
+ - Total time: 00:13:16.9504214
8
+ - Agent: codex/gpt5-codex
9
+ - Status: completed
10
+
11
+ ## Plan Used For This Task
12
+
13
+ 1. Create the Node.js project scaffold, env config, and ignore files.
14
+ 2. Implement an OpenAI-compatible chat completions proxy at `POST /v1/chat/completions`.
15
+ 3. Normalize media input so images accept URL/base64 and audio accepts base64 or URL, with audio URLs downloaded and converted to mp3 base64 before upstream forwarding.
16
+ 4. Normalize media output so audio base64 is also exposed as a proxy URL, and image data URLs returned by compatible backends are also exposed as proxy URLs.
17
+ 5. Add tests, a temp-folder build script, and the required documentation updates.
18
+
19
+ ## Unfinished Task Handoff Format
20
+
21
+ - Put the active plan here before implementation starts.
22
+ - Record the start timestamp here and in `doc/update.md`.
23
+ - Move completed task records into `doc/updates.md` with the newest task first.
doc/common_mistakes.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Common Mistakes
2
+
3
+ - Do not commit `.env`; it is intentionally ignored and should stay local.
4
+ - Use `POST /v1/chat/completions` with JSON only. Multipart upload is not implemented.
5
+ - For image input, send either an `http(s)` URL, a data URL, or raw base64 on `image_url.url`.
6
+ - For audio input, send base64 on `input_audio.data` with `format: "mp3"` or `"wav"`, or send `input_audio.url` and let the proxy download and convert it to mp3.
7
+ - Streamed chat completions are passed through directly, so proxy-hosted media URLs are only added on non-stream responses.
8
+ - Proxy-hosted media files are stored in memory and expire after `MEDIA_TTL_SECONDS`.
9
+ - Keep modules small and focused; this project follows the principle of simple modules with clear responsibility.
doc/overview.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Overview
2
+
3
+ ## Current Status
4
+
5
+ - Project type: Node.js OpenAI-compatible chat completions proxy.
6
+ - Status: working baseline implemented and verified.
7
+ - Verified with `npm test`, `npm run build`, and a live `GET /v1/health` startup check.
8
+ - Build output folder: `C:\Users\a\AppData\Local\Temp\oapix-build`
9
+
10
+ ## What The App Does
11
+
12
+ - Exposes `POST /v1/chat/completions` as a proxy to an OpenAI-compatible upstream base URL.
13
+ - Accepts image input as URL, data URL, or raw base64 on `image_url.url`.
14
+ - Accepts audio input as base64 on `input_audio.data` or as `input_audio.url`.
15
+ - Downloads audio URLs, converts them to mp3, base64-encodes them, and forwards them upstream.
16
+ - Exposes audio output base64 as a temporary proxy URL on non-stream responses.
17
+ - Exposes image data URLs returned by compatible upstreams as temporary proxy URLs on non-stream responses.
18
+ - Exposes `GET /v1/media/:mediaId` for temporary media retrieval and `GET /v1/health` for health checks.
19
+
20
+ ## Runtime Notes
21
+
22
+ - Configure `OPENAI_API_KEY` and `OPENAI_BASE_URL` in the local `.env` file.
23
+ - Media files are stored in memory and expire after `MEDIA_TTL_SECONDS`.
24
+ - Stream responses are passed through directly and do not get extra proxy media URLs added.
25
+ - The build script writes a bundled server file and deployment manifest files into the temp build folder.
26
+
27
+ ## Project Structure
28
+
29
+ ```text
30
+ oapix/
31
+ ├─ .env.example # Example environment variables for local setup
32
+ ├─ .gitignore # Node.js and local secret ignore rules
33
+ ├─ package-lock.json # Locked dependency versions
34
+ ├─ package.json # Scripts, runtime deps, and build deps
35
+ ├─ doc/
36
+ │ ├─ agent.md # Agent task record and current-task workflow
37
+ │ ├─ common_mistakes.md # Project-specific pitfalls to avoid
38
+ │ ├─ overview.md # Current project status and structure
39
+ │ ├─ update.md # Latest finished-task summary
40
+ │ └─ updates.md # Historical task log, newest first
41
+ ├─ scripts/
42
+ │ └─ build.mjs # Temp-folder build output script
43
+ ├─ src/
44
+ │ ├─ app.js # Express app factory and error handling
45
+ │ ├─ config.js # Env loading and validation
46
+ │ ├─ server.js # Dependency wiring and server startup
47
+ │ ├─ controllers/
48
+ │ │ ├─ chatController.js # Chat proxy request handling
49
+ │ │ └─ mediaController.js # Temporary media download handling
50
+ │ ├─ routes/
51
+ │ │ └─ apiRouter.js # `/v1` routes
52
+ │ ├─ services/
53
+ │ │ ├─ audioConversionService.js # Audio URL download and mp3 conversion
54
+ │ │ ├─ mediaStore.js # In-memory temporary media storage
55
+ │ │ ├─ openAiService.js # Upstream OpenAI-compatible fetch client
56
+ │ │ ├─ requestNormalizationService.js # Input media normalization
57
+ │ │ └─ responseNormalizationService.js# Output media normalization
58
+ │ └─ utils/
59
+ │ ├─ dataUrl.js # Data URL and base64 helpers
60
+ │ ├─ httpError.js # HTTP error shape
61
+ │ └─ mediaTypes.js # Audio/image MIME helpers
62
+ └─ test/
63
+ ├─ requestNormalization.test.js # Request normalization tests
64
+ └─ responseNormalization.test.js # Response normalization tests
65
+ ```
66
+
67
+ ## Main Commands
68
+
69
+ - `npm install`
70
+ - `npm start`
71
+ - `npm test`
72
+ - `npm run build`
doc/update.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Latest Update
2
+
3
+ ## 2026-03-01 OpenAI Chat Proxy
4
+
5
+ - Start: 2026-03-01T00:59:31.8639973+08:00
6
+ - End: 2026-03-01T01:12:48.8144187+08:00
7
+ - Total: 00:13:16.9504214
8
+ - By: codex/gpt5-codex
9
+ - Status: completed
10
+
11
+ - Built a Node.js OpenAI-compatible chat completions proxy with image/audio input normalization.
12
+ - Added audio URL download plus mp3 conversion before upstream forwarding.
13
+ - Added proxy-hosted media URLs for audio output and compatible image output.
14
+ - Verified with tests, a temp-folder build, and a live health-check run.
doc/updates.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Updates
2
+
3
+ ## 2026-03-01 OpenAI Chat Proxy
4
+
5
+ - Start: 2026-03-01T00:59:31.8639973+08:00
6
+ - End: 2026-03-01T01:12:48.8144187+08:00
7
+ - Total: 00:13:16.9504214
8
+ - By: codex/gpt5-codex
9
+ - Status: completed
10
+
11
+ - Added a Node.js OpenAI-compatible chat completions proxy.
12
+ - Added media normalization for image URL/base64 and audio URL/base64.
13
+ - Added in-memory proxy media URLs for audio output and data-URL image output.
14
+ - Added tests, env config, and a temp build script.
package-lock.json ADDED
@@ -0,0 +1,1506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "oapix",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "oapix",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "dotenv": "^16.6.1",
12
+ "express": "^5.1.0",
13
+ "ffmpeg-static": "^5.2.0"
14
+ },
15
+ "devDependencies": {
16
+ "esbuild": "^0.25.9"
17
+ },
18
+ "engines": {
19
+ "node": ">=20"
20
+ }
21
+ },
22
+ "node_modules/@derhuerst/http-basic": {
23
+ "version": "8.2.4",
24
+ "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz",
25
+ "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "caseless": "^0.12.0",
29
+ "concat-stream": "^2.0.0",
30
+ "http-response-object": "^3.0.1",
31
+ "parse-cache-control": "^1.0.1"
32
+ },
33
+ "engines": {
34
+ "node": ">=6.0.0"
35
+ }
36
+ },
37
+ "node_modules/@esbuild/aix-ppc64": {
38
+ "version": "0.25.12",
39
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
40
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
41
+ "cpu": [
42
+ "ppc64"
43
+ ],
44
+ "dev": true,
45
+ "license": "MIT",
46
+ "optional": true,
47
+ "os": [
48
+ "aix"
49
+ ],
50
+ "engines": {
51
+ "node": ">=18"
52
+ }
53
+ },
54
+ "node_modules/@esbuild/android-arm": {
55
+ "version": "0.25.12",
56
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
57
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
58
+ "cpu": [
59
+ "arm"
60
+ ],
61
+ "dev": true,
62
+ "license": "MIT",
63
+ "optional": true,
64
+ "os": [
65
+ "android"
66
+ ],
67
+ "engines": {
68
+ "node": ">=18"
69
+ }
70
+ },
71
+ "node_modules/@esbuild/android-arm64": {
72
+ "version": "0.25.12",
73
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
74
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
75
+ "cpu": [
76
+ "arm64"
77
+ ],
78
+ "dev": true,
79
+ "license": "MIT",
80
+ "optional": true,
81
+ "os": [
82
+ "android"
83
+ ],
84
+ "engines": {
85
+ "node": ">=18"
86
+ }
87
+ },
88
+ "node_modules/@esbuild/android-x64": {
89
+ "version": "0.25.12",
90
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
91
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
92
+ "cpu": [
93
+ "x64"
94
+ ],
95
+ "dev": true,
96
+ "license": "MIT",
97
+ "optional": true,
98
+ "os": [
99
+ "android"
100
+ ],
101
+ "engines": {
102
+ "node": ">=18"
103
+ }
104
+ },
105
+ "node_modules/@esbuild/darwin-arm64": {
106
+ "version": "0.25.12",
107
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
108
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
109
+ "cpu": [
110
+ "arm64"
111
+ ],
112
+ "dev": true,
113
+ "license": "MIT",
114
+ "optional": true,
115
+ "os": [
116
+ "darwin"
117
+ ],
118
+ "engines": {
119
+ "node": ">=18"
120
+ }
121
+ },
122
+ "node_modules/@esbuild/darwin-x64": {
123
+ "version": "0.25.12",
124
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
125
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
126
+ "cpu": [
127
+ "x64"
128
+ ],
129
+ "dev": true,
130
+ "license": "MIT",
131
+ "optional": true,
132
+ "os": [
133
+ "darwin"
134
+ ],
135
+ "engines": {
136
+ "node": ">=18"
137
+ }
138
+ },
139
+ "node_modules/@esbuild/freebsd-arm64": {
140
+ "version": "0.25.12",
141
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
142
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
143
+ "cpu": [
144
+ "arm64"
145
+ ],
146
+ "dev": true,
147
+ "license": "MIT",
148
+ "optional": true,
149
+ "os": [
150
+ "freebsd"
151
+ ],
152
+ "engines": {
153
+ "node": ">=18"
154
+ }
155
+ },
156
+ "node_modules/@esbuild/freebsd-x64": {
157
+ "version": "0.25.12",
158
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
159
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
160
+ "cpu": [
161
+ "x64"
162
+ ],
163
+ "dev": true,
164
+ "license": "MIT",
165
+ "optional": true,
166
+ "os": [
167
+ "freebsd"
168
+ ],
169
+ "engines": {
170
+ "node": ">=18"
171
+ }
172
+ },
173
+ "node_modules/@esbuild/linux-arm": {
174
+ "version": "0.25.12",
175
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
176
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
177
+ "cpu": [
178
+ "arm"
179
+ ],
180
+ "dev": true,
181
+ "license": "MIT",
182
+ "optional": true,
183
+ "os": [
184
+ "linux"
185
+ ],
186
+ "engines": {
187
+ "node": ">=18"
188
+ }
189
+ },
190
+ "node_modules/@esbuild/linux-arm64": {
191
+ "version": "0.25.12",
192
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
193
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
194
+ "cpu": [
195
+ "arm64"
196
+ ],
197
+ "dev": true,
198
+ "license": "MIT",
199
+ "optional": true,
200
+ "os": [
201
+ "linux"
202
+ ],
203
+ "engines": {
204
+ "node": ">=18"
205
+ }
206
+ },
207
+ "node_modules/@esbuild/linux-ia32": {
208
+ "version": "0.25.12",
209
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
210
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
211
+ "cpu": [
212
+ "ia32"
213
+ ],
214
+ "dev": true,
215
+ "license": "MIT",
216
+ "optional": true,
217
+ "os": [
218
+ "linux"
219
+ ],
220
+ "engines": {
221
+ "node": ">=18"
222
+ }
223
+ },
224
+ "node_modules/@esbuild/linux-loong64": {
225
+ "version": "0.25.12",
226
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
227
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
228
+ "cpu": [
229
+ "loong64"
230
+ ],
231
+ "dev": true,
232
+ "license": "MIT",
233
+ "optional": true,
234
+ "os": [
235
+ "linux"
236
+ ],
237
+ "engines": {
238
+ "node": ">=18"
239
+ }
240
+ },
241
+ "node_modules/@esbuild/linux-mips64el": {
242
+ "version": "0.25.12",
243
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
244
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
245
+ "cpu": [
246
+ "mips64el"
247
+ ],
248
+ "dev": true,
249
+ "license": "MIT",
250
+ "optional": true,
251
+ "os": [
252
+ "linux"
253
+ ],
254
+ "engines": {
255
+ "node": ">=18"
256
+ }
257
+ },
258
+ "node_modules/@esbuild/linux-ppc64": {
259
+ "version": "0.25.12",
260
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
261
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
262
+ "cpu": [
263
+ "ppc64"
264
+ ],
265
+ "dev": true,
266
+ "license": "MIT",
267
+ "optional": true,
268
+ "os": [
269
+ "linux"
270
+ ],
271
+ "engines": {
272
+ "node": ">=18"
273
+ }
274
+ },
275
+ "node_modules/@esbuild/linux-riscv64": {
276
+ "version": "0.25.12",
277
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
278
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
279
+ "cpu": [
280
+ "riscv64"
281
+ ],
282
+ "dev": true,
283
+ "license": "MIT",
284
+ "optional": true,
285
+ "os": [
286
+ "linux"
287
+ ],
288
+ "engines": {
289
+ "node": ">=18"
290
+ }
291
+ },
292
+ "node_modules/@esbuild/linux-s390x": {
293
+ "version": "0.25.12",
294
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
295
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
296
+ "cpu": [
297
+ "s390x"
298
+ ],
299
+ "dev": true,
300
+ "license": "MIT",
301
+ "optional": true,
302
+ "os": [
303
+ "linux"
304
+ ],
305
+ "engines": {
306
+ "node": ">=18"
307
+ }
308
+ },
309
+ "node_modules/@esbuild/linux-x64": {
310
+ "version": "0.25.12",
311
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
312
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
313
+ "cpu": [
314
+ "x64"
315
+ ],
316
+ "dev": true,
317
+ "license": "MIT",
318
+ "optional": true,
319
+ "os": [
320
+ "linux"
321
+ ],
322
+ "engines": {
323
+ "node": ">=18"
324
+ }
325
+ },
326
+ "node_modules/@esbuild/netbsd-arm64": {
327
+ "version": "0.25.12",
328
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
329
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
330
+ "cpu": [
331
+ "arm64"
332
+ ],
333
+ "dev": true,
334
+ "license": "MIT",
335
+ "optional": true,
336
+ "os": [
337
+ "netbsd"
338
+ ],
339
+ "engines": {
340
+ "node": ">=18"
341
+ }
342
+ },
343
+ "node_modules/@esbuild/netbsd-x64": {
344
+ "version": "0.25.12",
345
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
346
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
347
+ "cpu": [
348
+ "x64"
349
+ ],
350
+ "dev": true,
351
+ "license": "MIT",
352
+ "optional": true,
353
+ "os": [
354
+ "netbsd"
355
+ ],
356
+ "engines": {
357
+ "node": ">=18"
358
+ }
359
+ },
360
+ "node_modules/@esbuild/openbsd-arm64": {
361
+ "version": "0.25.12",
362
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
363
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
364
+ "cpu": [
365
+ "arm64"
366
+ ],
367
+ "dev": true,
368
+ "license": "MIT",
369
+ "optional": true,
370
+ "os": [
371
+ "openbsd"
372
+ ],
373
+ "engines": {
374
+ "node": ">=18"
375
+ }
376
+ },
377
+ "node_modules/@esbuild/openbsd-x64": {
378
+ "version": "0.25.12",
379
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
380
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
381
+ "cpu": [
382
+ "x64"
383
+ ],
384
+ "dev": true,
385
+ "license": "MIT",
386
+ "optional": true,
387
+ "os": [
388
+ "openbsd"
389
+ ],
390
+ "engines": {
391
+ "node": ">=18"
392
+ }
393
+ },
394
+ "node_modules/@esbuild/openharmony-arm64": {
395
+ "version": "0.25.12",
396
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
397
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
398
+ "cpu": [
399
+ "arm64"
400
+ ],
401
+ "dev": true,
402
+ "license": "MIT",
403
+ "optional": true,
404
+ "os": [
405
+ "openharmony"
406
+ ],
407
+ "engines": {
408
+ "node": ">=18"
409
+ }
410
+ },
411
+ "node_modules/@esbuild/sunos-x64": {
412
+ "version": "0.25.12",
413
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
414
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
415
+ "cpu": [
416
+ "x64"
417
+ ],
418
+ "dev": true,
419
+ "license": "MIT",
420
+ "optional": true,
421
+ "os": [
422
+ "sunos"
423
+ ],
424
+ "engines": {
425
+ "node": ">=18"
426
+ }
427
+ },
428
+ "node_modules/@esbuild/win32-arm64": {
429
+ "version": "0.25.12",
430
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
431
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
432
+ "cpu": [
433
+ "arm64"
434
+ ],
435
+ "dev": true,
436
+ "license": "MIT",
437
+ "optional": true,
438
+ "os": [
439
+ "win32"
440
+ ],
441
+ "engines": {
442
+ "node": ">=18"
443
+ }
444
+ },
445
+ "node_modules/@esbuild/win32-ia32": {
446
+ "version": "0.25.12",
447
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
448
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
449
+ "cpu": [
450
+ "ia32"
451
+ ],
452
+ "dev": true,
453
+ "license": "MIT",
454
+ "optional": true,
455
+ "os": [
456
+ "win32"
457
+ ],
458
+ "engines": {
459
+ "node": ">=18"
460
+ }
461
+ },
462
+ "node_modules/@esbuild/win32-x64": {
463
+ "version": "0.25.12",
464
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
465
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
466
+ "cpu": [
467
+ "x64"
468
+ ],
469
+ "dev": true,
470
+ "license": "MIT",
471
+ "optional": true,
472
+ "os": [
473
+ "win32"
474
+ ],
475
+ "engines": {
476
+ "node": ">=18"
477
+ }
478
+ },
479
+ "node_modules/@types/node": {
480
+ "version": "10.17.60",
481
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
482
+ "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
483
+ "license": "MIT"
484
+ },
485
+ "node_modules/accepts": {
486
+ "version": "2.0.0",
487
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
488
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
489
+ "license": "MIT",
490
+ "dependencies": {
491
+ "mime-types": "^3.0.0",
492
+ "negotiator": "^1.0.0"
493
+ },
494
+ "engines": {
495
+ "node": ">= 0.6"
496
+ }
497
+ },
498
+ "node_modules/agent-base": {
499
+ "version": "6.0.2",
500
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
501
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
502
+ "license": "MIT",
503
+ "dependencies": {
504
+ "debug": "4"
505
+ },
506
+ "engines": {
507
+ "node": ">= 6.0.0"
508
+ }
509
+ },
510
+ "node_modules/body-parser": {
511
+ "version": "2.2.2",
512
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
513
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
514
+ "license": "MIT",
515
+ "dependencies": {
516
+ "bytes": "^3.1.2",
517
+ "content-type": "^1.0.5",
518
+ "debug": "^4.4.3",
519
+ "http-errors": "^2.0.0",
520
+ "iconv-lite": "^0.7.0",
521
+ "on-finished": "^2.4.1",
522
+ "qs": "^6.14.1",
523
+ "raw-body": "^3.0.1",
524
+ "type-is": "^2.0.1"
525
+ },
526
+ "engines": {
527
+ "node": ">=18"
528
+ },
529
+ "funding": {
530
+ "type": "opencollective",
531
+ "url": "https://opencollective.com/express"
532
+ }
533
+ },
534
+ "node_modules/buffer-from": {
535
+ "version": "1.1.2",
536
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
537
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
538
+ "license": "MIT"
539
+ },
540
+ "node_modules/bytes": {
541
+ "version": "3.1.2",
542
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
543
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
544
+ "license": "MIT",
545
+ "engines": {
546
+ "node": ">= 0.8"
547
+ }
548
+ },
549
+ "node_modules/call-bind-apply-helpers": {
550
+ "version": "1.0.2",
551
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
552
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
553
+ "license": "MIT",
554
+ "dependencies": {
555
+ "es-errors": "^1.3.0",
556
+ "function-bind": "^1.1.2"
557
+ },
558
+ "engines": {
559
+ "node": ">= 0.4"
560
+ }
561
+ },
562
+ "node_modules/call-bound": {
563
+ "version": "1.0.4",
564
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
565
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
566
+ "license": "MIT",
567
+ "dependencies": {
568
+ "call-bind-apply-helpers": "^1.0.2",
569
+ "get-intrinsic": "^1.3.0"
570
+ },
571
+ "engines": {
572
+ "node": ">= 0.4"
573
+ },
574
+ "funding": {
575
+ "url": "https://github.com/sponsors/ljharb"
576
+ }
577
+ },
578
+ "node_modules/caseless": {
579
+ "version": "0.12.0",
580
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
581
+ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
582
+ "license": "Apache-2.0"
583
+ },
584
+ "node_modules/concat-stream": {
585
+ "version": "2.0.0",
586
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
587
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
588
+ "engines": [
589
+ "node >= 6.0"
590
+ ],
591
+ "license": "MIT",
592
+ "dependencies": {
593
+ "buffer-from": "^1.0.0",
594
+ "inherits": "^2.0.3",
595
+ "readable-stream": "^3.0.2",
596
+ "typedarray": "^0.0.6"
597
+ }
598
+ },
599
+ "node_modules/content-disposition": {
600
+ "version": "1.0.1",
601
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
602
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
603
+ "license": "MIT",
604
+ "engines": {
605
+ "node": ">=18"
606
+ },
607
+ "funding": {
608
+ "type": "opencollective",
609
+ "url": "https://opencollective.com/express"
610
+ }
611
+ },
612
+ "node_modules/content-type": {
613
+ "version": "1.0.5",
614
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
615
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
616
+ "license": "MIT",
617
+ "engines": {
618
+ "node": ">= 0.6"
619
+ }
620
+ },
621
+ "node_modules/cookie": {
622
+ "version": "0.7.2",
623
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
624
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
625
+ "license": "MIT",
626
+ "engines": {
627
+ "node": ">= 0.6"
628
+ }
629
+ },
630
+ "node_modules/cookie-signature": {
631
+ "version": "1.2.2",
632
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
633
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
634
+ "license": "MIT",
635
+ "engines": {
636
+ "node": ">=6.6.0"
637
+ }
638
+ },
639
+ "node_modules/debug": {
640
+ "version": "4.4.3",
641
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
642
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
643
+ "license": "MIT",
644
+ "dependencies": {
645
+ "ms": "^2.1.3"
646
+ },
647
+ "engines": {
648
+ "node": ">=6.0"
649
+ },
650
+ "peerDependenciesMeta": {
651
+ "supports-color": {
652
+ "optional": true
653
+ }
654
+ }
655
+ },
656
+ "node_modules/depd": {
657
+ "version": "2.0.0",
658
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
659
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
660
+ "license": "MIT",
661
+ "engines": {
662
+ "node": ">= 0.8"
663
+ }
664
+ },
665
+ "node_modules/dotenv": {
666
+ "version": "16.6.1",
667
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
668
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
669
+ "license": "BSD-2-Clause",
670
+ "engines": {
671
+ "node": ">=12"
672
+ },
673
+ "funding": {
674
+ "url": "https://dotenvx.com"
675
+ }
676
+ },
677
+ "node_modules/dunder-proto": {
678
+ "version": "1.0.1",
679
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
680
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
681
+ "license": "MIT",
682
+ "dependencies": {
683
+ "call-bind-apply-helpers": "^1.0.1",
684
+ "es-errors": "^1.3.0",
685
+ "gopd": "^1.2.0"
686
+ },
687
+ "engines": {
688
+ "node": ">= 0.4"
689
+ }
690
+ },
691
+ "node_modules/ee-first": {
692
+ "version": "1.1.1",
693
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
694
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
695
+ "license": "MIT"
696
+ },
697
+ "node_modules/encodeurl": {
698
+ "version": "2.0.0",
699
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
700
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
701
+ "license": "MIT",
702
+ "engines": {
703
+ "node": ">= 0.8"
704
+ }
705
+ },
706
+ "node_modules/env-paths": {
707
+ "version": "2.2.1",
708
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
709
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
710
+ "license": "MIT",
711
+ "engines": {
712
+ "node": ">=6"
713
+ }
714
+ },
715
+ "node_modules/es-define-property": {
716
+ "version": "1.0.1",
717
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
718
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
719
+ "license": "MIT",
720
+ "engines": {
721
+ "node": ">= 0.4"
722
+ }
723
+ },
724
+ "node_modules/es-errors": {
725
+ "version": "1.3.0",
726
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
727
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
728
+ "license": "MIT",
729
+ "engines": {
730
+ "node": ">= 0.4"
731
+ }
732
+ },
733
+ "node_modules/es-object-atoms": {
734
+ "version": "1.1.1",
735
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
736
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
737
+ "license": "MIT",
738
+ "dependencies": {
739
+ "es-errors": "^1.3.0"
740
+ },
741
+ "engines": {
742
+ "node": ">= 0.4"
743
+ }
744
+ },
745
+ "node_modules/esbuild": {
746
+ "version": "0.25.12",
747
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
748
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
749
+ "dev": true,
750
+ "hasInstallScript": true,
751
+ "license": "MIT",
752
+ "bin": {
753
+ "esbuild": "bin/esbuild"
754
+ },
755
+ "engines": {
756
+ "node": ">=18"
757
+ },
758
+ "optionalDependencies": {
759
+ "@esbuild/aix-ppc64": "0.25.12",
760
+ "@esbuild/android-arm": "0.25.12",
761
+ "@esbuild/android-arm64": "0.25.12",
762
+ "@esbuild/android-x64": "0.25.12",
763
+ "@esbuild/darwin-arm64": "0.25.12",
764
+ "@esbuild/darwin-x64": "0.25.12",
765
+ "@esbuild/freebsd-arm64": "0.25.12",
766
+ "@esbuild/freebsd-x64": "0.25.12",
767
+ "@esbuild/linux-arm": "0.25.12",
768
+ "@esbuild/linux-arm64": "0.25.12",
769
+ "@esbuild/linux-ia32": "0.25.12",
770
+ "@esbuild/linux-loong64": "0.25.12",
771
+ "@esbuild/linux-mips64el": "0.25.12",
772
+ "@esbuild/linux-ppc64": "0.25.12",
773
+ "@esbuild/linux-riscv64": "0.25.12",
774
+ "@esbuild/linux-s390x": "0.25.12",
775
+ "@esbuild/linux-x64": "0.25.12",
776
+ "@esbuild/netbsd-arm64": "0.25.12",
777
+ "@esbuild/netbsd-x64": "0.25.12",
778
+ "@esbuild/openbsd-arm64": "0.25.12",
779
+ "@esbuild/openbsd-x64": "0.25.12",
780
+ "@esbuild/openharmony-arm64": "0.25.12",
781
+ "@esbuild/sunos-x64": "0.25.12",
782
+ "@esbuild/win32-arm64": "0.25.12",
783
+ "@esbuild/win32-ia32": "0.25.12",
784
+ "@esbuild/win32-x64": "0.25.12"
785
+ }
786
+ },
787
+ "node_modules/escape-html": {
788
+ "version": "1.0.3",
789
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
790
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
791
+ "license": "MIT"
792
+ },
793
+ "node_modules/etag": {
794
+ "version": "1.8.1",
795
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
796
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
797
+ "license": "MIT",
798
+ "engines": {
799
+ "node": ">= 0.6"
800
+ }
801
+ },
802
+ "node_modules/express": {
803
+ "version": "5.2.1",
804
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
805
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
806
+ "license": "MIT",
807
+ "dependencies": {
808
+ "accepts": "^2.0.0",
809
+ "body-parser": "^2.2.1",
810
+ "content-disposition": "^1.0.0",
811
+ "content-type": "^1.0.5",
812
+ "cookie": "^0.7.1",
813
+ "cookie-signature": "^1.2.1",
814
+ "debug": "^4.4.0",
815
+ "depd": "^2.0.0",
816
+ "encodeurl": "^2.0.0",
817
+ "escape-html": "^1.0.3",
818
+ "etag": "^1.8.1",
819
+ "finalhandler": "^2.1.0",
820
+ "fresh": "^2.0.0",
821
+ "http-errors": "^2.0.0",
822
+ "merge-descriptors": "^2.0.0",
823
+ "mime-types": "^3.0.0",
824
+ "on-finished": "^2.4.1",
825
+ "once": "^1.4.0",
826
+ "parseurl": "^1.3.3",
827
+ "proxy-addr": "^2.0.7",
828
+ "qs": "^6.14.0",
829
+ "range-parser": "^1.2.1",
830
+ "router": "^2.2.0",
831
+ "send": "^1.1.0",
832
+ "serve-static": "^2.2.0",
833
+ "statuses": "^2.0.1",
834
+ "type-is": "^2.0.1",
835
+ "vary": "^1.1.2"
836
+ },
837
+ "engines": {
838
+ "node": ">= 18"
839
+ },
840
+ "funding": {
841
+ "type": "opencollective",
842
+ "url": "https://opencollective.com/express"
843
+ }
844
+ },
845
+ "node_modules/ffmpeg-static": {
846
+ "version": "5.3.0",
847
+ "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
848
+ "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==",
849
+ "hasInstallScript": true,
850
+ "license": "GPL-3.0-or-later",
851
+ "dependencies": {
852
+ "@derhuerst/http-basic": "^8.2.0",
853
+ "env-paths": "^2.2.0",
854
+ "https-proxy-agent": "^5.0.0",
855
+ "progress": "^2.0.3"
856
+ },
857
+ "engines": {
858
+ "node": ">=16"
859
+ }
860
+ },
861
+ "node_modules/finalhandler": {
862
+ "version": "2.1.1",
863
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
864
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
865
+ "license": "MIT",
866
+ "dependencies": {
867
+ "debug": "^4.4.0",
868
+ "encodeurl": "^2.0.0",
869
+ "escape-html": "^1.0.3",
870
+ "on-finished": "^2.4.1",
871
+ "parseurl": "^1.3.3",
872
+ "statuses": "^2.0.1"
873
+ },
874
+ "engines": {
875
+ "node": ">= 18.0.0"
876
+ },
877
+ "funding": {
878
+ "type": "opencollective",
879
+ "url": "https://opencollective.com/express"
880
+ }
881
+ },
882
+ "node_modules/forwarded": {
883
+ "version": "0.2.0",
884
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
885
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
886
+ "license": "MIT",
887
+ "engines": {
888
+ "node": ">= 0.6"
889
+ }
890
+ },
891
+ "node_modules/fresh": {
892
+ "version": "2.0.0",
893
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
894
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
895
+ "license": "MIT",
896
+ "engines": {
897
+ "node": ">= 0.8"
898
+ }
899
+ },
900
+ "node_modules/function-bind": {
901
+ "version": "1.1.2",
902
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
903
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
904
+ "license": "MIT",
905
+ "funding": {
906
+ "url": "https://github.com/sponsors/ljharb"
907
+ }
908
+ },
909
+ "node_modules/get-intrinsic": {
910
+ "version": "1.3.0",
911
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
912
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
913
+ "license": "MIT",
914
+ "dependencies": {
915
+ "call-bind-apply-helpers": "^1.0.2",
916
+ "es-define-property": "^1.0.1",
917
+ "es-errors": "^1.3.0",
918
+ "es-object-atoms": "^1.1.1",
919
+ "function-bind": "^1.1.2",
920
+ "get-proto": "^1.0.1",
921
+ "gopd": "^1.2.0",
922
+ "has-symbols": "^1.1.0",
923
+ "hasown": "^2.0.2",
924
+ "math-intrinsics": "^1.1.0"
925
+ },
926
+ "engines": {
927
+ "node": ">= 0.4"
928
+ },
929
+ "funding": {
930
+ "url": "https://github.com/sponsors/ljharb"
931
+ }
932
+ },
933
+ "node_modules/get-proto": {
934
+ "version": "1.0.1",
935
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
936
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
937
+ "license": "MIT",
938
+ "dependencies": {
939
+ "dunder-proto": "^1.0.1",
940
+ "es-object-atoms": "^1.0.0"
941
+ },
942
+ "engines": {
943
+ "node": ">= 0.4"
944
+ }
945
+ },
946
+ "node_modules/gopd": {
947
+ "version": "1.2.0",
948
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
949
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
950
+ "license": "MIT",
951
+ "engines": {
952
+ "node": ">= 0.4"
953
+ },
954
+ "funding": {
955
+ "url": "https://github.com/sponsors/ljharb"
956
+ }
957
+ },
958
+ "node_modules/has-symbols": {
959
+ "version": "1.1.0",
960
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
961
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
962
+ "license": "MIT",
963
+ "engines": {
964
+ "node": ">= 0.4"
965
+ },
966
+ "funding": {
967
+ "url": "https://github.com/sponsors/ljharb"
968
+ }
969
+ },
970
+ "node_modules/hasown": {
971
+ "version": "2.0.2",
972
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
973
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
974
+ "license": "MIT",
975
+ "dependencies": {
976
+ "function-bind": "^1.1.2"
977
+ },
978
+ "engines": {
979
+ "node": ">= 0.4"
980
+ }
981
+ },
982
+ "node_modules/http-errors": {
983
+ "version": "2.0.1",
984
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
985
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
986
+ "license": "MIT",
987
+ "dependencies": {
988
+ "depd": "~2.0.0",
989
+ "inherits": "~2.0.4",
990
+ "setprototypeof": "~1.2.0",
991
+ "statuses": "~2.0.2",
992
+ "toidentifier": "~1.0.1"
993
+ },
994
+ "engines": {
995
+ "node": ">= 0.8"
996
+ },
997
+ "funding": {
998
+ "type": "opencollective",
999
+ "url": "https://opencollective.com/express"
1000
+ }
1001
+ },
1002
+ "node_modules/http-response-object": {
1003
+ "version": "3.0.2",
1004
+ "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
1005
+ "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
1006
+ "license": "MIT",
1007
+ "dependencies": {
1008
+ "@types/node": "^10.0.3"
1009
+ }
1010
+ },
1011
+ "node_modules/https-proxy-agent": {
1012
+ "version": "5.0.1",
1013
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
1014
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
1015
+ "license": "MIT",
1016
+ "dependencies": {
1017
+ "agent-base": "6",
1018
+ "debug": "4"
1019
+ },
1020
+ "engines": {
1021
+ "node": ">= 6"
1022
+ }
1023
+ },
1024
+ "node_modules/iconv-lite": {
1025
+ "version": "0.7.2",
1026
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
1027
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
1028
+ "license": "MIT",
1029
+ "dependencies": {
1030
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
1031
+ },
1032
+ "engines": {
1033
+ "node": ">=0.10.0"
1034
+ },
1035
+ "funding": {
1036
+ "type": "opencollective",
1037
+ "url": "https://opencollective.com/express"
1038
+ }
1039
+ },
1040
+ "node_modules/inherits": {
1041
+ "version": "2.0.4",
1042
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1043
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1044
+ "license": "ISC"
1045
+ },
1046
+ "node_modules/ipaddr.js": {
1047
+ "version": "1.9.1",
1048
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1049
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1050
+ "license": "MIT",
1051
+ "engines": {
1052
+ "node": ">= 0.10"
1053
+ }
1054
+ },
1055
+ "node_modules/is-promise": {
1056
+ "version": "4.0.0",
1057
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
1058
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1059
+ "license": "MIT"
1060
+ },
1061
+ "node_modules/math-intrinsics": {
1062
+ "version": "1.1.0",
1063
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1064
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1065
+ "license": "MIT",
1066
+ "engines": {
1067
+ "node": ">= 0.4"
1068
+ }
1069
+ },
1070
+ "node_modules/media-typer": {
1071
+ "version": "1.1.0",
1072
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
1073
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
1074
+ "license": "MIT",
1075
+ "engines": {
1076
+ "node": ">= 0.8"
1077
+ }
1078
+ },
1079
+ "node_modules/merge-descriptors": {
1080
+ "version": "2.0.0",
1081
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
1082
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
1083
+ "license": "MIT",
1084
+ "engines": {
1085
+ "node": ">=18"
1086
+ },
1087
+ "funding": {
1088
+ "url": "https://github.com/sponsors/sindresorhus"
1089
+ }
1090
+ },
1091
+ "node_modules/mime-db": {
1092
+ "version": "1.54.0",
1093
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
1094
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
1095
+ "license": "MIT",
1096
+ "engines": {
1097
+ "node": ">= 0.6"
1098
+ }
1099
+ },
1100
+ "node_modules/mime-types": {
1101
+ "version": "3.0.2",
1102
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
1103
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
1104
+ "license": "MIT",
1105
+ "dependencies": {
1106
+ "mime-db": "^1.54.0"
1107
+ },
1108
+ "engines": {
1109
+ "node": ">=18"
1110
+ },
1111
+ "funding": {
1112
+ "type": "opencollective",
1113
+ "url": "https://opencollective.com/express"
1114
+ }
1115
+ },
1116
+ "node_modules/ms": {
1117
+ "version": "2.1.3",
1118
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1119
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1120
+ "license": "MIT"
1121
+ },
1122
+ "node_modules/negotiator": {
1123
+ "version": "1.0.0",
1124
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
1125
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
1126
+ "license": "MIT",
1127
+ "engines": {
1128
+ "node": ">= 0.6"
1129
+ }
1130
+ },
1131
+ "node_modules/object-inspect": {
1132
+ "version": "1.13.4",
1133
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1134
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1135
+ "license": "MIT",
1136
+ "engines": {
1137
+ "node": ">= 0.4"
1138
+ },
1139
+ "funding": {
1140
+ "url": "https://github.com/sponsors/ljharb"
1141
+ }
1142
+ },
1143
+ "node_modules/on-finished": {
1144
+ "version": "2.4.1",
1145
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1146
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1147
+ "license": "MIT",
1148
+ "dependencies": {
1149
+ "ee-first": "1.1.1"
1150
+ },
1151
+ "engines": {
1152
+ "node": ">= 0.8"
1153
+ }
1154
+ },
1155
+ "node_modules/once": {
1156
+ "version": "1.4.0",
1157
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1158
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1159
+ "license": "ISC",
1160
+ "dependencies": {
1161
+ "wrappy": "1"
1162
+ }
1163
+ },
1164
+ "node_modules/parse-cache-control": {
1165
+ "version": "1.0.1",
1166
+ "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
1167
+ "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
1168
+ },
1169
+ "node_modules/parseurl": {
1170
+ "version": "1.3.3",
1171
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1172
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1173
+ "license": "MIT",
1174
+ "engines": {
1175
+ "node": ">= 0.8"
1176
+ }
1177
+ },
1178
+ "node_modules/path-to-regexp": {
1179
+ "version": "8.3.0",
1180
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
1181
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
1182
+ "license": "MIT",
1183
+ "funding": {
1184
+ "type": "opencollective",
1185
+ "url": "https://opencollective.com/express"
1186
+ }
1187
+ },
1188
+ "node_modules/progress": {
1189
+ "version": "2.0.3",
1190
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
1191
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
1192
+ "license": "MIT",
1193
+ "engines": {
1194
+ "node": ">=0.4.0"
1195
+ }
1196
+ },
1197
+ "node_modules/proxy-addr": {
1198
+ "version": "2.0.7",
1199
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1200
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1201
+ "license": "MIT",
1202
+ "dependencies": {
1203
+ "forwarded": "0.2.0",
1204
+ "ipaddr.js": "1.9.1"
1205
+ },
1206
+ "engines": {
1207
+ "node": ">= 0.10"
1208
+ }
1209
+ },
1210
+ "node_modules/qs": {
1211
+ "version": "6.15.0",
1212
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
1213
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
1214
+ "license": "BSD-3-Clause",
1215
+ "dependencies": {
1216
+ "side-channel": "^1.1.0"
1217
+ },
1218
+ "engines": {
1219
+ "node": ">=0.6"
1220
+ },
1221
+ "funding": {
1222
+ "url": "https://github.com/sponsors/ljharb"
1223
+ }
1224
+ },
1225
+ "node_modules/range-parser": {
1226
+ "version": "1.2.1",
1227
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1228
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1229
+ "license": "MIT",
1230
+ "engines": {
1231
+ "node": ">= 0.6"
1232
+ }
1233
+ },
1234
+ "node_modules/raw-body": {
1235
+ "version": "3.0.2",
1236
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
1237
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
1238
+ "license": "MIT",
1239
+ "dependencies": {
1240
+ "bytes": "~3.1.2",
1241
+ "http-errors": "~2.0.1",
1242
+ "iconv-lite": "~0.7.0",
1243
+ "unpipe": "~1.0.0"
1244
+ },
1245
+ "engines": {
1246
+ "node": ">= 0.10"
1247
+ }
1248
+ },
1249
+ "node_modules/readable-stream": {
1250
+ "version": "3.6.2",
1251
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
1252
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
1253
+ "license": "MIT",
1254
+ "dependencies": {
1255
+ "inherits": "^2.0.3",
1256
+ "string_decoder": "^1.1.1",
1257
+ "util-deprecate": "^1.0.1"
1258
+ },
1259
+ "engines": {
1260
+ "node": ">= 6"
1261
+ }
1262
+ },
1263
+ "node_modules/router": {
1264
+ "version": "2.2.0",
1265
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
1266
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
1267
+ "license": "MIT",
1268
+ "dependencies": {
1269
+ "debug": "^4.4.0",
1270
+ "depd": "^2.0.0",
1271
+ "is-promise": "^4.0.0",
1272
+ "parseurl": "^1.3.3",
1273
+ "path-to-regexp": "^8.0.0"
1274
+ },
1275
+ "engines": {
1276
+ "node": ">= 18"
1277
+ }
1278
+ },
1279
+ "node_modules/safe-buffer": {
1280
+ "version": "5.2.1",
1281
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1282
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1283
+ "funding": [
1284
+ {
1285
+ "type": "github",
1286
+ "url": "https://github.com/sponsors/feross"
1287
+ },
1288
+ {
1289
+ "type": "patreon",
1290
+ "url": "https://www.patreon.com/feross"
1291
+ },
1292
+ {
1293
+ "type": "consulting",
1294
+ "url": "https://feross.org/support"
1295
+ }
1296
+ ],
1297
+ "license": "MIT"
1298
+ },
1299
+ "node_modules/safer-buffer": {
1300
+ "version": "2.1.2",
1301
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1302
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1303
+ "license": "MIT"
1304
+ },
1305
+ "node_modules/send": {
1306
+ "version": "1.2.1",
1307
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
1308
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
1309
+ "license": "MIT",
1310
+ "dependencies": {
1311
+ "debug": "^4.4.3",
1312
+ "encodeurl": "^2.0.0",
1313
+ "escape-html": "^1.0.3",
1314
+ "etag": "^1.8.1",
1315
+ "fresh": "^2.0.0",
1316
+ "http-errors": "^2.0.1",
1317
+ "mime-types": "^3.0.2",
1318
+ "ms": "^2.1.3",
1319
+ "on-finished": "^2.4.1",
1320
+ "range-parser": "^1.2.1",
1321
+ "statuses": "^2.0.2"
1322
+ },
1323
+ "engines": {
1324
+ "node": ">= 18"
1325
+ },
1326
+ "funding": {
1327
+ "type": "opencollective",
1328
+ "url": "https://opencollective.com/express"
1329
+ }
1330
+ },
1331
+ "node_modules/serve-static": {
1332
+ "version": "2.2.1",
1333
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
1334
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
1335
+ "license": "MIT",
1336
+ "dependencies": {
1337
+ "encodeurl": "^2.0.0",
1338
+ "escape-html": "^1.0.3",
1339
+ "parseurl": "^1.3.3",
1340
+ "send": "^1.2.0"
1341
+ },
1342
+ "engines": {
1343
+ "node": ">= 18"
1344
+ },
1345
+ "funding": {
1346
+ "type": "opencollective",
1347
+ "url": "https://opencollective.com/express"
1348
+ }
1349
+ },
1350
+ "node_modules/setprototypeof": {
1351
+ "version": "1.2.0",
1352
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1353
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1354
+ "license": "ISC"
1355
+ },
1356
+ "node_modules/side-channel": {
1357
+ "version": "1.1.0",
1358
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1359
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1360
+ "license": "MIT",
1361
+ "dependencies": {
1362
+ "es-errors": "^1.3.0",
1363
+ "object-inspect": "^1.13.3",
1364
+ "side-channel-list": "^1.0.0",
1365
+ "side-channel-map": "^1.0.1",
1366
+ "side-channel-weakmap": "^1.0.2"
1367
+ },
1368
+ "engines": {
1369
+ "node": ">= 0.4"
1370
+ },
1371
+ "funding": {
1372
+ "url": "https://github.com/sponsors/ljharb"
1373
+ }
1374
+ },
1375
+ "node_modules/side-channel-list": {
1376
+ "version": "1.0.0",
1377
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1378
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1379
+ "license": "MIT",
1380
+ "dependencies": {
1381
+ "es-errors": "^1.3.0",
1382
+ "object-inspect": "^1.13.3"
1383
+ },
1384
+ "engines": {
1385
+ "node": ">= 0.4"
1386
+ },
1387
+ "funding": {
1388
+ "url": "https://github.com/sponsors/ljharb"
1389
+ }
1390
+ },
1391
+ "node_modules/side-channel-map": {
1392
+ "version": "1.0.1",
1393
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1394
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1395
+ "license": "MIT",
1396
+ "dependencies": {
1397
+ "call-bound": "^1.0.2",
1398
+ "es-errors": "^1.3.0",
1399
+ "get-intrinsic": "^1.2.5",
1400
+ "object-inspect": "^1.13.3"
1401
+ },
1402
+ "engines": {
1403
+ "node": ">= 0.4"
1404
+ },
1405
+ "funding": {
1406
+ "url": "https://github.com/sponsors/ljharb"
1407
+ }
1408
+ },
1409
+ "node_modules/side-channel-weakmap": {
1410
+ "version": "1.0.2",
1411
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1412
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1413
+ "license": "MIT",
1414
+ "dependencies": {
1415
+ "call-bound": "^1.0.2",
1416
+ "es-errors": "^1.3.0",
1417
+ "get-intrinsic": "^1.2.5",
1418
+ "object-inspect": "^1.13.3",
1419
+ "side-channel-map": "^1.0.1"
1420
+ },
1421
+ "engines": {
1422
+ "node": ">= 0.4"
1423
+ },
1424
+ "funding": {
1425
+ "url": "https://github.com/sponsors/ljharb"
1426
+ }
1427
+ },
1428
+ "node_modules/statuses": {
1429
+ "version": "2.0.2",
1430
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1431
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1432
+ "license": "MIT",
1433
+ "engines": {
1434
+ "node": ">= 0.8"
1435
+ }
1436
+ },
1437
+ "node_modules/string_decoder": {
1438
+ "version": "1.3.0",
1439
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
1440
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
1441
+ "license": "MIT",
1442
+ "dependencies": {
1443
+ "safe-buffer": "~5.2.0"
1444
+ }
1445
+ },
1446
+ "node_modules/toidentifier": {
1447
+ "version": "1.0.1",
1448
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1449
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1450
+ "license": "MIT",
1451
+ "engines": {
1452
+ "node": ">=0.6"
1453
+ }
1454
+ },
1455
+ "node_modules/type-is": {
1456
+ "version": "2.0.1",
1457
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
1458
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
1459
+ "license": "MIT",
1460
+ "dependencies": {
1461
+ "content-type": "^1.0.5",
1462
+ "media-typer": "^1.1.0",
1463
+ "mime-types": "^3.0.0"
1464
+ },
1465
+ "engines": {
1466
+ "node": ">= 0.6"
1467
+ }
1468
+ },
1469
+ "node_modules/typedarray": {
1470
+ "version": "0.0.6",
1471
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
1472
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
1473
+ "license": "MIT"
1474
+ },
1475
+ "node_modules/unpipe": {
1476
+ "version": "1.0.0",
1477
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1478
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1479
+ "license": "MIT",
1480
+ "engines": {
1481
+ "node": ">= 0.8"
1482
+ }
1483
+ },
1484
+ "node_modules/util-deprecate": {
1485
+ "version": "1.0.2",
1486
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1487
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1488
+ "license": "MIT"
1489
+ },
1490
+ "node_modules/vary": {
1491
+ "version": "1.1.2",
1492
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1493
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1494
+ "license": "MIT",
1495
+ "engines": {
1496
+ "node": ">= 0.8"
1497
+ }
1498
+ },
1499
+ "node_modules/wrappy": {
1500
+ "version": "1.0.2",
1501
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1502
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1503
+ "license": "ISC"
1504
+ }
1505
+ }
1506
+ }
package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "oapix",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/server.js",
11
+ "dev": "node --watch src/server.js",
12
+ "test": "node --test",
13
+ "build": "node scripts/build.mjs"
14
+ },
15
+ "dependencies": {
16
+ "dotenv": "^16.6.1",
17
+ "express": "^5.1.0",
18
+ "ffmpeg-static": "^5.2.0"
19
+ },
20
+ "devDependencies": {
21
+ "esbuild": "^0.25.9"
22
+ }
23
+ }
scripts/build.mjs ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ import { build } from "esbuild";
5
+
6
+ const outputDir = process.env.BUILD_OUT_DIR || path.join(os.tmpdir(), "oapix-build");
7
+
8
+ await fs.rm(outputDir, { force: true, recursive: true });
9
+ await fs.mkdir(outputDir, { recursive: true });
10
+
11
+ await build({
12
+ entryPoints: ["src/server.js"],
13
+ outfile: path.join(outputDir, "server.js"),
14
+ bundle: true,
15
+ format: "esm",
16
+ platform: "node",
17
+ sourcemap: true,
18
+ external: ["express", "dotenv", "ffmpeg-static"]
19
+ });
20
+
21
+ await fs.copyFile("package.json", path.join(outputDir, "package.json"));
22
+ await fs.copyFile("package-lock.json", path.join(outputDir, "package-lock.json")).catch(() => {});
23
+ await fs.copyFile(".env.example", path.join(outputDir, ".env.example"));
24
+
25
+ console.log(outputDir);
src/app.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import { createApiRouter } from "./routes/apiRouter.js";
3
+ import { HttpError } from "./utils/httpError.js";
4
+
5
+ export function createApp({
6
+ jsonLimit,
7
+ chatController,
8
+ mediaController
9
+ }) {
10
+ const app = express();
11
+
12
+ app.disable("x-powered-by");
13
+ app.use(express.json({ limit: jsonLimit }));
14
+
15
+ app.use("/v1", createApiRouter({ chatController, mediaController }));
16
+
17
+ app.use((req, _res, next) => {
18
+ next(new HttpError(404, `Route not found: ${req.method} ${req.originalUrl}`));
19
+ });
20
+
21
+ app.use((error, _req, res, _next) => {
22
+ const statusCode = error.statusCode ?? 500;
23
+ res.status(statusCode).json({
24
+ error: {
25
+ message: error.message ?? "Internal server error",
26
+ details: error.details ?? null
27
+ }
28
+ });
29
+ });
30
+
31
+ return app;
32
+ }
src/config.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dotenv from "dotenv";
2
+
3
+ dotenv.config();
4
+
5
+ function numberFromEnv(env, key, fallback) {
6
+ const value = env[key];
7
+ if (value === undefined || value === "") {
8
+ return fallback;
9
+ }
10
+
11
+ const parsed = Number(value);
12
+ return Number.isFinite(parsed) ? parsed : fallback;
13
+ }
14
+
15
+ export function loadConfig(env = process.env) {
16
+ return {
17
+ openAiApiKey: env.OPENAI_API_KEY ?? "",
18
+ openAiBaseUrl: (env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/+$/, ""),
19
+ port: numberFromEnv(env, "PORT", 3000),
20
+ mediaTtlSeconds: numberFromEnv(env, "MEDIA_TTL_SECONDS", 3600),
21
+ requestTimeoutMs: numberFromEnv(env, "REQUEST_TIMEOUT_MS", 60000),
22
+ jsonLimit: env.JSON_LIMIT ?? "50mb",
23
+ maxAudioDownloadMb: numberFromEnv(env, "MAX_AUDIO_DOWNLOAD_MB", 25)
24
+ };
25
+ }
26
+
27
+ export function validateConfig(config) {
28
+ const missing = [];
29
+
30
+ if (!config.openAiApiKey) {
31
+ missing.push("OPENAI_API_KEY");
32
+ }
33
+
34
+ if (!config.openAiBaseUrl) {
35
+ missing.push("OPENAI_BASE_URL");
36
+ }
37
+
38
+ if (missing.length > 0) {
39
+ throw new Error(`Missing required environment variables: ${missing.join(", ")}`);
40
+ }
41
+ }
src/controllers/chatController.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Readable } from "node:stream";
2
+
3
+ async function relayError(response, res) {
4
+ const contentType = response.headers.get("content-type") ?? "application/json";
5
+ const rawBody = await response.text();
6
+
7
+ res.status(response.status);
8
+ res.setHeader("content-type", contentType);
9
+ res.send(rawBody);
10
+ }
11
+
12
+ export function createChatController({
13
+ openAiService,
14
+ requestNormalizationService,
15
+ responseNormalizationService
16
+ }) {
17
+ return async function chatController(req, res, next) {
18
+ try {
19
+ const { normalizedBody, responseContext } = await requestNormalizationService.normalize(req.body);
20
+ const upstreamResponse = await openAiService.createChatCompletion(normalizedBody);
21
+
22
+ if (!upstreamResponse.ok) {
23
+ await relayError(upstreamResponse, res);
24
+ return;
25
+ }
26
+
27
+ if (normalizedBody.stream) {
28
+ res.status(upstreamResponse.status);
29
+ res.setHeader("content-type", upstreamResponse.headers.get("content-type") ?? "text/event-stream");
30
+ Readable.fromWeb(upstreamResponse.body).pipe(res);
31
+ return;
32
+ }
33
+
34
+ const responseJson = await upstreamResponse.json();
35
+ const publicBaseUrl = `${req.protocol}://${req.get("host")}`;
36
+ const normalizedResponse = responseNormalizationService.normalize(responseJson, {
37
+ publicBaseUrl,
38
+ audioFormat: responseContext.audioFormat,
39
+ exposeMediaUrls: responseContext.exposeMediaUrls
40
+ });
41
+
42
+ res.status(upstreamResponse.status).json(normalizedResponse);
43
+ } catch (error) {
44
+ next(error);
45
+ }
46
+ };
47
+ }
src/controllers/mediaController.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HttpError } from "../utils/httpError.js";
2
+
3
+ export function createMediaController({ mediaStore }) {
4
+ return function mediaController(req, res, next) {
5
+ try {
6
+ const media = mediaStore.get(req.params.mediaId);
7
+ if (!media) {
8
+ throw new HttpError(404, "Media file not found or expired.");
9
+ }
10
+
11
+ res.setHeader("content-type", media.mimeType);
12
+ res.setHeader("cache-control", "private, max-age=3600");
13
+ res.send(media.buffer);
14
+ } catch (error) {
15
+ next(error);
16
+ }
17
+ };
18
+ }
src/routes/apiRouter.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from "express";
2
+
3
+ export function createApiRouter({ chatController, mediaController }) {
4
+ const router = Router();
5
+
6
+ router.get("/health", (_req, res) => {
7
+ res.json({ ok: true });
8
+ });
9
+
10
+ router.post("/chat/completions", chatController);
11
+ router.get("/media/:mediaId", mediaController);
12
+
13
+ return router;
14
+ }
src/server.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { loadConfig, validateConfig } from "./config.js";
2
+ import { createApp } from "./app.js";
3
+ import { InMemoryMediaStore } from "./services/mediaStore.js";
4
+ import { createAudioConversionService } from "./services/audioConversionService.js";
5
+ import { createRequestNormalizationService } from "./services/requestNormalizationService.js";
6
+ import { createResponseNormalizationService } from "./services/responseNormalizationService.js";
7
+ import { createOpenAiService } from "./services/openAiService.js";
8
+ import { createChatController } from "./controllers/chatController.js";
9
+ import { createMediaController } from "./controllers/mediaController.js";
10
+
11
+ const config = loadConfig();
12
+ validateConfig(config);
13
+
14
+ const mediaStore = new InMemoryMediaStore({ ttlSeconds: config.mediaTtlSeconds });
15
+ const audioConversionService = createAudioConversionService({
16
+ maxAudioDownloadMb: config.maxAudioDownloadMb
17
+ });
18
+ const requestNormalizationService = createRequestNormalizationService({
19
+ audioConversionService
20
+ });
21
+ const responseNormalizationService = createResponseNormalizationService({
22
+ mediaStore
23
+ });
24
+ const openAiService = createOpenAiService({
25
+ apiKey: config.openAiApiKey,
26
+ baseUrl: config.openAiBaseUrl,
27
+ timeoutMs: config.requestTimeoutMs
28
+ });
29
+
30
+ const chatController = createChatController({
31
+ openAiService,
32
+ requestNormalizationService,
33
+ responseNormalizationService
34
+ });
35
+ const mediaController = createMediaController({ mediaStore });
36
+
37
+ const app = createApp({
38
+ jsonLimit: config.jsonLimit,
39
+ chatController,
40
+ mediaController
41
+ });
42
+
43
+ app.listen(config.port, () => {
44
+ console.log(`oapix listening on http://localhost:${config.port}`);
45
+ });
src/services/audioConversionService.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { promises as fs } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import ffmpegPath from "ffmpeg-static";
6
+ import { HttpError } from "../utils/httpError.js";
7
+
8
+ function runFfmpeg(inputPath, outputPath) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(ffmpegPath, [
11
+ "-y",
12
+ "-i",
13
+ inputPath,
14
+ "-vn",
15
+ "-acodec",
16
+ "libmp3lame",
17
+ outputPath
18
+ ]);
19
+
20
+ let stderr = "";
21
+
22
+ child.stderr.on("data", (chunk) => {
23
+ stderr += chunk.toString();
24
+ });
25
+
26
+ child.on("error", reject);
27
+ child.on("close", (code) => {
28
+ if (code === 0) {
29
+ resolve();
30
+ return;
31
+ }
32
+
33
+ reject(new HttpError(400, "Failed to convert audio to mp3.", stderr.trim() || undefined));
34
+ });
35
+ });
36
+ }
37
+
38
+ export function createAudioConversionService({ fetchImpl = fetch, maxAudioDownloadMb = 25 } = {}) {
39
+ const maxBytes = maxAudioDownloadMb * 1024 * 1024;
40
+
41
+ return {
42
+ async downloadAndConvertToMp3Base64(url) {
43
+ const parsedUrl = new URL(url);
44
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
45
+ throw new HttpError(400, "Audio URL must use http or https.");
46
+ }
47
+
48
+ const response = await fetchImpl(url);
49
+ if (!response.ok) {
50
+ throw new HttpError(400, `Failed to download audio URL: ${response.status} ${response.statusText}`);
51
+ }
52
+
53
+ const audioBuffer = Buffer.from(await response.arrayBuffer());
54
+ if (audioBuffer.length > maxBytes) {
55
+ throw new HttpError(413, `Audio URL exceeded ${maxAudioDownloadMb}MB download limit.`);
56
+ }
57
+
58
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "oapix-audio-"));
59
+ const inputPath = path.join(tempDir, "input-media");
60
+ const outputPath = path.join(tempDir, "output.mp3");
61
+
62
+ try {
63
+ await fs.writeFile(inputPath, audioBuffer);
64
+ await runFfmpeg(inputPath, outputPath);
65
+ const mp3Buffer = await fs.readFile(outputPath);
66
+
67
+ return {
68
+ data: mp3Buffer.toString("base64"),
69
+ format: "mp3"
70
+ };
71
+ } finally {
72
+ await fs.rm(tempDir, { force: true, recursive: true });
73
+ }
74
+ }
75
+ };
76
+ }
src/services/mediaStore.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export class InMemoryMediaStore {
4
+ constructor({ ttlSeconds = 3600 } = {}) {
5
+ this.ttlSeconds = ttlSeconds;
6
+ this.items = new Map();
7
+ }
8
+
9
+ save({ buffer, mimeType, extension }) {
10
+ this.cleanup();
11
+
12
+ const id = randomUUID();
13
+ const expiresAt = Date.now() + (this.ttlSeconds * 1000);
14
+
15
+ this.items.set(id, {
16
+ buffer,
17
+ mimeType,
18
+ extension,
19
+ expiresAt
20
+ });
21
+
22
+ return { id, expiresAt };
23
+ }
24
+
25
+ get(id) {
26
+ this.cleanup();
27
+
28
+ const item = this.items.get(id);
29
+ if (!item) {
30
+ return null;
31
+ }
32
+
33
+ return item;
34
+ }
35
+
36
+ cleanup() {
37
+ const now = Date.now();
38
+
39
+ for (const [id, item] of this.items.entries()) {
40
+ if (item.expiresAt <= now) {
41
+ this.items.delete(id);
42
+ }
43
+ }
44
+ }
45
+ }
src/services/openAiService.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function createOpenAiService({ apiKey, baseUrl, timeoutMs = 60000, fetchImpl = fetch }) {
2
+ return {
3
+ async createChatCompletion(body) {
4
+ const controller = new AbortController();
5
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
6
+
7
+ try {
8
+ return await fetchImpl(`${baseUrl}/chat/completions`, {
9
+ method: "POST",
10
+ headers: {
11
+ "authorization": `Bearer ${apiKey}`,
12
+ "content-type": "application/json"
13
+ },
14
+ body: JSON.stringify(body),
15
+ signal: controller.signal
16
+ });
17
+ } finally {
18
+ clearTimeout(timeout);
19
+ }
20
+ }
21
+ };
22
+ }
src/services/requestNormalizationService.js ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HttpError } from "../utils/httpError.js";
2
+ import { imageMimeType } from "../utils/mediaTypes.js";
3
+ import { isDataUrl, isLikelyBase64, parseDataUrl, stripDataUrl } from "../utils/dataUrl.js";
4
+
5
+ const SUPPORTED_AUDIO_FORMATS = new Set(["mp3", "wav"]);
6
+
7
+ function normalizeImageSource(source, mimeType) {
8
+ if (typeof source !== "string" || source.length === 0) {
9
+ throw new HttpError(400, "Image input must include a non-empty URL or base64 payload.");
10
+ }
11
+
12
+ if (source.startsWith("http://") || source.startsWith("https://") || isDataUrl(source)) {
13
+ return source;
14
+ }
15
+
16
+ if (isLikelyBase64(source)) {
17
+ return `data:${mimeType ?? imageMimeType("png")};base64,${source}`;
18
+ }
19
+
20
+ throw new HttpError(400, "Image input must be an http(s) URL, data URL, or raw base64 string.");
21
+ }
22
+
23
+ function normalizeImagePart(part) {
24
+ const image = typeof part.image_url === "string" ? { url: part.image_url } : part.image_url;
25
+ const source = image?.url ?? part.input_image?.url ?? part.input_image?.data;
26
+ const mimeType = part.mime_type ?? part.input_image?.mime_type;
27
+
28
+ return {
29
+ type: "image_url",
30
+ image_url: {
31
+ ...image,
32
+ url: normalizeImageSource(source, mimeType)
33
+ }
34
+ };
35
+ }
36
+
37
+ function normalizeAudioBase64(audio) {
38
+ const format = audio.format?.toLowerCase();
39
+ if (!SUPPORTED_AUDIO_FORMATS.has(format)) {
40
+ throw new HttpError(400, "Audio input format must be mp3 or wav.");
41
+ }
42
+
43
+ if (typeof audio.data !== "string" || audio.data.length === 0) {
44
+ throw new HttpError(400, "Audio input must include base64 data.");
45
+ }
46
+
47
+ const parsed = parseDataUrl(audio.data);
48
+ return {
49
+ data: stripDataUrl(audio.data),
50
+ format: parsed?.mimeType === "audio/wav" ? "wav" : format
51
+ };
52
+ }
53
+
54
+ export function createRequestNormalizationService({ audioConversionService }) {
55
+ return {
56
+ async normalize(body) {
57
+ if (!body || !Array.isArray(body.messages)) {
58
+ throw new HttpError(400, "Request body must include a messages array.");
59
+ }
60
+
61
+ const normalized = structuredClone(body);
62
+ const proxyOptions = normalized.proxy ?? {};
63
+ delete normalized.proxy;
64
+
65
+ for (const message of normalized.messages) {
66
+ if (!Array.isArray(message.content)) {
67
+ continue;
68
+ }
69
+
70
+ const nextParts = [];
71
+ for (const part of message.content) {
72
+ if (part.type === "image_url" || part.type === "input_image") {
73
+ nextParts.push(normalizeImagePart(part));
74
+ continue;
75
+ }
76
+
77
+ if (part.type === "input_audio") {
78
+ const audio = part.input_audio ?? {};
79
+
80
+ if (audio.url) {
81
+ const converted = await audioConversionService.downloadAndConvertToMp3Base64(audio.url);
82
+ nextParts.push({
83
+ type: "input_audio",
84
+ input_audio: converted
85
+ });
86
+ continue;
87
+ }
88
+
89
+ nextParts.push({
90
+ type: "input_audio",
91
+ input_audio: normalizeAudioBase64(audio)
92
+ });
93
+ continue;
94
+ }
95
+
96
+ nextParts.push(part);
97
+ }
98
+
99
+ message.content = nextParts;
100
+ }
101
+
102
+ return {
103
+ normalizedBody: normalized,
104
+ responseContext: {
105
+ audioFormat: normalized.audio?.format ?? "mp3",
106
+ exposeMediaUrls: proxyOptions.expose_media_urls !== false
107
+ }
108
+ };
109
+ }
110
+ };
111
+ }
src/services/responseNormalizationService.js ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Buffer } from "node:buffer";
2
+ import { isDataUrl, parseDataUrl } from "../utils/dataUrl.js";
3
+ import { audioMimeType, extensionFromMimeType } from "../utils/mediaTypes.js";
4
+
5
+ function mediaUrl(publicBaseUrl, mediaId) {
6
+ return `${publicBaseUrl}/v1/media/${mediaId}`;
7
+ }
8
+
9
+ export function createResponseNormalizationService({ mediaStore }) {
10
+ return {
11
+ normalize(body, { publicBaseUrl, audioFormat = "mp3", exposeMediaUrls = true } = {}) {
12
+ if (!body || !exposeMediaUrls) {
13
+ return body;
14
+ }
15
+
16
+ const normalized = structuredClone(body);
17
+ const proxyMedia = [];
18
+
19
+ for (const [choiceIndex, choice] of (normalized.choices ?? []).entries()) {
20
+ const message = choice?.message;
21
+ if (!message) {
22
+ continue;
23
+ }
24
+
25
+ if (message.audio?.data) {
26
+ const format = message.audio.format ?? audioFormat;
27
+ const media = mediaStore.save({
28
+ buffer: Buffer.from(message.audio.data, "base64"),
29
+ mimeType: audioMimeType(format),
30
+ extension: format
31
+ });
32
+
33
+ message.audio.format = format;
34
+ message.audio.url = mediaUrl(publicBaseUrl, media.id);
35
+ proxyMedia.push({
36
+ choice: choiceIndex,
37
+ type: "audio",
38
+ url: message.audio.url
39
+ });
40
+ }
41
+
42
+ if (!Array.isArray(message.content)) {
43
+ continue;
44
+ }
45
+
46
+ for (const [partIndex, part] of message.content.entries()) {
47
+ const imageUrl = part?.image_url?.url;
48
+ if (!isDataUrl(imageUrl)) {
49
+ continue;
50
+ }
51
+
52
+ const parsed = parseDataUrl(imageUrl);
53
+ if (!parsed) {
54
+ continue;
55
+ }
56
+
57
+ const media = mediaStore.save({
58
+ buffer: Buffer.from(parsed.base64, "base64"),
59
+ mimeType: parsed.mimeType,
60
+ extension: extensionFromMimeType(parsed.mimeType)
61
+ });
62
+
63
+ part.image_url.proxy_url = mediaUrl(publicBaseUrl, media.id);
64
+ proxyMedia.push({
65
+ choice: choiceIndex,
66
+ part: partIndex,
67
+ type: "image",
68
+ url: part.image_url.proxy_url
69
+ });
70
+ }
71
+ }
72
+
73
+ if (proxyMedia.length > 0) {
74
+ normalized.proxy = {
75
+ media: proxyMedia
76
+ };
77
+ }
78
+
79
+ return normalized;
80
+ }
81
+ };
82
+ }
src/utils/dataUrl.js ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const DATA_URL_PATTERN = /^data:([^;,]+);base64,(.+)$/s;
2
+ const BASE64_PATTERN = /^[A-Za-z0-9+/=]+$/;
3
+
4
+ export function isDataUrl(value) {
5
+ return typeof value === "string" && value.startsWith("data:");
6
+ }
7
+
8
+ export function parseDataUrl(value) {
9
+ const match = value.match(DATA_URL_PATTERN);
10
+ if (!match) {
11
+ return null;
12
+ }
13
+
14
+ return {
15
+ mimeType: match[1],
16
+ base64: match[2]
17
+ };
18
+ }
19
+
20
+ export function isLikelyBase64(value) {
21
+ if (typeof value !== "string") {
22
+ return false;
23
+ }
24
+
25
+ const compact = value.replace(/\s+/g, "");
26
+ return compact.length > 0 && compact.length % 4 === 0 && BASE64_PATTERN.test(compact);
27
+ }
28
+
29
+ export function stripDataUrl(value) {
30
+ if (!isDataUrl(value)) {
31
+ return value;
32
+ }
33
+
34
+ const parsed = parseDataUrl(value);
35
+ return parsed ? parsed.base64 : value;
36
+ }
src/utils/httpError.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export class HttpError extends Error {
2
+ constructor(statusCode, message, details) {
3
+ super(message);
4
+ this.name = "HttpError";
5
+ this.statusCode = statusCode;
6
+ this.details = details;
7
+ }
8
+ }
src/utils/mediaTypes.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const AUDIO_MIME = {
2
+ mp3: "audio/mpeg",
3
+ wav: "audio/wav",
4
+ flac: "audio/flac",
5
+ opus: "audio/opus",
6
+ pcm16: "audio/L16"
7
+ };
8
+
9
+ const IMAGE_MIME = {
10
+ jpg: "image/jpeg",
11
+ jpeg: "image/jpeg",
12
+ png: "image/png",
13
+ webp: "image/webp",
14
+ gif: "image/gif"
15
+ };
16
+
17
+ export function audioMimeType(format) {
18
+ return AUDIO_MIME[format] ?? "application/octet-stream";
19
+ }
20
+
21
+ export function imageMimeType(format) {
22
+ return IMAGE_MIME[format] ?? "image/png";
23
+ }
24
+
25
+ export function extensionFromMimeType(mimeType) {
26
+ const mimeMap = {
27
+ "audio/mpeg": "mp3",
28
+ "audio/wav": "wav",
29
+ "audio/flac": "flac",
30
+ "audio/opus": "opus",
31
+ "audio/L16": "pcm16",
32
+ "image/jpeg": "jpg",
33
+ "image/png": "png",
34
+ "image/webp": "webp",
35
+ "image/gif": "gif"
36
+ };
37
+
38
+ return mimeMap[mimeType] ?? "bin";
39
+ }
test/requestNormalization.test.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createRequestNormalizationService } from "../src/services/requestNormalizationService.js";
4
+
5
+ test("normalizes audio URLs to mp3 base64 and raw image base64 to data URLs", async () => {
6
+ const service = createRequestNormalizationService({
7
+ audioConversionService: {
8
+ async downloadAndConvertToMp3Base64(url) {
9
+ assert.equal(url, "https://example.com/sample.wav");
10
+ return {
11
+ data: "ZmFrZS1tcDM=",
12
+ format: "mp3"
13
+ };
14
+ }
15
+ }
16
+ });
17
+
18
+ const { normalizedBody, responseContext } = await service.normalize({
19
+ messages: [
20
+ {
21
+ role: "user",
22
+ content: [
23
+ {
24
+ type: "image_url",
25
+ image_url: {
26
+ url: "iVBORw0KGgoAAAANSUhEUgAAAAUA"
27
+ },
28
+ mime_type: "image/png"
29
+ },
30
+ {
31
+ type: "input_audio",
32
+ input_audio: {
33
+ url: "https://example.com/sample.wav"
34
+ }
35
+ }
36
+ ]
37
+ }
38
+ ],
39
+ audio: {
40
+ format: "wav"
41
+ }
42
+ });
43
+
44
+ assert.equal(normalizedBody.messages[0].content[0].image_url.url.startsWith("data:image/png;base64,"), true);
45
+ assert.deepEqual(normalizedBody.messages[0].content[1], {
46
+ type: "input_audio",
47
+ input_audio: {
48
+ data: "ZmFrZS1tcDM=",
49
+ format: "mp3"
50
+ }
51
+ });
52
+ assert.equal(responseContext.audioFormat, "wav");
53
+ });
test/responseNormalization.test.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { InMemoryMediaStore } from "../src/services/mediaStore.js";
4
+ import { createResponseNormalizationService } from "../src/services/responseNormalizationService.js";
5
+
6
+ test("adds proxy URLs for audio and data-url image outputs", () => {
7
+ const mediaStore = new InMemoryMediaStore({ ttlSeconds: 3600 });
8
+ const service = createResponseNormalizationService({ mediaStore });
9
+
10
+ const normalized = service.normalize({
11
+ choices: [
12
+ {
13
+ message: {
14
+ audio: {
15
+ data: "ZmFrZS1hdWRpbw=="
16
+ },
17
+ content: [
18
+ {
19
+ type: "image_url",
20
+ image_url: {
21
+ url: "data:image/png;base64,aW1hZ2U="
22
+ }
23
+ }
24
+ ]
25
+ }
26
+ }
27
+ ]
28
+ }, {
29
+ publicBaseUrl: "http://localhost:3000",
30
+ audioFormat: "mp3",
31
+ exposeMediaUrls: true
32
+ });
33
+
34
+ assert.match(normalized.choices[0].message.audio.url, /^http:\/\/localhost:3000\/v1\/media\//);
35
+ assert.match(normalized.choices[0].message.content[0].image_url.proxy_url, /^http:\/\/localhost:3000\/v1\/media\//);
36
+ assert.equal(normalized.proxy.media.length, 2);
37
+ });