@iislee/opencodex 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Application Default Credentials (ADC) resolution for Vertex AI.
3
+ *
4
+ * Direct WebCrypto + REST implementation (no `google-auth-library`). Sources, in priority order:
5
+ * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT
6
+ * exchange) or `type: "authorized_user"` (refresh-token exchange).
7
+ * 2. gcloud user ADC (`CLOUDSDK_CONFIG`, else `%APPDATA%\gcloud` on Windows, else
8
+ * `~/.config/gcloud`) `application_default_credentials.json` (authorized_user flow).
9
+ * 3. GCE / Cloud Run metadata server.
10
+ *
11
+ * Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` (default 60s)
12
+ * before expiry. Concurrent callers waiting on a refresh share one in-flight promise.
13
+ *
14
+ * Security: never logs the access token, private key, or refresh token.
15
+ */
16
+
17
+ import { Buffer } from "node:buffer";
18
+ import * as os from "node:os";
19
+ import * as path from "node:path";
20
+ import { readFileSync, existsSync, statSync } from "node:fs";
21
+ import {
22
+ captureConfigGeneration,
23
+ sweepExpiredOnWrite,
24
+ type GenerationContext,
25
+ } from "./state-store-sweeper";
26
+
27
+ /** Injectable fetch (tests pass a mock); defaults to the global fetch. */
28
+ export type FetchImpl = typeof fetch;
29
+
30
+ const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
31
+ const METADATA_TOKEN_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
32
+ const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
33
+ const JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
34
+
35
+ const TOKEN_TIMEOUT_MS = 15_000;
36
+ const TOKEN_ATTEMPTS = 3;
37
+ const TOKEN_RETRY_BASE_MS = 300;
38
+
39
+ interface CachedToken {
40
+ token: string;
41
+ expiresAtMs: number;
42
+ }
43
+
44
+ interface ServiceAccountCredentials {
45
+ type: "service_account";
46
+ client_email: string;
47
+ private_key: string;
48
+ private_key_id?: string;
49
+ }
50
+
51
+ interface AuthorizedUserCredentials {
52
+ type: "authorized_user";
53
+ client_id: string;
54
+ client_secret: string;
55
+ refresh_token: string;
56
+ }
57
+
58
+ type AdcFileCredentials = ServiceAccountCredentials | AuthorizedUserCredentials;
59
+
60
+ interface TokenResponse {
61
+ access_token: string;
62
+ expires_in: number;
63
+ token_type?: string;
64
+ }
65
+
66
+ const tokenCache = new Map<string, CachedToken>();
67
+ const inflight = new Map<string, Promise<string>>();
68
+ let lastReconciledGeneration = 0;
69
+ let liveSources = new Set<string>();
70
+
71
+ export function sweepExpiredGcpAdcTokens(now = Date.now()): number {
72
+ const skew = getRefreshSkewMs();
73
+ let removed = 0;
74
+ for (const [source, cached] of tokenCache) {
75
+ if (cached.expiresAtMs - skew > now) continue;
76
+ tokenCache.delete(source);
77
+ removed += 1;
78
+ }
79
+ return removed;
80
+ }
81
+
82
+ export function reconcileGcpAdcTokens(context: GenerationContext): number {
83
+ if (context.generation <= lastReconciledGeneration) return 0;
84
+ const currentSource = currentAdcSourceKey();
85
+ let removed = 0;
86
+ for (const source of tokenCache.keys()) {
87
+ if (source === currentSource) continue;
88
+ tokenCache.delete(source);
89
+ removed += 1;
90
+ }
91
+ liveSources = new Set([currentSource]);
92
+ lastReconciledGeneration = context.generation;
93
+ return removed;
94
+ }
95
+
96
+ function getRefreshSkewMs(): number {
97
+ const raw = Number(process.env.GOOGLE_VERTEX_REFRESH_SKEW_MS);
98
+ return Number.isFinite(raw) && raw > 0 ? raw : 60_000;
99
+ }
100
+
101
+ /** gcloud config dir, matching the SDK's own resolution order. */
102
+ function gcloudConfigDir(): string {
103
+ const explicit = process.env.CLOUDSDK_CONFIG?.trim();
104
+ if (explicit) return explicit;
105
+ if (process.platform === "win32" && process.env.APPDATA) {
106
+ return path.join(process.env.APPDATA, "gcloud");
107
+ }
108
+ return path.join(os.homedir(), ".config", "gcloud");
109
+ }
110
+
111
+ function userAdcPath(): string {
112
+ return path.join(gcloudConfigDir(), "application_default_credentials.json");
113
+ }
114
+
115
+ /**
116
+ * A content-freshness fingerprint for a credential file, so an in-place rewrite (e.g.
117
+ * `gcloud auth application-default login`, or a rotated service-account key at the same path)
118
+ * changes the cache key and invalidates the stale token. Falls back to the bare path when stat fails.
119
+ */
120
+ function fileSourceTag(prefix: string, filePath: string): string {
121
+ try {
122
+ const st = statSync(filePath);
123
+ return `${prefix}:${filePath}:${st.size}:${Math.floor(st.mtimeMs)}`;
124
+ } catch {
125
+ return `${prefix}:${filePath}`;
126
+ }
127
+ }
128
+
129
+ function readJsonFile<T>(filePath: string): T | undefined {
130
+ if (!existsSync(filePath)) return undefined;
131
+ return JSON.parse(readFileSync(filePath, "utf8")) as T;
132
+ }
133
+
134
+ function loadAdcCredentials(): { source: string; creds: AdcFileCredentials } | undefined {
135
+ const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
136
+ if (gacPath) {
137
+ const creds = readJsonFile<AdcFileCredentials>(gacPath);
138
+ if (!creds) throw new Error(`GOOGLE_APPLICATION_CREDENTIALS points to a missing file: ${gacPath}`);
139
+ return { source: fileSourceTag("gac", gacPath), creds };
140
+ }
141
+ const userPath = userAdcPath();
142
+ const creds = readJsonFile<AdcFileCredentials>(userPath);
143
+ if (creds) return { source: fileSourceTag("user", userPath), creds };
144
+ return undefined;
145
+ }
146
+
147
+ /**
148
+ * The cache key for the source the NEXT resolve would use, computed cheaply (no network). Lets the
149
+ * cache return a token only when it still matches the active credential source, so an in-process
150
+ * change to GOOGLE_APPLICATION_CREDENTIALS (or the user ADC file) does not keep serving a stale
151
+ * token from a different source. Falls back to "metadata" when no file/env ADC is present.
152
+ */
153
+ function currentAdcSourceKey(): string {
154
+ const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
155
+ if (gacPath) return fileSourceTag("gac", gacPath);
156
+ const userPath = userAdcPath();
157
+ if (existsSync(userPath)) return fileSourceTag("user", userPath);
158
+ return "metadata";
159
+ }
160
+
161
+ function base64UrlEncode(bytes: Uint8Array | string): string {
162
+ const buf = typeof bytes === "string" ? Buffer.from(bytes, "utf8") : Buffer.from(bytes);
163
+ return buf.toString("base64url");
164
+ }
165
+
166
+ function pemToPkcs8(pem: string): ArrayBuffer {
167
+ const body = pem
168
+ .replace(/-----BEGIN [^-]+-----/g, "")
169
+ .replace(/-----END [^-]+-----/g, "")
170
+ .replace(/\s+/g, "");
171
+ if (!body) throw new Error("Invalid PEM: empty body");
172
+ const buf = Buffer.from(body, "base64");
173
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
174
+ }
175
+
176
+ async function signJwtRs256(claims: Record<string, unknown>, privateKeyPem: string, keyId?: string): Promise<string> {
177
+ const header: Record<string, unknown> = { alg: "RS256", typ: "JWT" };
178
+ if (keyId) header.kid = keyId;
179
+ const payload = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(claims))}`;
180
+ const key = await globalThis.crypto.subtle.importKey(
181
+ "pkcs8",
182
+ pemToPkcs8(privateKeyPem),
183
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
184
+ false,
185
+ ["sign"],
186
+ );
187
+ const signature = new Uint8Array(
188
+ await globalThis.crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(payload)),
189
+ );
190
+ return `${payload}.${base64UrlEncode(signature)}`;
191
+ }
192
+
193
+ function tokenRetryDelayMs(attempt: number): number {
194
+ const exp = TOKEN_RETRY_BASE_MS * 2 ** attempt;
195
+ return Math.floor(exp * (0.8 + Math.random() * 0.4));
196
+ }
197
+
198
+ function isRetryableTokenStatus(status: number): boolean {
199
+ return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
200
+ }
201
+
202
+ function tokenTimeoutSignal(parent: AbortSignal | undefined): AbortSignal {
203
+ const timeout = AbortSignal.timeout(TOKEN_TIMEOUT_MS);
204
+ return parent ? AbortSignal.any([parent, timeout]) : timeout;
205
+ }
206
+
207
+ /**
208
+ * Exchange a grant for a Google access token, hardened with a per-attempt timeout + bounded retry
209
+ * on transient failures (network errors and 429/5xx). Non-retryable statuses (e.g. 400/401 from a
210
+ * bad grant) fail fast. The error message carries only the status code, never the response body
211
+ * (which can leak grant/account details) or the token/key.
212
+ */
213
+ async function postForToken(body: URLSearchParams, signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<TokenResponse> {
214
+ let lastError: unknown;
215
+ for (let attempt = 0; attempt < TOKEN_ATTEMPTS; attempt++) {
216
+ if (signal?.aborted) throw signal.reason ?? new Error("Google OAuth token exchange aborted");
217
+ let response: Response;
218
+ try {
219
+ response = await fetchImpl(OAUTH_TOKEN_URL, {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
222
+ body: body.toString(),
223
+ signal: tokenTimeoutSignal(signal),
224
+ });
225
+ } catch (err) {
226
+ if (signal?.aborted) throw err;
227
+ lastError = err;
228
+ if (attempt === TOKEN_ATTEMPTS - 1) break;
229
+ await new Promise(resolve => setTimeout(resolve, tokenRetryDelayMs(attempt)));
230
+ continue;
231
+ }
232
+ if (response.ok) return (await response.json()) as TokenResponse;
233
+ if (!isRetryableTokenStatus(response.status) || attempt === TOKEN_ATTEMPTS - 1) {
234
+ throw new Error(`Google OAuth token exchange failed (${response.status})`);
235
+ }
236
+ lastError = new Error(`Google OAuth token exchange failed (${response.status})`);
237
+ await response.body?.cancel().catch(() => {});
238
+ await new Promise(resolve => setTimeout(resolve, tokenRetryDelayMs(attempt)));
239
+ }
240
+ throw lastError instanceof Error ? lastError : new Error("Google OAuth token exchange failed");
241
+ }
242
+
243
+ async function exchangeJwtForToken(creds: ServiceAccountCredentials, signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<TokenResponse> {
244
+ const now = Math.floor(Date.now() / 1000);
245
+ const assertion = await signJwtRs256(
246
+ { iss: creds.client_email, scope: CLOUD_PLATFORM_SCOPE, aud: OAUTH_TOKEN_URL, exp: now + 3600, iat: now },
247
+ creds.private_key,
248
+ creds.private_key_id,
249
+ );
250
+ return postForToken(new URLSearchParams({ grant_type: JWT_BEARER_GRANT, assertion }), signal, fetchImpl);
251
+ }
252
+
253
+ async function exchangeRefreshToken(creds: AuthorizedUserCredentials, signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<TokenResponse> {
254
+ return postForToken(
255
+ new URLSearchParams({
256
+ client_id: creds.client_id,
257
+ client_secret: creds.client_secret,
258
+ refresh_token: creds.refresh_token,
259
+ grant_type: "refresh_token",
260
+ }),
261
+ signal,
262
+ fetchImpl,
263
+ );
264
+ }
265
+
266
+ async function fetchMetadataToken(signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<TokenResponse | undefined> {
267
+ const timeout = AbortSignal.timeout(2000);
268
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
269
+ try {
270
+ const response = await fetchImpl(METADATA_TOKEN_URL, {
271
+ method: "GET",
272
+ headers: { "Metadata-Flavor": "Google" },
273
+ signal: combined,
274
+ });
275
+ if (!response.ok) return undefined;
276
+ return (await response.json()) as TokenResponse;
277
+ } catch {
278
+ return undefined;
279
+ }
280
+ }
281
+
282
+ async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
283
+ const adc = loadAdcCredentials();
284
+ if (adc) {
285
+ const token = adc.creds.type === "service_account"
286
+ ? await exchangeJwtForToken(adc.creds, signal, fetchImpl)
287
+ : await exchangeRefreshToken(adc.creds, signal, fetchImpl);
288
+ return { source: adc.source, token };
289
+ }
290
+ const metadata = await fetchMetadataToken(signal, fetchImpl);
291
+ if (metadata) return { source: "metadata", token: metadata };
292
+ throw new Error(
293
+ "Vertex AI requires Application Default Credentials. Set GOOGLE_APPLICATION_CREDENTIALS, run `gcloud auth application-default login`, or run on a GCE/Cloud Run instance with a service account.",
294
+ );
295
+ }
296
+
297
+ /** Returns a Bearer access token for the `Authorization` header on Vertex AI calls (cached + refreshed). */
298
+ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl }): Promise<string> {
299
+ const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
300
+ const skew = getRefreshSkewMs();
301
+ const now = Date.now();
302
+
303
+ // Only serve a cached token that matches the source the next resolve would actually use; prune
304
+ // expired or now-stale (different-source) entries so a credential-source change is honored.
305
+ const expectedSource = currentAdcSourceKey();
306
+ const writerGeneration = captureConfigGeneration();
307
+ for (const [source, cached] of tokenCache) {
308
+ if (source === expectedSource && cached.expiresAtMs - skew > now) return cached.token;
309
+ if (cached.expiresAtMs - skew <= now) tokenCache.delete(source);
310
+ }
311
+
312
+ // Dedup in-flight fetches PER source, not globally: if the credential source changes while a
313
+ // fetch is in flight, a new caller must not reuse the old source's promise (cross-source bleed).
314
+ const cacheKey = expectedSource;
315
+ const existing = inflight.get(cacheKey);
316
+ if (existing) return existing;
317
+
318
+ const promise = (async () => {
319
+ try {
320
+ const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
321
+ const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
322
+ if (writerGeneration >= lastReconciledGeneration || liveSources.has(source)) {
323
+ tokenCache.set(source, { token: token.access_token, expiresAtMs });
324
+ sweepExpiredOnWrite(Date.now());
325
+ }
326
+ return token.access_token;
327
+ } finally {
328
+ inflight.delete(cacheKey);
329
+ }
330
+ })();
331
+ inflight.set(cacheKey, promise);
332
+ return promise;
333
+ }
334
+
335
+ /** Test seam: restores complete process-local token and reconciliation state. */
336
+ export function __resetVertexTokenCache(): void {
337
+ tokenCache.clear();
338
+ inflight.clear();
339
+ lastReconciledGeneration = 0;
340
+ liveSources = new Set();
341
+ }
@@ -0,0 +1,58 @@
1
+ /** In-memory ring buffer of multi-agent guidance-injection / effort-cap log lines.
2
+ *
3
+ * Injection debug lines were previously console-only, so the GUI had an "Injection log"
4
+ * toggle with nothing to display. This buffer mirrors the provider debug buffer so the
5
+ * management API and GUI can tail injection lines the same way. Callers keep their own
6
+ * `isInjectionDebugEnabled()` guard; this module only stores what it is given. */
7
+
8
+ import type { DebugLogEntry } from "./debug-log-buffer";
9
+ import { retainedUtf8Bytes, truncateRetainedUtf8 } from "./admission";
10
+ import { enforceAppOwnedMemoryBudget } from "./app-owned-memory";
11
+
12
+ const MAX_LINES = 2_000;
13
+ const MAX_DEBUG_LINE_BYTES = 16 * 1024;
14
+ const buffer: DebugLogEntry[] = [];
15
+ let nextSeq = 1;
16
+ let bufferBytes = 0;
17
+
18
+ function removeOldestEntry(): number {
19
+ const entry = buffer.shift();
20
+ if (!entry) return 0;
21
+ const bytes = retainedUtf8Bytes(entry.line);
22
+ bufferBytes -= bytes;
23
+ return bytes;
24
+ }
25
+
26
+ /** Append a line to the injection buffer and echo it to the server console. */
27
+ export function injectionDebugLog(line: string): void {
28
+ const retainedLine = truncateRetainedUtf8(line, MAX_DEBUG_LINE_BYTES);
29
+ const entry: DebugLogEntry = { seq: nextSeq++, at: Date.now(), line: retainedLine };
30
+ buffer.push(entry);
31
+ bufferBytes += retainedUtf8Bytes(retainedLine);
32
+ while (buffer.length > MAX_LINES) removeOldestEntry();
33
+ enforceAppOwnedMemoryBudget();
34
+ console.log(line);
35
+ }
36
+
37
+ export function getInjectionDebugLogEntries(options?: { after?: number; limit?: number }): DebugLogEntry[] {
38
+ const after = options?.after ?? 0;
39
+ const limit = options?.limit ?? 500;
40
+ const filtered = after > 0 ? buffer.filter(entry => entry.seq > after) : buffer;
41
+ if (filtered.length <= limit) return filtered;
42
+ return filtered.slice(-limit);
43
+ }
44
+
45
+ export function injectionBufferMetrics(): { entries: number; bytes: number; oldestAt: number | null } {
46
+ return { entries: buffer.length, bytes: bufferBytes, oldestAt: buffer[0]?.at ?? null };
47
+ }
48
+
49
+ export function evictOldestInjectionEntryForBudget(): number {
50
+ return removeOldestEntry();
51
+ }
52
+
53
+ /** Test isolation. */
54
+ export function resetInjectionDebugLogBufferForTests(): void {
55
+ buffer.length = 0;
56
+ bufferBytes = 0;
57
+ nextSeq = 1;
58
+ }
@@ -0,0 +1,25 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ function windowsRundll32(): string {
6
+ const windowsRoot = process.env.SystemRoot || process.env.WINDIR || "C:\\Windows";
7
+ const candidate = join(windowsRoot, "System32", "rundll32.exe");
8
+ return existsSync(candidate) ? candidate : "rundll32";
9
+ }
10
+
11
+ export function openUrl(url: string): void {
12
+ if (!/^https?:\/\//i.test(url)) return;
13
+ const cmd =
14
+ process.platform === "darwin" ? "open"
15
+ : process.platform === "win32" ? windowsRundll32()
16
+ : "xdg-open";
17
+ const args = process.platform === "win32"
18
+ ? ["url.dll,FileProtocolHandler", url]
19
+ : [url];
20
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore", shell: false });
21
+ // Headless hosts (no xdg-open) emit ENOENT as an async 'error' event; without a
22
+ // listener that is an uncaught exception that kills the whole proxy/login flow.
23
+ child.on("error", () => {});
24
+ child.unref();
25
+ }
@@ -0,0 +1,151 @@
1
+ import http, { type IncomingMessage, type RequestOptions } from "node:http";
2
+ import https from "node:https";
3
+
4
+ export type PinnedAddress = { address: string; family: number };
5
+
6
+ export interface PinnedHttpGetOptions {
7
+ headers?: HeadersInit;
8
+ maxBytes?: number;
9
+ idleTimeoutMs?: number;
10
+ rejectUnauthorized?: boolean;
11
+ context?: string;
12
+ }
13
+
14
+ /**
15
+ * GET a URL through one previously validated address. The original hostname
16
+ * remains authoritative for Host, SNI, and certificate verification.
17
+ */
18
+ export function pinnedHttpGet(
19
+ url: string,
20
+ pinned: PinnedAddress,
21
+ signal?: AbortSignal,
22
+ options?: PinnedHttpGetOptions,
23
+ ): Promise<Response> {
24
+ const parsed = new URL(url);
25
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
26
+ throw new Error(`${options?.context ?? "request"} must use HTTP or HTTPS, got ${parsed.protocol}`);
27
+ }
28
+ const context = options?.context ?? "request";
29
+ const idleTimeoutMs = options?.idleTimeoutMs ?? 60_000;
30
+ const maxBytes = options?.maxBytes;
31
+ const headers = new Headers(options?.headers);
32
+ headers.set("host", parsed.host);
33
+ const requestHeaders: Record<string, string> = {};
34
+ headers.forEach((value, key) => { requestHeaders[key] = value; });
35
+
36
+ return new Promise<Response>((resolve, reject) => {
37
+ if (signal?.aborted) {
38
+ reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
39
+ return;
40
+ }
41
+
42
+ let settled = false;
43
+ const fail = (error: unknown) => {
44
+ try { req.destroy(); } catch { /* ignore */ }
45
+ if (settled) return;
46
+ settled = true;
47
+ reject(error instanceof Error ? error : new Error(String(error)));
48
+ };
49
+ const requestOptions: RequestOptions & { servername?: string } = {
50
+ protocol: parsed.protocol,
51
+ hostname: parsed.hostname,
52
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
53
+ path: `${parsed.pathname}${parsed.search}`,
54
+ method: "GET",
55
+ headers: requestHeaders,
56
+ ...(parsed.protocol === "https:"
57
+ ? {
58
+ servername: parsed.hostname,
59
+ rejectUnauthorized: options?.rejectUnauthorized ?? true,
60
+ }
61
+ : {}),
62
+ lookup(_hostname, lookupOptions, callback) {
63
+ const opts = typeof lookupOptions === "function" ? undefined : lookupOptions;
64
+ const cb = typeof lookupOptions === "function" ? lookupOptions : callback;
65
+ if (!cb) return;
66
+ if (opts && typeof opts === "object" && "all" in opts && opts.all) {
67
+ (cb as (error: NodeJS.ErrnoException | null, addresses: PinnedAddress[]) => void)(
68
+ null,
69
+ [{ address: pinned.address, family: pinned.family }],
70
+ );
71
+ return;
72
+ }
73
+ (cb as (error: NodeJS.ErrnoException | null, address: string, family: 4 | 6) => void)(
74
+ null,
75
+ pinned.address,
76
+ pinned.family as 4 | 6,
77
+ );
78
+ },
79
+ };
80
+
81
+ const onResponse = (response: IncomingMessage) => {
82
+ const status = response.statusCode ?? 0;
83
+ const responseHeaders = new Headers();
84
+ for (const [key, value] of Object.entries(response.headers)) {
85
+ if (value === undefined || value === null) continue;
86
+ if (Array.isArray(value)) {
87
+ for (const item of value) responseHeaders.append(key, String(item));
88
+ } else {
89
+ responseHeaders.set(key, String(value));
90
+ }
91
+ }
92
+
93
+ if (status < 200 || status >= 300) {
94
+ try { response.destroy(); } catch { /* ignore */ }
95
+ try { req.destroy(); } catch { /* ignore */ }
96
+ if (settled) return;
97
+ settled = true;
98
+ resolve(new Response(null, { status, headers: responseHeaders }));
99
+ return;
100
+ }
101
+
102
+ let received = 0;
103
+ const stream = new ReadableStream<Uint8Array>({
104
+ start(controller) {
105
+ response.setTimeout(idleTimeoutMs, () => {
106
+ const error = new Error(`${context} stalled`);
107
+ fail(error);
108
+ try { controller.error(error); } catch { /* closed */ }
109
+ });
110
+ response.on("data", (chunk: Buffer | string) => {
111
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
112
+ received += buffer.byteLength;
113
+ if (maxBytes !== undefined && received > maxBytes) {
114
+ const error = new Error(`${context} exceeds ${maxBytes} byte cap`);
115
+ fail(error);
116
+ try { controller.error(error); } catch { /* closed */ }
117
+ return;
118
+ }
119
+ try { controller.enqueue(buffer); } catch { /* closed */ }
120
+ });
121
+ response.on("end", () => {
122
+ try { controller.close(); } catch { /* closed */ }
123
+ });
124
+ response.on("error", (error: Error) => {
125
+ fail(error);
126
+ try { controller.error(error); } catch { /* closed */ }
127
+ });
128
+ },
129
+ cancel() {
130
+ req.destroy();
131
+ },
132
+ });
133
+
134
+ if (settled) return;
135
+ settled = true;
136
+ resolve(new Response(stream, { status, headers: responseHeaders }));
137
+ };
138
+
139
+ const requestFn = parsed.protocol === "https:" ? https.request : http.request;
140
+ const req = requestFn(requestOptions, onResponse);
141
+ const onAbort = () => fail(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
142
+ signal?.addEventListener("abort", onAbort, { once: true });
143
+ req.setTimeout(idleTimeoutMs, () => fail(new Error(`${context} timed out`)));
144
+ req.on("error", error => {
145
+ signal?.removeEventListener("abort", onAbort);
146
+ fail(error);
147
+ });
148
+ req.on("close", () => signal?.removeEventListener("abort", onAbort));
149
+ req.end();
150
+ });
151
+ }
@@ -0,0 +1,20 @@
1
+ export function maskEmail(value: string | null | undefined): string | null {
2
+ if (!value) return null;
3
+ const at = value.indexOf("@");
4
+ if (at <= 0) return value;
5
+ const local = value.slice(0, at);
6
+ const domain = value.slice(at + 1);
7
+ if (!domain) return value;
8
+ if (local.length === 1) return `*@${domain}`;
9
+ if (local.length === 2) return `${local[0]}*@${domain}`;
10
+ return `${local[0]}***${local[local.length - 1]}@${domain}`;
11
+ }
12
+
13
+ export function maskAccountId(value: string | null | undefined): string | null {
14
+ if (!value) return null;
15
+ const id = value.trim();
16
+ if (!id) return null;
17
+ // Short IDs would disclose the entire identifier via the suffix; use a non-identifying placeholder.
18
+ if (id.length <= 4) return "account-…";
19
+ return `account-…${id.slice(-4)}`;
20
+ }