dsh-coding-subscription-oauth 0.6.4 → 0.6.5

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 (144) hide show
  1. package/CHANGELOG.md +280 -255
  2. package/CONTRIBUTING.md +138 -129
  3. package/INSTALL.md +256 -255
  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 +320 -320
  12. package/README.pt-BR.md +304 -304
  13. package/README.ru.md +304 -304
  14. package/README.zh-CN.md +318 -318
  15. package/compatibility/dsh-bom.json +30 -30
  16. package/cordis.patch.yml +13 -13
  17. package/docs/00-project-rules.md +212 -212
  18. package/docs/02-architecture.md +141 -137
  19. package/docs/02-architecture.zh-CN.md +141 -137
  20. package/lib/bin.js.map +1 -1
  21. package/lib/client.js +1 -1
  22. package/lib/client.js.map +1 -1
  23. package/lib/gateway-protocol.d.ts +3 -46
  24. package/lib/gateway-protocol.d.ts.map +1 -1
  25. package/lib/gateway-routes.d.ts.map +1 -1
  26. package/lib/grok-errors.d.ts +2 -12
  27. package/lib/grok-errors.d.ts.map +1 -1
  28. package/lib/grok-imagine/client.d.ts +73 -0
  29. package/lib/grok-imagine/client.d.ts.map +1 -0
  30. package/lib/grok-imagine/index.d.ts +8 -0
  31. package/lib/grok-imagine/index.d.ts.map +1 -0
  32. package/lib/grok-imagine/net.d.ts +10 -0
  33. package/lib/grok-imagine/net.d.ts.map +1 -0
  34. package/lib/grok-imagine/parse.d.ts +20 -0
  35. package/lib/grok-imagine/parse.d.ts.map +1 -0
  36. package/lib/grok-imagine/types.d.ts +191 -0
  37. package/lib/grok-imagine/types.d.ts.map +1 -0
  38. package/lib/grok-imagine.d.ts +3 -263
  39. package/lib/grok-imagine.d.ts.map +1 -1
  40. package/lib/http-json.d.ts +2 -9
  41. package/lib/http-json.d.ts.map +1 -1
  42. package/lib/index.js +57 -37
  43. package/lib/index.js.map +4 -4
  44. package/lib/invariant.js.map +1 -1
  45. package/lib/kimi-errors.d.ts +2 -12
  46. package/lib/kimi-errors.d.ts.map +1 -1
  47. package/lib/runtime/gateway-protocol.d.ts +47 -0
  48. package/lib/runtime/gateway-protocol.d.ts.map +1 -0
  49. package/lib/runtime/grok-errors.d.ts +13 -0
  50. package/lib/runtime/grok-errors.d.ts.map +1 -0
  51. package/lib/runtime/http-json.d.ts +10 -0
  52. package/lib/runtime/http-json.d.ts.map +1 -0
  53. package/lib/runtime/kimi-errors.d.ts +13 -0
  54. package/lib/runtime/kimi-errors.d.ts.map +1 -0
  55. package/media/en/settings_accounts.png +0 -0
  56. package/media/en/settings_capabilities.png +0 -0
  57. package/media/en/settings_gateway.png +0 -0
  58. package/media/settings_accounts.png +0 -0
  59. package/media/settings_capabilities.png +0 -0
  60. package/media/settings_gateway.png +0 -0
  61. package/media/settings_overview.png +0 -0
  62. package/media/zh-CN/settings_accounts.png +0 -0
  63. package/media/zh-CN/settings_capabilities.png +0 -0
  64. package/media/zh-CN/settings_gateway.png +0 -0
  65. package/package.json +225 -220
  66. package/patches/dsh-agy@0.1.2.patch +25 -25
  67. package/scripts/release.mjs +187 -186
  68. package/scripts/smoke-deployed-routes.mjs +146 -146
  69. package/scripts/verify-deployed-catalog.mjs +87 -87
  70. package/src/adapter.ts +0 -348
  71. package/src/alias-adapter.ts +0 -147
  72. package/src/auth-routes.ts +0 -921
  73. package/src/auth.ts +0 -67
  74. package/src/bin.ts +0 -350
  75. package/src/capability-routes.ts +0 -279
  76. package/src/capability-runtime.ts +0 -314
  77. package/src/capability-settings.ts +0 -671
  78. package/src/capability-tools.ts +0 -685
  79. package/src/catalog.ts +0 -271
  80. package/src/client/GrokBuildSettings.tsx +0 -771
  81. package/src/client/api.ts +0 -88
  82. package/src/client/components/AboutTab.tsx +0 -30
  83. package/src/client/components/AccountsTab.tsx +0 -241
  84. package/src/client/components/Badge.tsx +0 -33
  85. package/src/client/components/CapabilitiesTab.tsx +0 -265
  86. package/src/client/components/CliPullPreview.tsx +0 -116
  87. package/src/client/components/CopyButton.tsx +0 -57
  88. package/src/client/components/GatewayTab.tsx +0 -469
  89. package/src/client/components/NoticeBanner.tsx +0 -46
  90. package/src/client/components/ProgressBar.tsx +0 -53
  91. package/src/client/components/ProviderCard.tsx +0 -606
  92. package/src/client/components/SettingsTabs.tsx +0 -75
  93. package/src/client/components/ToggleSwitch.tsx +0 -71
  94. package/src/client/constants.ts +0 -230
  95. package/src/client/display.ts +0 -61
  96. package/src/client/dshClientAdapter.ts +0 -127
  97. package/src/client/gatewaySnippets.ts +0 -37
  98. package/src/client/index.tsx +0 -156
  99. package/src/client/locales.ts +0 -540
  100. package/src/client/microStyles.ts +0 -52
  101. package/src/client/parsers.ts +0 -398
  102. package/src/client/styles.ts +0 -325
  103. package/src/client/types.ts +0 -199
  104. package/src/codex-http.ts +0 -447
  105. package/src/codex-images.ts +0 -503
  106. package/src/codex-model-capabilities.ts +0 -320
  107. package/src/codex-search.ts +0 -245
  108. package/src/codex-usage.ts +0 -263
  109. package/src/compatibility.ts +0 -55
  110. package/src/dsh-host-adapter.ts +0 -173
  111. package/src/gateway-anthropic-messages.ts +0 -84
  112. package/src/gateway-auth.ts +0 -102
  113. package/src/gateway-backend.ts +0 -274
  114. package/src/gateway-body.ts +0 -49
  115. package/src/gateway-config.ts +0 -76
  116. package/src/gateway-http.ts +0 -104
  117. package/src/gateway-openai-chat.ts +0 -124
  118. package/src/gateway-openai-responses.ts +0 -53
  119. package/src/gateway-parse.ts +0 -224
  120. package/src/gateway-protocol.ts +0 -52
  121. package/src/gateway-routes.ts +0 -158
  122. package/src/gateway.ts +0 -258
  123. package/src/grok-errors.ts +0 -24
  124. package/src/grok-imagine.ts +0 -1627
  125. package/src/grok-import.ts +0 -151
  126. package/src/http-json.ts +0 -82
  127. package/src/ids.ts +0 -59
  128. package/src/imagine-routes.ts +0 -463
  129. package/src/index.ts +0 -735
  130. package/src/invariant.ts +0 -17
  131. package/src/kimi-errors.ts +0 -26
  132. package/src/media-store.ts +0 -927
  133. package/src/oauth-import-routes.ts +0 -324
  134. package/src/oauth-providers.ts +0 -152
  135. package/src/oauth-session.ts +0 -183
  136. package/src/oauth-sources.ts +0 -1104
  137. package/src/oauth.ts +0 -620
  138. package/src/provider.ts +0 -128
  139. package/src/proxy.ts +0 -11
  140. package/src/redact.ts +0 -72
  141. package/src/session.ts +0 -218
  142. package/src/store.ts +0 -217
  143. package/src/web-origin.ts +0 -296
  144. 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
- }