@visual-z/xeno 0.8.4 → 0.9.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 (350) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,853 +0,0 @@
1
- /**
2
- * Subscription OAuth: PKCE, authorization-code exchange, refresh, identity.
3
- *
4
- * Ported from pi (earendil-works/pi, MIT) — packages/ai/src/auth/oauth/pkce.ts
5
- * and packages/ai/src/auth/oauth/anthropic.ts / openai-codex.ts / github-copilot.ts.
6
- *
7
- * D18 is the one place this is NOT a port: Anthropic OAuth requests send
8
- * `Authorization: Bearer` and `anthropic-beta: oauth-2025-04-20`, identify as
9
- * `xeno/<version>`, and never as `claude-cli`. A subscription endpoint that
10
- * rejects an honest User-Agent is reported, not papered over.
11
- *
12
- * Tokens, refresh tokens and `code_verifier` stay out of errors and logs.
13
- * Failures name the HTTP status, never the body that might echo a secret.
14
- */
15
-
16
- import { createServer, type Server } from "node:http";
17
- import { pollDeviceCode } from "./oauth-device.ts";
18
- import { parseDeviceAuthorization, parseOAuthTokens } from "../core/oauth-fields.ts";
19
- import { XENO_VERSION } from "./version.ts";
20
-
21
- /**
22
- * Minutes subtracted from server expiry so a request issued at the edge
23
- * refreshes first. Copied from pi's Anthropic client; a zero margin fails
24
- * at the boundary, which is the request that most needed to succeed.
25
- */
26
- export const OAUTH_EXPIRY_MARGIN_MS = 5 * 60 * 1000;
27
-
28
- export const SUBSCRIPTION_NOTICE =
29
- "This uses first-party client credentials. The terms-of-service risk is on your account, not this project.";
30
-
31
- export const SUBSCRIPTION_OPTIONS = [
32
- { id: "claude-pro", provider: "anthropic", label: "Claude Pro/Max" },
33
- { id: "codex", provider: "openai-codex", label: "ChatGPT Plus/Pro (Codex)" },
34
- { id: "github-copilot", provider: "github-copilot", label: "GitHub Copilot" },
35
- { id: "grok", provider: "xai-grok", label: "Grok (official account)" },
36
- ] as const;
37
-
38
- export type SubscriptionId = (typeof SUBSCRIPTION_OPTIONS)[number]["id"];
39
-
40
- export interface OAuthCredential {
41
- type: "oauth";
42
- access: string;
43
- refresh: string;
44
- expires: number;
45
- accountId?: string;
46
- enterpriseUrl?: string;
47
- }
48
-
49
- export interface PkcePair {
50
- verifier: string;
51
- challenge: string;
52
- }
53
-
54
- /** Call signature only — `typeof fetch` also has `preconnect`, which a test spy does not. */
55
- export type OAuthFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
56
-
57
- export type CallbackOutcome = { ok: true; code: string } | { ok: false; reason: string };
58
-
59
- export interface CallbackServer {
60
- readonly host: string;
61
- readonly port: number;
62
- waitForCode(): Promise<CallbackOutcome>;
63
- close(): Promise<void>;
64
- }
65
-
66
- export interface RefreshEffects {
67
- fetch: OAuthFetch;
68
- now: () => number;
69
- signal?: AbortSignal;
70
- }
71
-
72
- export interface OAuthInteraction {
73
- notify: (message: string) => void;
74
- prompt: (message: string, signal: AbortSignal) => Promise<string>;
75
- fetch: OAuthFetch;
76
- signal: AbortSignal;
77
- now?: () => number;
78
- openBrowser?: (url: string) => void;
79
- }
80
-
81
- const decode = (s: string) => atob(s);
82
- // First-party client IDs, stored the same way pi stores them. They are not
83
- // credentials we were issued; the ToS risk is the user's. See the card.
84
- const ANTHROPIC_CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
85
- const COPILOT_CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
86
- const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
87
-
88
- const ANTHROPIC_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
89
- const ANTHROPIC_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
90
- export const ANTHROPIC_CALLBACK_PORT = 53692;
91
- export const ANTHROPIC_CALLBACK_PATH = "/callback";
92
- export const ANTHROPIC_REDIRECT_URI = `http://localhost:${ANTHROPIC_CALLBACK_PORT}${ANTHROPIC_CALLBACK_PATH}`;
93
- const ANTHROPIC_SCOPES =
94
- "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
95
-
96
- const OPENAI_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
97
- const OPENAI_TOKEN_URL = "https://auth.openai.com/oauth/token";
98
- export const OPENAI_CALLBACK_PORT = 1455;
99
- export const OPENAI_CALLBACK_PATH = "/auth/callback";
100
- export const OPENAI_REDIRECT_URI = `http://localhost:${OPENAI_CALLBACK_PORT}${OPENAI_CALLBACK_PATH}`;
101
- const OPENAI_SCOPE = "openid profile email offline_access";
102
- const OPENAI_JWT_CLAIM = "https://api.openai.com/auth";
103
-
104
- // Official Grok Build config.rs / device_code.rs, verified 2026-10-07.
105
- // This is the public first-party client, with xeno attribution retained (D18).
106
- const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
107
- const GROK_AUTH_ORIGIN = "https://auth.x.ai";
108
- const GROK_TOKEN_URL = `${GROK_AUTH_ORIGIN}/oauth2/token`;
109
- // Only inference and refresh scopes: xeno does not store remote conversations/workspaces.
110
- const GROK_SCOPE = "openid profile email offline_access grok-cli:access api:access";
111
- export const GROK_PROVIDER = "xai-grok";
112
- export const GROK_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
113
- /** Official HTTP timeout: issuer requests cannot keep a cancelled or expired grant hanging indefinitely. */
114
- const GROK_REQUEST_TIMEOUT_MS = 15_000;
115
- /**
116
- * The proxy's version gate reads only `x-grok-client-version`
117
- * (grok-build xai-grok-sampler/src/client.rs; agent/proxy_headers.rs: "required
118
- * by the proxy's version-gate check"); without it every inference is HTTP 426
119
- * "version (none) is outdated". It names the protocol revision xeno speaks,
120
- * not xeno's own version -- the User-Agent and client identifier still say
121
- * xeno (D18). Pinned, not fetched: a registry lookup would put a network
122
- * call in front of every session. 1.0.46 is what CLIProxyAPI and Cherry
123
- * Studio send (minimum 1.0.13 as of 2026-10-09; stable 1.0.50). When the
124
- * minimum passes it, 426 comes back and grokVersionHint() says so.
125
- */
126
- export const GROK_PROTOCOL_VERSION = "1.0.46";
127
- /** Shown when the proxy's version gate refuses a request. */
128
- export function grokVersionHint(status: number | undefined, provider: string): string | undefined {
129
- return status === 426 && provider === GROK_PROVIDER
130
- ? `the Grok proxy now requires a newer client protocol than xeno sends (${GROK_PROTOCOL_VERSION}); update xeno, or use the xai provider with XAI_API_KEY meanwhile`
131
- : undefined;
132
- }
133
-
134
- async function grokPost(url: string, fields: Record<string, string>, effects: Pick<RefreshEffects, "fetch" | "signal">): Promise<{ status: number; json: unknown }> {
135
- const timeout = AbortSignal.timeout(GROK_REQUEST_TIMEOUT_MS);
136
- const signal = effects.signal ? AbortSignal.any([effects.signal, timeout]) : timeout;
137
- try {
138
- const response = await effects.fetch(url, { method: "POST", redirect: "error", signal,
139
- headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", "User-Agent": `xeno/${XENO_VERSION}` },
140
- body: new URLSearchParams(fields) });
141
- let json: unknown;
142
- try { json = await response.json(); } catch { json = undefined; }
143
- return { status: response.status, json };
144
- } catch {
145
- // Network errors can include request details; never expose a credential-bearing body.
146
- throw new Error(effects.signal?.aborted ? "Login cancelled" : "Grok authentication request failed");
147
- }
148
- }
149
-
150
- export async function loginGrok(interaction: OAuthInteraction, sleep?: Parameters<typeof pollDeviceCode>[0]["sleep"]): Promise<OAuthCredential> {
151
- const started = await grokPost(`${GROK_AUTH_ORIGIN}/oauth2/device/code`,
152
- { client_id: GROK_CLIENT_ID, scope: GROK_SCOPE, referrer: "xeno" }, interaction);
153
- if (started.status < 200 || started.status >= 300) throw new Error(`Grok device code request failed (HTTP ${started.status})`);
154
- const device = parseDeviceAuthorization(started.json, [GROK_AUTH_ORIGIN, "https://accounts.x.ai"]);
155
- if (!device) throw new Error("Grok device code response was incomplete or untrusted");
156
- interaction.signal.throwIfAborted();
157
- const url = device.verificationUriComplete ?? device.verificationUri;
158
- interaction.notify(`Open ${url}\nand enter code: ${device.userCode}`);
159
- tryOpen(url, interaction.openBrowser);
160
- return pollDeviceCode<OAuthCredential>({
161
- expiresInSeconds: device.expiresIn, waitBeforeFirstPoll: true, signal: interaction.signal,
162
- ...(device.interval !== undefined ? { intervalSeconds: device.interval } : {}), ...(sleep ? { sleep } : {}),
163
- poll: async () => {
164
- const got = await grokPost(GROK_TOKEN_URL,
165
- { client_id: GROK_CLIENT_ID, device_code: device.deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }, interaction);
166
- const body = asRecord(got.json);
167
- // RFC 8628 errors use HTTP 400; they are polling states, never completed logins.
168
- if (got.status === 400 && body?.["error"] === "authorization_pending") return { status: "pending" };
169
- if (got.status === 400 && body?.["error"] === "slow_down") return { status: "slow_down" };
170
- if (got.status < 200 || got.status >= 300) return { status: "failed", message: `Grok device login failed (HTTP ${got.status})` };
171
- const fields = parseOAuthTokens(got.json);
172
- if (!fields) return { status: "failed", message: "Grok token response was incomplete" };
173
- interaction.signal.throwIfAborted();
174
- return { status: "complete", value: { type: "oauth", access: fields.access, refresh: fields.refresh,
175
- expires: expiryFromExpiresIn(fields.expiresIn, (interaction.now ?? Date.now)()) } };
176
- },
177
- });
178
- }
179
-
180
- async function refreshGrok(refresh: string, effects: RefreshEffects): Promise<{ ok: true; credential: OAuthCredential } | { ok: false; reason: string }> {
181
- try {
182
- const got = await grokPost(GROK_TOKEN_URL, { client_id: GROK_CLIENT_ID, grant_type: "refresh_token", refresh_token: refresh }, effects);
183
- if (effects.signal?.aborted) return { ok: false, reason: "Login cancelled" };
184
- if (got.status < 200 || got.status >= 300) return { ok: false, reason: `HTTP ${got.status}` };
185
- const fields = parseOAuthTokens(got.json, refresh);
186
- if (!fields) return { ok: false, reason: "unexpected body" };
187
- return { ok: true, credential: { type: "oauth", access: fields.access, refresh: fields.refresh,
188
- expires: expiryFromExpiresIn(fields.expiresIn, effects.now()) } };
189
- } catch { return { ok: false, reason: "Grok token refresh failed" }; }
190
- }
191
-
192
- export function isSubscriptionLogin(id: string): boolean {
193
- return subscriptionById(id) !== undefined;
194
- }
195
-
196
- export function subscriptionById(id: string): (typeof SUBSCRIPTION_OPTIONS)[number] | undefined {
197
- // Separate account providers accept their catalog identity too; anthropic still means an API key.
198
- return SUBSCRIPTION_OPTIONS.find(o => o.id === id || (o.provider === id && o.provider !== "anthropic"));
199
- }
200
-
201
- export function expiryFromExpiresIn(expiresIn: number, now: number): number {
202
- return now + expiresIn * 1000 - OAUTH_EXPIRY_MARGIN_MS;
203
- }
204
-
205
- function base64urlEncode(bytes: Uint8Array): string {
206
- let binary = "";
207
- for (const byte of bytes) binary += String.fromCharCode(byte);
208
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
209
- }
210
-
211
- /**
212
- * Generate PKCE code verifier and challenge.
213
- * Ported from pi (earendil-works/pi, MIT) — packages/ai/src/auth/oauth/pkce.ts
214
- */
215
- export async function generatePKCE(): Promise<PkcePair> {
216
- const verifierBytes = new Uint8Array(32);
217
- crypto.getRandomValues(verifierBytes);
218
- const verifier = base64urlEncode(verifierBytes);
219
- const data = new TextEncoder().encode(verifier);
220
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
221
- const challenge = base64urlEncode(new Uint8Array(hashBuffer));
222
- return { verifier, challenge };
223
- }
224
-
225
- /** Independent of the PKCE verifier so the verifier is never printed in the URL. */
226
- export function generateState(): string {
227
- const bytes = new Uint8Array(16);
228
- crypto.getRandomValues(bytes);
229
- return base64urlEncode(bytes);
230
- }
231
-
232
- export function anthropicOAuthHeaders(): Record<string, string> {
233
- return {
234
- "user-agent": `xeno/${XENO_VERSION}`,
235
- "anthropic-beta": "oauth-2025-04-20",
236
- };
237
- }
238
-
239
- export async function dispatchAnthropicOAuthRequest(
240
- accessToken: string,
241
- opts: { fetch: OAuthFetch; url: string },
242
- ): Promise<Response> {
243
- return opts.fetch(opts.url, {
244
- method: "POST",
245
- headers: {
246
- Authorization: `Bearer ${accessToken}`,
247
- ...anthropicOAuthHeaders(),
248
- },
249
- });
250
- }
251
-
252
- export function anthropicAuthorizeUrl(opts: { challenge: string; state: string }): string {
253
- const params = new URLSearchParams({
254
- code: "true",
255
- client_id: ANTHROPIC_CLIENT_ID,
256
- response_type: "code",
257
- redirect_uri: ANTHROPIC_REDIRECT_URI,
258
- scope: ANTHROPIC_SCOPES,
259
- code_challenge: opts.challenge,
260
- code_challenge_method: "S256",
261
- state: opts.state,
262
- });
263
- return `${ANTHROPIC_AUTHORIZE_URL}?${params.toString()}`;
264
- }
265
-
266
- export function openaiCodexAuthorizeUrl(opts: { challenge: string; state: string }): string {
267
- const url = new URL(OPENAI_AUTHORIZE_URL);
268
- url.searchParams.set("response_type", "code");
269
- url.searchParams.set("client_id", OPENAI_CODEX_CLIENT_ID);
270
- url.searchParams.set("redirect_uri", OPENAI_REDIRECT_URI);
271
- url.searchParams.set("scope", OPENAI_SCOPE);
272
- url.searchParams.set("code_challenge", opts.challenge);
273
- url.searchParams.set("code_challenge_method", "S256");
274
- url.searchParams.set("state", opts.state);
275
- url.searchParams.set("id_token_add_organizations", "true");
276
- url.searchParams.set("codex_cli_simplified_flow", "true");
277
- url.searchParams.set("originator", "xeno");
278
- return url.toString();
279
- }
280
-
281
- function htmlPage(title: string, body: string): string {
282
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>${title}</title></head><body><h1>${title}</h1><p>${body}</p></body></html>`;
283
- }
284
-
285
- export async function startCallbackServer(opts: {
286
- expectedState: string;
287
- host: string;
288
- port: number;
289
- path: string;
290
- }): Promise<CallbackServer> {
291
- // Binding anything other than loopback would expose the authorization code
292
- // to the local network. localhost is refused too: it can resolve to ::1
293
- // and listen on every interface depending on the stack.
294
- if (opts.host !== "127.0.0.1") {
295
- throw new Error("OAuth callback must bind 127.0.0.1");
296
- }
297
-
298
- let settle: ((value: CallbackOutcome) => void) | undefined;
299
- const waitForCodePromise = new Promise<CallbackOutcome>((resolve) => {
300
- let settled = false;
301
- settle = (value) => {
302
- if (settled) return;
303
- settled = true;
304
- resolve(value);
305
- };
306
- });
307
-
308
- const server: Server = createServer((req, res) => {
309
- try {
310
- const url = new URL(req.url ?? "", "http://127.0.0.1");
311
- if (url.pathname !== opts.path) {
312
- res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
313
- res.end(htmlPage("Not found", "Callback route not found."));
314
- return;
315
- }
316
- const error = url.searchParams.get("error");
317
- if (error) {
318
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
319
- res.end(htmlPage("Authentication failed", "The provider returned an error."));
320
- return;
321
- }
322
- const state = url.searchParams.get("state");
323
- const code = url.searchParams.get("code");
324
- if (!code || !state) {
325
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
326
- res.end(htmlPage("Authentication failed", "Missing code or state."));
327
- return;
328
- }
329
- if (state !== opts.expectedState) {
330
- // CSRF: any local process can POST here. Rejecting is the whole point
331
- // of state. Do not settle waitForCode with this code.
332
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
333
- res.end(htmlPage("Authentication failed", "State mismatch."));
334
- return;
335
- }
336
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
337
- res.end(htmlPage("Authentication successful", "You can close this window."));
338
- settle?.({ ok: true, code });
339
- } catch {
340
- res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
341
- res.end("Internal error");
342
- }
343
- });
344
-
345
- await new Promise<void>((resolve, reject) => {
346
- server.once("error", reject);
347
- server.listen(opts.port, opts.host, () => resolve());
348
- });
349
-
350
- const addr = server.address();
351
- if (addr === null || typeof addr === "string") {
352
- server.close();
353
- throw new Error("OAuth callback server has no address");
354
- }
355
-
356
- let closed = false;
357
- const close = (): Promise<void> => {
358
- if (closed) return Promise.resolve();
359
- closed = true;
360
- settle?.({ ok: false, reason: "cancelled" });
361
- return new Promise((resolve) => {
362
- server.close(() => resolve());
363
- });
364
- };
365
-
366
- return {
367
- host: opts.host,
368
- port: addr.port,
369
- waitForCode: () => waitForCodePromise,
370
- close,
371
- };
372
- }
373
-
374
- async function readJson(
375
- fetchImpl: OAuthFetch,
376
- url: string,
377
- init: RequestInit,
378
- ): Promise<{ ok: true; json: unknown } | { ok: false; status: number }> {
379
- const response = await fetchImpl(url, init);
380
- if (!response.ok) return { ok: false, status: response.status };
381
- try {
382
- return { ok: true, json: await response.json() };
383
- } catch {
384
- return { ok: false, status: response.status };
385
- }
386
- }
387
-
388
- function asRecord(value: unknown): Record<string, unknown> | null {
389
- return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
390
- }
391
-
392
- function tokenFields(json: unknown): { access: string; refresh: string; expiresIn: number } | null {
393
- const rec = asRecord(json);
394
- if (rec === null) return null;
395
- const access = rec["access_token"];
396
- const refresh = rec["refresh_token"];
397
- const expiresIn = rec["expires_in"];
398
- if (typeof access !== "string" || typeof refresh !== "string" || typeof expiresIn !== "number") {
399
- return null;
400
- }
401
- return { access, refresh, expiresIn };
402
- }
403
-
404
- export async function exchangeAnthropicCode(opts: {
405
- code: string;
406
- state: string;
407
- verifier: string;
408
- redirectUri: string;
409
- fetch: OAuthFetch;
410
- signal?: AbortSignal;
411
- now?: () => number;
412
- }): Promise<OAuthCredential> {
413
- const init: RequestInit = {
414
- method: "POST",
415
- headers: { "Content-Type": "application/json", Accept: "application/json" },
416
- body: JSON.stringify({
417
- grant_type: "authorization_code",
418
- client_id: ANTHROPIC_CLIENT_ID,
419
- code: opts.code,
420
- state: opts.state,
421
- redirect_uri: opts.redirectUri,
422
- code_verifier: opts.verifier,
423
- }),
424
- };
425
- if (opts.signal !== undefined) init.signal = opts.signal;
426
- const got = await readJson(opts.fetch, ANTHROPIC_TOKEN_URL, init);
427
- if (!got.ok) throw new Error(`Anthropic token exchange failed (HTTP ${got.status})`);
428
- const fields = tokenFields(got.json);
429
- if (fields === null) throw new Error("Anthropic token exchange returned an unexpected body");
430
- const now = opts.now ?? Date.now;
431
- return {
432
- type: "oauth",
433
- access: fields.access,
434
- refresh: fields.refresh,
435
- expires: expiryFromExpiresIn(fields.expiresIn, now()),
436
- };
437
- }
438
-
439
- async function refreshAnthropic(
440
- refreshToken: string,
441
- opts: RefreshEffects,
442
- ): Promise<{ ok: true; credential: OAuthCredential } | { ok: false; reason: string }> {
443
- const init: RequestInit = {
444
- method: "POST",
445
- headers: { "Content-Type": "application/json", Accept: "application/json" },
446
- body: JSON.stringify({
447
- grant_type: "refresh_token",
448
- client_id: ANTHROPIC_CLIENT_ID,
449
- refresh_token: refreshToken,
450
- }),
451
- };
452
- if (opts.signal !== undefined) init.signal = opts.signal;
453
- const got = await readJson(opts.fetch, ANTHROPIC_TOKEN_URL, init);
454
- if (!got.ok) return { ok: false, reason: `HTTP ${got.status}` };
455
- const fields = tokenFields(got.json);
456
- if (fields === null) return { ok: false, reason: "unexpected body" };
457
- return {
458
- ok: true,
459
- credential: {
460
- type: "oauth",
461
- access: fields.access,
462
- refresh: fields.refresh,
463
- expires: expiryFromExpiresIn(fields.expiresIn, opts.now()),
464
- },
465
- };
466
- }
467
-
468
- export async function exchangeOpenAICode(opts: {
469
- code: string;
470
- verifier: string;
471
- redirectUri: string;
472
- fetch: OAuthFetch;
473
- signal?: AbortSignal;
474
- now?: () => number;
475
- }): Promise<OAuthCredential> {
476
- const init: RequestInit = {
477
- method: "POST",
478
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
479
- body: new URLSearchParams({
480
- grant_type: "authorization_code",
481
- client_id: OPENAI_CODEX_CLIENT_ID,
482
- code: opts.code,
483
- code_verifier: opts.verifier,
484
- redirect_uri: opts.redirectUri,
485
- }),
486
- };
487
- if (opts.signal !== undefined) init.signal = opts.signal;
488
- const got = await readJson(opts.fetch, OPENAI_TOKEN_URL, init);
489
- if (!got.ok) throw new Error(`OpenAI Codex token exchange failed (HTTP ${got.status})`);
490
- const fields = tokenFields(got.json);
491
- if (fields === null) throw new Error("OpenAI Codex token exchange returned an unexpected body");
492
- return credentialsFromOpenAIToken(fields, opts.now ?? Date.now);
493
- }
494
-
495
- async function refreshOpenAI(
496
- refreshToken: string,
497
- opts: RefreshEffects,
498
- ): Promise<{ ok: true; credential: OAuthCredential } | { ok: false; reason: string }> {
499
- const init: RequestInit = {
500
- method: "POST",
501
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
502
- body: new URLSearchParams({
503
- grant_type: "refresh_token",
504
- refresh_token: refreshToken,
505
- client_id: OPENAI_CODEX_CLIENT_ID,
506
- }),
507
- };
508
- if (opts.signal !== undefined) init.signal = opts.signal;
509
- const got = await readJson(opts.fetch, OPENAI_TOKEN_URL, init);
510
- if (!got.ok) return { ok: false, reason: `HTTP ${got.status}` };
511
- const fields = tokenFields(got.json);
512
- if (fields === null) return { ok: false, reason: "unexpected body" };
513
- try {
514
- return { ok: true, credential: credentialsFromOpenAIToken(fields, opts.now) };
515
- } catch {
516
- return { ok: false, reason: "token missing account id" };
517
- }
518
- }
519
-
520
- function credentialsFromOpenAIToken(
521
- fields: { access: string; refresh: string; expiresIn: number },
522
- now: () => number,
523
- ): OAuthCredential {
524
- const accountId = accountIdFromJwt(fields.access);
525
- const cred: OAuthCredential = {
526
- type: "oauth",
527
- access: fields.access,
528
- refresh: fields.refresh,
529
- expires: expiryFromExpiresIn(fields.expiresIn, now()),
530
- };
531
- if (accountId !== null) return { ...cred, accountId };
532
- throw new Error("OpenAI Codex token missing account id");
533
- }
534
-
535
- function accountIdFromJwt(token: string): string | null {
536
- try {
537
- const parts = token.split(".");
538
- const payload = parts[1];
539
- if (parts.length !== 3 || payload === undefined) return null;
540
- const json = JSON.parse(atob(payload)) as unknown;
541
- const rec = asRecord(json);
542
- const auth = rec === null ? null : asRecord(rec[OPENAI_JWT_CLAIM]);
543
- const id = auth?.["chatgpt_account_id"];
544
- return typeof id === "string" && id.length > 0 ? id : null;
545
- } catch {
546
- return null;
547
- }
548
- }
549
-
550
- async function refreshCopilot(
551
- githubToken: string,
552
- opts: RefreshEffects,
553
- enterpriseUrl?: string,
554
- ): Promise<{ ok: true; credential: OAuthCredential } | { ok: false; reason: string }> {
555
- const domain = enterpriseUrl && enterpriseUrl.length > 0 ? enterpriseUrl : "github.com";
556
- const url = `https://api.${domain}/copilot_internal/v2/token`;
557
- const init: RequestInit = {
558
- headers: {
559
- Accept: "application/json",
560
- Authorization: `Bearer ${githubToken}`,
561
- },
562
- };
563
- if (opts.signal !== undefined) init.signal = opts.signal;
564
- const got = await readJson(opts.fetch, url, init);
565
- if (!got.ok) return { ok: false, reason: `HTTP ${got.status}` };
566
- const rec = asRecord(got.json);
567
- const token = rec?.["token"];
568
- const expiresAt = rec?.["expires_at"];
569
- if (typeof token !== "string" || typeof expiresAt !== "number") {
570
- return { ok: false, reason: "unexpected body" };
571
- }
572
- const cred: OAuthCredential = {
573
- type: "oauth",
574
- access: token,
575
- refresh: githubToken,
576
- expires: expiresAt * 1000 - OAUTH_EXPIRY_MARGIN_MS,
577
- };
578
- if (enterpriseUrl !== undefined && enterpriseUrl.length > 0) {
579
- return { ok: true, credential: { ...cred, enterpriseUrl } };
580
- }
581
- return { ok: true, credential: cred };
582
- }
583
-
584
- export async function refreshOAuthCredential(
585
- provider: string,
586
- credential: OAuthCredential,
587
- opts: RefreshEffects,
588
- ): Promise<{ ok: true; credential: OAuthCredential } | { ok: false; reason: string }> {
589
- switch (provider) {
590
- case "anthropic":
591
- return refreshAnthropic(credential.refresh, opts);
592
- case "openai":
593
- case "openai-codex":
594
- return refreshOpenAI(credential.refresh, opts);
595
- case "github-copilot":
596
- return refreshCopilot(credential.refresh, opts, credential.enterpriseUrl);
597
- case GROK_PROVIDER:
598
- return refreshGrok(credential.refresh, opts);
599
- default:
600
- return { ok: false, reason: `no oauth refresh for ${provider}` };
601
- }
602
- }
603
-
604
- export function parseAuthorizationInput(input: string): { code?: string; state?: string } {
605
- const value = input.trim();
606
- if (!value) return {};
607
- try {
608
- const url = new URL(value);
609
- const parsed: { code?: string; state?: string } = {};
610
- const code = url.searchParams.get("code");
611
- const state = url.searchParams.get("state");
612
- if (code) parsed.code = code;
613
- if (state) parsed.state = state;
614
- return parsed;
615
- } catch {
616
- // not a URL
617
- }
618
- if (value.includes("#")) {
619
- const [code, state] = value.split("#", 2);
620
- const parsed: { code?: string; state?: string } = {};
621
- if (code) parsed.code = code;
622
- if (state) parsed.state = state;
623
- return parsed;
624
- }
625
- if (value.includes("code=")) {
626
- const params = new URLSearchParams(value);
627
- const parsed: { code?: string; state?: string } = {};
628
- const code = params.get("code");
629
- const state = params.get("state");
630
- if (code) parsed.code = code;
631
- if (state) parsed.state = state;
632
- return parsed;
633
- }
634
- return { code: value };
635
- }
636
-
637
- async function raceCode(
638
- server: CallbackServer,
639
- interaction: OAuthInteraction,
640
- expectedState: string,
641
- ): Promise<string> {
642
- const manualAbort = new AbortController();
643
- const onAbort = (): void => {
644
- void server.close();
645
- };
646
- interaction.signal.addEventListener("abort", onAbort, { once: true });
647
- if (interaction.signal.aborted) onAbort();
648
- try {
649
- const pastePromise = interaction
650
- .prompt(
651
- "Complete login in the browser, or paste the redirect URL here:",
652
- manualAbort.signal,
653
- )
654
- .then((input) => ({ kind: "paste" as const, input }))
655
- .catch((err: unknown) => ({ kind: "paste-error" as const, err }));
656
- const callbackPromise = server.waitForCode().then((got) => ({ kind: "callback" as const, got }));
657
- const first = await Promise.race([pastePromise, callbackPromise]);
658
- if (first.kind === "callback" && first.got.ok) return first.got.code;
659
- if (first.kind === "paste-error") {
660
- throw first.err instanceof Error ? first.err : new Error("login cancelled");
661
- }
662
- if (first.kind === "paste") {
663
- const parsed = parseAuthorizationInput(first.input);
664
- if (parsed.state !== undefined && parsed.state !== expectedState) {
665
- throw new Error("OAuth state mismatch");
666
- }
667
- if (!parsed.code) throw new Error("Missing authorization code");
668
- return parsed.code;
669
- }
670
- // Callback settled without a code: wait for paste if it is still coming.
671
- const paste = await pastePromise;
672
- if (paste.kind === "paste-error") {
673
- throw paste.err instanceof Error ? paste.err : new Error("login cancelled");
674
- }
675
- const parsed = parseAuthorizationInput(paste.input);
676
- if (parsed.state !== undefined && parsed.state !== expectedState) {
677
- throw new Error("OAuth state mismatch");
678
- }
679
- if (!parsed.code) throw new Error("Missing authorization code");
680
- return parsed.code;
681
- } finally {
682
- interaction.signal.removeEventListener("abort", onAbort);
683
- manualAbort.abort();
684
- await server.close();
685
- }
686
- }
687
-
688
- function tryOpen(url: string, openBrowser?: (url: string) => void): void {
689
- if (openBrowser) {
690
- openBrowser(url);
691
- return;
692
- }
693
- try {
694
- Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore", stdin: "ignore" });
695
- } catch {
696
- // Headless or remote: the printed URL is the fallback.
697
- }
698
- }
699
-
700
- export async function loginAnthropic(interaction: OAuthInteraction): Promise<OAuthCredential> {
701
- const pkce = await generatePKCE();
702
- const state = generateState();
703
- const url = anthropicAuthorizeUrl({ challenge: pkce.challenge, state });
704
- const server = await startCallbackServer({
705
- expectedState: state,
706
- host: "127.0.0.1",
707
- port: ANTHROPIC_CALLBACK_PORT,
708
- path: ANTHROPIC_CALLBACK_PATH,
709
- });
710
- interaction.notify(url);
711
- interaction.notify("If the browser is on another machine, paste the final redirect URL.");
712
- tryOpen(url, interaction.openBrowser);
713
- const code = await raceCode(server, interaction, state);
714
- const exchange: Parameters<typeof exchangeAnthropicCode>[0] = {
715
- code,
716
- state,
717
- verifier: pkce.verifier,
718
- redirectUri: ANTHROPIC_REDIRECT_URI,
719
- fetch: interaction.fetch,
720
- signal: interaction.signal,
721
- };
722
- if (interaction.now !== undefined) exchange.now = interaction.now;
723
- return exchangeAnthropicCode(exchange);
724
- }
725
-
726
- export async function loginOpenAICodex(interaction: OAuthInteraction): Promise<OAuthCredential> {
727
- const pkce = await generatePKCE();
728
- const state = generateState();
729
- const url = openaiCodexAuthorizeUrl({ challenge: pkce.challenge, state });
730
- const server = await startCallbackServer({
731
- expectedState: state,
732
- host: "127.0.0.1",
733
- port: OPENAI_CALLBACK_PORT,
734
- path: OPENAI_CALLBACK_PATH,
735
- });
736
- interaction.notify(url);
737
- interaction.notify("If the browser is on another machine, paste the final redirect URL.");
738
- tryOpen(url, interaction.openBrowser);
739
- const code = await raceCode(server, interaction, state);
740
- const exchange: Parameters<typeof exchangeOpenAICode>[0] = {
741
- code,
742
- verifier: pkce.verifier,
743
- redirectUri: OPENAI_REDIRECT_URI,
744
- fetch: interaction.fetch,
745
- signal: interaction.signal,
746
- };
747
- if (interaction.now !== undefined) exchange.now = interaction.now;
748
- return exchangeOpenAICode(exchange);
749
- }
750
-
751
- export async function loginGitHubCopilot(interaction: OAuthInteraction): Promise<OAuthCredential> {
752
- const init: RequestInit = {
753
- method: "POST",
754
- headers: {
755
- Accept: "application/json",
756
- "Content-Type": "application/x-www-form-urlencoded",
757
- },
758
- body: new URLSearchParams({
759
- client_id: COPILOT_CLIENT_ID,
760
- scope: "read:user",
761
- }),
762
- signal: interaction.signal,
763
- };
764
- const started = await readJson(interaction.fetch, "https://github.com/login/device/code", init);
765
- if (!started.ok) throw new Error(`GitHub device code request failed (HTTP ${started.status})`);
766
- const rec = asRecord(started.json);
767
- const deviceCode = rec?.["device_code"];
768
- const userCode = rec?.["user_code"];
769
- const verificationUri = rec?.["verification_uri"];
770
- const interval = rec?.["interval"];
771
- const expiresIn = rec?.["expires_in"];
772
- if (
773
- typeof deviceCode !== "string" ||
774
- typeof userCode !== "string" ||
775
- typeof verificationUri !== "string" ||
776
- typeof expiresIn !== "number"
777
- ) {
778
- throw new Error("GitHub device code response was incomplete");
779
- }
780
- let parsedUri: URL;
781
- try {
782
- parsedUri = new URL(verificationUri);
783
- } catch {
784
- throw new Error("Untrusted verification_uri in device code response");
785
- }
786
- if (parsedUri.protocol !== "https:" && parsedUri.protocol !== "http:") {
787
- throw new Error("Untrusted verification_uri in device code response");
788
- }
789
- interaction.notify(`Open ${parsedUri.href} and enter code: ${userCode}`);
790
- const pollOpts: Parameters<typeof pollDeviceCode<string>>[0] = {
791
- expiresInSeconds: expiresIn,
792
- waitBeforeFirstPoll: true,
793
- signal: interaction.signal,
794
- poll: async () => {
795
- const tokenInit: RequestInit = {
796
- method: "POST",
797
- headers: {
798
- Accept: "application/json",
799
- "Content-Type": "application/x-www-form-urlencoded",
800
- },
801
- body: new URLSearchParams({
802
- client_id: COPILOT_CLIENT_ID,
803
- device_code: deviceCode,
804
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
805
- }),
806
- signal: interaction.signal,
807
- };
808
- const raw = await readJson(interaction.fetch, "https://github.com/login/oauth/access_token", tokenInit);
809
- if (!raw.ok) return { status: "failed", message: `GitHub device token failed (HTTP ${raw.status})` };
810
- const body = asRecord(raw.json);
811
- if (body !== null && typeof body["access_token"] === "string") {
812
- return { status: "complete", value: body["access_token"] };
813
- }
814
- const err = body?.["error"];
815
- if (err === "authorization_pending") return { status: "pending" };
816
- if (err === "slow_down") {
817
- const next = body?.["interval"];
818
- return typeof next === "number" ? { status: "slow_down", intervalSeconds: next } : { status: "slow_down" };
819
- }
820
- if (typeof err === "string") return { status: "failed", message: `Device flow failed: ${err}` };
821
- return { status: "failed", message: "Invalid device token response" };
822
- },
823
- };
824
- if (typeof interval === "number") pollOpts.intervalSeconds = interval;
825
- const githubToken = await pollDeviceCode(pollOpts);
826
- const refreshed = await refreshCopilot(githubToken, {
827
- fetch: interaction.fetch,
828
- now: interaction.now ?? Date.now,
829
- signal: interaction.signal,
830
- });
831
- if (!refreshed.ok) throw new Error(`GitHub Copilot token exchange failed (${refreshed.reason})`);
832
- return refreshed.credential;
833
- }
834
-
835
- export async function loginSubscription(
836
- id: SubscriptionId,
837
- interaction: OAuthInteraction,
838
- ): Promise<{ provider: string; credential: OAuthCredential }> {
839
- switch (id) {
840
- case "claude-pro":
841
- return { provider: "anthropic", credential: await loginAnthropic(interaction) };
842
- case "codex":
843
- return { provider: "openai-codex", credential: await loginOpenAICodex(interaction) };
844
- case "github-copilot":
845
- return { provider: "github-copilot", credential: await loginGitHubCopilot(interaction) };
846
- case "grok":
847
- return { provider: GROK_PROVIDER, credential: await loginGrok(interaction) };
848
- default: {
849
- const never: never = id;
850
- throw new Error(`unknown subscription login: ${String(never)}`);
851
- }
852
- }
853
- }