@gajae-code/ai 0.1.1

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 (349) hide show
  1. package/CHANGELOG.md +2644 -0
  2. package/README.md +1181 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +66 -0
  5. package/dist/types/auth-broker/index.d.ts +5 -0
  6. package/dist/types/auth-broker/refresher.d.ts +25 -0
  7. package/dist/types/auth-broker/remote-store.d.ts +96 -0
  8. package/dist/types/auth-broker/server.d.ts +32 -0
  9. package/dist/types/auth-broker/types.d.ts +105 -0
  10. package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
  11. package/dist/types/auth-gateway/http.d.ts +39 -0
  12. package/dist/types/auth-gateway/index.d.ts +3 -0
  13. package/dist/types/auth-gateway/server.d.ts +17 -0
  14. package/dist/types/auth-gateway/types.d.ts +115 -0
  15. package/dist/types/auth-storage.d.ts +641 -0
  16. package/dist/types/cli.d.ts +2 -0
  17. package/dist/types/index.d.ts +49 -0
  18. package/dist/types/model-cache.d.ts +17 -0
  19. package/dist/types/model-manager.d.ts +62 -0
  20. package/dist/types/model-thinking.d.ts +71 -0
  21. package/dist/types/models.d.ts +12 -0
  22. package/dist/types/provider-details.d.ts +24 -0
  23. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  24. package/dist/types/provider-models/descriptors.d.ts +48 -0
  25. package/dist/types/provider-models/google.d.ts +20 -0
  26. package/dist/types/provider-models/index.d.ts +5 -0
  27. package/dist/types/provider-models/ollama.d.ts +7 -0
  28. package/dist/types/provider-models/openai-compat.d.ts +237 -0
  29. package/dist/types/provider-models/special.d.ts +16 -0
  30. package/dist/types/providers/amazon-bedrock.d.ts +36 -0
  31. package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
  32. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  33. package/dist/types/providers/anthropic.d.ts +188 -0
  34. package/dist/types/providers/aws-credentials.d.ts +43 -0
  35. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  36. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  37. package/dist/types/providers/azure-openai-responses.d.ts +15 -0
  38. package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
  39. package/dist/types/providers/cursor.d.ts +42 -0
  40. package/dist/types/providers/error-message.d.ts +27 -0
  41. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  42. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  43. package/dist/types/providers/google-auth.d.ts +24 -0
  44. package/dist/types/providers/google-gemini-cli.d.ts +72 -0
  45. package/dist/types/providers/google-gemini-headers.d.ts +18 -0
  46. package/dist/types/providers/google-shared.d.ts +163 -0
  47. package/dist/types/providers/google-types.d.ts +138 -0
  48. package/dist/types/providers/google-vertex.d.ts +7 -0
  49. package/dist/types/providers/google.d.ts +4 -0
  50. package/dist/types/providers/grammar.d.ts +1 -0
  51. package/dist/types/providers/kimi.d.ts +27 -0
  52. package/dist/types/providers/mock.d.ts +175 -0
  53. package/dist/types/providers/ollama.d.ts +6 -0
  54. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  55. package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
  56. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  57. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  58. package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
  59. package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
  60. package/dist/types/providers/openai-codex-responses.d.ts +67 -0
  61. package/dist/types/providers/openai-completions-compat.d.ts +25 -0
  62. package/dist/types/providers/openai-completions.d.ts +33 -0
  63. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  64. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  65. package/dist/types/providers/openai-responses-shared.d.ts +89 -0
  66. package/dist/types/providers/openai-responses.d.ts +32 -0
  67. package/dist/types/providers/pi-native-client.d.ts +13 -0
  68. package/dist/types/providers/pi-native-server.d.ts +68 -0
  69. package/dist/types/providers/register-builtins.d.ts +31 -0
  70. package/dist/types/providers/synthetic.d.ts +26 -0
  71. package/dist/types/providers/transform-messages.d.ts +12 -0
  72. package/dist/types/providers/vision-guard.d.ts +8 -0
  73. package/dist/types/rate-limit-utils.d.ts +19 -0
  74. package/dist/types/stream.d.ts +24 -0
  75. package/dist/types/types.d.ts +746 -0
  76. package/dist/types/usage/claude.d.ts +3 -0
  77. package/dist/types/usage/gemini.d.ts +2 -0
  78. package/dist/types/usage/github-copilot.d.ts +7 -0
  79. package/dist/types/usage/google-antigravity.d.ts +2 -0
  80. package/dist/types/usage/kimi.d.ts +2 -0
  81. package/dist/types/usage/minimax-code.d.ts +2 -0
  82. package/dist/types/usage/openai-codex.d.ts +3 -0
  83. package/dist/types/usage/shared.d.ts +1 -0
  84. package/dist/types/usage/zai.d.ts +2 -0
  85. package/dist/types/usage.d.ts +258 -0
  86. package/dist/types/utils/abort.d.ts +19 -0
  87. package/dist/types/utils/anthropic-auth.d.ts +31 -0
  88. package/dist/types/utils/discovery/antigravity.d.ts +61 -0
  89. package/dist/types/utils/discovery/codex.d.ts +38 -0
  90. package/dist/types/utils/discovery/cursor.d.ts +23 -0
  91. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  92. package/dist/types/utils/discovery/index.d.ts +4 -0
  93. package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
  94. package/dist/types/utils/event-stream.d.ts +28 -0
  95. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  96. package/dist/types/utils/foundry.d.ts +1 -0
  97. package/dist/types/utils/h2-fetch.d.ts +22 -0
  98. package/dist/types/utils/http-inspector.d.ts +31 -0
  99. package/dist/types/utils/idle-iterator.d.ts +67 -0
  100. package/dist/types/utils/json-parse.d.ts +10 -0
  101. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
  102. package/dist/types/utils/oauth/anthropic.d.ts +22 -0
  103. package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
  104. package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
  105. package/dist/types/utils/oauth/callback-server.d.ts +57 -0
  106. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  107. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  108. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  109. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  110. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  111. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  112. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  113. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  114. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  115. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  116. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  117. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  118. package/dist/types/utils/oauth/index.d.ts +38 -0
  119. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  120. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  121. package/dist/types/utils/oauth/kimi.d.ts +21 -0
  122. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  123. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  124. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  125. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  126. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  127. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  128. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  129. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  130. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  131. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  132. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  133. package/dist/types/utils/oauth/perplexity.d.ts +9 -0
  134. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  135. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  136. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  137. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  138. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  139. package/dist/types/utils/oauth/together.d.ts +1 -0
  140. package/dist/types/utils/oauth/types.d.ts +44 -0
  141. package/dist/types/utils/oauth/venice.d.ts +18 -0
  142. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  143. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  144. package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
  145. package/dist/types/utils/oauth/zai.d.ts +18 -0
  146. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  147. package/dist/types/utils/overflow.d.ts +54 -0
  148. package/dist/types/utils/parse-bind.d.ts +23 -0
  149. package/dist/types/utils/provider-response.d.ts +3 -0
  150. package/dist/types/utils/retry-after.d.ts +3 -0
  151. package/dist/types/utils/retry.d.ts +26 -0
  152. package/dist/types/utils/schema/adapt.d.ts +24 -0
  153. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  154. package/dist/types/utils/schema/dereference.d.ts +11 -0
  155. package/dist/types/utils/schema/draft.d.ts +10 -0
  156. package/dist/types/utils/schema/equality.d.ts +4 -0
  157. package/dist/types/utils/schema/fields.d.ts +49 -0
  158. package/dist/types/utils/schema/index.d.ts +13 -0
  159. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  160. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  161. package/dist/types/utils/schema/normalize.d.ts +93 -0
  162. package/dist/types/utils/schema/spill.d.ts +8 -0
  163. package/dist/types/utils/schema/stamps.d.ts +25 -0
  164. package/dist/types/utils/schema/types.d.ts +4 -0
  165. package/dist/types/utils/schema/wire.d.ts +54 -0
  166. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  167. package/dist/types/utils/sse-debug.d.ts +10 -0
  168. package/dist/types/utils/tool-call-healing.d.ts +71 -0
  169. package/dist/types/utils/tool-choice.d.ts +50 -0
  170. package/dist/types/utils/validation.d.ts +17 -0
  171. package/dist/types/utils.d.ts +28 -0
  172. package/package.json +146 -0
  173. package/src/api-registry.ts +96 -0
  174. package/src/auth-broker/client.ts +358 -0
  175. package/src/auth-broker/index.ts +5 -0
  176. package/src/auth-broker/refresher.ts +127 -0
  177. package/src/auth-broker/remote-store.ts +623 -0
  178. package/src/auth-broker/server.ts +644 -0
  179. package/src/auth-broker/types.ts +127 -0
  180. package/src/auth-broker/wire-schemas.ts +200 -0
  181. package/src/auth-gateway/http.ts +194 -0
  182. package/src/auth-gateway/index.ts +3 -0
  183. package/src/auth-gateway/server.ts +717 -0
  184. package/src/auth-gateway/types.ts +134 -0
  185. package/src/auth-storage.ts +4104 -0
  186. package/src/cli.ts +262 -0
  187. package/src/index.ts +54 -0
  188. package/src/model-cache.ts +129 -0
  189. package/src/model-manager.ts +450 -0
  190. package/src/model-thinking.ts +691 -0
  191. package/src/models.json +73853 -0
  192. package/src/models.json.d.ts +9 -0
  193. package/src/models.ts +56 -0
  194. package/src/prompts/turn-aborted-guidance.md +4 -0
  195. package/src/provider-details.ts +90 -0
  196. package/src/provider-models/bundled-references.ts +38 -0
  197. package/src/provider-models/descriptors.ts +308 -0
  198. package/src/provider-models/google.ts +91 -0
  199. package/src/provider-models/index.ts +5 -0
  200. package/src/provider-models/ollama.ts +153 -0
  201. package/src/provider-models/openai-compat.ts +2275 -0
  202. package/src/provider-models/special.ts +67 -0
  203. package/src/providers/amazon-bedrock.ts +849 -0
  204. package/src/providers/anthropic-messages-server-schema.ts +229 -0
  205. package/src/providers/anthropic-messages-server.ts +677 -0
  206. package/src/providers/anthropic.ts +2696 -0
  207. package/src/providers/aws-credentials.ts +501 -0
  208. package/src/providers/aws-eventstream.ts +185 -0
  209. package/src/providers/aws-sigv4.ts +218 -0
  210. package/src/providers/azure-openai-responses.ts +337 -0
  211. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  212. package/src/providers/cursor/proto/agent.proto +3526 -0
  213. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  214. package/src/providers/cursor/proto/buf.yaml +17 -0
  215. package/src/providers/cursor.ts +2561 -0
  216. package/src/providers/error-message.ts +21 -0
  217. package/src/providers/github-copilot-headers.ts +140 -0
  218. package/src/providers/gitlab-duo.ts +372 -0
  219. package/src/providers/google-auth.ts +252 -0
  220. package/src/providers/google-gemini-cli.ts +795 -0
  221. package/src/providers/google-gemini-headers.ts +41 -0
  222. package/src/providers/google-shared.ts +902 -0
  223. package/src/providers/google-types.ts +167 -0
  224. package/src/providers/google-vertex.ts +88 -0
  225. package/src/providers/google.ts +41 -0
  226. package/src/providers/grammar.ts +70 -0
  227. package/src/providers/kimi.ts +52 -0
  228. package/src/providers/mock.ts +500 -0
  229. package/src/providers/ollama.ts +544 -0
  230. package/src/providers/openai-anthropic-shim.ts +138 -0
  231. package/src/providers/openai-chat-server-schema.ts +243 -0
  232. package/src/providers/openai-chat-server.ts +628 -0
  233. package/src/providers/openai-codex/constants.ts +43 -0
  234. package/src/providers/openai-codex/request-transformer.ts +161 -0
  235. package/src/providers/openai-codex/response-handler.ts +81 -0
  236. package/src/providers/openai-codex-responses.ts +2598 -0
  237. package/src/providers/openai-completions-compat.ts +279 -0
  238. package/src/providers/openai-completions.ts +1853 -0
  239. package/src/providers/openai-responses-server-schema.ts +290 -0
  240. package/src/providers/openai-responses-server.ts +1183 -0
  241. package/src/providers/openai-responses-shared.ts +800 -0
  242. package/src/providers/openai-responses.ts +621 -0
  243. package/src/providers/pi-native-client.ts +228 -0
  244. package/src/providers/pi-native-server.ts +210 -0
  245. package/src/providers/register-builtins.ts +412 -0
  246. package/src/providers/synthetic.ts +50 -0
  247. package/src/providers/transform-messages.ts +309 -0
  248. package/src/providers/vision-guard.ts +31 -0
  249. package/src/rate-limit-utils.ts +84 -0
  250. package/src/stream.ts +895 -0
  251. package/src/types.ts +884 -0
  252. package/src/usage/claude.ts +431 -0
  253. package/src/usage/gemini.ts +250 -0
  254. package/src/usage/github-copilot.ts +421 -0
  255. package/src/usage/google-antigravity.ts +201 -0
  256. package/src/usage/kimi.ts +271 -0
  257. package/src/usage/minimax-code.ts +31 -0
  258. package/src/usage/openai-codex.ts +503 -0
  259. package/src/usage/shared.ts +10 -0
  260. package/src/usage/zai.ts +247 -0
  261. package/src/usage.ts +183 -0
  262. package/src/utils/abort.ts +51 -0
  263. package/src/utils/anthropic-auth.ts +87 -0
  264. package/src/utils/discovery/antigravity.ts +261 -0
  265. package/src/utils/discovery/codex.ts +371 -0
  266. package/src/utils/discovery/cursor.ts +306 -0
  267. package/src/utils/discovery/gemini.ts +248 -0
  268. package/src/utils/discovery/index.ts +4 -0
  269. package/src/utils/discovery/openai-compatible.ts +224 -0
  270. package/src/utils/event-stream.ts +142 -0
  271. package/src/utils/fireworks-model-id.ts +30 -0
  272. package/src/utils/foundry.ts +8 -0
  273. package/src/utils/h2-fetch.ts +60 -0
  274. package/src/utils/http-inspector.ts +176 -0
  275. package/src/utils/idle-iterator.ts +250 -0
  276. package/src/utils/json-parse.ts +148 -0
  277. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  278. package/src/utils/oauth/anthropic.ts +200 -0
  279. package/src/utils/oauth/api-key-login.ts +87 -0
  280. package/src/utils/oauth/api-key-validation.ts +92 -0
  281. package/src/utils/oauth/callback-server.ts +276 -0
  282. package/src/utils/oauth/cerebras.ts +16 -0
  283. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  284. package/src/utils/oauth/cursor.ts +157 -0
  285. package/src/utils/oauth/deepseek.ts +53 -0
  286. package/src/utils/oauth/firepass.ts +24 -0
  287. package/src/utils/oauth/fireworks.ts +15 -0
  288. package/src/utils/oauth/github-copilot.ts +362 -0
  289. package/src/utils/oauth/gitlab-duo.ts +123 -0
  290. package/src/utils/oauth/google-antigravity.ts +200 -0
  291. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  292. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  293. package/src/utils/oauth/huggingface.ts +62 -0
  294. package/src/utils/oauth/index.ts +444 -0
  295. package/src/utils/oauth/kagi.ts +47 -0
  296. package/src/utils/oauth/kilo.ts +87 -0
  297. package/src/utils/oauth/kimi.ts +254 -0
  298. package/src/utils/oauth/litellm.ts +47 -0
  299. package/src/utils/oauth/lm-studio.ts +38 -0
  300. package/src/utils/oauth/minimax-code.ts +78 -0
  301. package/src/utils/oauth/moonshot.ts +16 -0
  302. package/src/utils/oauth/nanogpt.ts +15 -0
  303. package/src/utils/oauth/nvidia.ts +70 -0
  304. package/src/utils/oauth/oauth.html +199 -0
  305. package/src/utils/oauth/ollama-cloud.ts +28 -0
  306. package/src/utils/oauth/ollama.ts +47 -0
  307. package/src/utils/oauth/openai-codex.ts +299 -0
  308. package/src/utils/oauth/opencode.ts +49 -0
  309. package/src/utils/oauth/parallel.ts +46 -0
  310. package/src/utils/oauth/perplexity.ts +206 -0
  311. package/src/utils/oauth/pkce.ts +18 -0
  312. package/src/utils/oauth/qianfan.ts +58 -0
  313. package/src/utils/oauth/qwen-portal.ts +60 -0
  314. package/src/utils/oauth/synthetic.ts +16 -0
  315. package/src/utils/oauth/tavily.ts +46 -0
  316. package/src/utils/oauth/together.ts +16 -0
  317. package/src/utils/oauth/types.ts +94 -0
  318. package/src/utils/oauth/venice.ts +59 -0
  319. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  320. package/src/utils/oauth/vllm.ts +40 -0
  321. package/src/utils/oauth/xiaomi.ts +137 -0
  322. package/src/utils/oauth/zai.ts +60 -0
  323. package/src/utils/oauth/zenmux.ts +15 -0
  324. package/src/utils/overflow.ts +137 -0
  325. package/src/utils/parse-bind.ts +54 -0
  326. package/src/utils/provider-response.ts +30 -0
  327. package/src/utils/retry-after.ts +110 -0
  328. package/src/utils/retry.ts +54 -0
  329. package/src/utils/schema/CONSTRAINTS.md +164 -0
  330. package/src/utils/schema/adapt.ts +36 -0
  331. package/src/utils/schema/compatibility.ts +435 -0
  332. package/src/utils/schema/dereference.ts +98 -0
  333. package/src/utils/schema/draft.ts +341 -0
  334. package/src/utils/schema/equality.ts +97 -0
  335. package/src/utils/schema/fields.ts +190 -0
  336. package/src/utils/schema/index.ts +13 -0
  337. package/src/utils/schema/json-schema-validator.ts +577 -0
  338. package/src/utils/schema/meta-validator.ts +167 -0
  339. package/src/utils/schema/normalize.ts +1588 -0
  340. package/src/utils/schema/spill.ts +43 -0
  341. package/src/utils/schema/stamps.ts +97 -0
  342. package/src/utils/schema/types.ts +11 -0
  343. package/src/utils/schema/wire.ts +213 -0
  344. package/src/utils/schema/zod-decontaminate.ts +331 -0
  345. package/src/utils/sse-debug.ts +289 -0
  346. package/src/utils/tool-call-healing.ts +271 -0
  347. package/src/utils/tool-choice.ts +99 -0
  348. package/src/utils/validation.ts +1019 -0
  349. package/src/utils.ts +166 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Perplexity login and token refresh.
3
+ *
4
+ * Login paths (in priority order):
5
+ * 1. macOS native app: reads JWT from NSUserDefaults (`defaults read ai.perplexity.mac authToken`)
6
+ * 2. HTTP email OTP: `GET /api/auth/csrf` → `POST /api/auth/signin-email` → `POST /api/auth/signin-otp`
7
+ *
8
+ * No browser or manual cookie paste required.
9
+ * Refresh: Socket.IO `refreshJWT` RPC over authenticated WebSocket connection.
10
+ *
11
+ * Protocol: Engine.IO v4 + Socket.IO v4 over WebSocket (bypasses Cloudflare managed challenge).
12
+ * Architecture reverse-engineered from Perplexity macOS app (ai.perplexity.mac).
13
+ */
14
+ import * as os from "node:os";
15
+ import { $env } from "@gajae-code/utils";
16
+ import { $ } from "bun";
17
+ import type { OAuthController, OAuthCredentials } from "./types";
18
+
19
+ const API_VERSION = "2.18";
20
+ const NATIVE_APP_BUNDLE = "ai.perplexity.mac";
21
+ const APP_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // JWT helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Extract expiry from a JWT. Perplexity tokens generally lack an `exp` claim
29
+ * (their sessions are server-side and effectively non-expiring from the client's
30
+ * point of view), so we return a far-future sentinel when no `exp` is present.
31
+ * When `exp` IS present, subtract a 5-minute safety margin.
32
+ */
33
+ const NEVER_EXPIRES = 8.64e15; // max safe Date value
34
+ function getJwtExpiry(token: string): number {
35
+ try {
36
+ const parts = token.split(".");
37
+ if (parts.length !== 3) return NEVER_EXPIRES;
38
+ const payload = parts[1] ?? "";
39
+ const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
40
+ if (typeof decoded?.exp === "number" && Number.isFinite(decoded.exp)) {
41
+ return decoded.exp * 1000 - 5 * 60_000;
42
+ }
43
+ } catch {
44
+ // Ignore decode errors
45
+ }
46
+ return NEVER_EXPIRES;
47
+ }
48
+
49
+ /** Build OAuthCredentials from a Perplexity JWT string. */
50
+ function jwtToCredentials(jwt: string, email?: string): OAuthCredentials {
51
+ return {
52
+ access: jwt,
53
+ refresh: jwt,
54
+ expires: getJwtExpiry(jwt),
55
+ email,
56
+ };
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Desktop app extraction
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Read the Perplexity JWT from the native macOS Catalyst app's UserDefaults.
65
+ * Tokens are stored in NSUserDefaults (not Keychain), readable by any same-UID process.
66
+ */
67
+ async function extractFromNativeApp(): Promise<string | null> {
68
+ if (os.platform() !== "darwin") return null;
69
+
70
+ try {
71
+ const result = await $`defaults read ${NATIVE_APP_BUNDLE} authToken`.quiet().nothrow();
72
+ if (result.exitCode !== 0) return null;
73
+ const token = result.text().trim();
74
+ if (!token || token === "(null)") return null;
75
+ return token;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Socket.IO email OTP login
83
+ // ---------------------------------------------------------------------------
84
+
85
+ /**
86
+ * Send email OTP and exchange it for a Perplexity JWT via HTTP endpoints.
87
+ */
88
+ async function httpEmailLogin(ctrl: OAuthController): Promise<OAuthCredentials> {
89
+ if (!ctrl.onPrompt) {
90
+ throw new Error("Perplexity login requires onPrompt callback");
91
+ }
92
+ const email = await ctrl.onPrompt({
93
+ message: "Enter your Perplexity email address",
94
+ placeholder: "user@example.com",
95
+ });
96
+ const trimmedEmail = email.trim();
97
+ if (!trimmedEmail) throw new Error("Email is required for Perplexity login");
98
+ if (ctrl.signal?.aborted) throw new Error("Login cancelled");
99
+
100
+ ctrl.onProgress?.("Fetching Perplexity CSRF token...");
101
+ const csrfResponse = await fetch("https://www.perplexity.ai/api/auth/csrf", {
102
+ headers: {
103
+ "User-Agent": APP_USER_AGENT,
104
+ "X-App-ApiVersion": API_VERSION,
105
+ },
106
+ signal: ctrl.signal,
107
+ });
108
+
109
+ if (!csrfResponse.ok) {
110
+ throw new Error(`Perplexity CSRF request failed: ${csrfResponse.status}`);
111
+ }
112
+
113
+ const csrfData = (await csrfResponse.json()) as { csrfToken?: string };
114
+ if (!csrfData.csrfToken) {
115
+ throw new Error("Perplexity CSRF response missing csrfToken");
116
+ }
117
+ ctrl.onProgress?.("Sending login code to your email...");
118
+ const sendResponse = await fetch("https://www.perplexity.ai/api/auth/signin-email", {
119
+ method: "POST",
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ "User-Agent": APP_USER_AGENT,
123
+ "X-App-ApiVersion": API_VERSION,
124
+ },
125
+ body: JSON.stringify({
126
+ email: trimmedEmail,
127
+ csrfToken: csrfData.csrfToken,
128
+ }),
129
+ signal: ctrl.signal,
130
+ });
131
+
132
+ if (!sendResponse.ok) {
133
+ const body = await sendResponse.text();
134
+ throw new Error(`Perplexity send login code failed (${sendResponse.status}): ${body}`);
135
+ }
136
+ const otp = await ctrl.onPrompt({
137
+ message: "Enter the code sent to your email",
138
+ placeholder: "123456",
139
+ });
140
+ const trimmedOtp = otp.trim();
141
+ if (!trimmedOtp) throw new Error("OTP code is required");
142
+ if (ctrl.signal?.aborted) throw new Error("Login cancelled");
143
+ ctrl.onProgress?.("Verifying login code...");
144
+ const verifyResponse = await fetch("https://www.perplexity.ai/api/auth/signin-otp", {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/json",
148
+ "User-Agent": APP_USER_AGENT,
149
+ "X-App-ApiVersion": API_VERSION,
150
+ },
151
+ body: JSON.stringify({
152
+ email: trimmedEmail,
153
+ otp: trimmedOtp,
154
+ csrfToken: csrfData.csrfToken,
155
+ }),
156
+ signal: ctrl.signal,
157
+ });
158
+
159
+ const verifyData = (await verifyResponse.json()) as {
160
+ token?: string;
161
+ status?: string;
162
+ error_code?: string;
163
+ text?: string;
164
+ };
165
+
166
+ if (!verifyResponse.ok) {
167
+ const reason = verifyData.text ?? verifyData.error_code ?? verifyData.status ?? "OTP verification failed";
168
+ throw new Error(`Perplexity OTP verification failed: ${reason}`);
169
+ }
170
+
171
+ if (!verifyData.token) {
172
+ throw new Error("Perplexity OTP verification response missing token");
173
+ }
174
+
175
+ return jwtToCredentials(verifyData.token, trimmedEmail);
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Public API
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /**
183
+ * Login to Perplexity.
184
+ *
185
+ * Tries auto-extraction from the desktop app, then runs HTTP email OTP login.
186
+ *
187
+ * No browser/manual token paste fallback is used.
188
+ */
189
+ export async function loginPerplexity(ctrl: OAuthController): Promise<OAuthCredentials> {
190
+ if (!ctrl.onPrompt) {
191
+ throw new Error("Perplexity login requires onPrompt callback");
192
+ }
193
+
194
+ // Path 1: Native macOS app JWT (skip if PI_AUTH_NO_BORROW=1)
195
+ if (!$env.PI_AUTH_NO_BORROW) {
196
+ ctrl.onProgress?.("Checking for Perplexity desktop app...");
197
+ const nativeJwt = await extractFromNativeApp();
198
+ if (nativeJwt) {
199
+ ctrl.onProgress?.("Found Perplexity JWT from native app");
200
+ return jwtToCredentials(nativeJwt);
201
+ }
202
+ }
203
+
204
+ // Path 2: HTTP email OTP
205
+ return httpEmailLogin(ctrl);
206
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Generate PKCE code verifier and challenge.
3
+ * Uses Web Crypto API for cross-platform compatibility.
4
+ */
5
+ export async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
6
+ // Generate random verifier
7
+ const verifierBytes = new Uint8Array(96);
8
+ crypto.getRandomValues(verifierBytes);
9
+ const verifier = Buffer.from(verifierBytes).toString("base64url");
10
+
11
+ // Compute SHA-256 challenge
12
+ const encoder = new TextEncoder();
13
+ const data = encoder.encode(verifier);
14
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
15
+ const challenge = Buffer.from(hashBuffer).toString("base64url");
16
+
17
+ return { verifier, challenge };
18
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Qianfan login flow.
3
+ *
4
+ * Qianfan provides an OpenAI-compatible API endpoint.
5
+ * Login is API-key based:
6
+ * 1. Open browser to Qianfan API key console
7
+ * 2. User copies API key
8
+ * 3. User pastes key into CLI prompt
9
+ */
10
+
11
+ import { validateOpenAICompatibleApiKey } from "./api-key-validation";
12
+ import type { OAuthController } from "./types";
13
+
14
+ const AUTH_URL = "https://console.bce.baidu.com/qianfan/ais/console/apiKey";
15
+ const API_BASE_URL = "https://qianfan.baidubce.com/v2";
16
+ const VALIDATION_MODEL = "deepseek-v3.2";
17
+
18
+ /**
19
+ * Login to Qianfan.
20
+ *
21
+ * Opens browser to API key page, prompts user to paste their API key.
22
+ * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
23
+ */
24
+ export async function loginQianfan(options: OAuthController): Promise<string> {
25
+ if (!options.onPrompt) {
26
+ throw new Error("Qianfan login requires onPrompt callback");
27
+ }
28
+
29
+ options.onAuth?.({
30
+ url: AUTH_URL,
31
+ instructions: "Copy your Qianfan API key from the console",
32
+ });
33
+
34
+ const apiKey = await options.onPrompt({
35
+ message: "Paste your Qianfan API key",
36
+ placeholder: "bce-v3/ALTAK-...",
37
+ });
38
+
39
+ if (options.signal?.aborted) {
40
+ throw new Error("Login cancelled");
41
+ }
42
+
43
+ const trimmed = apiKey.trim();
44
+ if (!trimmed) {
45
+ throw new Error("API key is required");
46
+ }
47
+
48
+ options.onProgress?.("Validating API key...");
49
+ await validateOpenAICompatibleApiKey({
50
+ provider: "qianfan",
51
+ apiKey: trimmed,
52
+ baseUrl: API_BASE_URL,
53
+ model: VALIDATION_MODEL,
54
+ signal: options.signal,
55
+ });
56
+
57
+ return trimmed;
58
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Qwen Portal login flow.
3
+ *
4
+ * Qwen Portal exposes an OpenAI-compatible endpoint at https://portal.qwen.ai/v1
5
+ * and accepts OAuth bearer tokens or API keys.
6
+ *
7
+ * This is a token/API-key flow:
8
+ * 1. Open Qwen Portal
9
+ * 2. Copy either your OAuth token or API key
10
+ * 3. Paste it into the CLI
11
+ */
12
+
13
+ import { validateOpenAICompatibleApiKey } from "./api-key-validation";
14
+ import type { OAuthController } from "./types";
15
+
16
+ const AUTH_URL = "https://chat.qwen.ai";
17
+ const API_BASE_URL = "https://portal.qwen.ai/v1";
18
+ const VALIDATION_MODEL = "coder-model";
19
+
20
+ /**
21
+ * Login to Qwen Portal.
22
+ *
23
+ * Prompts for either `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY` value.
24
+ * Returns the value directly (stored as api_key credential in auth storage).
25
+ */
26
+ export async function loginQwenPortal(options: OAuthController): Promise<string> {
27
+ if (!options.onPrompt) {
28
+ throw new Error("Qwen Portal login requires onPrompt callback");
29
+ }
30
+
31
+ options.onAuth?.({
32
+ url: AUTH_URL,
33
+ instructions: "Copy your Qwen OAuth token or API key",
34
+ });
35
+
36
+ const token = await options.onPrompt({
37
+ message: "Paste your Qwen OAuth token or API key",
38
+ placeholder: "sk-...",
39
+ });
40
+
41
+ if (options.signal?.aborted) {
42
+ throw new Error("Login cancelled");
43
+ }
44
+
45
+ const trimmed = token.trim();
46
+ if (!trimmed) {
47
+ throw new Error("Qwen token/API key is required");
48
+ }
49
+
50
+ options.onProgress?.("Validating credentials...");
51
+ await validateOpenAICompatibleApiKey({
52
+ provider: "qwen-portal",
53
+ apiKey: trimmed,
54
+ baseUrl: API_BASE_URL,
55
+ model: VALIDATION_MODEL,
56
+ signal: options.signal,
57
+ });
58
+
59
+ return trimmed;
60
+ }
@@ -0,0 +1,16 @@
1
+ /** Synthetic login flow (API key paste against https://api.synthetic.new/openai/v1). */
2
+ import { createApiKeyLogin } from "./api-key-login";
3
+
4
+ export const loginSynthetic = createApiKeyLogin({
5
+ providerLabel: "Synthetic",
6
+ authUrl: "https://dev.synthetic.new/docs/api/overview",
7
+ instructions: "Copy your API key from the Synthetic dashboard",
8
+ promptMessage: "Paste your Synthetic API key",
9
+ placeholder: "sk-...",
10
+ validation: {
11
+ kind: "chat-completions",
12
+ provider: "Synthetic",
13
+ baseUrl: "https://api.synthetic.new/openai/v1",
14
+ model: "hf:moonshotai/Kimi-K2.5",
15
+ },
16
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Tavily login flow.
3
+ *
4
+ * Tavily web search uses an API key from the account settings page.
5
+ * This is an API key flow:
6
+ * 1. Open browser to Tavily settings
7
+ * 2. User copies API key
8
+ * 3. User pastes key into CLI
9
+ */
10
+
11
+ import type { OAuthController } from "./types";
12
+
13
+ const AUTH_URL = "https://app.tavily.com/home";
14
+
15
+ /**
16
+ * Login to Tavily.
17
+ *
18
+ * Opens browser to API keys page and prompts user to paste their API key.
19
+ * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
20
+ */
21
+ export async function loginTavily(options: OAuthController): Promise<string> {
22
+ if (!options.onPrompt) {
23
+ throw new Error("Tavily login requires onPrompt callback");
24
+ }
25
+
26
+ options.onAuth?.({
27
+ url: AUTH_URL,
28
+ instructions: "Copy your Tavily API key from the API Keys page.",
29
+ });
30
+
31
+ const apiKey = await options.onPrompt({
32
+ message: "Paste your Tavily API key",
33
+ placeholder: "tvly-...",
34
+ });
35
+
36
+ if (options.signal?.aborted) {
37
+ throw new Error("Login cancelled");
38
+ }
39
+
40
+ const trimmed = apiKey.trim();
41
+ if (!trimmed) {
42
+ throw new Error("API key is required");
43
+ }
44
+
45
+ return trimmed;
46
+ }
@@ -0,0 +1,16 @@
1
+ /** Together login flow (API key paste against https://api.together.xyz/v1). */
2
+ import { createApiKeyLogin } from "./api-key-login";
3
+
4
+ export const loginTogether = createApiKeyLogin({
5
+ providerLabel: "Together",
6
+ authUrl: "https://api.together.xyz/settings/api-keys",
7
+ instructions: "Copy your API key from the Together dashboard",
8
+ promptMessage: "Paste your Together API key",
9
+ placeholder: "sk-...",
10
+ validation: {
11
+ kind: "chat-completions",
12
+ provider: "together",
13
+ baseUrl: "https://api.together.xyz/v1",
14
+ model: "moonshotai/Kimi-K2.5",
15
+ },
16
+ });
@@ -0,0 +1,94 @@
1
+ export type OAuthCredentials = {
2
+ refresh: string;
3
+ access: string;
4
+ expires: number;
5
+ enterpriseUrl?: string;
6
+ projectId?: string;
7
+ email?: string;
8
+ accountId?: string;
9
+ };
10
+
11
+ export type OAuthProvider =
12
+ | "alibaba-coding-plan"
13
+ | "anthropic"
14
+ | "cerebras"
15
+ | "cloudflare-ai-gateway"
16
+ | "cursor"
17
+ | "deepseek"
18
+ | "fireworks"
19
+ | "firepass"
20
+ | "github-copilot"
21
+ | "google-gemini-cli"
22
+ | "google-antigravity"
23
+ | "gitlab-duo"
24
+ | "huggingface"
25
+ | "kimi-code"
26
+ | "kilo"
27
+ | "kagi"
28
+ | "litellm"
29
+ | "lm-studio"
30
+ | "minimax-code"
31
+ | "minimax-code-cn"
32
+ | "moonshot"
33
+ | "nvidia"
34
+ | "nanogpt"
35
+ | "ollama"
36
+ | "ollama-cloud"
37
+ | "openai-codex"
38
+ | "openai-codex-device"
39
+ | "opencode-go"
40
+ | "opencode-zen"
41
+ | "parallel"
42
+ | "perplexity"
43
+ | "qianfan"
44
+ | "qwen-portal"
45
+ | "synthetic"
46
+ | "tavily"
47
+ | "together"
48
+ | "venice"
49
+ | "vercel-ai-gateway"
50
+ | "vllm"
51
+ | "xiaomi"
52
+ | "zenmux"
53
+ | "zai";
54
+
55
+ export type OAuthProviderId = OAuthProvider | (string & {});
56
+
57
+ export type OAuthPrompt = {
58
+ message: string;
59
+ placeholder?: string;
60
+ allowEmpty?: boolean;
61
+ };
62
+
63
+ export type OAuthAuthInfo = {
64
+ url: string;
65
+ instructions?: string;
66
+ };
67
+
68
+ export interface OAuthProviderInfo {
69
+ id: OAuthProviderId;
70
+ name: string;
71
+ available: boolean;
72
+ }
73
+
74
+ export interface OAuthController {
75
+ onAuth?(info: OAuthAuthInfo): void;
76
+ onProgress?(message: string): void;
77
+ onManualCodeInput?(): Promise<string>;
78
+ onPrompt?(prompt: OAuthPrompt): Promise<string>;
79
+ signal?: AbortSignal;
80
+ }
81
+
82
+ export interface OAuthLoginCallbacks extends OAuthController {
83
+ onAuth: (info: OAuthAuthInfo) => void;
84
+ onPrompt: (prompt: OAuthPrompt) => Promise<string>;
85
+ }
86
+
87
+ export interface OAuthProviderInterface {
88
+ readonly id: OAuthProviderId;
89
+ readonly name: string;
90
+ readonly sourceId?: string;
91
+ login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials | string>;
92
+ refreshToken?(credentials: OAuthCredentials): Promise<OAuthCredentials>;
93
+ getApiKey?(credentials: OAuthCredentials): string;
94
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Venice login flow.
3
+ *
4
+ * Venice provides OpenAI-compatible models via https://api.venice.ai/api/v1.
5
+ *
6
+ * This is not OAuth - it's a simple API key flow:
7
+ * 1. Open browser to Venice API key settings
8
+ * 2. User copies their API key
9
+ * 3. User pastes the API key into the CLI
10
+ */
11
+
12
+ import { validateOpenAICompatibleApiKey } from "./api-key-validation";
13
+ import type { OAuthController } from "./types";
14
+
15
+ const AUTH_URL = "https://venice.ai/settings/api";
16
+ const API_BASE_URL = "https://api.venice.ai/api/v1";
17
+ const VALIDATION_MODEL = "qwen3-4b";
18
+
19
+ /**
20
+ * Login to Venice.
21
+ *
22
+ * Opens browser to API keys page, prompts user to paste their API key.
23
+ * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
24
+ */
25
+ export async function loginVenice(options: OAuthController): Promise<string> {
26
+ if (!options.onPrompt) {
27
+ throw new Error("Venice login requires onPrompt callback");
28
+ }
29
+
30
+ options.onAuth?.({
31
+ url: AUTH_URL,
32
+ instructions: "Copy your API key from the Venice dashboard",
33
+ });
34
+
35
+ const apiKey = await options.onPrompt({
36
+ message: "Paste your Venice API key",
37
+ placeholder: "vapi_...",
38
+ });
39
+
40
+ if (options.signal?.aborted) {
41
+ throw new Error("Login cancelled");
42
+ }
43
+
44
+ const trimmed = apiKey.trim();
45
+ if (!trimmed) {
46
+ throw new Error("API key is required");
47
+ }
48
+
49
+ options.onProgress?.("Validating API key...");
50
+ await validateOpenAICompatibleApiKey({
51
+ provider: "Venice",
52
+ apiKey: trimmed,
53
+ baseUrl: API_BASE_URL,
54
+ model: VALIDATION_MODEL,
55
+ signal: options.signal,
56
+ });
57
+
58
+ return trimmed;
59
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Vercel AI Gateway login flow.
3
+ *
4
+ * Vercel AI Gateway proxies upstream model providers through a unified endpoint.
5
+ *
6
+ * This is not OAuth - it's a simple API key flow:
7
+ * 1. Open Vercel AI Gateway docs
8
+ * 2. User copies their API key
9
+ * 3. User pastes the API key into the CLI
10
+ */
11
+
12
+ import type { OAuthController } from "./types";
13
+
14
+ const AUTH_URL = "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys&title=AI+Gateway+API+Keys";
15
+
16
+ /**
17
+ * Login to Vercel AI Gateway.
18
+ *
19
+ * Opens browser to Vercel AI Gateway docs and prompts for an API key.
20
+ * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
21
+ */
22
+ export async function loginVercelAiGateway(options: OAuthController): Promise<string> {
23
+ if (!options.onPrompt) {
24
+ throw new Error("Vercel AI Gateway login requires onPrompt callback");
25
+ }
26
+
27
+ options.onAuth?.({
28
+ url: AUTH_URL,
29
+ instructions: "Copy your Vercel AI Gateway API key from the Vercel dashboard",
30
+ });
31
+
32
+ const apiKey = await options.onPrompt({
33
+ message: "Paste your Vercel AI Gateway API key",
34
+ placeholder: "vck_...",
35
+ });
36
+
37
+ if (options.signal?.aborted) {
38
+ throw new Error("Login cancelled");
39
+ }
40
+
41
+ const trimmed = apiKey.trim();
42
+ if (!trimmed) {
43
+ throw new Error("API key is required");
44
+ }
45
+
46
+ return trimmed;
47
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * vLLM login flow.
3
+ *
4
+ * vLLM is commonly self-hosted with an OpenAI-compatible API at a local base URL.
5
+ * Some deployments require a bearer token, others allow unauthenticated access.
6
+ *
7
+ * This flow stores an API-key-style credential used by `/login` and auth storage.
8
+ */
9
+
10
+ import type { OAuthController, OAuthProvider } from "./types";
11
+
12
+ const PROVIDER_ID: OAuthProvider = "vllm";
13
+ const AUTH_URL = "https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html";
14
+ const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:8000/v1";
15
+ const DEFAULT_LOCAL_TOKEN = "vllm-local";
16
+ /**
17
+ * Login to vLLM.
18
+ *
19
+ * Opens vLLM OpenAI-compatible auth docs, prompts for an optional token,
20
+ * and returns a stored key value.
21
+ */
22
+ export async function loginVllm(options: OAuthController): Promise<string> {
23
+ if (!options.onPrompt) {
24
+ throw new Error(`${PROVIDER_ID} login requires onPrompt callback`);
25
+ }
26
+ options.onAuth?.({
27
+ url: AUTH_URL,
28
+ instructions: `Paste your vLLM API key if your server requires auth. Leave empty for local no-auth mode (default base URL: ${DEFAULT_LOCAL_BASE_URL}).`,
29
+ });
30
+ const apiKey = await options.onPrompt({
31
+ message: "Paste your vLLM API key (optional for local no-auth)",
32
+ placeholder: DEFAULT_LOCAL_TOKEN,
33
+ allowEmpty: true,
34
+ });
35
+ if (options.signal?.aborted) {
36
+ throw new Error("Login cancelled");
37
+ }
38
+ const trimmed = apiKey.trim();
39
+ return trimmed || DEFAULT_LOCAL_TOKEN;
40
+ }