icebear0828 Claude Opus 4.6 commited on
Commit
92c5df7
·
1 Parent(s): ab2754a

feat: OpenCode platform support, vitest tests, changelog & Docker hardening

Browse files

- Add opencode.json provider config for OpenCode IDE integration
- Add CHANGELOG.md covering all versions (v0.1.0–v0.8.0)
- Add vitest with unit tests for account-pool, codex-api, codex-event-extractor
- Add request-id middleware to global middleware chain
- Harden Dockerfile: non-root user, HEALTHCHECK probe
- Simplify /health endpoint to return pool summary only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

CHANGELOG.md ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ 本项目的所有重要变更都将记录在此文件中。
4
+
5
+ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - OpenCode 平台支持(`opencode.json` 配置文件)
12
+ - Vitest 测试框架(account-pool、codex-api、codex-event-extractor 单元测试)
13
+ - request-id 中间件注入全局请求链路 ID
14
+ - Dockerfile 安全加固(非 root 用户运行、HEALTHCHECK 探针)
15
+
16
+ ### Changed
17
+
18
+ - `/health` 端点精简,仅返回 pool 摘要(total / active)
19
+
20
+ ## [v0.8.0](https://github.com/icebear0828/codex-proxy/releases/tag/v0.8.0) - 2026-02-24
21
+
22
+ ### Added
23
+
24
+ - 原生 function_call / tool_calls 支持(所有协议)
25
+
26
+ ### Fixed
27
+
28
+ - 格式错误的 chat payload 返回 400 `invalid_json` 错误
29
+
30
+ ## [v0.7.0](https://github.com/icebear0828/codex-proxy/releases/tag/v0.7.0) - 2026-02-22
31
+
32
+ ### Added
33
+
34
+ - `developer` 角色支持(OpenAI 协议)
35
+ - 数组格式 content 支持
36
+ - tool / function 消息兼容(所有协议)
37
+ - 模型响应中自动过滤 Codex Desktop 指令
38
+
39
+ ### Changed
40
+
41
+ - 清理无用代码、未使用配置,修复类型违规
42
+
43
+ ### Fixed
44
+
45
+ - 启动日志显示配置的 `proxy_api_key` 而非随机哈希
46
+ - 首次 OAuth 登录后 `useStatus` 未刷新
47
+
48
+ ## [v0.6.0](https://github.com/icebear0828/codex-proxy/releases/tag/v0.6.0) - 2026-02-21
49
+
50
+ ### Added
51
+
52
+ - libcurl-impersonate FFI 传输层,Chrome TLS 指纹
53
+ - pnpm / bun 包管理器支持
54
+
55
+ ### Changed
56
+
57
+ - README 快速开始按平台重组
58
+
59
+ ### Fixed
60
+
61
+ - Docker 构建完整修复链(代理配置、BuildKit 冲突、host 网络、源码复制顺序、layer 优化)
62
+ - `.env` 行内注释被误解析为 JWT token
63
+ - Anthropic / Gemini 代码示例跟随所选模型
64
+ - `proxy_api_key` 配置未在前端和认证验证中使用
65
+ - 删除按钮始终可见,不被状态徽章遮挡
66
+
67
+ ## [v0.5.0](https://github.com/icebear0828/codex-proxy/releases/tag/v0.5.0) - 2026-02-20
68
+
69
+ ### Added
70
+
71
+ - Dashboard 暗色 / 亮色主题切换
72
+ - 国际化支持(中文 / 英文)
73
+ - 自动代理检测(mihomo / clash / v2ray)
74
+ - 局域网登录分步教程
75
+ - Preact + Vite 前端架构
76
+ - Docker 容器部署支持
77
+ - 共享代理处理器,消除路由重复
78
+
79
+ ### Changed
80
+
81
+ - Dashboard 重写为 Tailwind CSS
82
+ - 协议 / 语言两级标签页(OpenAI / Anthropic / Gemini × Python / cURL / Node.js)
83
+ - 内联 SVG 图标替换字体图标
84
+ - 系统字体替换 Google Fonts
85
+ - 架构审计修复(P0-P2 稳定性与可靠性)
86
+
87
+ ### Fixed
88
+
89
+ - 移除所有 `any` 类型
90
+ - 修复图标文字闪烁(FOUC)
91
+ - 修复未认证时的重定向循环
92
+ - 移除虚假的 Claude / Gemini 模型别名,使用动态目录
93
+ - Dashboard 配置改为只读,修复 HTTP 复制按钮
94
+ - 恢复模型下拉选择器
95
+
96
+ ## [v0.4.0](https://github.com/icebear0828/codex-proxy/releases/tag/v0.4.0) - 2026-02-19
97
+
98
+ ### Added
99
+
100
+ - Anthropic Messages API 兼容路由(`POST /v1/messages`)
101
+ - Google Gemini API 兼容路由
102
+ - 桌面端上下文注入(模拟 Codex Desktop 请求特征)
103
+ - 多轮对话会话管理
104
+ - 自动更新检查管道(Appcast 轮询 + 版本提取)
105
+ - 中英双语 README
106
+
107
+ ## [v0.3.0](https://github.com/icebear0828/codex-proxy/releases/tag/v0.3.0) - 2026-02-18
108
+
109
+ ### Added
110
+
111
+ - curl-impersonate TLS 指纹模拟
112
+ - Chromium 版本自动检测与动态 `sec-ch-ua` 生成
113
+ - 请求时序 jitter 随机化
114
+ - Dashboard 实时代码示例与配额显示
115
+
116
+ ### Fixed
117
+
118
+ - curl 请求修复
119
+
120
+ ## [v0.2.0](https://github.com/icebear0828/codex-proxy/releases/tag/v0.2.0) - 2026-02-17
121
+
122
+ ### Added
123
+
124
+ - Dashboard 多账户管理 UI
125
+ - OAuth PKCE 登录流程(固定 `localhost:1455` 回调)
126
+ - 架构审计:伪装加固、自动更新机制、健壮性提升
127
+
128
+ ### Changed
129
+
130
+ - 硬编码值提取到配置文件
131
+ - 清理无用代码
132
+
133
+ ## [v0.1.0](https://github.com/icebear0828/codex-proxy/releases/tag/v0.1.0) - 2026-02-17
134
+
135
+ ### Added
136
+
137
+ - OpenAI `/v1/chat/completions` → Codex Responses API 反向代理核心
138
+ - 配额 API 查询(`/auth/accounts?quota=true`)
139
+ - Cloudflare TLS 指纹绕过
140
+ - SSE 流式响应转换
141
+ - 模型列表端点(`GET /v1/models`)
142
+ - 健康检查端点(`GET /health`)
Dockerfile CHANGED
@@ -32,4 +32,12 @@ RUN npm prune --omit=dev && npm install --no-save tsx
32
 
33
  VOLUME /app/data
34
  EXPOSE 8080
 
 
 
 
 
 
 
 
35
  CMD ["node", "dist/index.js"]
 
32
 
33
  VOLUME /app/data
34
  EXPOSE 8080
35
+
36
+ # Ensure writable directories for non-root user
37
+ RUN mkdir -p /app/data && chown -R node:node /app/data /app/config
38
+
39
+ USER node
40
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
41
+ CMD curl -fs http://localhost:8080/health || exit 1
42
+
43
  CMD ["node", "dist/index.js"]
opencode.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "provider": {
4
+ "codex-proxy": {
5
+ "npm": "@ai-sdk/openai-compatible",
6
+ "name": "Codex Proxy",
7
+ "options": {
8
+ "baseURL": "http://localhost:8080/v1",
9
+ "apiKey": "pwd"
10
+ },
11
+ "models": {
12
+ "codex": {
13
+ "name": "Codex (GPT-5.3)",
14
+ "attachment": true,
15
+ "limit": {
16
+ "context": 200000,
17
+ "output": 65536
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
package-lock.json CHANGED
@@ -12,7 +12,6 @@
12
  "@hono/node-server": "^1.0.0",
13
  "hono": "^4.0.0",
14
  "js-yaml": "^4.1.0",
15
- "koffi": "^2.15.1",
16
  "undici": "^7.0.0",
17
  "zod": "^3.23.0"
18
  },
@@ -22,7 +21,8 @@
22
  "@types/node": "^22.0.0",
23
  "js-beautify": "^1.15.0",
24
  "tsx": "^4.0.0",
25
- "typescript": "^5.5.0"
 
26
  },
27
  "optionalDependencies": {
28
  "koffi": "^2.15.1"
@@ -518,39 +518,536 @@
518
  "node": ">=12"
519
  }
520
  },
 
 
 
 
 
 
 
521
  "node_modules/@one-ini/wasm": {
522
  "version": "0.1.1",
523
  "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz",
524
  "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
525
  "dev": true,
526
- "license": "MIT"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  },
528
- "node_modules/@pkgjs/parseargs": {
529
- "version": "0.11.0",
530
- "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
531
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
532
  "dev": true,
533
  "license": "MIT",
534
- "optional": true,
535
- "engines": {
536
- "node": ">=14"
 
 
 
 
537
  }
538
  },
539
- "node_modules/@types/js-yaml": {
540
- "version": "4.0.9",
541
- "resolved": "https://registry.npmmirror.com/@types/js-yaml/-/js-yaml-4.0.9.tgz",
542
- "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
543
  "dev": true,
544
- "license": "MIT"
 
 
 
 
 
 
545
  },
546
- "node_modules/@types/node": {
547
- "version": "22.19.11",
548
- "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.11.tgz",
549
- "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
550
  "dev": true,
551
  "license": "MIT",
552
  "dependencies": {
553
- "undici-types": "~6.21.0"
 
 
 
 
 
554
  }
555
  },
556
  "node_modules/abbrev": {
@@ -595,6 +1092,16 @@
595
  "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
596
  "license": "Python-2.0"
597
  },
 
 
 
 
 
 
 
 
 
 
598
  "node_modules/balanced-match": {
599
  "version": "1.0.2",
600
  "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -613,6 +1120,43 @@
613
  "concat-map": "0.0.1"
614
  }
615
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
  "node_modules/color-convert": {
617
  "version": "2.0.1",
618
  "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -676,6 +1220,34 @@
676
  "node": ">= 8"
677
  }
678
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  "node_modules/eastasianwidth": {
680
  "version": "0.2.0",
681
  "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -745,6 +1317,13 @@
745
  "dev": true,
746
  "license": "MIT"
747
  },
 
 
 
 
 
 
 
748
  "node_modules/esbuild": {
749
  "version": "0.27.3",
750
  "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz",
@@ -787,6 +1366,44 @@
787
  "@esbuild/win32-x64": "0.27.3"
788
  }
789
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
790
  "node_modules/foreground-child": {
791
  "version": "3.3.1",
792
  "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -1008,6 +1625,13 @@
1008
  "node": ">=14"
1009
  }
1010
  },
 
 
 
 
 
 
 
1011
  "node_modules/js-yaml": {
1012
  "version": "4.1.1",
1013
  "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -1031,6 +1655,13 @@
1031
  "url": "https://liberapay.com/Koromix"
1032
  }
1033
  },
 
 
 
 
 
 
 
1034
  "node_modules/lru-cache": {
1035
  "version": "10.4.3",
1036
  "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -1038,6 +1669,16 @@
1038
  "dev": true,
1039
  "license": "ISC"
1040
  },
 
 
 
 
 
 
 
 
 
 
1041
  "node_modules/minimatch": {
1042
  "version": "3.1.2",
1043
  "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
@@ -1061,6 +1702,32 @@
1061
  "node": ">=16 || 14 >=14.17"
1062
  }
1063
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1064
  "node_modules/nopt": {
1065
  "version": "7.2.1",
1066
  "resolved": "https://registry.npmmirror.com/nopt/-/nopt-7.2.1.tgz",
@@ -1131,6 +1798,72 @@
1131
  "url": "https://github.com/sponsors/isaacs"
1132
  }
1133
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1134
  "node_modules/proto-list": {
1135
  "version": "1.2.4",
1136
  "resolved": "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz",
@@ -1148,6 +1881,51 @@
1148
  "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
1149
  }
1150
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1151
  "node_modules/semver": {
1152
  "version": "7.7.4",
1153
  "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
@@ -1184,6 +1962,13 @@
1184
  "node": ">=8"
1185
  }
1186
  },
 
 
 
 
 
 
 
1187
  "node_modules/signal-exit": {
1188
  "version": "4.1.0",
1189
  "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -1197,6 +1982,30 @@
1197
  "url": "https://github.com/sponsors/isaacs"
1198
  }
1199
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
  "node_modules/string-width": {
1201
  "version": "5.1.2",
1202
  "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
@@ -1301,6 +2110,80 @@
1301
  "node": ">=8"
1302
  }
1303
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1304
  "node_modules/tsx": {
1305
  "version": "4.21.0",
1306
  "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz",
@@ -1351,6 +2234,177 @@
1351
  "dev": true,
1352
  "license": "MIT"
1353
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1354
  "node_modules/which": {
1355
  "version": "2.0.2",
1356
  "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
@@ -1367,6 +2421,23 @@
1367
  "node": ">= 8"
1368
  }
1369
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1370
  "node_modules/wrap-ansi": {
1371
  "version": "8.1.0",
1372
  "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
 
12
  "@hono/node-server": "^1.0.0",
13
  "hono": "^4.0.0",
14
  "js-yaml": "^4.1.0",
 
15
  "undici": "^7.0.0",
16
  "zod": "^3.23.0"
17
  },
 
21
  "@types/node": "^22.0.0",
22
  "js-beautify": "^1.15.0",
23
  "tsx": "^4.0.0",
24
+ "typescript": "^5.5.0",
25
+ "vitest": "^3.2.4"
26
  },
27
  "optionalDependencies": {
28
  "koffi": "^2.15.1"
 
518
  "node": ">=12"
519
  }
520
  },
521
+ "node_modules/@jridgewell/sourcemap-codec": {
522
+ "version": "1.5.5",
523
+ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
524
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
525
+ "dev": true,
526
+ "license": "MIT"
527
+ },
528
  "node_modules/@one-ini/wasm": {
529
  "version": "0.1.1",
530
  "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz",
531
  "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
532
  "dev": true,
533
+ "license": "MIT"
534
+ },
535
+ "node_modules/@pkgjs/parseargs": {
536
+ "version": "0.11.0",
537
+ "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
538
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
539
+ "dev": true,
540
+ "license": "MIT",
541
+ "optional": true,
542
+ "engines": {
543
+ "node": ">=14"
544
+ }
545
+ },
546
+ "node_modules/@rollup/rollup-android-arm-eabi": {
547
+ "version": "4.59.0",
548
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
549
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
550
+ "cpu": [
551
+ "arm"
552
+ ],
553
+ "dev": true,
554
+ "license": "MIT",
555
+ "optional": true,
556
+ "os": [
557
+ "android"
558
+ ]
559
+ },
560
+ "node_modules/@rollup/rollup-android-arm64": {
561
+ "version": "4.59.0",
562
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
563
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
564
+ "cpu": [
565
+ "arm64"
566
+ ],
567
+ "dev": true,
568
+ "license": "MIT",
569
+ "optional": true,
570
+ "os": [
571
+ "android"
572
+ ]
573
+ },
574
+ "node_modules/@rollup/rollup-darwin-arm64": {
575
+ "version": "4.59.0",
576
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
577
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
578
+ "cpu": [
579
+ "arm64"
580
+ ],
581
+ "dev": true,
582
+ "license": "MIT",
583
+ "optional": true,
584
+ "os": [
585
+ "darwin"
586
+ ]
587
+ },
588
+ "node_modules/@rollup/rollup-darwin-x64": {
589
+ "version": "4.59.0",
590
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
591
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
592
+ "cpu": [
593
+ "x64"
594
+ ],
595
+ "dev": true,
596
+ "license": "MIT",
597
+ "optional": true,
598
+ "os": [
599
+ "darwin"
600
+ ]
601
+ },
602
+ "node_modules/@rollup/rollup-freebsd-arm64": {
603
+ "version": "4.59.0",
604
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
605
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
606
+ "cpu": [
607
+ "arm64"
608
+ ],
609
+ "dev": true,
610
+ "license": "MIT",
611
+ "optional": true,
612
+ "os": [
613
+ "freebsd"
614
+ ]
615
+ },
616
+ "node_modules/@rollup/rollup-freebsd-x64": {
617
+ "version": "4.59.0",
618
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
619
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
620
+ "cpu": [
621
+ "x64"
622
+ ],
623
+ "dev": true,
624
+ "license": "MIT",
625
+ "optional": true,
626
+ "os": [
627
+ "freebsd"
628
+ ]
629
+ },
630
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
631
+ "version": "4.59.0",
632
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
633
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
634
+ "cpu": [
635
+ "arm"
636
+ ],
637
+ "dev": true,
638
+ "license": "MIT",
639
+ "optional": true,
640
+ "os": [
641
+ "linux"
642
+ ]
643
+ },
644
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
645
+ "version": "4.59.0",
646
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
647
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
648
+ "cpu": [
649
+ "arm"
650
+ ],
651
+ "dev": true,
652
+ "license": "MIT",
653
+ "optional": true,
654
+ "os": [
655
+ "linux"
656
+ ]
657
+ },
658
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
659
+ "version": "4.59.0",
660
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
661
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
662
+ "cpu": [
663
+ "arm64"
664
+ ],
665
+ "dev": true,
666
+ "license": "MIT",
667
+ "optional": true,
668
+ "os": [
669
+ "linux"
670
+ ]
671
+ },
672
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
673
+ "version": "4.59.0",
674
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
675
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
676
+ "cpu": [
677
+ "arm64"
678
+ ],
679
+ "dev": true,
680
+ "license": "MIT",
681
+ "optional": true,
682
+ "os": [
683
+ "linux"
684
+ ]
685
+ },
686
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
687
+ "version": "4.59.0",
688
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
689
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
690
+ "cpu": [
691
+ "loong64"
692
+ ],
693
+ "dev": true,
694
+ "license": "MIT",
695
+ "optional": true,
696
+ "os": [
697
+ "linux"
698
+ ]
699
+ },
700
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
701
+ "version": "4.59.0",
702
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
703
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
704
+ "cpu": [
705
+ "loong64"
706
+ ],
707
+ "dev": true,
708
+ "license": "MIT",
709
+ "optional": true,
710
+ "os": [
711
+ "linux"
712
+ ]
713
+ },
714
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
715
+ "version": "4.59.0",
716
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
717
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
718
+ "cpu": [
719
+ "ppc64"
720
+ ],
721
+ "dev": true,
722
+ "license": "MIT",
723
+ "optional": true,
724
+ "os": [
725
+ "linux"
726
+ ]
727
+ },
728
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
729
+ "version": "4.59.0",
730
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
731
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
732
+ "cpu": [
733
+ "ppc64"
734
+ ],
735
+ "dev": true,
736
+ "license": "MIT",
737
+ "optional": true,
738
+ "os": [
739
+ "linux"
740
+ ]
741
+ },
742
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
743
+ "version": "4.59.0",
744
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
745
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
746
+ "cpu": [
747
+ "riscv64"
748
+ ],
749
+ "dev": true,
750
+ "license": "MIT",
751
+ "optional": true,
752
+ "os": [
753
+ "linux"
754
+ ]
755
+ },
756
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
757
+ "version": "4.59.0",
758
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
759
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
760
+ "cpu": [
761
+ "riscv64"
762
+ ],
763
+ "dev": true,
764
+ "license": "MIT",
765
+ "optional": true,
766
+ "os": [
767
+ "linux"
768
+ ]
769
+ },
770
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
771
+ "version": "4.59.0",
772
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
773
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
774
+ "cpu": [
775
+ "s390x"
776
+ ],
777
+ "dev": true,
778
+ "license": "MIT",
779
+ "optional": true,
780
+ "os": [
781
+ "linux"
782
+ ]
783
+ },
784
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
785
+ "version": "4.59.0",
786
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
787
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
788
+ "cpu": [
789
+ "x64"
790
+ ],
791
+ "dev": true,
792
+ "license": "MIT",
793
+ "optional": true,
794
+ "os": [
795
+ "linux"
796
+ ]
797
+ },
798
+ "node_modules/@rollup/rollup-linux-x64-musl": {
799
+ "version": "4.59.0",
800
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
801
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
802
+ "cpu": [
803
+ "x64"
804
+ ],
805
+ "dev": true,
806
+ "license": "MIT",
807
+ "optional": true,
808
+ "os": [
809
+ "linux"
810
+ ]
811
+ },
812
+ "node_modules/@rollup/rollup-openbsd-x64": {
813
+ "version": "4.59.0",
814
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
815
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
816
+ "cpu": [
817
+ "x64"
818
+ ],
819
+ "dev": true,
820
+ "license": "MIT",
821
+ "optional": true,
822
+ "os": [
823
+ "openbsd"
824
+ ]
825
+ },
826
+ "node_modules/@rollup/rollup-openharmony-arm64": {
827
+ "version": "4.59.0",
828
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
829
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
830
+ "cpu": [
831
+ "arm64"
832
+ ],
833
+ "dev": true,
834
+ "license": "MIT",
835
+ "optional": true,
836
+ "os": [
837
+ "openharmony"
838
+ ]
839
+ },
840
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
841
+ "version": "4.59.0",
842
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
843
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
844
+ "cpu": [
845
+ "arm64"
846
+ ],
847
+ "dev": true,
848
+ "license": "MIT",
849
+ "optional": true,
850
+ "os": [
851
+ "win32"
852
+ ]
853
+ },
854
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
855
+ "version": "4.59.0",
856
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
857
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
858
+ "cpu": [
859
+ "ia32"
860
+ ],
861
+ "dev": true,
862
+ "license": "MIT",
863
+ "optional": true,
864
+ "os": [
865
+ "win32"
866
+ ]
867
+ },
868
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
869
+ "version": "4.59.0",
870
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
871
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
872
+ "cpu": [
873
+ "x64"
874
+ ],
875
+ "dev": true,
876
+ "license": "MIT",
877
+ "optional": true,
878
+ "os": [
879
+ "win32"
880
+ ]
881
+ },
882
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
883
+ "version": "4.59.0",
884
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
885
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
886
+ "cpu": [
887
+ "x64"
888
+ ],
889
+ "dev": true,
890
+ "license": "MIT",
891
+ "optional": true,
892
+ "os": [
893
+ "win32"
894
+ ]
895
+ },
896
+ "node_modules/@types/chai": {
897
+ "version": "5.2.3",
898
+ "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz",
899
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
900
+ "dev": true,
901
+ "license": "MIT",
902
+ "dependencies": {
903
+ "@types/deep-eql": "*",
904
+ "assertion-error": "^2.0.1"
905
+ }
906
+ },
907
+ "node_modules/@types/deep-eql": {
908
+ "version": "4.0.2",
909
+ "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz",
910
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
911
+ "dev": true,
912
+ "license": "MIT"
913
+ },
914
+ "node_modules/@types/estree": {
915
+ "version": "1.0.8",
916
+ "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
917
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
918
+ "dev": true,
919
+ "license": "MIT"
920
+ },
921
+ "node_modules/@types/js-yaml": {
922
+ "version": "4.0.9",
923
+ "resolved": "https://registry.npmmirror.com/@types/js-yaml/-/js-yaml-4.0.9.tgz",
924
+ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
925
+ "dev": true,
926
+ "license": "MIT"
927
+ },
928
+ "node_modules/@types/node": {
929
+ "version": "22.19.11",
930
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.11.tgz",
931
+ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
932
+ "dev": true,
933
+ "license": "MIT",
934
+ "dependencies": {
935
+ "undici-types": "~6.21.0"
936
+ }
937
+ },
938
+ "node_modules/@vitest/expect": {
939
+ "version": "3.2.4",
940
+ "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-3.2.4.tgz",
941
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
942
+ "dev": true,
943
+ "license": "MIT",
944
+ "dependencies": {
945
+ "@types/chai": "^5.2.2",
946
+ "@vitest/spy": "3.2.4",
947
+ "@vitest/utils": "3.2.4",
948
+ "chai": "^5.2.0",
949
+ "tinyrainbow": "^2.0.0"
950
+ },
951
+ "funding": {
952
+ "url": "https://opencollective.com/vitest"
953
+ }
954
+ },
955
+ "node_modules/@vitest/mocker": {
956
+ "version": "3.2.4",
957
+ "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-3.2.4.tgz",
958
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
959
+ "dev": true,
960
+ "license": "MIT",
961
+ "dependencies": {
962
+ "@vitest/spy": "3.2.4",
963
+ "estree-walker": "^3.0.3",
964
+ "magic-string": "^0.30.17"
965
+ },
966
+ "funding": {
967
+ "url": "https://opencollective.com/vitest"
968
+ },
969
+ "peerDependencies": {
970
+ "msw": "^2.4.9",
971
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
972
+ },
973
+ "peerDependenciesMeta": {
974
+ "msw": {
975
+ "optional": true
976
+ },
977
+ "vite": {
978
+ "optional": true
979
+ }
980
+ }
981
+ },
982
+ "node_modules/@vitest/pretty-format": {
983
+ "version": "3.2.4",
984
+ "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
985
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
986
+ "dev": true,
987
+ "license": "MIT",
988
+ "dependencies": {
989
+ "tinyrainbow": "^2.0.0"
990
+ },
991
+ "funding": {
992
+ "url": "https://opencollective.com/vitest"
993
+ }
994
+ },
995
+ "node_modules/@vitest/runner": {
996
+ "version": "3.2.4",
997
+ "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-3.2.4.tgz",
998
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
999
+ "dev": true,
1000
+ "license": "MIT",
1001
+ "dependencies": {
1002
+ "@vitest/utils": "3.2.4",
1003
+ "pathe": "^2.0.3",
1004
+ "strip-literal": "^3.0.0"
1005
+ },
1006
+ "funding": {
1007
+ "url": "https://opencollective.com/vitest"
1008
+ }
1009
  },
1010
+ "node_modules/@vitest/snapshot": {
1011
+ "version": "3.2.4",
1012
+ "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-3.2.4.tgz",
1013
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
1014
  "dev": true,
1015
  "license": "MIT",
1016
+ "dependencies": {
1017
+ "@vitest/pretty-format": "3.2.4",
1018
+ "magic-string": "^0.30.17",
1019
+ "pathe": "^2.0.3"
1020
+ },
1021
+ "funding": {
1022
+ "url": "https://opencollective.com/vitest"
1023
  }
1024
  },
1025
+ "node_modules/@vitest/spy": {
1026
+ "version": "3.2.4",
1027
+ "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-3.2.4.tgz",
1028
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
1029
  "dev": true,
1030
+ "license": "MIT",
1031
+ "dependencies": {
1032
+ "tinyspy": "^4.0.3"
1033
+ },
1034
+ "funding": {
1035
+ "url": "https://opencollective.com/vitest"
1036
+ }
1037
  },
1038
+ "node_modules/@vitest/utils": {
1039
+ "version": "3.2.4",
1040
+ "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-3.2.4.tgz",
1041
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
1042
  "dev": true,
1043
  "license": "MIT",
1044
  "dependencies": {
1045
+ "@vitest/pretty-format": "3.2.4",
1046
+ "loupe": "^3.1.4",
1047
+ "tinyrainbow": "^2.0.0"
1048
+ },
1049
+ "funding": {
1050
+ "url": "https://opencollective.com/vitest"
1051
  }
1052
  },
1053
  "node_modules/abbrev": {
 
1092
  "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
1093
  "license": "Python-2.0"
1094
  },
1095
+ "node_modules/assertion-error": {
1096
+ "version": "2.0.1",
1097
+ "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
1098
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
1099
+ "dev": true,
1100
+ "license": "MIT",
1101
+ "engines": {
1102
+ "node": ">=12"
1103
+ }
1104
+ },
1105
  "node_modules/balanced-match": {
1106
  "version": "1.0.2",
1107
  "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
 
1120
  "concat-map": "0.0.1"
1121
  }
1122
  },
1123
+ "node_modules/cac": {
1124
+ "version": "6.7.14",
1125
+ "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz",
1126
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
1127
+ "dev": true,
1128
+ "license": "MIT",
1129
+ "engines": {
1130
+ "node": ">=8"
1131
+ }
1132
+ },
1133
+ "node_modules/chai": {
1134
+ "version": "5.3.3",
1135
+ "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz",
1136
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
1137
+ "dev": true,
1138
+ "license": "MIT",
1139
+ "dependencies": {
1140
+ "assertion-error": "^2.0.1",
1141
+ "check-error": "^2.1.1",
1142
+ "deep-eql": "^5.0.1",
1143
+ "loupe": "^3.1.0",
1144
+ "pathval": "^2.0.0"
1145
+ },
1146
+ "engines": {
1147
+ "node": ">=18"
1148
+ }
1149
+ },
1150
+ "node_modules/check-error": {
1151
+ "version": "2.1.3",
1152
+ "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz",
1153
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
1154
+ "dev": true,
1155
+ "license": "MIT",
1156
+ "engines": {
1157
+ "node": ">= 16"
1158
+ }
1159
+ },
1160
  "node_modules/color-convert": {
1161
  "version": "2.0.1",
1162
  "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
 
1220
  "node": ">= 8"
1221
  }
1222
  },
1223
+ "node_modules/debug": {
1224
+ "version": "4.4.3",
1225
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
1226
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1227
+ "dev": true,
1228
+ "license": "MIT",
1229
+ "dependencies": {
1230
+ "ms": "^2.1.3"
1231
+ },
1232
+ "engines": {
1233
+ "node": ">=6.0"
1234
+ },
1235
+ "peerDependenciesMeta": {
1236
+ "supports-color": {
1237
+ "optional": true
1238
+ }
1239
+ }
1240
+ },
1241
+ "node_modules/deep-eql": {
1242
+ "version": "5.0.2",
1243
+ "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz",
1244
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
1245
+ "dev": true,
1246
+ "license": "MIT",
1247
+ "engines": {
1248
+ "node": ">=6"
1249
+ }
1250
+ },
1251
  "node_modules/eastasianwidth": {
1252
  "version": "0.2.0",
1253
  "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
 
1317
  "dev": true,
1318
  "license": "MIT"
1319
  },
1320
+ "node_modules/es-module-lexer": {
1321
+ "version": "1.7.0",
1322
+ "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
1323
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
1324
+ "dev": true,
1325
+ "license": "MIT"
1326
+ },
1327
  "node_modules/esbuild": {
1328
  "version": "0.27.3",
1329
  "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz",
 
1366
  "@esbuild/win32-x64": "0.27.3"
1367
  }
1368
  },
1369
+ "node_modules/estree-walker": {
1370
+ "version": "3.0.3",
1371
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
1372
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
1373
+ "dev": true,
1374
+ "license": "MIT",
1375
+ "dependencies": {
1376
+ "@types/estree": "^1.0.0"
1377
+ }
1378
+ },
1379
+ "node_modules/expect-type": {
1380
+ "version": "1.3.0",
1381
+ "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz",
1382
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
1383
+ "dev": true,
1384
+ "license": "Apache-2.0",
1385
+ "engines": {
1386
+ "node": ">=12.0.0"
1387
+ }
1388
+ },
1389
+ "node_modules/fdir": {
1390
+ "version": "6.5.0",
1391
+ "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
1392
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1393
+ "dev": true,
1394
+ "license": "MIT",
1395
+ "engines": {
1396
+ "node": ">=12.0.0"
1397
+ },
1398
+ "peerDependencies": {
1399
+ "picomatch": "^3 || ^4"
1400
+ },
1401
+ "peerDependenciesMeta": {
1402
+ "picomatch": {
1403
+ "optional": true
1404
+ }
1405
+ }
1406
+ },
1407
  "node_modules/foreground-child": {
1408
  "version": "3.3.1",
1409
  "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
 
1625
  "node": ">=14"
1626
  }
1627
  },
1628
+ "node_modules/js-tokens": {
1629
+ "version": "9.0.1",
1630
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
1631
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
1632
+ "dev": true,
1633
+ "license": "MIT"
1634
+ },
1635
  "node_modules/js-yaml": {
1636
  "version": "4.1.1",
1637
  "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
 
1655
  "url": "https://liberapay.com/Koromix"
1656
  }
1657
  },
1658
+ "node_modules/loupe": {
1659
+ "version": "3.2.1",
1660
+ "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz",
1661
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
1662
+ "dev": true,
1663
+ "license": "MIT"
1664
+ },
1665
  "node_modules/lru-cache": {
1666
  "version": "10.4.3",
1667
  "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
 
1669
  "dev": true,
1670
  "license": "ISC"
1671
  },
1672
+ "node_modules/magic-string": {
1673
+ "version": "0.30.21",
1674
+ "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
1675
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
1676
+ "dev": true,
1677
+ "license": "MIT",
1678
+ "dependencies": {
1679
+ "@jridgewell/sourcemap-codec": "^1.5.5"
1680
+ }
1681
+ },
1682
  "node_modules/minimatch": {
1683
  "version": "3.1.2",
1684
  "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
 
1702
  "node": ">=16 || 14 >=14.17"
1703
  }
1704
  },
1705
+ "node_modules/ms": {
1706
+ "version": "2.1.3",
1707
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
1708
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1709
+ "dev": true,
1710
+ "license": "MIT"
1711
+ },
1712
+ "node_modules/nanoid": {
1713
+ "version": "3.3.11",
1714
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
1715
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1716
+ "dev": true,
1717
+ "funding": [
1718
+ {
1719
+ "type": "github",
1720
+ "url": "https://github.com/sponsors/ai"
1721
+ }
1722
+ ],
1723
+ "license": "MIT",
1724
+ "bin": {
1725
+ "nanoid": "bin/nanoid.cjs"
1726
+ },
1727
+ "engines": {
1728
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1729
+ }
1730
+ },
1731
  "node_modules/nopt": {
1732
  "version": "7.2.1",
1733
  "resolved": "https://registry.npmmirror.com/nopt/-/nopt-7.2.1.tgz",
 
1798
  "url": "https://github.com/sponsors/isaacs"
1799
  }
1800
  },
1801
+ "node_modules/pathe": {
1802
+ "version": "2.0.3",
1803
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
1804
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
1805
+ "dev": true,
1806
+ "license": "MIT"
1807
+ },
1808
+ "node_modules/pathval": {
1809
+ "version": "2.0.1",
1810
+ "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz",
1811
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
1812
+ "dev": true,
1813
+ "license": "MIT",
1814
+ "engines": {
1815
+ "node": ">= 14.16"
1816
+ }
1817
+ },
1818
+ "node_modules/picocolors": {
1819
+ "version": "1.1.1",
1820
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
1821
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1822
+ "dev": true,
1823
+ "license": "ISC"
1824
+ },
1825
+ "node_modules/picomatch": {
1826
+ "version": "4.0.3",
1827
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
1828
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1829
+ "dev": true,
1830
+ "license": "MIT",
1831
+ "engines": {
1832
+ "node": ">=12"
1833
+ },
1834
+ "funding": {
1835
+ "url": "https://github.com/sponsors/jonschlinkert"
1836
+ }
1837
+ },
1838
+ "node_modules/postcss": {
1839
+ "version": "8.5.6",
1840
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
1841
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1842
+ "dev": true,
1843
+ "funding": [
1844
+ {
1845
+ "type": "opencollective",
1846
+ "url": "https://opencollective.com/postcss/"
1847
+ },
1848
+ {
1849
+ "type": "tidelift",
1850
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1851
+ },
1852
+ {
1853
+ "type": "github",
1854
+ "url": "https://github.com/sponsors/ai"
1855
+ }
1856
+ ],
1857
+ "license": "MIT",
1858
+ "dependencies": {
1859
+ "nanoid": "^3.3.11",
1860
+ "picocolors": "^1.1.1",
1861
+ "source-map-js": "^1.2.1"
1862
+ },
1863
+ "engines": {
1864
+ "node": "^10 || ^12 || >=14"
1865
+ }
1866
+ },
1867
  "node_modules/proto-list": {
1868
  "version": "1.2.4",
1869
  "resolved": "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz",
 
1881
  "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
1882
  }
1883
  },
1884
+ "node_modules/rollup": {
1885
+ "version": "4.59.0",
1886
+ "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz",
1887
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
1888
+ "dev": true,
1889
+ "license": "MIT",
1890
+ "dependencies": {
1891
+ "@types/estree": "1.0.8"
1892
+ },
1893
+ "bin": {
1894
+ "rollup": "dist/bin/rollup"
1895
+ },
1896
+ "engines": {
1897
+ "node": ">=18.0.0",
1898
+ "npm": ">=8.0.0"
1899
+ },
1900
+ "optionalDependencies": {
1901
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
1902
+ "@rollup/rollup-android-arm64": "4.59.0",
1903
+ "@rollup/rollup-darwin-arm64": "4.59.0",
1904
+ "@rollup/rollup-darwin-x64": "4.59.0",
1905
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
1906
+ "@rollup/rollup-freebsd-x64": "4.59.0",
1907
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
1908
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
1909
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
1910
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
1911
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
1912
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
1913
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
1914
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
1915
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
1916
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
1917
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
1918
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
1919
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
1920
+ "@rollup/rollup-openbsd-x64": "4.59.0",
1921
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
1922
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
1923
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
1924
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
1925
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
1926
+ "fsevents": "~2.3.2"
1927
+ }
1928
+ },
1929
  "node_modules/semver": {
1930
  "version": "7.7.4",
1931
  "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
 
1962
  "node": ">=8"
1963
  }
1964
  },
1965
+ "node_modules/siginfo": {
1966
+ "version": "2.0.0",
1967
+ "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz",
1968
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
1969
+ "dev": true,
1970
+ "license": "ISC"
1971
+ },
1972
  "node_modules/signal-exit": {
1973
  "version": "4.1.0",
1974
  "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
 
1982
  "url": "https://github.com/sponsors/isaacs"
1983
  }
1984
  },
1985
+ "node_modules/source-map-js": {
1986
+ "version": "1.2.1",
1987
+ "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
1988
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1989
+ "dev": true,
1990
+ "license": "BSD-3-Clause",
1991
+ "engines": {
1992
+ "node": ">=0.10.0"
1993
+ }
1994
+ },
1995
+ "node_modules/stackback": {
1996
+ "version": "0.0.2",
1997
+ "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz",
1998
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
1999
+ "dev": true,
2000
+ "license": "MIT"
2001
+ },
2002
+ "node_modules/std-env": {
2003
+ "version": "3.10.0",
2004
+ "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz",
2005
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
2006
+ "dev": true,
2007
+ "license": "MIT"
2008
+ },
2009
  "node_modules/string-width": {
2010
  "version": "5.1.2",
2011
  "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
 
2110
  "node": ">=8"
2111
  }
2112
  },
2113
+ "node_modules/strip-literal": {
2114
+ "version": "3.1.0",
2115
+ "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.1.0.tgz",
2116
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
2117
+ "dev": true,
2118
+ "license": "MIT",
2119
+ "dependencies": {
2120
+ "js-tokens": "^9.0.1"
2121
+ },
2122
+ "funding": {
2123
+ "url": "https://github.com/sponsors/antfu"
2124
+ }
2125
+ },
2126
+ "node_modules/tinybench": {
2127
+ "version": "2.9.0",
2128
+ "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
2129
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
2130
+ "dev": true,
2131
+ "license": "MIT"
2132
+ },
2133
+ "node_modules/tinyexec": {
2134
+ "version": "0.3.2",
2135
+ "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz",
2136
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
2137
+ "dev": true,
2138
+ "license": "MIT"
2139
+ },
2140
+ "node_modules/tinyglobby": {
2141
+ "version": "0.2.15",
2142
+ "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
2143
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
2144
+ "dev": true,
2145
+ "license": "MIT",
2146
+ "dependencies": {
2147
+ "fdir": "^6.5.0",
2148
+ "picomatch": "^4.0.3"
2149
+ },
2150
+ "engines": {
2151
+ "node": ">=12.0.0"
2152
+ },
2153
+ "funding": {
2154
+ "url": "https://github.com/sponsors/SuperchupuDev"
2155
+ }
2156
+ },
2157
+ "node_modules/tinypool": {
2158
+ "version": "1.1.1",
2159
+ "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz",
2160
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
2161
+ "dev": true,
2162
+ "license": "MIT",
2163
+ "engines": {
2164
+ "node": "^18.0.0 || >=20.0.0"
2165
+ }
2166
+ },
2167
+ "node_modules/tinyrainbow": {
2168
+ "version": "2.0.0",
2169
+ "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
2170
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
2171
+ "dev": true,
2172
+ "license": "MIT",
2173
+ "engines": {
2174
+ "node": ">=14.0.0"
2175
+ }
2176
+ },
2177
+ "node_modules/tinyspy": {
2178
+ "version": "4.0.4",
2179
+ "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-4.0.4.tgz",
2180
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
2181
+ "dev": true,
2182
+ "license": "MIT",
2183
+ "engines": {
2184
+ "node": ">=14.0.0"
2185
+ }
2186
+ },
2187
  "node_modules/tsx": {
2188
  "version": "4.21.0",
2189
  "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz",
 
2234
  "dev": true,
2235
  "license": "MIT"
2236
  },
2237
+ "node_modules/vite": {
2238
+ "version": "7.3.1",
2239
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz",
2240
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
2241
+ "dev": true,
2242
+ "license": "MIT",
2243
+ "dependencies": {
2244
+ "esbuild": "^0.27.0",
2245
+ "fdir": "^6.5.0",
2246
+ "picomatch": "^4.0.3",
2247
+ "postcss": "^8.5.6",
2248
+ "rollup": "^4.43.0",
2249
+ "tinyglobby": "^0.2.15"
2250
+ },
2251
+ "bin": {
2252
+ "vite": "bin/vite.js"
2253
+ },
2254
+ "engines": {
2255
+ "node": "^20.19.0 || >=22.12.0"
2256
+ },
2257
+ "funding": {
2258
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2259
+ },
2260
+ "optionalDependencies": {
2261
+ "fsevents": "~2.3.3"
2262
+ },
2263
+ "peerDependencies": {
2264
+ "@types/node": "^20.19.0 || >=22.12.0",
2265
+ "jiti": ">=1.21.0",
2266
+ "less": "^4.0.0",
2267
+ "lightningcss": "^1.21.0",
2268
+ "sass": "^1.70.0",
2269
+ "sass-embedded": "^1.70.0",
2270
+ "stylus": ">=0.54.8",
2271
+ "sugarss": "^5.0.0",
2272
+ "terser": "^5.16.0",
2273
+ "tsx": "^4.8.1",
2274
+ "yaml": "^2.4.2"
2275
+ },
2276
+ "peerDependenciesMeta": {
2277
+ "@types/node": {
2278
+ "optional": true
2279
+ },
2280
+ "jiti": {
2281
+ "optional": true
2282
+ },
2283
+ "less": {
2284
+ "optional": true
2285
+ },
2286
+ "lightningcss": {
2287
+ "optional": true
2288
+ },
2289
+ "sass": {
2290
+ "optional": true
2291
+ },
2292
+ "sass-embedded": {
2293
+ "optional": true
2294
+ },
2295
+ "stylus": {
2296
+ "optional": true
2297
+ },
2298
+ "sugarss": {
2299
+ "optional": true
2300
+ },
2301
+ "terser": {
2302
+ "optional": true
2303
+ },
2304
+ "tsx": {
2305
+ "optional": true
2306
+ },
2307
+ "yaml": {
2308
+ "optional": true
2309
+ }
2310
+ }
2311
+ },
2312
+ "node_modules/vite-node": {
2313
+ "version": "3.2.4",
2314
+ "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz",
2315
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
2316
+ "dev": true,
2317
+ "license": "MIT",
2318
+ "dependencies": {
2319
+ "cac": "^6.7.14",
2320
+ "debug": "^4.4.1",
2321
+ "es-module-lexer": "^1.7.0",
2322
+ "pathe": "^2.0.3",
2323
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
2324
+ },
2325
+ "bin": {
2326
+ "vite-node": "vite-node.mjs"
2327
+ },
2328
+ "engines": {
2329
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2330
+ },
2331
+ "funding": {
2332
+ "url": "https://opencollective.com/vitest"
2333
+ }
2334
+ },
2335
+ "node_modules/vitest": {
2336
+ "version": "3.2.4",
2337
+ "resolved": "https://registry.npmmirror.com/vitest/-/vitest-3.2.4.tgz",
2338
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
2339
+ "dev": true,
2340
+ "license": "MIT",
2341
+ "dependencies": {
2342
+ "@types/chai": "^5.2.2",
2343
+ "@vitest/expect": "3.2.4",
2344
+ "@vitest/mocker": "3.2.4",
2345
+ "@vitest/pretty-format": "^3.2.4",
2346
+ "@vitest/runner": "3.2.4",
2347
+ "@vitest/snapshot": "3.2.4",
2348
+ "@vitest/spy": "3.2.4",
2349
+ "@vitest/utils": "3.2.4",
2350
+ "chai": "^5.2.0",
2351
+ "debug": "^4.4.1",
2352
+ "expect-type": "^1.2.1",
2353
+ "magic-string": "^0.30.17",
2354
+ "pathe": "^2.0.3",
2355
+ "picomatch": "^4.0.2",
2356
+ "std-env": "^3.9.0",
2357
+ "tinybench": "^2.9.0",
2358
+ "tinyexec": "^0.3.2",
2359
+ "tinyglobby": "^0.2.14",
2360
+ "tinypool": "^1.1.1",
2361
+ "tinyrainbow": "^2.0.0",
2362
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
2363
+ "vite-node": "3.2.4",
2364
+ "why-is-node-running": "^2.3.0"
2365
+ },
2366
+ "bin": {
2367
+ "vitest": "vitest.mjs"
2368
+ },
2369
+ "engines": {
2370
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2371
+ },
2372
+ "funding": {
2373
+ "url": "https://opencollective.com/vitest"
2374
+ },
2375
+ "peerDependencies": {
2376
+ "@edge-runtime/vm": "*",
2377
+ "@types/debug": "^4.1.12",
2378
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2379
+ "@vitest/browser": "3.2.4",
2380
+ "@vitest/ui": "3.2.4",
2381
+ "happy-dom": "*",
2382
+ "jsdom": "*"
2383
+ },
2384
+ "peerDependenciesMeta": {
2385
+ "@edge-runtime/vm": {
2386
+ "optional": true
2387
+ },
2388
+ "@types/debug": {
2389
+ "optional": true
2390
+ },
2391
+ "@types/node": {
2392
+ "optional": true
2393
+ },
2394
+ "@vitest/browser": {
2395
+ "optional": true
2396
+ },
2397
+ "@vitest/ui": {
2398
+ "optional": true
2399
+ },
2400
+ "happy-dom": {
2401
+ "optional": true
2402
+ },
2403
+ "jsdom": {
2404
+ "optional": true
2405
+ }
2406
+ }
2407
+ },
2408
  "node_modules/which": {
2409
  "version": "2.0.2",
2410
  "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
 
2421
  "node": ">= 8"
2422
  }
2423
  },
2424
+ "node_modules/why-is-node-running": {
2425
+ "version": "2.3.0",
2426
+ "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
2427
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
2428
+ "dev": true,
2429
+ "license": "MIT",
2430
+ "dependencies": {
2431
+ "siginfo": "^2.0.0",
2432
+ "stackback": "0.0.2"
2433
+ },
2434
+ "bin": {
2435
+ "why-is-node-running": "cli.js"
2436
+ },
2437
+ "engines": {
2438
+ "node": ">=8"
2439
+ }
2440
+ },
2441
  "node_modules/wrap-ansi": {
2442
  "version": "8.1.0",
2443
  "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
package.json CHANGED
@@ -4,6 +4,8 @@
4
  "description": "Reverse proxy that exposes Codex Desktop Responses API as OpenAI-compatible /v1/chat/completions",
5
  "type": "module",
6
  "scripts": {
 
 
7
  "dev": "tsx watch src/index.ts",
8
  "dev:web": "cd web && npx vite",
9
  "build:web": "cd web && npx vite build",
@@ -34,6 +36,7 @@
34
  "@types/node": "^22.0.0",
35
  "js-beautify": "^1.15.0",
36
  "tsx": "^4.0.0",
37
- "typescript": "^5.5.0"
 
38
  }
39
  }
 
4
  "description": "Reverse proxy that exposes Codex Desktop Responses API as OpenAI-compatible /v1/chat/completions",
5
  "type": "module",
6
  "scripts": {
7
+ "test": "vitest run",
8
+ "test:watch": "vitest",
9
  "dev": "tsx watch src/index.ts",
10
  "dev:web": "cd web && npx vite",
11
  "build:web": "cd web && npx vite build",
 
36
  "@types/node": "^22.0.0",
37
  "js-beautify": "^1.15.0",
38
  "tsx": "^4.0.0",
39
+ "typescript": "^5.5.0",
40
+ "vitest": "^3.2.4"
41
  }
42
  }
src/auth/__tests__/account-pool.test.ts ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for AccountPool core scheduling logic.
3
+ *
4
+ * Uses vi.mock to stub filesystem and JWT utilities so tests run
5
+ * without actual data files or valid JWT tokens.
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
9
+
10
+ // Mock fs before importing AccountPool
11
+ vi.mock("fs", () => ({
12
+ readFileSync: vi.fn(() => { throw new Error("ENOENT"); }),
13
+ writeFileSync: vi.fn(),
14
+ renameSync: vi.fn(),
15
+ existsSync: vi.fn(() => false),
16
+ mkdirSync: vi.fn(),
17
+ }));
18
+
19
+ // Mock config
20
+ vi.mock("../../config.js", () => ({
21
+ getConfig: vi.fn(() => ({
22
+ auth: {
23
+ jwt_token: null,
24
+ rotation_strategy: "least_used",
25
+ rate_limit_backoff_seconds: 60,
26
+ },
27
+ server: {
28
+ proxy_api_key: null,
29
+ },
30
+ })),
31
+ }));
32
+
33
+ // Mock JWT utilities — all tokens are "valid"
34
+ vi.mock("../jwt-utils.js", () => ({
35
+ decodeJwtPayload: vi.fn(() => ({ exp: Math.floor(Date.now() / 1000) + 3600 })),
36
+ extractChatGptAccountId: vi.fn((token: string) => `acct-${token.slice(0, 8)}`),
37
+ extractUserProfile: vi.fn((token: string) => ({
38
+ email: `${token.slice(0, 4)}@test.com`,
39
+ chatgpt_plan_type: "free",
40
+ })),
41
+ isTokenExpired: vi.fn(() => false),
42
+ }));
43
+
44
+ // Mock jitter to return the exact value (no randomness in tests)
45
+ vi.mock("../../utils/jitter.js", () => ({
46
+ jitter: vi.fn((val: number) => val),
47
+ }));
48
+
49
+ import { AccountPool } from "../account-pool.js";
50
+ import { getConfig } from "../../config.js";
51
+ import { isTokenExpired } from "../jwt-utils.js";
52
+
53
+ describe("AccountPool", () => {
54
+ let pool: AccountPool;
55
+
56
+ beforeEach(() => {
57
+ // Reset mock implementations to defaults (clearAllMocks only clears call history)
58
+ vi.mocked(isTokenExpired).mockReturnValue(false);
59
+ vi.mocked(getConfig).mockReturnValue({
60
+ auth: {
61
+ jwt_token: null,
62
+ rotation_strategy: "least_used",
63
+ rate_limit_backoff_seconds: 60,
64
+ },
65
+ server: { proxy_api_key: null },
66
+ } as ReturnType<typeof getConfig>);
67
+ pool = new AccountPool();
68
+ });
69
+
70
+ afterEach(() => {
71
+ pool.destroy();
72
+ });
73
+
74
+ describe("addAccount + acquire", () => {
75
+ it("adds an account and acquires it", () => {
76
+ pool.addAccount("token-aaa");
77
+
78
+ const acquired = pool.acquire();
79
+ expect(acquired).not.toBeNull();
80
+ expect(acquired!.token).toBe("token-aaa");
81
+ });
82
+
83
+ it("deduplicates by accountId", () => {
84
+ const id1 = pool.addAccount("token-aaa");
85
+ const id2 = pool.addAccount("token-aaa"); // same prefix → same accountId
86
+
87
+ expect(id1).toBe(id2);
88
+ });
89
+
90
+ it("returns null when no accounts exist", () => {
91
+ expect(pool.acquire()).toBeNull();
92
+ });
93
+ });
94
+
95
+ describe("least_used rotation", () => {
96
+ it("selects the account with lowest request_count", () => {
97
+ pool.addAccount("token-aaa");
98
+ pool.addAccount("token-bbb");
99
+
100
+ // Use account A once
101
+ const first = pool.acquire()!;
102
+ pool.release(first.entryId, { input_tokens: 10, output_tokens: 5 });
103
+
104
+ // Next acquire should pick the other account (0 requests)
105
+ const second = pool.acquire()!;
106
+ expect(second.entryId).not.toBe(first.entryId);
107
+ });
108
+ });
109
+
110
+ describe("round_robin rotation", () => {
111
+ it("cycles through accounts in order", () => {
112
+ vi.mocked(getConfig).mockReturnValue({
113
+ auth: {
114
+ jwt_token: null,
115
+ rotation_strategy: "round_robin",
116
+ rate_limit_backoff_seconds: 60,
117
+ },
118
+ server: { proxy_api_key: null },
119
+ } as ReturnType<typeof getConfig>);
120
+
121
+ // Create a fresh pool with round_robin config
122
+ const rrPool = new AccountPool();
123
+ rrPool.addAccount("token-aaa");
124
+ rrPool.addAccount("token-bbb");
125
+
126
+ const a1 = rrPool.acquire()!;
127
+ rrPool.release(a1.entryId);
128
+
129
+ const a2 = rrPool.acquire()!;
130
+ rrPool.release(a2.entryId);
131
+
132
+ const a3 = rrPool.acquire()!;
133
+ rrPool.release(a3.entryId);
134
+
135
+ // a3 should wrap around to same as a1
136
+ expect(a3.entryId).toBe(a1.entryId);
137
+ expect(a1.entryId).not.toBe(a2.entryId);
138
+
139
+ rrPool.destroy();
140
+ });
141
+ });
142
+
143
+ describe("release", () => {
144
+ it("increments request_count and token usage", () => {
145
+ pool.addAccount("token-aaa");
146
+
147
+ const acquired = pool.acquire()!;
148
+ pool.release(acquired.entryId, { input_tokens: 100, output_tokens: 50 });
149
+
150
+ const accounts = pool.getAccounts();
151
+ expect(accounts[0].usage.request_count).toBe(1);
152
+ expect(accounts[0].usage.input_tokens).toBe(100);
153
+ expect(accounts[0].usage.output_tokens).toBe(50);
154
+ expect(accounts[0].usage.last_used).not.toBeNull();
155
+ });
156
+
157
+ it("unlocks account after release", () => {
158
+ pool.addAccount("token-aaa");
159
+
160
+ const a1 = pool.acquire()!;
161
+ // While locked, acquire returns null (only 1 account)
162
+ expect(pool.acquire()).toBeNull();
163
+
164
+ pool.release(a1.entryId);
165
+ // After release, can acquire again
166
+ expect(pool.acquire()).not.toBeNull();
167
+ });
168
+ });
169
+
170
+ describe("markRateLimited", () => {
171
+ it("marks account as rate_limited and skips it in acquire", () => {
172
+ pool.addAccount("token-aaa");
173
+ pool.addAccount("token-bbb");
174
+
175
+ const first = pool.acquire()!;
176
+ pool.markRateLimited(first.entryId);
177
+
178
+ // Pool summary should show 1 rate_limited
179
+ const summary = pool.getPoolSummary();
180
+ expect(summary.rate_limited).toBe(1);
181
+ expect(summary.active).toBe(1);
182
+
183
+ // Next acquire should skip the rate-limited account
184
+ const second = pool.acquire()!;
185
+ expect(second.entryId).not.toBe(first.entryId);
186
+ });
187
+
188
+ it("countRequest option increments usage on 429", () => {
189
+ pool.addAccount("token-aaa");
190
+
191
+ const acquired = pool.acquire()!;
192
+ pool.markRateLimited(acquired.entryId, { countRequest: true });
193
+
194
+ const accounts = pool.getAccounts();
195
+ expect(accounts[0].usage.request_count).toBe(1);
196
+ });
197
+
198
+ it("auto-recovers after rate_limit_until passes", () => {
199
+ pool.addAccount("token-aaa");
200
+
201
+ const acquired = pool.acquire()!;
202
+ // Set rate limit to already expired
203
+ pool.markRateLimited(acquired.entryId, { retryAfterSec: -1 });
204
+
205
+ // refreshStatus should detect the expired rate_limit_until
206
+ const summary = pool.getPoolSummary();
207
+ expect(summary.active).toBe(1);
208
+ expect(summary.rate_limited).toBe(0);
209
+ });
210
+ });
211
+
212
+ describe("stale lock auto-release", () => {
213
+ it("releases locks older than 5 minutes", () => {
214
+ pool.addAccount("token-aaa");
215
+
216
+ const acquired = pool.acquire()!;
217
+
218
+ // Manually backdate the lock by manipulating the acquireLocks map
219
+ // Access private field for testing — unavoidable for TTL tests
220
+ const locks = (pool as unknown as { acquireLocks: Map<string, number> }).acquireLocks;
221
+ locks.set(acquired.entryId, Date.now() - 6 * 60 * 1000); // 6 minutes ago
222
+
223
+ // Next acquire should auto-release the stale lock and return the same account
224
+ const reacquired = pool.acquire()!;
225
+ expect(reacquired).not.toBeNull();
226
+ expect(reacquired.entryId).toBe(acquired.entryId);
227
+ });
228
+ });
229
+
230
+ describe("expired tokens", () => {
231
+ it("skips expired accounts in acquire", () => {
232
+ vi.mocked(isTokenExpired).mockReturnValue(true);
233
+ pool.addAccount("token-expired");
234
+
235
+ expect(pool.acquire()).toBeNull();
236
+ expect(pool.getPoolSummary().expired).toBe(1);
237
+ });
238
+ });
239
+
240
+ describe("removeAccount", () => {
241
+ it("removes an account and clears its lock", () => {
242
+ pool.addAccount("token-aaa");
243
+
244
+ const acquired = pool.acquire()!;
245
+ pool.removeAccount(acquired.entryId);
246
+
247
+ expect(pool.getPoolSummary().total).toBe(0);
248
+ expect(pool.acquire()).toBeNull();
249
+ });
250
+ });
251
+
252
+ describe("resetUsage", () => {
253
+ it("resets counters to zero", () => {
254
+ pool.addAccount("token-aaa");
255
+
256
+ const acquired = pool.acquire()!;
257
+ pool.release(acquired.entryId, { input_tokens: 100, output_tokens: 50 });
258
+ pool.resetUsage(acquired.entryId);
259
+
260
+ const accounts = pool.getAccounts();
261
+ expect(accounts[0].usage.request_count).toBe(0);
262
+ expect(accounts[0].usage.input_tokens).toBe(0);
263
+ expect(accounts[0].usage.output_tokens).toBe(0);
264
+ });
265
+ });
266
+
267
+ describe("validateProxyApiKey", () => {
268
+ it("validates per-account proxy API key", () => {
269
+ pool.addAccount("token-aaa");
270
+
271
+ const accounts = pool.getAccounts();
272
+ // Each account gets a generated proxyApiKey — we can't predict it,
273
+ // but we can read it and validate
274
+ const entry = pool.getEntry(accounts[0].id)!;
275
+ expect(pool.validateProxyApiKey(entry.proxyApiKey)).toBe(true);
276
+ expect(pool.validateProxyApiKey("wrong-key")).toBe(false);
277
+ });
278
+
279
+ it("validates config-level proxy API key", () => {
280
+ vi.mocked(getConfig).mockReturnValue({
281
+ auth: {
282
+ jwt_token: null,
283
+ rotation_strategy: "least_used",
284
+ rate_limit_backoff_seconds: 60,
285
+ },
286
+ server: { proxy_api_key: "global-key-123" },
287
+ } as ReturnType<typeof getConfig>);
288
+
289
+ expect(pool.validateProxyApiKey("global-key-123")).toBe(true);
290
+ });
291
+ });
292
+ });
src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { loadConfig, loadFingerprint, getConfig } from "./config.js";
4
  import { AccountPool } from "./auth/account-pool.js";
5
  import { RefreshScheduler } from "./auth/refresh-scheduler.js";
6
  import { SessionManager } from "./session/manager.js";
 
7
  import { logger } from "./middleware/logger.js";
8
  import { errorHandler } from "./middleware/error-handler.js";
9
  import { createAuthRoutes } from "./routes/auth.js";
@@ -48,6 +49,7 @@ async function main() {
48
  const app = new Hono();
49
 
50
  // Global middleware
 
51
  app.use("*", logger);
52
  app.use("*", errorHandler);
53
 
 
4
  import { AccountPool } from "./auth/account-pool.js";
5
  import { RefreshScheduler } from "./auth/refresh-scheduler.js";
6
  import { SessionManager } from "./session/manager.js";
7
+ import { requestId } from "./middleware/request-id.js";
8
  import { logger } from "./middleware/logger.js";
9
  import { errorHandler } from "./middleware/error-handler.js";
10
  import { createAuthRoutes } from "./routes/auth.js";
 
49
  const app = new Hono();
50
 
51
  // Global middleware
52
+ app.use("*", requestId);
53
  app.use("*", logger);
54
  app.use("*", errorHandler);
55
 
src/proxy/__tests__/codex-api.test.ts ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for CodexApi SSE parsing.
3
+ *
4
+ * parseStream() is the most fragile code path — it processes real-time
5
+ * byte streams from curl where chunks can split at any boundary.
6
+ */
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import { CodexApi, type CodexSSEEvent } from "../codex-api.js";
10
+
11
+ /** Create a Response whose body emits the given string chunks sequentially. */
12
+ function mockResponse(...chunks: string[]): Response {
13
+ const encoder = new TextEncoder();
14
+ let idx = 0;
15
+ const stream = new ReadableStream<Uint8Array>({
16
+ pull(controller) {
17
+ if (idx < chunks.length) {
18
+ controller.enqueue(encoder.encode(chunks[idx]));
19
+ idx++;
20
+ } else {
21
+ controller.close();
22
+ }
23
+ },
24
+ });
25
+ return new Response(stream);
26
+ }
27
+
28
+ /** Collect all events from parseStream into an array. */
29
+ async function collectEvents(api: CodexApi, response: Response): Promise<CodexSSEEvent[]> {
30
+ const events: CodexSSEEvent[] = [];
31
+ for await (const evt of api.parseStream(response)) {
32
+ events.push(evt);
33
+ }
34
+ return events;
35
+ }
36
+
37
+ // CodexApi constructor requires a token — value is irrelevant for parsing tests
38
+ function createApi(): CodexApi {
39
+ return new CodexApi("test-token", null);
40
+ }
41
+
42
+ describe("CodexApi.parseStream", () => {
43
+ it("parses a complete SSE event in a single chunk", async () => {
44
+ const api = createApi();
45
+ const response = mockResponse(
46
+ 'event: response.output_text.delta\ndata: {"delta":"Hello"}\n\n',
47
+ );
48
+
49
+ const events = await collectEvents(api, response);
50
+ expect(events).toHaveLength(1);
51
+ expect(events[0].event).toBe("response.output_text.delta");
52
+ expect(events[0].data).toEqual({ delta: "Hello" });
53
+ });
54
+
55
+ it("handles multiple events in a single chunk", async () => {
56
+ const api = createApi();
57
+ const response = mockResponse(
58
+ 'event: response.created\ndata: {"response":{"id":"resp_1"}}\n\n' +
59
+ 'event: response.output_text.delta\ndata: {"delta":"Hi"}\n\n' +
60
+ 'event: response.completed\ndata: {"response":{"id":"resp_1","usage":{"input_tokens":10,"output_tokens":5}}}\n\n',
61
+ );
62
+
63
+ const events = await collectEvents(api, response);
64
+ expect(events).toHaveLength(3);
65
+ expect(events[0].event).toBe("response.created");
66
+ expect(events[1].event).toBe("response.output_text.delta");
67
+ expect(events[2].event).toBe("response.completed");
68
+ });
69
+
70
+ it("reassembles events split across chunk boundaries", async () => {
71
+ const api = createApi();
72
+ // Split in the middle of the JSON data
73
+ const response = mockResponse(
74
+ 'event: response.output_text.delta\ndata: {"del',
75
+ 'ta":"world"}\n\n',
76
+ );
77
+
78
+ const events = await collectEvents(api, response);
79
+ expect(events).toHaveLength(1);
80
+ expect(events[0].data).toEqual({ delta: "world" });
81
+ });
82
+
83
+ it("handles chunk split at \\n\\n boundary", async () => {
84
+ const api = createApi();
85
+ // First chunk ends with first \n, second starts with second \n
86
+ const response = mockResponse(
87
+ 'event: response.output_text.delta\ndata: {"delta":"a"}\n',
88
+ '\nevent: response.output_text.delta\ndata: {"delta":"b"}\n\n',
89
+ );
90
+
91
+ const events = await collectEvents(api, response);
92
+ expect(events).toHaveLength(2);
93
+ expect(events[0].data).toEqual({ delta: "a" });
94
+ expect(events[1].data).toEqual({ delta: "b" });
95
+ });
96
+
97
+ it("handles many small single-character chunks", async () => {
98
+ const api = createApi();
99
+ const full = 'event: response.output_text.delta\ndata: {"delta":"x"}\n\n';
100
+ // Split into individual characters
101
+ const chunks = full.split("");
102
+ const response = mockResponse(...chunks);
103
+
104
+ const events = await collectEvents(api, response);
105
+ expect(events).toHaveLength(1);
106
+ expect(events[0].data).toEqual({ delta: "x" });
107
+ });
108
+
109
+ it("skips [DONE] marker without crashing", async () => {
110
+ const api = createApi();
111
+ const response = mockResponse(
112
+ 'event: response.output_text.delta\ndata: {"delta":"hi"}\n\n' +
113
+ "data: [DONE]\n\n",
114
+ );
115
+
116
+ const events = await collectEvents(api, response);
117
+ expect(events).toHaveLength(1);
118
+ expect(events[0].data).toEqual({ delta: "hi" });
119
+ });
120
+
121
+ it("returns raw string when data is not valid JSON", async () => {
122
+ const api = createApi();
123
+ const response = mockResponse(
124
+ 'event: response.output_text.delta\ndata: not-json-at-all\n\n',
125
+ );
126
+
127
+ const events = await collectEvents(api, response);
128
+ expect(events).toHaveLength(1);
129
+ expect(events[0].data).toBe("not-json-at-all");
130
+ });
131
+
132
+ it("handles malformed JSON (unclosed brace) gracefully", async () => {
133
+ const api = createApi();
134
+ const response = mockResponse(
135
+ 'event: response.output_text.delta\ndata: {"delta":"unclosed\n\n',
136
+ );
137
+
138
+ const events = await collectEvents(api, response);
139
+ expect(events).toHaveLength(1);
140
+ // Should not throw — falls through to raw string
141
+ expect(typeof events[0].data).toBe("string");
142
+ });
143
+
144
+ it("skips empty blocks between events", async () => {
145
+ const api = createApi();
146
+ const response = mockResponse(
147
+ 'event: response.output_text.delta\ndata: {"delta":"a"}\n\n' +
148
+ "\n\n" + // empty block
149
+ 'event: response.output_text.delta\ndata: {"delta":"b"}\n\n',
150
+ );
151
+
152
+ const events = await collectEvents(api, response);
153
+ expect(events).toHaveLength(2);
154
+ });
155
+
156
+ it("processes remaining buffer after stream ends", async () => {
157
+ const api = createApi();
158
+ // No trailing \n\n — the event is only in the residual buffer
159
+ const response = mockResponse(
160
+ 'event: response.output_text.delta\ndata: {"delta":"last"}',
161
+ );
162
+
163
+ const events = await collectEvents(api, response);
164
+ expect(events).toHaveLength(1);
165
+ expect(events[0].data).toEqual({ delta: "last" });
166
+ });
167
+
168
+ it("handles multi-line data fields", async () => {
169
+ const api = createApi();
170
+ const response = mockResponse(
171
+ 'event: response.output_text.delta\ndata: {"delta":\n' +
172
+ 'data: "multi-line"}\n\n',
173
+ );
174
+
175
+ const events = await collectEvents(api, response);
176
+ expect(events).toHaveLength(1);
177
+ // data lines are joined with \n: '{"delta":\n"multi-line"}'
178
+ expect(events[0].data).toEqual({ delta: "multi-line" });
179
+ });
180
+
181
+ it("returns null body error", async () => {
182
+ const api = createApi();
183
+ // Create a response with null body
184
+ const response = new Response(null);
185
+
186
+ await expect(async () => {
187
+ await collectEvents(api, response);
188
+ }).rejects.toThrow("Response body is null");
189
+ });
190
+
191
+ it("throws on buffer overflow (>10MB)", async () => {
192
+ const api = createApi();
193
+ // Create a chunk that exceeds the 10MB SSE buffer limit
194
+ const hugeData = "x".repeat(11 * 1024 * 1024);
195
+ const response = mockResponse(hugeData);
196
+
197
+ await expect(async () => {
198
+ await collectEvents(api, response);
199
+ }).rejects.toThrow("SSE buffer exceeded");
200
+ });
201
+ });
src/routes/web.ts CHANGED
@@ -4,7 +4,6 @@ import { readFileSync, existsSync } from "fs";
4
  import { resolve } from "path";
5
  import type { AccountPool } from "../auth/account-pool.js";
6
  import { getConfig, getFingerprint } from "../config.js";
7
- import { getUpdateState } from "../update-checker.js";
8
 
9
  export function createWebRoutes(accountPool: AccountPool): Hono {
10
  const app = new Hono();
@@ -27,14 +26,11 @@ export function createWebRoutes(accountPool: AccountPool): Hono {
27
 
28
  app.get("/health", async (c) => {
29
  const authenticated = accountPool.isAuthenticated();
30
- const userInfo = accountPool.getUserInfo();
31
  const poolSummary = accountPool.getPoolSummary();
32
  return c.json({
33
  status: "ok",
34
  authenticated,
35
- user: authenticated ? userInfo : null,
36
- pool: poolSummary,
37
- update: getUpdateState(),
38
  timestamp: new Date().toISOString(),
39
  });
40
  });
 
4
  import { resolve } from "path";
5
  import type { AccountPool } from "../auth/account-pool.js";
6
  import { getConfig, getFingerprint } from "../config.js";
 
7
 
8
  export function createWebRoutes(accountPool: AccountPool): Hono {
9
  const app = new Hono();
 
26
 
27
  app.get("/health", async (c) => {
28
  const authenticated = accountPool.isAuthenticated();
 
29
  const poolSummary = accountPool.getPoolSummary();
30
  return c.json({
31
  status: "ok",
32
  authenticated,
33
+ pool: { total: poolSummary.total, active: poolSummary.active },
 
 
34
  timestamp: new Date().toISOString(),
35
  });
36
  });
src/translation/__tests__/codex-event-extractor.test.ts ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for parseCodexEvent — the shared event type parser.
3
+ *
4
+ * Tests the discriminated union mapping from raw SSE events to typed events.
5
+ * This is the IR hub of the hub-and-spoke translation architecture.
6
+ */
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import { parseCodexEvent } from "../../types/codex-events.js";
10
+ import type { CodexSSEEvent } from "../../proxy/codex-api.js";
11
+
12
+ describe("parseCodexEvent", () => {
13
+ it("parses response.created with id", () => {
14
+ const raw: CodexSSEEvent = {
15
+ event: "response.created",
16
+ data: { response: { id: "resp_abc123" } },
17
+ };
18
+ const typed = parseCodexEvent(raw);
19
+ expect(typed.type).toBe("response.created");
20
+ if (typed.type === "response.created") {
21
+ expect(typed.response.id).toBe("resp_abc123");
22
+ }
23
+ });
24
+
25
+ it("parses response.in_progress", () => {
26
+ const raw: CodexSSEEvent = {
27
+ event: "response.in_progress",
28
+ data: { response: { id: "resp_abc123" } },
29
+ };
30
+ const typed = parseCodexEvent(raw);
31
+ expect(typed.type).toBe("response.in_progress");
32
+ if (typed.type === "response.in_progress") {
33
+ expect(typed.response.id).toBe("resp_abc123");
34
+ }
35
+ });
36
+
37
+ it("parses response.output_text.delta", () => {
38
+ const raw: CodexSSEEvent = {
39
+ event: "response.output_text.delta",
40
+ data: { delta: "Hello, world!" },
41
+ };
42
+ const typed = parseCodexEvent(raw);
43
+ expect(typed.type).toBe("response.output_text.delta");
44
+ if (typed.type === "response.output_text.delta") {
45
+ expect(typed.delta).toBe("Hello, world!");
46
+ }
47
+ });
48
+
49
+ it("parses response.output_text.done", () => {
50
+ const raw: CodexSSEEvent = {
51
+ event: "response.output_text.done",
52
+ data: { text: "Complete response text" },
53
+ };
54
+ const typed = parseCodexEvent(raw);
55
+ expect(typed.type).toBe("response.output_text.done");
56
+ if (typed.type === "response.output_text.done") {
57
+ expect(typed.text).toBe("Complete response text");
58
+ }
59
+ });
60
+
61
+ it("parses response.completed with usage", () => {
62
+ const raw: CodexSSEEvent = {
63
+ event: "response.completed",
64
+ data: {
65
+ response: {
66
+ id: "resp_abc123",
67
+ usage: { input_tokens: 150, output_tokens: 42 },
68
+ },
69
+ },
70
+ };
71
+ const typed = parseCodexEvent(raw);
72
+ expect(typed.type).toBe("response.completed");
73
+ if (typed.type === "response.completed") {
74
+ expect(typed.response.id).toBe("resp_abc123");
75
+ expect(typed.response.usage).toEqual({
76
+ input_tokens: 150,
77
+ output_tokens: 42,
78
+ });
79
+ }
80
+ });
81
+
82
+ it("parses response.completed without usage", () => {
83
+ const raw: CodexSSEEvent = {
84
+ event: "response.completed",
85
+ data: { response: { id: "resp_abc123" } },
86
+ };
87
+ const typed = parseCodexEvent(raw);
88
+ expect(typed.type).toBe("response.completed");
89
+ if (typed.type === "response.completed") {
90
+ expect(typed.response.usage).toBeUndefined();
91
+ }
92
+ });
93
+
94
+ it("returns unknown for unrecognized event types", () => {
95
+ const raw: CodexSSEEvent = {
96
+ event: "response.some_future_event",
97
+ data: { foo: "bar" },
98
+ };
99
+ const typed = parseCodexEvent(raw);
100
+ expect(typed.type).toBe("unknown");
101
+ if (typed.type === "unknown") {
102
+ expect(typed.raw).toEqual({ foo: "bar" });
103
+ }
104
+ });
105
+
106
+ it("returns unknown when response.created has no response object", () => {
107
+ const raw: CodexSSEEvent = {
108
+ event: "response.created",
109
+ data: "not an object",
110
+ };
111
+ const typed = parseCodexEvent(raw);
112
+ expect(typed.type).toBe("unknown");
113
+ });
114
+
115
+ it("returns unknown when response.created data has no response field", () => {
116
+ const raw: CodexSSEEvent = {
117
+ event: "response.created",
118
+ data: { something_else: true },
119
+ };
120
+ const typed = parseCodexEvent(raw);
121
+ expect(typed.type).toBe("unknown");
122
+ });
123
+
124
+ it("returns unknown when delta is not a string", () => {
125
+ const raw: CodexSSEEvent = {
126
+ event: "response.output_text.delta",
127
+ data: { delta: 123 },
128
+ };
129
+ const typed = parseCodexEvent(raw);
130
+ expect(typed.type).toBe("unknown");
131
+ });
132
+
133
+ it("handles empty delta string", () => {
134
+ const raw: CodexSSEEvent = {
135
+ event: "response.output_text.delta",
136
+ data: { delta: "" },
137
+ };
138
+ const typed = parseCodexEvent(raw);
139
+ expect(typed.type).toBe("response.output_text.delta");
140
+ if (typed.type === "response.output_text.delta") {
141
+ expect(typed.delta).toBe("");
142
+ }
143
+ });
144
+
145
+ it("defaults usage token counts to 0 for non-numeric values", () => {
146
+ const raw: CodexSSEEvent = {
147
+ event: "response.completed",
148
+ data: {
149
+ response: {
150
+ id: "resp_1",
151
+ usage: { input_tokens: "not a number", output_tokens: null },
152
+ },
153
+ },
154
+ };
155
+ const typed = parseCodexEvent(raw);
156
+ expect(typed.type).toBe("response.completed");
157
+ if (typed.type === "response.completed") {
158
+ expect(typed.response.usage).toEqual({
159
+ input_tokens: 0,
160
+ output_tokens: 0,
161
+ });
162
+ }
163
+ });
164
+
165
+ it("handles null data", () => {
166
+ const raw: CodexSSEEvent = {
167
+ event: "response.created",
168
+ data: null,
169
+ };
170
+ const typed = parseCodexEvent(raw);
171
+ expect(typed.type).toBe("unknown");
172
+ });
173
+
174
+ it("handles array data", () => {
175
+ const raw: CodexSSEEvent = {
176
+ event: "response.output_text.delta",
177
+ data: [1, 2, 3],
178
+ };
179
+ const typed = parseCodexEvent(raw);
180
+ expect(typed.type).toBe("unknown");
181
+ });
182
+ });
vitest.config.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ root: "src",
6
+ environment: "node",
7
+ },
8
+ });