dsh-coding-subscription-oauth 0.6.4 → 0.7.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 (151) hide show
  1. package/CHANGELOG.md +291 -255
  2. package/CONTRIBUTING.md +138 -129
  3. package/INSTALL.md +261 -256
  4. package/LICENSE +19 -19
  5. package/NOTICE +11 -11
  6. package/README.de.md +303 -303
  7. package/README.es.md +304 -304
  8. package/README.fr.md +304 -304
  9. package/README.ja.md +304 -304
  10. package/README.ko.md +304 -304
  11. package/README.md +321 -320
  12. package/README.pt-BR.md +304 -304
  13. package/README.ru.md +304 -304
  14. package/README.zh-CN.md +319 -318
  15. package/compatibility/dsh-bom.json +36 -30
  16. package/cordis.patch.yml +13 -13
  17. package/docs/00-project-rules.md +213 -212
  18. package/docs/02-architecture.md +142 -138
  19. package/docs/02-architecture.zh-CN.md +142 -138
  20. package/lib/auth-routes.d.ts +10 -1
  21. package/lib/auth-routes.d.ts.map +1 -1
  22. package/lib/bin.js +483 -50
  23. package/lib/bin.js.map +4 -4
  24. package/lib/client.js +5 -5
  25. package/lib/client.js.map +4 -4
  26. package/lib/compatibility.d.ts +0 -1
  27. package/lib/compatibility.d.ts.map +1 -1
  28. package/lib/gateway-protocol.d.ts +3 -46
  29. package/lib/gateway-protocol.d.ts.map +1 -1
  30. package/lib/gateway-routes.d.ts.map +1 -1
  31. package/lib/grok-errors.d.ts +2 -12
  32. package/lib/grok-errors.d.ts.map +1 -1
  33. package/lib/grok-imagine/client.d.ts +73 -0
  34. package/lib/grok-imagine/client.d.ts.map +1 -0
  35. package/lib/grok-imagine/index.d.ts +8 -0
  36. package/lib/grok-imagine/index.d.ts.map +1 -0
  37. package/lib/grok-imagine/net.d.ts +10 -0
  38. package/lib/grok-imagine/net.d.ts.map +1 -0
  39. package/lib/grok-imagine/parse.d.ts +20 -0
  40. package/lib/grok-imagine/parse.d.ts.map +1 -0
  41. package/lib/grok-imagine/types.d.ts +191 -0
  42. package/lib/grok-imagine/types.d.ts.map +1 -0
  43. package/lib/grok-imagine.d.ts +3 -263
  44. package/lib/grok-imagine.d.ts.map +1 -1
  45. package/lib/http-json.d.ts +2 -9
  46. package/lib/http-json.d.ts.map +1 -1
  47. package/lib/ids.d.ts +4 -0
  48. package/lib/ids.d.ts.map +1 -1
  49. package/lib/index.d.ts +2 -1
  50. package/lib/index.d.ts.map +1 -1
  51. package/lib/index.js +730 -196
  52. package/lib/index.js.map +4 -4
  53. package/lib/invariant.js.map +1 -1
  54. package/lib/kimi-errors.d.ts +2 -12
  55. package/lib/kimi-errors.d.ts.map +1 -1
  56. package/lib/oauth-session.d.ts +2 -0
  57. package/lib/oauth-session.d.ts.map +1 -1
  58. package/lib/session.d.ts +2 -0
  59. package/lib/session.d.ts.map +1 -1
  60. package/lib/store.d.ts +73 -1
  61. package/lib/store.d.ts.map +1 -1
  62. package/media/en/settings_accounts.png +0 -0
  63. package/media/en/settings_capabilities.png +0 -0
  64. package/media/en/settings_gateway.png +0 -0
  65. package/media/settings_accounts.png +0 -0
  66. package/media/settings_capabilities.png +0 -0
  67. package/media/settings_gateway.png +0 -0
  68. package/media/settings_overview.png +0 -0
  69. package/media/zh-CN/settings_accounts.png +0 -0
  70. package/media/zh-CN/settings_capabilities.png +0 -0
  71. package/media/zh-CN/settings_gateway.png +0 -0
  72. package/package.json +224 -221
  73. package/patches/dsh-agy@0.1.2.patch +25 -25
  74. package/scripts/release.mjs +187 -186
  75. package/scripts/smoke-deployed-routes.mjs +146 -146
  76. package/scripts/verify-deployed-catalog.mjs +87 -87
  77. package/src/adapter.ts +0 -348
  78. package/src/alias-adapter.ts +0 -147
  79. package/src/auth-routes.ts +0 -921
  80. package/src/auth.ts +0 -67
  81. package/src/bin.ts +0 -350
  82. package/src/capability-routes.ts +0 -279
  83. package/src/capability-runtime.ts +0 -314
  84. package/src/capability-settings.ts +0 -671
  85. package/src/capability-tools.ts +0 -685
  86. package/src/catalog.ts +0 -271
  87. package/src/client/GrokBuildSettings.tsx +0 -771
  88. package/src/client/api.ts +0 -88
  89. package/src/client/components/AboutTab.tsx +0 -30
  90. package/src/client/components/AccountsTab.tsx +0 -241
  91. package/src/client/components/Badge.tsx +0 -33
  92. package/src/client/components/CapabilitiesTab.tsx +0 -265
  93. package/src/client/components/CliPullPreview.tsx +0 -116
  94. package/src/client/components/CopyButton.tsx +0 -57
  95. package/src/client/components/GatewayTab.tsx +0 -469
  96. package/src/client/components/NoticeBanner.tsx +0 -46
  97. package/src/client/components/ProgressBar.tsx +0 -53
  98. package/src/client/components/ProviderCard.tsx +0 -606
  99. package/src/client/components/SettingsTabs.tsx +0 -75
  100. package/src/client/components/ToggleSwitch.tsx +0 -71
  101. package/src/client/constants.ts +0 -230
  102. package/src/client/display.ts +0 -61
  103. package/src/client/dshClientAdapter.ts +0 -127
  104. package/src/client/gatewaySnippets.ts +0 -37
  105. package/src/client/index.tsx +0 -156
  106. package/src/client/locales.ts +0 -540
  107. package/src/client/microStyles.ts +0 -52
  108. package/src/client/parsers.ts +0 -398
  109. package/src/client/styles.ts +0 -325
  110. package/src/client/types.ts +0 -199
  111. package/src/codex-http.ts +0 -447
  112. package/src/codex-images.ts +0 -503
  113. package/src/codex-model-capabilities.ts +0 -320
  114. package/src/codex-search.ts +0 -245
  115. package/src/codex-usage.ts +0 -263
  116. package/src/compatibility.ts +0 -55
  117. package/src/dsh-host-adapter.ts +0 -173
  118. package/src/gateway-anthropic-messages.ts +0 -84
  119. package/src/gateway-auth.ts +0 -102
  120. package/src/gateway-backend.ts +0 -274
  121. package/src/gateway-body.ts +0 -49
  122. package/src/gateway-config.ts +0 -76
  123. package/src/gateway-http.ts +0 -104
  124. package/src/gateway-openai-chat.ts +0 -124
  125. package/src/gateway-openai-responses.ts +0 -53
  126. package/src/gateway-parse.ts +0 -224
  127. package/src/gateway-protocol.ts +0 -52
  128. package/src/gateway-routes.ts +0 -158
  129. package/src/gateway.ts +0 -258
  130. package/src/grok-errors.ts +0 -24
  131. package/src/grok-imagine.ts +0 -1627
  132. package/src/grok-import.ts +0 -151
  133. package/src/http-json.ts +0 -82
  134. package/src/ids.ts +0 -59
  135. package/src/imagine-routes.ts +0 -463
  136. package/src/index.ts +0 -735
  137. package/src/invariant.ts +0 -17
  138. package/src/kimi-errors.ts +0 -26
  139. package/src/media-store.ts +0 -927
  140. package/src/oauth-import-routes.ts +0 -324
  141. package/src/oauth-providers.ts +0 -152
  142. package/src/oauth-session.ts +0 -183
  143. package/src/oauth-sources.ts +0 -1104
  144. package/src/oauth.ts +0 -620
  145. package/src/provider.ts +0 -128
  146. package/src/proxy.ts +0 -11
  147. package/src/redact.ts +0 -72
  148. package/src/session.ts +0 -218
  149. package/src/store.ts +0 -217
  150. package/src/web-origin.ts +0 -296
  151. package/src/web-routes.ts +0 -38
@@ -1,921 +0,0 @@
1
- /** Same-origin Web settings routes for Grok Build OAuth. */
2
-
3
- import type { ServerResponse } from "node:http";
4
- import type { Context } from "@deepseek-ai/cordis";
5
- import type {} from "@deepseek-ai/dsh-host-webserver";
6
- import type {} from "@deepseek-ai/dsh-llm";
7
- import type { AuthEvent, AuthPrompt } from "@earendil-works/pi-ai";
8
- import { CODING_OAUTH_CORE_ABI } from "dsh-coding-oauth-core";
9
- import { grokBuildAuthStatus, importGrokBuildSession, loginGrokBuildSession } from "./auth.ts";
10
- import type { CatalogSource } from "./catalog.ts";
11
- import type { DshCompatibility } from "./compatibility.ts";
12
- import { probeGrokAuth } from "./grok-import.ts";
13
- import { readJsonRequest, requestErrorStatus } from "./http-json.ts";
14
- import {
15
- ANTIGRAVITY_ROUTE,
16
- CODING_OAUTH_LOGIN_CANCEL_PATH,
17
- CODING_OAUTH_LOGIN_CODE_PATH,
18
- CODING_OAUTH_LOGIN_PATH,
19
- CODING_OAUTH_LOGOUT_PATH,
20
- CODING_OAUTH_MODELS_PATH,
21
- CODING_OAUTH_STATUS_PATH,
22
- type CodingOAuthProviderSlug,
23
- GROK_BUILD_AUTH_IMPORT_PATH,
24
- GROK_BUILD_AUTH_LOGIN_CANCEL_PATH,
25
- GROK_BUILD_AUTH_LOGIN_CODE_PATH,
26
- GROK_BUILD_AUTH_LOGIN_PATH,
27
- GROK_BUILD_AUTH_LOGOUT_PATH,
28
- GROK_BUILD_AUTH_MODELS_PATH,
29
- GROK_BUILD_AUTH_STATUS_PATH,
30
- XAI_PI_PROVIDER,
31
- } from "./ids.ts";
32
- import { loginGrokBuildPkce } from "./oauth.ts";
33
- import type { SubscriptionLoginMethod } from "./oauth-providers.ts";
34
- import type { OAuthProviderSession } from "./oauth-session.ts";
35
- import { safeMessage } from "./redact.ts";
36
- import type { GrokBuildSession } from "./session.ts";
37
- import { LOOPBACK_OWNER_REQUEST_POLICY, type OwnerAccessMode, type OwnerRequestPolicy } from "./web-origin.ts";
38
- import { registerWebRouteSetupAtomically } from "./web-routes.ts";
39
-
40
- export {
41
- CODING_OAUTH_LOGIN_CANCEL_PATH,
42
- CODING_OAUTH_LOGIN_CODE_PATH,
43
- CODING_OAUTH_LOGIN_PATH,
44
- CODING_OAUTH_LOGOUT_PATH,
45
- CODING_OAUTH_MODELS_PATH,
46
- CODING_OAUTH_STATUS_PATH,
47
- GROK_BUILD_AUTH_IMPORT_PATH,
48
- GROK_BUILD_AUTH_LOGIN_CANCEL_PATH,
49
- GROK_BUILD_AUTH_LOGIN_CODE_PATH,
50
- GROK_BUILD_AUTH_LOGIN_PATH,
51
- GROK_BUILD_AUTH_LOGOUT_PATH,
52
- GROK_BUILD_AUTH_MODELS_PATH,
53
- GROK_BUILD_AUTH_STATUS_PATH,
54
- } from "./ids.ts";
55
-
56
- export type GrokBuildLoginMethod = "pkce" | "device";
57
-
58
- export type GrokBuildWebAuthStatus =
59
- | { status: "signed-out"; grokImportAvailable: boolean }
60
- | {
61
- status: "signing-in";
62
- method: GrokBuildLoginMethod;
63
- url?: string;
64
- userCode?: string;
65
- grokImportAvailable: boolean;
66
- }
67
- | {
68
- status: "signed-in";
69
- models: string[];
70
- available: string[];
71
- selected: string[];
72
- catalogSource: CatalogSource;
73
- catalogError?: string;
74
- grokImportAvailable: boolean;
75
- }
76
- | { status: "error"; message: string; grokImportAvailable: boolean };
77
-
78
- export interface LoginChallenge {
79
- method: GrokBuildLoginMethod;
80
- url: string;
81
- userCode?: string;
82
- }
83
-
84
- function waitForPromptAbort(prompt: AuthPrompt): Promise<string> {
85
- const signal = prompt.signal;
86
- if (signal === undefined) return new Promise<string>(() => {});
87
- if (signal.aborted) return Promise.reject(signal.reason);
88
- return new Promise<string>((_resolve, reject) => {
89
- signal.addEventListener(
90
- "abort",
91
- () => {
92
- reject(signal.reason);
93
- },
94
- { once: true },
95
- );
96
- });
97
- }
98
-
99
- async function grokImportAvailable(): Promise<boolean> {
100
- return (await probeGrokAuth()).available;
101
- }
102
-
103
- /**
104
- * One lifecycle owner for the pending login (PKCE or device), the published
105
- * challenge, the pasted-code channel, and the public status.
106
- */
107
- export class GrokBuildWebAuth {
108
- private state: GrokBuildWebAuthStatus = { status: "signed-out", grokImportAvailable: false };
109
- private operation: Promise<void> | undefined;
110
- private cancellation: AbortController | undefined;
111
- private method: GrokBuildLoginMethod = "pkce";
112
- private challenge: LoginChallenge | undefined;
113
- private challengeWaiters: Array<{ resolve(value: LoginChallenge): void; reject(error: unknown): void }> = [];
114
- private codeResolver: ((code: string) => void) | undefined;
115
-
116
- constructor(private readonly session: GrokBuildSession) {}
117
-
118
- async status(): Promise<GrokBuildWebAuthStatus> {
119
- if (this.operation !== undefined) return this.state;
120
- if (this.state.status === "error") {
121
- return { ...this.state, grokImportAvailable: await grokImportAvailable() };
122
- }
123
- return this.readStoredStatus();
124
- }
125
-
126
- /** Start (or join) a login. A different method aborts and restarts the flow. */
127
- async signIn(method: GrokBuildLoginMethod): Promise<LoginChallenge> {
128
- if (this.operation !== undefined && this.method !== method) {
129
- await this.cancel();
130
- }
131
- if (this.operation === undefined) this.start(method);
132
- if (this.challenge !== undefined) return this.challenge;
133
- return new Promise<LoginChallenge>((resolve, reject) => {
134
- this.challengeWaiters.push({ resolve, reject });
135
- });
136
- }
137
-
138
- /** Hand a pasted authorization code (or redirect URL) to a pending PKCE login. */
139
- async submitCode(code: string): Promise<void> {
140
- const resolver = this.codeResolver;
141
- if (resolver === undefined) {
142
- throw new Error("grok-build: no authorization-code login is waiting for a code");
143
- }
144
- this.codeResolver = undefined;
145
- resolver(code);
146
- }
147
-
148
- /** Abort a pending login without touching any stored credential. */
149
- async cancel(): Promise<void> {
150
- this.cancellation?.abort(new Error("grok-build: sign-in cancelled"));
151
- await this.operation?.catch(() => undefined);
152
- this.codeResolver = undefined;
153
- this.challenge = undefined;
154
- this.state = await this.readStoredStatus();
155
- }
156
-
157
- async importGrok(): Promise<void> {
158
- this.cancellation?.abort(new Error("grok-build: sign-in cancelled"));
159
- await this.operation?.catch(() => undefined);
160
- this.codeResolver = undefined;
161
- await importGrokBuildSession(this.session);
162
- this.challenge = undefined;
163
- this.state = await this.readStoredStatus();
164
- }
165
-
166
- async setModels(ids: readonly string[]): Promise<void> {
167
- await this.session.setSelectedModels(ids);
168
- this.state = await this.readStoredStatus();
169
- }
170
-
171
- async signOut(): Promise<void> {
172
- this.cancellation?.abort(new Error("grok-build: sign-in cancelled"));
173
- await this.operation?.catch(() => undefined);
174
- this.codeResolver = undefined;
175
- await this.session.logout();
176
- this.state = { status: "signed-out", grokImportAvailable: await grokImportAvailable() };
177
- this.challenge = undefined;
178
- }
179
-
180
- async dispose(): Promise<void> {
181
- const failure = new Error("grok-build: plugin disposed");
182
- this.cancellation?.abort(failure);
183
- this.rejectChallenge(failure);
184
- await this.operation?.catch(() => undefined);
185
- this.codeResolver = undefined;
186
- this.challenge = undefined;
187
- }
188
-
189
- private start(method: GrokBuildLoginMethod): void {
190
- const cancellation = new AbortController();
191
- this.cancellation = cancellation;
192
- this.method = method;
193
- this.challenge = undefined;
194
- this.state = { status: "signing-in", method, grokImportAvailable: false };
195
- const run = method === "pkce" ? this.runPkce(cancellation) : this.runDevice(cancellation);
196
- this.operation = run
197
- .then(
198
- async () => {
199
- this.state = await this.readStoredStatus();
200
- },
201
- (error: unknown) => {
202
- this.rejectChallenge(error);
203
- this.state = { status: "error", message: safeMessage(error), grokImportAvailable: false };
204
- },
205
- )
206
- .finally(() => {
207
- this.operation = undefined;
208
- this.cancellation = undefined;
209
- this.codeResolver = undefined;
210
- });
211
- }
212
-
213
- private async runPkce(cancellation: AbortController): Promise<void> {
214
- const credential = await loginGrokBuildPkce({
215
- signal: cancellation.signal,
216
- onAuthorizeUrl: (url) => this.acceptChallenge({ method: "pkce", url }),
217
- awaitCode: (signal) =>
218
- new Promise<string>((resolve, reject) => {
219
- this.codeResolver = resolve;
220
- const onAbort = (): void => {
221
- this.codeResolver = undefined;
222
- reject(new Error("grok-build: sign-in cancelled"));
223
- };
224
- if (signal.aborted) {
225
- onAbort();
226
- return;
227
- }
228
- signal.addEventListener("abort", onAbort, { once: true });
229
- }),
230
- });
231
- const written = await this.session.store.modify(XAI_PI_PROVIDER, async () => credential);
232
- if (written === undefined || written.type !== "oauth") {
233
- throw new Error("grok-build: failed to persist the login credential");
234
- }
235
- await this.session.refreshLiveCatalog();
236
- }
237
-
238
- private async runDevice(cancellation: AbortController): Promise<void> {
239
- await loginGrokBuildSession(
240
- {
241
- signal: cancellation.signal,
242
- prompt: (prompt) =>
243
- prompt.type === "select"
244
- ? Promise.resolve(
245
- prompt.options.some((option) => option.id === "oauth") ? "oauth" : (prompt.options[0]?.id ?? "oauth"),
246
- )
247
- : waitForPromptAbort(prompt),
248
- notify: (event) => {
249
- this.onEvent(event);
250
- },
251
- },
252
- this.session,
253
- );
254
- }
255
-
256
- private onEvent(event: AuthEvent): void {
257
- if (event.type === "device_code") {
258
- this.acceptChallenge({
259
- method: "device",
260
- url: event.verificationUri,
261
- ...(event.userCode.length > 0 ? { userCode: event.userCode } : {}),
262
- });
263
- return;
264
- }
265
- if (event.type === "auth_url") {
266
- this.acceptChallenge({ method: this.method, url: event.url });
267
- }
268
- }
269
-
270
- private acceptChallenge(challenge: LoginChallenge): void {
271
- try {
272
- const url = new URL(challenge.url);
273
- if (url.protocol !== "https:") {
274
- const error = new Error("xAI returned an unsafe authorization URL");
275
- this.cancellation?.abort(error);
276
- this.rejectChallenge(error);
277
- return;
278
- }
279
- } catch {
280
- const error = new Error("xAI returned an invalid authorization URL");
281
- this.cancellation?.abort(error);
282
- this.rejectChallenge(error);
283
- return;
284
- }
285
- this.challenge = challenge;
286
- this.state = {
287
- status: "signing-in",
288
- method: challenge.method,
289
- url: challenge.url,
290
- grokImportAvailable: false,
291
- ...(challenge.userCode === undefined ? {} : { userCode: challenge.userCode }),
292
- };
293
- for (const waiter of this.challengeWaiters.splice(0)) waiter.resolve(challenge);
294
- }
295
-
296
- private async readStoredStatus(): Promise<GrokBuildWebAuthStatus> {
297
- const [stored, grok] = await Promise.all([grokBuildAuthStatus(this.session.store), grokImportAvailable()]);
298
- if (!stored.authenticated) return { status: "signed-out", grokImportAvailable: grok };
299
- const available = this.session.availableModels().map((model) => model.id);
300
- const selected = this.session.selectedModelIds();
301
- return {
302
- status: "signed-in",
303
- models: this.session.visibleModels().map((model) => model.id),
304
- available,
305
- selected: selected ?? available,
306
- catalogSource: this.session.catalogSource,
307
- grokImportAvailable: grok,
308
- ...(this.session.catalogError === undefined ? {} : { catalogError: this.session.catalogError }),
309
- };
310
- }
311
-
312
- private rejectChallenge(error: unknown): void {
313
- for (const waiter of this.challengeWaiters.splice(0)) waiter.reject(error);
314
- }
315
- }
316
-
317
- export type SubscriptionWebAuthStatus = {
318
- provider: Exclude<CodingOAuthProviderSlug, "grok">;
319
- route: string;
320
- displayName: string;
321
- loginMethods: readonly SubscriptionLoginMethod[];
322
- recommendedLoginMethod: SubscriptionLoginMethod;
323
- models: string[];
324
- available: string[];
325
- selected: string[];
326
- } & (
327
- | { status: "signed-out" }
328
- | { status: "signing-in"; method: SubscriptionLoginMethod; url?: string; userCode?: string }
329
- | { status: "signed-in"; expiresAt?: number }
330
- | { status: "error"; message: string }
331
- );
332
-
333
- export interface SubscriptionLoginChallenge {
334
- method: SubscriptionLoginMethod;
335
- url: string;
336
- userCode?: string;
337
- }
338
-
339
- function optionForLoginMethod(
340
- prompt: Extract<AuthPrompt, { type: "select" }>,
341
- method: SubscriptionLoginMethod,
342
- ): string {
343
- const exactIds =
344
- method === "device" ? ["device_code", "device-code", "device"] : ["browser", "browser_login", "browser-login"];
345
- for (const id of exactIds) {
346
- if (prompt.options.some((option) => option.id === id)) return id;
347
- }
348
- const label = method === "device" ? /device|headless/iu : /browser|pkce/iu;
349
- return prompt.options.find((option) => label.test(option.label))?.id ?? prompt.options[0]?.id ?? "";
350
- }
351
-
352
- /** Web lifecycle for one pi-ai subscription OAuth provider. */
353
- export class SubscriptionWebAuth {
354
- private state: SubscriptionWebAuthStatus | undefined;
355
- private operation: Promise<void> | undefined;
356
- private cancellation: AbortController | undefined;
357
- private method: SubscriptionLoginMethod;
358
- private challenge: SubscriptionLoginChallenge | undefined;
359
- private challengeWaiters: Array<{ resolve(value: SubscriptionLoginChallenge): void; reject(error: unknown): void }> =
360
- [];
361
- private codeResolver: ((code: string) => void) | undefined;
362
-
363
- constructor(
364
- readonly session: OAuthProviderSession,
365
- private readonly challengeTimeoutMs = 60_000,
366
- ) {
367
- this.method = session.definition.recommendedLoginMethod;
368
- }
369
-
370
- async status(): Promise<SubscriptionWebAuthStatus> {
371
- if (this.operation !== undefined && this.state !== undefined) return this.state;
372
- return this.readStoredStatus();
373
- }
374
-
375
- async signIn(method: SubscriptionLoginMethod): Promise<SubscriptionLoginChallenge> {
376
- if (!this.session.definition.loginMethods.includes(method)) {
377
- throw new Error(`${this.session.definition.route}: login method "${method}" is not supported`);
378
- }
379
- if (this.operation !== undefined && this.method !== method) await this.cancel();
380
- if (this.operation === undefined) this.start(method);
381
- if (this.challenge !== undefined) return this.challenge;
382
- return new Promise<SubscriptionLoginChallenge>((resolve, reject) => {
383
- let timer: ReturnType<typeof setTimeout> | undefined;
384
- const waiter = {
385
- resolve: (challenge: SubscriptionLoginChallenge): void => {
386
- if (timer !== undefined) clearTimeout(timer);
387
- resolve(challenge);
388
- },
389
- reject: (error: unknown): void => {
390
- if (timer !== undefined) clearTimeout(timer);
391
- reject(error);
392
- },
393
- };
394
- this.challengeWaiters.push(waiter);
395
- timer = setTimeout(() => {
396
- const index = this.challengeWaiters.indexOf(waiter);
397
- if (index >= 0) this.challengeWaiters.splice(index, 1);
398
- const failure = new Error(`${this.session.definition.route}: timed out waiting for an OAuth challenge`);
399
- this.cancellation?.abort(failure);
400
- reject(failure);
401
- }, this.challengeTimeoutMs);
402
- });
403
- }
404
-
405
- async submitCode(code: string): Promise<void> {
406
- const resolver = this.codeResolver;
407
- if (resolver === undefined) {
408
- throw new Error(`${this.session.definition.route}: no authorization-code login is waiting for a code`);
409
- }
410
- this.codeResolver = undefined;
411
- resolver(code);
412
- }
413
-
414
- async cancel(): Promise<void> {
415
- this.cancellation?.abort(new Error(`${this.session.definition.route}: sign-in cancelled`));
416
- await this.operation?.catch(() => undefined);
417
- this.codeResolver = undefined;
418
- this.challenge = undefined;
419
- this.state = await this.readStoredStatus();
420
- }
421
-
422
- async setModels(ids: readonly string[]): Promise<void> {
423
- await this.session.setSelectedModels(ids);
424
- this.state = await this.readStoredStatus();
425
- }
426
-
427
- async signOut(): Promise<void> {
428
- this.cancellation?.abort(new Error(`${this.session.definition.route}: sign-in cancelled`));
429
- await this.operation?.catch(() => undefined);
430
- this.codeResolver = undefined;
431
- await this.session.logout();
432
- this.challenge = undefined;
433
- this.state = await this.readStoredStatus();
434
- }
435
-
436
- async dispose(): Promise<void> {
437
- this.cancellation?.abort(new Error(`${this.session.definition.route}: plugin disposed`));
438
- await this.operation?.catch(() => undefined);
439
- this.codeResolver = undefined;
440
- this.rejectChallenge(new Error(`${this.session.definition.route}: plugin disposed`));
441
- }
442
-
443
- private baseStatus(): Omit<
444
- SubscriptionWebAuthStatus,
445
- "status" | "method" | "url" | "userCode" | "message" | "expiresAt"
446
- > {
447
- const available = this.session.availableModels().map((model) => model.id);
448
- const selected = this.session.selectedModelIds() ?? available;
449
- return {
450
- provider: this.session.definition.slug,
451
- route: this.session.definition.route,
452
- displayName: this.session.definition.displayName,
453
- loginMethods: this.session.definition.loginMethods,
454
- recommendedLoginMethod: this.session.definition.recommendedLoginMethod,
455
- models: this.session.visibleModels().map((model) => model.id),
456
- available,
457
- selected,
458
- };
459
- }
460
-
461
- private async readStoredStatus(): Promise<SubscriptionWebAuthStatus> {
462
- const base = this.baseStatus();
463
- try {
464
- const stored = await this.session.status();
465
- return stored.authenticated
466
- ? { ...base, status: "signed-in", ...(stored.expiresAt === undefined ? {} : { expiresAt: stored.expiresAt }) }
467
- : { ...base, status: "signed-out" };
468
- } catch (error: unknown) {
469
- return { ...base, status: "error", message: safeMessage(error) };
470
- }
471
- }
472
-
473
- private start(method: SubscriptionLoginMethod): void {
474
- const cancellation = new AbortController();
475
- this.cancellation = cancellation;
476
- this.method = method;
477
- this.challenge = undefined;
478
- this.state = { ...this.baseStatus(), status: "signing-in", method };
479
- this.operation = this.run(cancellation)
480
- .then(
481
- async () => {
482
- if (this.challenge === undefined)
483
- this.rejectChallenge(new Error(`${this.session.definition.route}: login completed without a challenge`));
484
- this.state = await this.readStoredStatus();
485
- },
486
- (error: unknown) => {
487
- this.rejectChallenge(error);
488
- this.state = { ...this.baseStatus(), status: "error", message: safeMessage(error) };
489
- },
490
- )
491
- .finally(() => {
492
- this.operation = undefined;
493
- this.cancellation = undefined;
494
- this.codeResolver = undefined;
495
- });
496
- }
497
-
498
- private async run(cancellation: AbortController): Promise<void> {
499
- await this.session.login({
500
- signal: cancellation.signal,
501
- prompt: (prompt) => {
502
- if (prompt.type === "select") return Promise.resolve(optionForLoginMethod(prompt, this.method));
503
- if (prompt.type === "manual_code") return this.awaitCode(prompt, cancellation.signal);
504
- return Promise.reject(new Error(`${this.session.definition.route}: unsupported OAuth prompt ${prompt.type}`));
505
- },
506
- notify: (event) => {
507
- this.onEvent(event);
508
- },
509
- });
510
- }
511
-
512
- private awaitCode(prompt: AuthPrompt, cancellation: AbortSignal): Promise<string> {
513
- const signal = prompt.signal === undefined ? cancellation : AbortSignal.any([prompt.signal, cancellation]);
514
- return new Promise<string>((resolve, reject) => {
515
- const resolver = (code: string): void => {
516
- signal.removeEventListener("abort", onAbort);
517
- resolve(code);
518
- };
519
- const onAbort = (): void => {
520
- if (this.codeResolver === resolver) this.codeResolver = undefined;
521
- reject(signal.reason);
522
- };
523
- this.codeResolver = resolver;
524
- if (signal.aborted) {
525
- onAbort();
526
- return;
527
- }
528
- signal.addEventListener("abort", onAbort, { once: true });
529
- });
530
- }
531
-
532
- private onEvent(event: AuthEvent): void {
533
- if (event.type === "device_code") {
534
- this.acceptChallenge({
535
- method: "device",
536
- url: event.verificationUri,
537
- ...(event.userCode.length > 0 ? { userCode: event.userCode } : {}),
538
- });
539
- return;
540
- }
541
- if (event.type === "auth_url") this.acceptChallenge({ method: this.method, url: event.url });
542
- }
543
-
544
- private acceptChallenge(challenge: SubscriptionLoginChallenge): void {
545
- try {
546
- const url = new URL(challenge.url);
547
- if (url.protocol !== "https:") throw new Error("authorization URL must use HTTPS");
548
- } catch (error) {
549
- const failure = new Error(`${this.session.definition.route}: invalid authorization URL`, { cause: error });
550
- this.cancellation?.abort(failure);
551
- this.rejectChallenge(failure);
552
- return;
553
- }
554
- this.challenge = challenge;
555
- this.state = {
556
- ...this.baseStatus(),
557
- status: "signing-in",
558
- method: challenge.method,
559
- url: challenge.url,
560
- ...(challenge.userCode === undefined ? {} : { userCode: challenge.userCode }),
561
- };
562
- for (const waiter of this.challengeWaiters.splice(0)) waiter.resolve(challenge);
563
- }
564
-
565
- private rejectChallenge(error: unknown): void {
566
- for (const waiter of this.challengeWaiters.splice(0)) waiter.reject(error);
567
- }
568
- }
569
-
570
- function json(res: ServerResponse, status: number, value: unknown): void {
571
- res.writeHead(status, {
572
- "content-type": "application/json; charset=utf-8",
573
- "cache-control": "no-store",
574
- "x-content-type-options": "nosniff",
575
- });
576
- res.end(JSON.stringify(value));
577
- }
578
-
579
- function readLoginMethod(body: unknown): GrokBuildLoginMethod {
580
- if (typeof body === "object" && body !== null && "method" in body && body.method === "device") return "device";
581
- return "pkce";
582
- }
583
-
584
- /** Register the plugin-owned OAuth routes when the Web server is composed. */
585
- export function registerGrokBuildAuthRoutes(
586
- ctx: Context,
587
- session: GrokBuildSession,
588
- existingAuth?: GrokBuildWebAuth,
589
- ownerRequestPolicy: OwnerRequestPolicy = LOOPBACK_OWNER_REQUEST_POLICY,
590
- ): void {
591
- const auth = existingAuth ?? new GrokBuildWebAuth(session);
592
- const ownsAuth = existingAuth === undefined;
593
- if (ownsAuth) {
594
- ctx.effect(() => () => auth.dispose(), "dsh-coding-subscription-oauth: Grok OAuth auth lifetime");
595
- }
596
- ctx.effect(() => {
597
- const releaseRoutes = registerWebRouteSetupAtomically(ctx.webServer, (webServer) => [
598
- webServer.register({
599
- kind: "exact",
600
- path: GROK_BUILD_AUTH_STATUS_PATH,
601
- handler: async (req, res) => {
602
- if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
603
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
604
- json(res, 200, await auth.status());
605
- },
606
- }),
607
- webServer.register({
608
- kind: "exact",
609
- path: GROK_BUILD_AUTH_LOGIN_PATH,
610
- handler: async (req, res) => {
611
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
612
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
613
- try {
614
- json(res, 200, await auth.signIn(readLoginMethod(await readJsonRequest(req))));
615
- } catch (error: unknown) {
616
- json(res, requestErrorStatus(error, 500), { error: safeMessage(error) });
617
- }
618
- },
619
- }),
620
- webServer.register({
621
- kind: "exact",
622
- path: GROK_BUILD_AUTH_LOGIN_CODE_PATH,
623
- handler: async (req, res) => {
624
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
625
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
626
- try {
627
- const body = await readJsonRequest(req);
628
- const code = typeof body === "object" && body !== null && "code" in body ? body.code : undefined;
629
- if (typeof code !== "string" || code.trim().length === 0) {
630
- return json(res, 400, { error: "code must be a non-empty string" });
631
- }
632
- await auth.submitCode(code);
633
- json(res, 200, { ok: true });
634
- } catch (error: unknown) {
635
- json(res, requestErrorStatus(error, 409), { error: safeMessage(error) });
636
- }
637
- },
638
- }),
639
- webServer.register({
640
- kind: "exact",
641
- path: GROK_BUILD_AUTH_LOGIN_CANCEL_PATH,
642
- handler: async (req, res) => {
643
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
644
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
645
- await auth.cancel();
646
- json(res, 200, await auth.status());
647
- },
648
- }),
649
- webServer.register({
650
- kind: "exact",
651
- path: GROK_BUILD_AUTH_IMPORT_PATH,
652
- handler: (req, res) => {
653
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
654
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
655
- json(res, 410, { error: "legacy import retired; use OAuth Pull preview and confirmation" });
656
- },
657
- }),
658
- webServer.register({
659
- kind: "exact",
660
- path: GROK_BUILD_AUTH_MODELS_PATH,
661
- handler: async (req, res) => {
662
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
663
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
664
- try {
665
- const body = await readJsonRequest(req);
666
- const selected =
667
- typeof body === "object" && body !== null && "selected" in body ? body.selected : undefined;
668
- if (!Array.isArray(selected) || selected.some((id) => typeof id !== "string")) {
669
- return json(res, 400, { error: "selected must be an array of model ids" });
670
- }
671
- await auth.setModels(selected);
672
- json(res, 200, await auth.status());
673
- } catch (error: unknown) {
674
- json(res, requestErrorStatus(error, 500), { error: safeMessage(error) });
675
- }
676
- },
677
- }),
678
- webServer.register({
679
- kind: "exact",
680
- path: GROK_BUILD_AUTH_LOGOUT_PATH,
681
- handler: async (req, res) => {
682
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
683
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
684
- await auth.signOut();
685
- json(res, 200, { ok: true });
686
- },
687
- }),
688
- ]);
689
- return releaseRoutes;
690
- }, "dsh-coding-subscription-oauth: Web OAuth routes");
691
- }
692
-
693
- export interface CodingOAuthWebStatus {
694
- accessMode: OwnerAccessMode;
695
- uiOwner: "standalone" | "hub";
696
- compatibility: DshCompatibility;
697
- providers: {
698
- grok: GrokBuildWebAuthStatus;
699
- codex: SubscriptionWebAuthStatus;
700
- kimi: SubscriptionWebAuthStatus;
701
- claude: SubscriptionWebAuthStatus;
702
- };
703
- antigravity: {
704
- installed: boolean;
705
- route: typeof ANTIGRAVITY_ROUTE;
706
- management: "cli";
707
- };
708
- }
709
-
710
- function recordBody(body: unknown): Record<string, unknown> {
711
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
712
- throw new Error("request body must be an object");
713
- }
714
- return body as Record<string, unknown>;
715
- }
716
-
717
- function providerSlug(body: unknown): CodingOAuthProviderSlug {
718
- const provider = recordBody(body)["provider"];
719
- if (provider === "grok" || provider === "codex" || provider === "kimi" || provider === "claude") return provider;
720
- throw new Error("provider must be one of grok, codex, kimi, or claude");
721
- }
722
-
723
- /** Register the unified Coding OAuth API plus the compatibility Grok routes. */
724
- export function registerCodingOAuthRoutes(
725
- ctx: Context,
726
- grokSession: GrokBuildSession,
727
- subscriptionSessions: readonly OAuthProviderSession[],
728
- ownerRequestPolicy: OwnerRequestPolicy = LOOPBACK_OWNER_REQUEST_POLICY,
729
- compatibility?: (accessMode: OwnerAccessMode) => DshCompatibility,
730
- ): void {
731
- const grok = new GrokBuildWebAuth(grokSession);
732
- const subscriptions = new Map(
733
- subscriptionSessions.map((session) => [session.definition.slug, new SubscriptionWebAuth(session)]),
734
- );
735
- const subscription = (slug: Exclude<CodingOAuthProviderSlug, "grok">): SubscriptionWebAuth => {
736
- const auth = subscriptions.get(slug);
737
- if (auth === undefined) throw new Error(`OAuth provider "${slug}" is not configured`);
738
- return auth;
739
- };
740
- ctx.effect(
741
- () => () =>
742
- Promise.all([grok.dispose(), ...[...subscriptions.values()].map((auth) => auth.dispose())]).then(() => undefined),
743
- "dsh-coding-subscription-oauth: Coding OAuth auth lifetime",
744
- );
745
-
746
- registerGrokBuildAuthRoutes(ctx, grokSession, grok, ownerRequestPolicy);
747
-
748
- const allStatus = async (accessMode: OwnerAccessMode): Promise<CodingOAuthWebStatus> => {
749
- const [grokStatus, codex, kimi, claude] = await Promise.all([
750
- grok.status().catch(
751
- async (error: unknown): Promise<GrokBuildWebAuthStatus> => ({
752
- status: "error",
753
- message: safeMessage(error),
754
- grokImportAvailable: await grokImportAvailable().catch(() => false),
755
- }),
756
- ),
757
- subscription("codex").status(),
758
- subscription("kimi").status(),
759
- subscription("claude").status(),
760
- ]);
761
- let antigravityInstalled = false;
762
- try {
763
- antigravityInstalled = ctx.llm.listProviders().some((provider) => provider.id === ANTIGRAVITY_ROUTE);
764
- } catch {
765
- // Account cards remain usable even when an unrelated adapter list fails.
766
- }
767
- return {
768
- accessMode,
769
- uiOwner: "standalone",
770
- compatibility: compatibility?.(accessMode) ?? fallbackCompatibility(accessMode),
771
- providers: { grok: grokStatus, codex, kimi, claude },
772
- antigravity: {
773
- installed: antigravityInstalled,
774
- route: ANTIGRAVITY_ROUTE,
775
- management: "cli",
776
- },
777
- };
778
- };
779
-
780
- ctx.effect(() => {
781
- const releaseRoutes = registerWebRouteSetupAtomically(ctx.webServer, (webServer) => [
782
- webServer.register({
783
- kind: "exact",
784
- path: CODING_OAUTH_STATUS_PATH,
785
- handler: async (req, res) => {
786
- if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
787
- const decision = ownerRequestPolicy.authorize(req);
788
- if (!decision.authorized || decision.accessMode === undefined) return json(res, 403, { error: "forbidden" });
789
- try {
790
- json(res, 200, await allStatus(decision.accessMode));
791
- } catch (error: unknown) {
792
- json(res, requestErrorStatus(error, 500), { error: safeMessage(error) });
793
- }
794
- },
795
- }),
796
- webServer.register({
797
- kind: "exact",
798
- path: CODING_OAUTH_LOGIN_PATH,
799
- handler: async (req, res) => {
800
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
801
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
802
- try {
803
- const body = await readJsonRequest(req);
804
- const slug = providerSlug(body);
805
- const value = recordBody(body);
806
- if (slug === "grok") {
807
- const method: GrokBuildLoginMethod = value["method"] === "device" ? "device" : "pkce";
808
- return json(res, 200, await grok.signIn(method));
809
- }
810
- const auth = subscription(slug);
811
- const requested = value["method"];
812
- const method: SubscriptionLoginMethod =
813
- requested === "browser" || requested === "device"
814
- ? requested
815
- : auth.session.definition.recommendedLoginMethod;
816
- json(res, 200, await auth.signIn(method));
817
- } catch (error: unknown) {
818
- json(res, requestErrorStatus(error, 500), { error: safeMessage(error) });
819
- }
820
- },
821
- }),
822
- webServer.register({
823
- kind: "exact",
824
- path: CODING_OAUTH_LOGIN_CODE_PATH,
825
- handler: async (req, res) => {
826
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
827
- if (!ownerRequestPolicy.authorize(req).authorized) return json(res, 403, { error: "forbidden" });
828
- try {
829
- const body = await readJsonRequest(req);
830
- const slug = providerSlug(body);
831
- const code = recordBody(body)["code"];
832
- if (typeof code !== "string" || code.trim().length === 0) {
833
- return json(res, 400, { error: "code must be a non-empty string" });
834
- }
835
- if (slug === "grok") await grok.submitCode(code);
836
- else await subscription(slug).submitCode(code);
837
- json(res, 200, { ok: true });
838
- } catch (error: unknown) {
839
- json(res, requestErrorStatus(error, 409), { error: safeMessage(error) });
840
- }
841
- },
842
- }),
843
- webServer.register({
844
- kind: "exact",
845
- path: CODING_OAUTH_LOGIN_CANCEL_PATH,
846
- handler: async (req, res) => {
847
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
848
- const decision = ownerRequestPolicy.authorize(req);
849
- if (!decision.authorized || decision.accessMode === undefined) return json(res, 403, { error: "forbidden" });
850
- try {
851
- const body = await readJsonRequest(req);
852
- const slug = providerSlug(body);
853
- if (slug === "grok") await grok.cancel();
854
- else await subscription(slug).cancel();
855
- json(res, 200, await allStatus(decision.accessMode));
856
- } catch (error: unknown) {
857
- json(res, requestErrorStatus(error, 500), { error: safeMessage(error) });
858
- }
859
- },
860
- }),
861
- webServer.register({
862
- kind: "exact",
863
- path: CODING_OAUTH_MODELS_PATH,
864
- handler: async (req, res) => {
865
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
866
- const decision = ownerRequestPolicy.authorize(req);
867
- if (!decision.authorized || decision.accessMode === undefined) return json(res, 403, { error: "forbidden" });
868
- try {
869
- const body = await readJsonRequest(req);
870
- const slug = providerSlug(body);
871
- const selected = recordBody(body)["selected"];
872
- if (!Array.isArray(selected) || selected.some((id) => typeof id !== "string")) {
873
- return json(res, 400, { error: "selected must be an array of model ids" });
874
- }
875
- if (slug === "grok") await grok.setModels(selected);
876
- else await subscription(slug).setModels(selected);
877
- json(res, 200, await allStatus(decision.accessMode));
878
- } catch (error: unknown) {
879
- json(res, requestErrorStatus(error, 500), { error: safeMessage(error) });
880
- }
881
- },
882
- }),
883
- webServer.register({
884
- kind: "exact",
885
- path: CODING_OAUTH_LOGOUT_PATH,
886
- handler: async (req, res) => {
887
- if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
888
- const decision = ownerRequestPolicy.authorize(req);
889
- if (!decision.authorized || decision.accessMode === undefined) return json(res, 403, { error: "forbidden" });
890
- try {
891
- const body = await readJsonRequest(req);
892
- const slug = providerSlug(body);
893
- if (slug === "grok") await grok.signOut();
894
- else await subscription(slug).signOut();
895
- json(res, 200, await allStatus(decision.accessMode));
896
- } catch (error: unknown) {
897
- json(res, requestErrorStatus(error, 500), { error: safeMessage(error) });
898
- }
899
- },
900
- }),
901
- ]);
902
- return releaseRoutes;
903
- }, "dsh-coding-subscription-oauth: Coding OAuth routes");
904
- }
905
-
906
- function fallbackCompatibility(accessMode: OwnerAccessMode): DshCompatibility {
907
- return {
908
- coreAbi: CODING_OAUTH_CORE_ABI,
909
- dshVersion: null,
910
- status: "degraded",
911
- uiOwner: "standalone",
912
- accessMode,
913
- capabilities: {
914
- webServer: { state: "available", contract: "exact-route-v1" },
915
- llm: { state: "available", contract: "llm-adapter-registry-v1" },
916
- settings: { state: "missing", contract: "settings-register-v1" },
917
- credentials: { state: "missing", contract: "credential-resolver-v1" },
918
- },
919
- diagnostics: [],
920
- };
921
- }