@ottocode/server 0.1.264 → 0.1.266

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 (74) hide show
  1. package/package.json +3 -3
  2. package/src/routes/auth/copilot.ts +699 -0
  3. package/src/routes/auth/oauth.ts +578 -0
  4. package/src/routes/auth/onboarding.ts +45 -0
  5. package/src/routes/auth/providers.ts +189 -0
  6. package/src/routes/auth/service.ts +167 -0
  7. package/src/routes/auth/state.ts +23 -0
  8. package/src/routes/auth/status.ts +203 -0
  9. package/src/routes/auth/wallet.ts +229 -0
  10. package/src/routes/auth.ts +12 -2080
  11. package/src/routes/config/models-service.ts +411 -0
  12. package/src/routes/config/models.ts +6 -426
  13. package/src/routes/config/providers-service.ts +237 -0
  14. package/src/routes/config/providers.ts +10 -242
  15. package/src/routes/files/handlers.ts +297 -0
  16. package/src/routes/files/service.ts +313 -0
  17. package/src/routes/files.ts +12 -608
  18. package/src/routes/git/commit-service.ts +207 -0
  19. package/src/routes/git/commit.ts +6 -220
  20. package/src/routes/git/remote-service.ts +116 -0
  21. package/src/routes/git/remote.ts +8 -115
  22. package/src/routes/git/staging-service.ts +111 -0
  23. package/src/routes/git/staging.ts +10 -205
  24. package/src/routes/mcp/auth.ts +338 -0
  25. package/src/routes/mcp/lifecycle.ts +263 -0
  26. package/src/routes/mcp/servers.ts +212 -0
  27. package/src/routes/mcp/service.ts +664 -0
  28. package/src/routes/mcp/state.ts +13 -0
  29. package/src/routes/mcp.ts +6 -1233
  30. package/src/routes/ottorouter/billing.ts +593 -0
  31. package/src/routes/ottorouter/service.ts +92 -0
  32. package/src/routes/ottorouter/topup.ts +301 -0
  33. package/src/routes/ottorouter/wallet.ts +370 -0
  34. package/src/routes/ottorouter.ts +6 -1319
  35. package/src/routes/research/service.ts +339 -0
  36. package/src/routes/research.ts +12 -390
  37. package/src/routes/sessions/crud.ts +563 -0
  38. package/src/routes/sessions/queue.ts +242 -0
  39. package/src/routes/sessions/retry.ts +121 -0
  40. package/src/routes/sessions/service.ts +768 -0
  41. package/src/routes/sessions/share.ts +434 -0
  42. package/src/routes/sessions.ts +8 -1977
  43. package/src/routes/skills/service.ts +221 -0
  44. package/src/routes/skills/spec.ts +309 -0
  45. package/src/routes/skills.ts +31 -909
  46. package/src/routes/terminals/service.ts +326 -0
  47. package/src/routes/terminals.ts +19 -295
  48. package/src/routes/tunnel/service.ts +217 -0
  49. package/src/routes/tunnel.ts +29 -219
  50. package/src/runtime/agent/registry-prompts.ts +147 -0
  51. package/src/runtime/agent/registry.ts +6 -124
  52. package/src/runtime/agent/runner-errors.ts +116 -0
  53. package/src/runtime/agent/runner-reminders.ts +45 -0
  54. package/src/runtime/agent/runner-setup-model.ts +75 -0
  55. package/src/runtime/agent/runner-setup-prompt.ts +185 -0
  56. package/src/runtime/agent/runner-setup-tools.ts +103 -0
  57. package/src/runtime/agent/runner-setup-utils.ts +21 -0
  58. package/src/runtime/agent/runner-setup.ts +54 -288
  59. package/src/runtime/agent/runner-telemetry.ts +112 -0
  60. package/src/runtime/agent/runner-text.ts +108 -0
  61. package/src/runtime/agent/runner-tool-observer.ts +86 -0
  62. package/src/runtime/agent/runner.ts +79 -378
  63. package/src/runtime/ask/service.ts +1 -0
  64. package/src/runtime/provider/custom.ts +73 -0
  65. package/src/runtime/provider/index.ts +6 -85
  66. package/src/runtime/provider/reasoning-builders.ts +280 -0
  67. package/src/runtime/provider/reasoning.ts +68 -264
  68. package/src/runtime/provider/xai.ts +8 -0
  69. package/src/tools/adapter/events.ts +116 -0
  70. package/src/tools/adapter/execution.ts +160 -0
  71. package/src/tools/adapter/pending.ts +37 -0
  72. package/src/tools/adapter/persistence.ts +166 -0
  73. package/src/tools/adapter/results.ts +97 -0
  74. package/src/tools/adapter.ts +124 -451
@@ -1,2084 +1,16 @@
1
1
  import type { Hono } from 'hono';
2
- import {
3
- getAllAuth,
4
- getAuth,
5
- setAuth,
6
- removeAuth,
7
- ensureOttoRouterWallet,
8
- getOttoRouterWallet,
9
- importWallet,
10
- loadConfig,
11
- catalog,
12
- isBuiltInProviderId,
13
- readEnvKey,
14
- getOnboardingComplete,
15
- setOnboardingComplete,
16
- authorize,
17
- exchange,
18
- authorizeWeb,
19
- exchangeWeb,
20
- authorizeOpenAIWeb,
21
- exchangeOpenAIWeb,
22
- authorizeCopilot,
23
- pollForCopilotTokenOnce,
24
- type ProviderId,
25
- } from '@ottocode/sdk';
26
- import { execFileSync, spawnSync } from 'node:child_process';
27
- import { logger } from '@ottocode/sdk';
28
- import { serializeError } from '../runtime/errors/api-error.ts';
29
- import { getProviderDetails } from './config/utils.ts';
30
- import { openApiRoute } from '../openapi/route.ts';
31
-
32
- const oauthVerifiers = new Map<
33
- string,
34
- { verifier: string; provider: string; createdAt: number; callbackUrl: string }
35
- >();
36
-
37
- const copilotDeviceSessions = new Map<
38
- string,
39
- { deviceCode: string; interval: number; provider: string; createdAt: number }
40
- >();
41
-
42
- const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
43
- const GH_CAPABILITY_CACHE_TTL_MS = 60 * 1000;
44
-
45
- let ghCapabilityCache: {
46
- expiresAt: number;
47
- value: { available: boolean; authenticated: boolean; reason?: string };
48
- } = {
49
- expiresAt: 0,
50
- value: {
51
- available: false,
52
- authenticated: false,
53
- reason: 'Not checked yet',
54
- },
55
- };
56
-
57
- function getGhImportCapability() {
58
- if (ghCapabilityCache.expiresAt > Date.now()) return ghCapabilityCache.value;
59
-
60
- const version = spawnSync('gh', ['--version'], {
61
- encoding: 'utf8',
62
- stdio: ['ignore', 'pipe', 'pipe'],
63
- });
64
- if (version.status !== 0) {
65
- ghCapabilityCache = {
66
- expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
67
- value: {
68
- available: false,
69
- authenticated: false,
70
- reason: 'GitHub CLI (gh) is not installed',
71
- },
72
- };
73
- return ghCapabilityCache.value;
74
- }
75
-
76
- const authStatus = spawnSync('gh', ['auth', 'status', '-h', 'github.com'], {
77
- encoding: 'utf8',
78
- stdio: ['ignore', 'pipe', 'pipe'],
79
- });
80
- if (authStatus.status !== 0) {
81
- ghCapabilityCache = {
82
- expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
83
- value: {
84
- available: true,
85
- authenticated: false,
86
- reason: 'Run `gh auth login` first',
87
- },
88
- };
89
- return ghCapabilityCache.value;
90
- }
91
-
92
- ghCapabilityCache = {
93
- expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
94
- value: {
95
- available: true,
96
- authenticated: true,
97
- },
98
- };
99
- return ghCapabilityCache.value;
100
- }
101
-
102
- function parseErrorMessageFromBody(text: string): string | undefined {
103
- if (!text) return undefined;
104
- try {
105
- const parsed = JSON.parse(text) as {
106
- message?: string;
107
- error?: { message?: string };
108
- };
109
- return parsed.error?.message ?? parsed.message;
110
- } catch {
111
- return undefined;
112
- }
113
- }
114
-
115
- async function fetchCopilotModels(token: string): Promise<
116
- | {
117
- ok: true;
118
- models: Set<string>;
119
- }
120
- | {
121
- ok: false;
122
- status: number;
123
- message: string;
124
- }
125
- > {
126
- try {
127
- const response = await fetch(COPILOT_MODELS_URL, {
128
- headers: {
129
- Authorization: `Bearer ${token}`,
130
- 'Openai-Intent': 'conversation-edits',
131
- 'User-Agent': 'ottocode',
132
- },
133
- });
134
- const text = await response.text();
135
- if (!response.ok) {
136
- return {
137
- ok: false,
138
- status: response.status,
139
- message:
140
- parseErrorMessageFromBody(text) ||
141
- `Copilot models endpoint returned ${response.status}`,
142
- };
143
- }
144
-
145
- const payload = JSON.parse(text) as {
146
- data?: Array<{ id?: string }>;
147
- };
148
- const models = new Set(
149
- (payload.data ?? [])
150
- .map((item) => item.id)
151
- .filter((id): id is string => Boolean(id)),
152
- );
153
- return { ok: true, models };
154
- } catch (error) {
155
- const message =
156
- error instanceof Error ? error.message : 'Failed to fetch Copilot models';
157
- return { ok: false, status: 0, message };
158
- }
159
- }
160
-
161
- async function detectOAuthOrgRestriction(token: string): Promise<{
162
- restricted: boolean;
163
- org?: string;
164
- message?: string;
165
- }> {
166
- try {
167
- const orgsResponse = await fetch('https://api.github.com/user/orgs', {
168
- headers: {
169
- Authorization: `Bearer ${token}`,
170
- 'User-Agent': 'ottocode',
171
- Accept: 'application/vnd.github+json',
172
- },
173
- });
174
- if (!orgsResponse.ok) {
175
- return { restricted: false };
176
- }
177
-
178
- const orgs = (await orgsResponse.json()) as Array<{ login?: string }>;
179
- for (const org of orgs) {
180
- if (!org.login) continue;
181
- const membershipResponse = await fetch(
182
- `https://api.github.com/user/memberships/orgs/${org.login}`,
183
- {
184
- headers: {
185
- Authorization: `Bearer ${token}`,
186
- 'User-Agent': 'ottocode',
187
- Accept: 'application/vnd.github+json',
188
- },
189
- },
190
- );
191
- if (membershipResponse.status !== 403) continue;
192
-
193
- const bodyText = await membershipResponse.text();
194
- const message = parseErrorMessageFromBody(bodyText) || bodyText;
195
- if (message.includes('enabled OAuth App access restrictions')) {
196
- return {
197
- restricted: true,
198
- org: org.login,
199
- message,
200
- };
201
- }
202
- }
203
- } catch {}
204
-
205
- return { restricted: false };
206
- }
207
-
208
- setInterval(() => {
209
- const now = Date.now();
210
- for (const [key, value] of oauthVerifiers.entries()) {
211
- if (now - value.createdAt > 10 * 60 * 1000) {
212
- oauthVerifiers.delete(key);
213
- }
214
- }
215
- for (const [key, value] of copilotDeviceSessions.entries()) {
216
- if (now - value.createdAt > 10 * 60 * 1000) {
217
- copilotDeviceSessions.delete(key);
218
- }
219
- }
220
- }, 60 * 1000);
2
+ import { registerAuthCopilotRoutes } from './auth/copilot.ts';
3
+ import { registerAuthOAuthRoutes } from './auth/oauth.ts';
4
+ import { registerAuthOnboardingRoutes } from './auth/onboarding.ts';
5
+ import { registerAuthProviderRoutes } from './auth/providers.ts';
6
+ import { registerAuthStatusRoutes } from './auth/status.ts';
7
+ import { registerAuthWalletRoutes } from './auth/wallet.ts';
221
8
 
222
9
  export function registerAuthRoutes(app: Hono) {
223
- openApiRoute(
224
- app,
225
- {
226
- method: 'get',
227
- path: '/v1/auth/status',
228
- tags: ['auth'],
229
- operationId: 'getAuthStatus',
230
- summary: 'Get auth status for all providers',
231
- responses: {
232
- '200': {
233
- description: 'OK',
234
- content: {
235
- 'application/json': {
236
- schema: {
237
- type: 'object',
238
- properties: {
239
- onboardingComplete: {
240
- type: 'boolean',
241
- },
242
- ottorouter: {
243
- type: 'object',
244
- properties: {
245
- configured: {
246
- type: 'boolean',
247
- },
248
- publicKey: {
249
- type: 'string',
250
- },
251
- },
252
- required: ['configured'],
253
- },
254
- providers: {
255
- type: 'object',
256
- additionalProperties: {
257
- type: 'object',
258
- properties: {
259
- configured: {
260
- type: 'boolean',
261
- },
262
- type: {
263
- type: 'string',
264
- enum: ['api', 'oauth', 'wallet'],
265
- },
266
- label: {
267
- type: 'string',
268
- },
269
- supportsOAuth: {
270
- type: 'boolean',
271
- },
272
- supportsToken: {
273
- type: 'boolean',
274
- },
275
- supportsGhImport: {
276
- type: 'boolean',
277
- },
278
- modelCount: {
279
- type: 'integer',
280
- },
281
- costRange: {
282
- type: 'object',
283
- nullable: true,
284
- properties: {
285
- min: {
286
- type: 'number',
287
- },
288
- max: {
289
- type: 'number',
290
- },
291
- },
292
- required: ['min', 'max'],
293
- },
294
- },
295
- required: [
296
- 'configured',
297
- 'label',
298
- 'supportsOAuth',
299
- 'modelCount',
300
- ],
301
- },
302
- },
303
- defaults: {
304
- type: 'object',
305
- properties: {
306
- agent: {
307
- type: 'string',
308
- },
309
- provider: {
310
- type: 'string',
311
- },
312
- model: {
313
- type: 'string',
314
- },
315
- },
316
- },
317
- },
318
- required: ['onboardingComplete', 'ottorouter', 'providers'],
319
- },
320
- },
321
- },
322
- },
323
- },
324
- },
325
- async (c) => {
326
- try {
327
- const projectRoot = process.cwd();
328
- const auth = await getAllAuth(projectRoot);
329
- const cfg = await loadConfig(projectRoot);
330
- const onboardingComplete = await getOnboardingComplete(projectRoot);
331
- const ottorouterWallet = await getOttoRouterWallet(projectRoot);
332
- const ghImportCapability = getGhImportCapability();
333
-
334
- const providers: Record<
335
- string,
336
- {
337
- configured: boolean;
338
- type?: 'api' | 'oauth' | 'wallet';
339
- label: string;
340
- supportsOAuth: boolean;
341
- supportsToken?: boolean;
342
- supportsGhImport?: boolean;
343
- custom?: boolean;
344
- modelCount: number;
345
- costRange?: { min: number; max: number };
346
- }
347
- > = {};
348
-
349
- for (const [id, entry] of Object.entries(catalog)) {
350
- const providerAuth = auth[id as ProviderId];
351
- const models = entry.models || [];
352
- const costs = models
353
- .map((m) => m.cost?.input)
354
- .filter((c): c is number => c !== undefined);
355
-
356
- providers[id] = {
357
- configured: !!providerAuth,
358
- type: providerAuth?.type,
359
- label: entry.label || id,
360
- supportsOAuth:
361
- id === 'anthropic' || id === 'openai' || id === 'copilot',
362
- supportsToken: id === 'copilot',
363
- supportsGhImport:
364
- id === 'copilot' ? ghImportCapability.available : false,
365
- modelCount: models.length,
366
- costRange:
367
- costs.length > 0
368
- ? {
369
- min: Math.min(...costs),
370
- max: Math.max(...costs),
371
- }
372
- : undefined,
373
- };
374
- }
375
-
376
- const providerDetails = await getProviderDetails(undefined, cfg);
377
- for (const detail of providerDetails) {
378
- if (!detail.custom || providers[detail.id]) continue;
379
- providers[detail.id] = {
380
- configured: detail.authorized,
381
- type: detail.authType,
382
- label: detail.label,
383
- supportsOAuth: false,
384
- custom: true,
385
- modelCount: detail.modelCount,
386
- };
387
- }
388
-
389
- return c.json({
390
- onboardingComplete,
391
- ottorouter: ottorouterWallet
392
- ? {
393
- configured: true,
394
- publicKey: ottorouterWallet.publicKey,
395
- }
396
- : {
397
- configured: false,
398
- },
399
- providers,
400
- defaults: cfg.defaults,
401
- });
402
- } catch (error) {
403
- logger.error('Failed to get auth status', error);
404
- const errorResponse = serializeError(error);
405
- return c.json(errorResponse, errorResponse.error.status || 500);
406
- }
407
- },
408
- );
409
-
410
- openApiRoute(
411
- app,
412
- {
413
- method: 'post',
414
- path: '/v1/auth/ottorouter/setup',
415
- tags: ['auth'],
416
- operationId: 'setupOttoRouterWallet',
417
- summary: 'Setup or ensure OttoRouter wallet',
418
- responses: {
419
- '200': {
420
- description: 'OK',
421
- content: {
422
- 'application/json': {
423
- schema: {
424
- type: 'object',
425
- properties: {
426
- success: {
427
- type: 'boolean',
428
- },
429
- publicKey: {
430
- type: 'string',
431
- },
432
- isNew: {
433
- type: 'boolean',
434
- },
435
- },
436
- required: ['success', 'publicKey', 'isNew'],
437
- },
438
- },
439
- },
440
- },
441
- },
442
- },
443
- async (c) => {
444
- try {
445
- const projectRoot = process.cwd();
446
- const existing = await getOttoRouterWallet(projectRoot);
447
- const wallet = await ensureOttoRouterWallet(projectRoot);
448
-
449
- return c.json({
450
- success: true,
451
- publicKey: wallet.publicKey,
452
- isNew: !existing,
453
- });
454
- } catch (error) {
455
- logger.error('Failed to setup OttoRouter wallet', error);
456
- const errorResponse = serializeError(error);
457
- return c.json(errorResponse, errorResponse.error.status || 500);
458
- }
459
- },
460
- );
461
-
462
- openApiRoute(
463
- app,
464
- {
465
- method: 'post',
466
- path: '/v1/auth/ottorouter/import',
467
- tags: ['auth'],
468
- operationId: 'importOttoRouterWallet',
469
- summary: 'Import OttoRouter wallet from private key',
470
- requestBody: {
471
- required: true,
472
- content: {
473
- 'application/json': {
474
- schema: {
475
- type: 'object',
476
- properties: {
477
- privateKey: {
478
- type: 'string',
479
- },
480
- },
481
- required: ['privateKey'],
482
- },
483
- },
484
- },
485
- },
486
- responses: {
487
- '200': {
488
- description: 'OK',
489
- content: {
490
- 'application/json': {
491
- schema: {
492
- type: 'object',
493
- properties: {
494
- success: {
495
- type: 'boolean',
496
- },
497
- publicKey: {
498
- type: 'string',
499
- },
500
- },
501
- required: ['success', 'publicKey'],
502
- },
503
- },
504
- },
505
- },
506
- '400': {
507
- description: 'Bad Request',
508
- content: {
509
- 'application/json': {
510
- schema: {
511
- type: 'object',
512
- properties: {
513
- error: {
514
- type: 'string',
515
- },
516
- },
517
- required: ['error'],
518
- },
519
- },
520
- },
521
- },
522
- },
523
- },
524
- async (c) => {
525
- try {
526
- const { privateKey } = await c.req.json<{ privateKey: string }>();
527
-
528
- if (!privateKey) {
529
- return c.json({ error: 'Private key required' }, 400);
530
- }
531
-
532
- try {
533
- const wallet = importWallet(privateKey);
534
- await setAuth(
535
- 'ottorouter',
536
- { type: 'wallet', secret: privateKey },
537
- undefined,
538
- 'global',
539
- );
540
-
541
- return c.json({
542
- success: true,
543
- publicKey: wallet.publicKey,
544
- });
545
- } catch {
546
- return c.json({ error: 'Invalid private key format' }, 400);
547
- }
548
- } catch (error) {
549
- logger.error('Failed to import OttoRouter wallet', error);
550
- const errorResponse = serializeError(error);
551
- return c.json(errorResponse, errorResponse.error.status || 500);
552
- }
553
- },
554
- );
555
-
556
- openApiRoute(
557
- app,
558
- {
559
- method: 'get',
560
- path: '/v1/auth/ottorouter/export',
561
- tags: ['auth'],
562
- operationId: 'exportOttoRouterWallet',
563
- summary: 'Export OttoRouter wallet private key',
564
- responses: {
565
- '200': {
566
- description: 'OK',
567
- content: {
568
- 'application/json': {
569
- schema: {
570
- type: 'object',
571
- properties: {
572
- success: {
573
- type: 'boolean',
574
- },
575
- publicKey: {
576
- type: 'string',
577
- },
578
- privateKey: {
579
- type: 'string',
580
- },
581
- },
582
- required: ['success', 'publicKey', 'privateKey'],
583
- },
584
- },
585
- },
586
- },
587
- '404': {
588
- description: 'Bad Request',
589
- content: {
590
- 'application/json': {
591
- schema: {
592
- type: 'object',
593
- properties: {
594
- error: {
595
- type: 'string',
596
- },
597
- },
598
- required: ['error'],
599
- },
600
- },
601
- },
602
- },
603
- },
604
- },
605
- async (c) => {
606
- try {
607
- const projectRoot = process.cwd();
608
- const wallet = await getOttoRouterWallet(projectRoot);
609
-
610
- if (!wallet) {
611
- return c.json({ error: 'OttoRouter wallet not configured' }, 404);
612
- }
613
-
614
- return c.json({
615
- success: true,
616
- publicKey: wallet.publicKey,
617
- privateKey: wallet.privateKey,
618
- });
619
- } catch (error) {
620
- logger.error('Failed to export OttoRouter wallet', error);
621
- const errorResponse = serializeError(error);
622
- return c.json(errorResponse, errorResponse.error.status || 500);
623
- }
624
- },
625
- );
626
-
627
- openApiRoute(
628
- app,
629
- {
630
- method: 'post',
631
- path: '/v1/auth/{provider}',
632
- tags: ['auth'],
633
- operationId: 'addProviderApiKey',
634
- summary: 'Add API key for a provider',
635
- parameters: [
636
- {
637
- in: 'path',
638
- name: 'provider',
639
- required: true,
640
- schema: {
641
- type: 'string',
642
- },
643
- },
644
- ],
645
- requestBody: {
646
- required: true,
647
- content: {
648
- 'application/json': {
649
- schema: {
650
- type: 'object',
651
- properties: {
652
- apiKey: {
653
- type: 'string',
654
- },
655
- },
656
- required: ['apiKey'],
657
- },
658
- },
659
- },
660
- },
661
- responses: {
662
- '200': {
663
- description: 'OK',
664
- content: {
665
- 'application/json': {
666
- schema: {
667
- type: 'object',
668
- properties: {
669
- success: {
670
- type: 'boolean',
671
- },
672
- provider: {
673
- type: 'string',
674
- },
675
- },
676
- required: ['success', 'provider'],
677
- },
678
- },
679
- },
680
- },
681
- '400': {
682
- description: 'Bad Request',
683
- content: {
684
- 'application/json': {
685
- schema: {
686
- type: 'object',
687
- properties: {
688
- error: {
689
- type: 'string',
690
- },
691
- },
692
- required: ['error'],
693
- },
694
- },
695
- },
696
- },
697
- },
698
- },
699
- async (c) => {
700
- try {
701
- const provider = c.req.param('provider') as ProviderId;
702
- const { apiKey } = await c.req.json<{ apiKey: string }>();
703
-
704
- if (!isBuiltInProviderId(provider) || !catalog[provider]) {
705
- return c.json({ error: 'Unknown provider' }, 400);
706
- }
707
-
708
- if (!apiKey) {
709
- return c.json({ error: 'API key required' }, 400);
710
- }
711
-
712
- await setAuth(
713
- provider,
714
- { type: 'api', key: apiKey },
715
- undefined,
716
- 'global',
717
- );
718
-
719
- return c.json({ success: true, provider });
720
- } catch (error) {
721
- logger.error('Failed to add provider', error);
722
- const errorResponse = serializeError(error);
723
- return c.json(errorResponse, errorResponse.error.status || 500);
724
- }
725
- },
726
- );
727
-
728
- openApiRoute(
729
- app,
730
- {
731
- method: 'post',
732
- path: '/v1/auth/{provider}/oauth/url',
733
- tags: ['auth'],
734
- operationId: 'getOAuthUrl',
735
- summary: 'Get OAuth authorization URL',
736
- parameters: [
737
- {
738
- in: 'path',
739
- name: 'provider',
740
- required: true,
741
- schema: {
742
- type: 'string',
743
- },
744
- },
745
- ],
746
- requestBody: {
747
- required: false,
748
- content: {
749
- 'application/json': {
750
- schema: {
751
- type: 'object',
752
- properties: {
753
- mode: {
754
- type: 'string',
755
- enum: ['max', 'console'],
756
- default: 'max',
757
- },
758
- },
759
- },
760
- },
761
- },
762
- },
763
- responses: {
764
- '200': {
765
- description: 'OK',
766
- content: {
767
- 'application/json': {
768
- schema: {
769
- type: 'object',
770
- properties: {
771
- url: {
772
- type: 'string',
773
- },
774
- sessionId: {
775
- type: 'string',
776
- },
777
- provider: {
778
- type: 'string',
779
- },
780
- },
781
- required: ['url', 'sessionId', 'provider'],
782
- },
783
- },
784
- },
785
- },
786
- '400': {
787
- description: 'Bad Request',
788
- content: {
789
- 'application/json': {
790
- schema: {
791
- type: 'object',
792
- properties: {
793
- error: {
794
- type: 'string',
795
- },
796
- },
797
- required: ['error'],
798
- },
799
- },
800
- },
801
- },
802
- },
803
- },
804
- async (c) => {
805
- try {
806
- const provider = c.req.param('provider');
807
- const body = await c.req
808
- .json<{ mode?: string }>()
809
- .catch(() => undefined);
810
- const mode: 'max' | 'console' =
811
- body?.mode === 'console' ? 'console' : 'max';
812
-
813
- let url: string;
814
- let verifier: string;
815
-
816
- if (provider === 'anthropic') {
817
- const result = await authorize(mode);
818
- url = result.url;
819
- verifier = result.verifier;
820
- } else if (provider === 'openai') {
821
- return c.json(
822
- {
823
- error:
824
- 'OpenAI OAuth requires localhost callback. Use the redirect flow instead.',
825
- },
826
- 400,
827
- );
828
- } else {
829
- return c.json(
830
- {
831
- error: `OAuth not supported for provider: ${provider}. Copilot uses device flow — use /v1/auth/copilot/device/start instead.`,
832
- },
833
- 400,
834
- );
835
- }
836
-
837
- const sessionId = crypto.randomUUID();
838
- oauthVerifiers.set(sessionId, {
839
- verifier,
840
- provider,
841
- createdAt: Date.now(),
842
- callbackUrl: '',
843
- });
844
-
845
- return c.json({ url, sessionId, provider });
846
- } catch (error) {
847
- const message =
848
- error instanceof Error
849
- ? error.message
850
- : 'OAuth initialization failed';
851
- logger.error('OAuth URL generation failed', error);
852
- return c.json({ error: message }, 500);
853
- }
854
- },
855
- );
856
-
857
- openApiRoute(
858
- app,
859
- {
860
- method: 'post',
861
- path: '/v1/auth/{provider}/oauth/exchange',
862
- tags: ['auth'],
863
- operationId: 'exchangeOAuthCode',
864
- summary: 'Exchange OAuth code for tokens',
865
- parameters: [
866
- {
867
- in: 'path',
868
- name: 'provider',
869
- required: true,
870
- schema: {
871
- type: 'string',
872
- },
873
- },
874
- ],
875
- requestBody: {
876
- required: true,
877
- content: {
878
- 'application/json': {
879
- schema: {
880
- type: 'object',
881
- properties: {
882
- code: {
883
- type: 'string',
884
- },
885
- sessionId: {
886
- type: 'string',
887
- },
888
- },
889
- required: ['code', 'sessionId'],
890
- },
891
- },
892
- },
893
- },
894
- responses: {
895
- '200': {
896
- description: 'OK',
897
- content: {
898
- 'application/json': {
899
- schema: {
900
- type: 'object',
901
- properties: {
902
- success: {
903
- type: 'boolean',
904
- },
905
- provider: {
906
- type: 'string',
907
- },
908
- },
909
- required: ['success', 'provider'],
910
- },
911
- },
912
- },
913
- },
914
- '400': {
915
- description: 'Bad Request',
916
- content: {
917
- 'application/json': {
918
- schema: {
919
- type: 'object',
920
- properties: {
921
- error: {
922
- type: 'string',
923
- },
924
- },
925
- required: ['error'],
926
- },
927
- },
928
- },
929
- },
930
- },
931
- },
932
- async (c) => {
933
- try {
934
- const provider = c.req.param('provider');
935
- const { code, sessionId } = await c.req.json<{
936
- code: string;
937
- sessionId: string;
938
- }>();
939
-
940
- if (!code || !sessionId) {
941
- return c.json({ error: 'Code and sessionId required' }, 400);
942
- }
943
-
944
- if (!oauthVerifiers.has(sessionId)) {
945
- return c.json({ error: 'Session expired or invalid' }, 400);
946
- }
947
-
948
- const verifierEntry = oauthVerifiers.get(sessionId);
949
- if (!verifierEntry) {
950
- return c.json({ error: 'Session expired or invalid' }, 400);
951
- }
952
- const { verifier } = verifierEntry;
953
- oauthVerifiers.delete(sessionId);
954
-
955
- if (provider === 'anthropic') {
956
- const tokens = await exchange(code, verifier);
957
- await setAuth(
958
- 'anthropic',
959
- {
960
- type: 'oauth',
961
- refresh: tokens.refresh,
962
- access: tokens.access,
963
- expires: tokens.expires,
964
- },
965
- undefined,
966
- 'global',
967
- );
968
- } else if (provider === 'openai') {
969
- return c.json({ error: 'Use redirect flow for OpenAI' }, 400);
970
- } else {
971
- return c.json({ error: 'Unknown provider' }, 400);
972
- }
973
-
974
- return c.json({ success: true, provider });
975
- } catch (error) {
976
- const message =
977
- error instanceof Error ? error.message : 'Token exchange failed';
978
- logger.error('OAuth exchange failed', error);
979
- return c.json({ error: message }, 500);
980
- }
981
- },
982
- );
983
-
984
- openApiRoute(
985
- app,
986
- {
987
- method: 'get',
988
- path: '/v1/auth/{provider}/oauth/start',
989
- tags: ['auth'],
990
- operationId: 'startOAuth',
991
- summary: 'Start OAuth flow with redirect',
992
- parameters: [
993
- {
994
- in: 'path',
995
- name: 'provider',
996
- required: true,
997
- schema: {
998
- type: 'string',
999
- },
1000
- },
1001
- {
1002
- in: 'query',
1003
- name: 'mode',
1004
- required: false,
1005
- schema: {
1006
- type: 'string',
1007
- enum: ['max', 'console'],
1008
- default: 'max',
1009
- },
1010
- },
1011
- ],
1012
- responses: {
1013
- '302': {
1014
- description: 'Redirect to OAuth provider',
1015
- },
1016
- '400': {
1017
- description: 'Bad Request',
1018
- content: {
1019
- 'application/json': {
1020
- schema: {
1021
- type: 'object',
1022
- properties: {
1023
- error: {
1024
- type: 'string',
1025
- },
1026
- },
1027
- required: ['error'],
1028
- },
1029
- },
1030
- },
1031
- },
1032
- },
1033
- },
1034
- async (c) => {
1035
- try {
1036
- const provider = c.req.param('provider');
1037
- const mode = c.req.query('mode') || 'max';
1038
- const host = c.req.header('host') || 'localhost:3000';
1039
- const protocol = c.req.header('x-forwarded-proto') || 'http';
1040
-
1041
- let url: string;
1042
- let verifier: string;
1043
- let callbackUrl = '';
1044
-
1045
- if (provider === 'anthropic') {
1046
- callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
1047
- const result = authorizeWeb(mode as 'max' | 'console', callbackUrl);
1048
- url = result.url;
1049
- verifier = result.verifier;
1050
- } else if (provider === 'openai') {
1051
- callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
1052
- const result = authorizeOpenAIWeb(callbackUrl);
1053
- url = result.url;
1054
- verifier = result.verifier;
1055
- } else {
1056
- return c.json(
1057
- { error: 'OAuth not supported for this provider' },
1058
- 400,
1059
- );
1060
- }
1061
-
1062
- const sessionId = crypto.randomUUID();
1063
- oauthVerifiers.set(sessionId, {
1064
- verifier,
1065
- provider,
1066
- createdAt: Date.now(),
1067
- callbackUrl,
1068
- });
1069
-
1070
- c.header(
1071
- 'Set-Cookie',
1072
- `oauth_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
1073
- );
1074
-
1075
- return c.redirect(url);
1076
- } catch (error) {
1077
- const message =
1078
- error instanceof Error
1079
- ? error.message
1080
- : 'OAuth initialization failed';
1081
- logger.error('OAuth start failed', error);
1082
- return c.json({ error: message }, 500);
1083
- }
1084
- },
1085
- );
1086
-
1087
- openApiRoute(
1088
- app,
1089
- {
1090
- method: 'get',
1091
- path: '/v1/auth/{provider}/oauth/callback',
1092
- tags: ['auth'],
1093
- operationId: 'oauthCallback',
1094
- summary: 'OAuth callback handler',
1095
- parameters: [
1096
- {
1097
- in: 'path',
1098
- name: 'provider',
1099
- required: true,
1100
- schema: {
1101
- type: 'string',
1102
- },
1103
- },
1104
- {
1105
- in: 'query',
1106
- name: 'code',
1107
- required: false,
1108
- schema: {
1109
- type: 'string',
1110
- },
1111
- },
1112
- {
1113
- in: 'query',
1114
- name: 'fragment',
1115
- required: false,
1116
- schema: {
1117
- type: 'string',
1118
- },
1119
- },
1120
- ],
1121
- responses: {
1122
- '200': {
1123
- description: 'HTML response',
1124
- content: {
1125
- 'text/html': {
1126
- schema: {
1127
- type: 'string',
1128
- },
1129
- },
1130
- },
1131
- },
1132
- },
1133
- },
1134
- async (c) => {
1135
- try {
1136
- const provider = c.req.param('provider');
1137
- const code = c.req.query('code');
1138
- const fragment = c.req.query('fragment');
1139
-
1140
- const cookies = c.req.header('Cookie') || '';
1141
- const sessionMatch = cookies.match(/oauth_session=([^;]+)/);
1142
- const sessionId = sessionMatch?.[1];
1143
-
1144
- if (!sessionId || !oauthVerifiers.has(sessionId)) {
1145
- return c.html(
1146
- '<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
1147
- );
1148
- }
1149
-
1150
- const callbackEntry = oauthVerifiers.get(sessionId);
1151
- if (!callbackEntry) {
1152
- return c.html(
1153
- '<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
1154
- );
1155
- }
1156
- const { verifier, callbackUrl } = callbackEntry;
1157
- oauthVerifiers.delete(sessionId);
1158
-
1159
- if (provider === 'anthropic') {
1160
- const fullCode = fragment ? `${code}#${fragment}` : (code ?? '');
1161
- const tokens = await exchangeWeb(fullCode, verifier, callbackUrl);
1162
-
1163
- await setAuth(
1164
- 'anthropic',
1165
- {
1166
- type: 'oauth',
1167
- refresh: tokens.refresh,
1168
- access: tokens.access,
1169
- expires: tokens.expires,
1170
- },
1171
- undefined,
1172
- 'global',
1173
- );
1174
- } else if (provider === 'openai') {
1175
- const tokens = await exchangeOpenAIWeb(
1176
- code ?? '',
1177
- verifier,
1178
- callbackUrl,
1179
- );
1180
-
1181
- await setAuth(
1182
- 'openai',
1183
- {
1184
- type: 'oauth',
1185
- refresh: tokens.refresh,
1186
- access: tokens.access,
1187
- expires: tokens.expires,
1188
- accountId: tokens.accountId,
1189
- idToken: tokens.idToken,
1190
- },
1191
- undefined,
1192
- 'global',
1193
- );
1194
- }
1195
-
1196
- return c.html(`
1197
- <html>
1198
- <head>
1199
- <title>Connected!</title>
1200
- <style>
1201
- body {
1202
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1203
- display: flex;
1204
- justify-content: center;
1205
- align-items: center;
1206
- height: 100vh;
1207
- margin: 0;
1208
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1209
- color: white;
1210
- }
1211
- .container {
1212
- text-align: center;
1213
- padding: 2rem;
1214
- background: rgba(255,255,255,0.1);
1215
- border-radius: 16px;
1216
- backdrop-filter: blur(10px);
1217
- }
1218
- .checkmark {
1219
- font-size: 4rem;
1220
- margin-bottom: 1rem;
1221
- }
1222
- h1 { margin: 0 0 0.5rem 0; }
1223
- p { margin: 0; opacity: 0.9; }
1224
- </style>
1225
- </head>
1226
- <body>
1227
- <div class="container">
1228
- <div class="checkmark">✓</div>
1229
- <h1>Connected!</h1>
1230
- <p>You can close this window.</p>
1231
- </div>
1232
- <script>
1233
- if (window.opener) {
1234
- window.opener.postMessage({ type: 'oauth-success', provider: '${provider}' }, '*');
1235
- }
1236
- setTimeout(() => window.close(), 1500);
1237
- </script>
1238
- </body>
1239
- </html>
1240
- `);
1241
- } catch (error) {
1242
- const message =
1243
- error instanceof Error ? error.message : 'Authentication failed';
1244
- logger.error('OAuth callback failed', error);
1245
- return c.html(`
1246
- <html>
1247
- <head>
1248
- <title>Error</title>
1249
- <style>
1250
- body {
1251
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1252
- display: flex;
1253
- justify-content: center;
1254
- align-items: center;
1255
- height: 100vh;
1256
- margin: 0;
1257
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
1258
- color: white;
1259
- }
1260
- .container {
1261
- text-align: center;
1262
- padding: 2rem;
1263
- background: rgba(255,255,255,0.1);
1264
- border-radius: 16px;
1265
- backdrop-filter: blur(10px);
1266
- }
1267
- .icon { font-size: 4rem; margin-bottom: 1rem; }
1268
- h1 { margin: 0 0 0.5rem 0; }
1269
- p { margin: 0; opacity: 0.9; }
1270
- </style>
1271
- </head>
1272
- <body>
1273
- <div class="container">
1274
- <div class="icon">✗</div>
1275
- <h1>Error</h1>
1276
- <p>${message}</p>
1277
- </div>
1278
- <script>
1279
- if (window.opener) {
1280
- window.opener.postMessage({ type: 'oauth-error', provider: '${c.req.param('provider')}', error: '${message}' }, '*');
1281
- }
1282
- setTimeout(() => window.close(), 3000);
1283
- </script>
1284
- </body>
1285
- </html>
1286
- `);
1287
- }
1288
- },
1289
- );
1290
-
1291
- openApiRoute(
1292
- app,
1293
- {
1294
- method: 'post',
1295
- path: '/v1/auth/copilot/device/start',
1296
- tags: ['auth'],
1297
- operationId: 'startCopilotDeviceFlow',
1298
- summary: 'Start Copilot device flow authentication',
1299
- responses: {
1300
- '200': {
1301
- description: 'OK',
1302
- content: {
1303
- 'application/json': {
1304
- schema: {
1305
- type: 'object',
1306
- properties: {
1307
- sessionId: {
1308
- type: 'string',
1309
- },
1310
- userCode: {
1311
- type: 'string',
1312
- },
1313
- verificationUri: {
1314
- type: 'string',
1315
- },
1316
- interval: {
1317
- type: 'integer',
1318
- },
1319
- },
1320
- required: [
1321
- 'sessionId',
1322
- 'userCode',
1323
- 'verificationUri',
1324
- 'interval',
1325
- ],
1326
- },
1327
- },
1328
- },
1329
- },
1330
- },
1331
- },
1332
- async (c) => {
1333
- try {
1334
- const deviceData = await authorizeCopilot();
1335
- const sessionId = crypto.randomUUID();
1336
- copilotDeviceSessions.set(sessionId, {
1337
- deviceCode: deviceData.deviceCode,
1338
- interval: deviceData.interval,
1339
- provider: 'copilot',
1340
- createdAt: Date.now(),
1341
- });
1342
- return c.json({
1343
- sessionId,
1344
- userCode: deviceData.userCode,
1345
- verificationUri: deviceData.verificationUri,
1346
- interval: deviceData.interval,
1347
- });
1348
- } catch (error) {
1349
- const message =
1350
- error instanceof Error
1351
- ? error.message
1352
- : 'Failed to start Copilot device flow';
1353
- logger.error('Copilot device flow start failed', error);
1354
- return c.json({ error: message }, 500);
1355
- }
1356
- },
1357
- );
1358
-
1359
- openApiRoute(
1360
- app,
1361
- {
1362
- method: 'post',
1363
- path: '/v1/auth/copilot/device/poll',
1364
- tags: ['auth'],
1365
- operationId: 'pollCopilotDeviceFlow',
1366
- summary: 'Poll Copilot device flow for completion',
1367
- requestBody: {
1368
- required: true,
1369
- content: {
1370
- 'application/json': {
1371
- schema: {
1372
- type: 'object',
1373
- properties: {
1374
- sessionId: {
1375
- type: 'string',
1376
- },
1377
- },
1378
- required: ['sessionId'],
1379
- },
1380
- },
1381
- },
1382
- },
1383
- responses: {
1384
- '200': {
1385
- description: 'OK',
1386
- content: {
1387
- 'application/json': {
1388
- schema: {
1389
- type: 'object',
1390
- properties: {
1391
- status: {
1392
- type: 'string',
1393
- enum: ['complete', 'pending', 'error'],
1394
- },
1395
- error: {
1396
- type: 'string',
1397
- },
1398
- },
1399
- required: ['status'],
1400
- },
1401
- },
1402
- },
1403
- },
1404
- '400': {
1405
- description: 'Bad Request',
1406
- content: {
1407
- 'application/json': {
1408
- schema: {
1409
- type: 'object',
1410
- properties: {
1411
- error: {
1412
- type: 'string',
1413
- },
1414
- },
1415
- required: ['error'],
1416
- },
1417
- },
1418
- },
1419
- },
1420
- },
1421
- },
1422
- async (c) => {
1423
- try {
1424
- const { sessionId } = await c.req.json<{ sessionId: string }>();
1425
- if (!sessionId || !copilotDeviceSessions.has(sessionId)) {
1426
- return c.json({ error: 'Session expired or invalid' }, 400);
1427
- }
1428
- const session = copilotDeviceSessions.get(sessionId);
1429
- if (!session) {
1430
- return c.json({ error: 'Session expired or invalid' }, 400);
1431
- }
1432
- const result = await pollForCopilotTokenOnce(session.deviceCode);
1433
- if (result.status === 'complete') {
1434
- copilotDeviceSessions.delete(sessionId);
1435
- await setAuth(
1436
- 'copilot',
1437
- {
1438
- type: 'oauth',
1439
- refresh: result.accessToken,
1440
- access: result.accessToken,
1441
- expires: 0,
1442
- },
1443
- undefined,
1444
- 'global',
1445
- );
1446
- return c.json({ status: 'complete' });
1447
- }
1448
- if (result.status === 'pending') {
1449
- return c.json({ status: 'pending' });
1450
- }
1451
- if (result.status === 'error') {
1452
- copilotDeviceSessions.delete(sessionId);
1453
- return c.json({ status: 'error', error: result.error });
1454
- }
1455
- return c.json({ status: 'pending' });
1456
- } catch (error) {
1457
- const message = error instanceof Error ? error.message : 'Poll failed';
1458
- logger.error('Copilot device poll failed', error);
1459
- return c.json({ error: message }, 500);
1460
- }
1461
- },
1462
- );
1463
-
1464
- openApiRoute(
1465
- app,
1466
- {
1467
- method: 'get',
1468
- path: '/v1/auth/copilot/methods',
1469
- tags: ['auth'],
1470
- operationId: 'getCopilotAuthMethods',
1471
- summary: 'Get available Copilot auth methods',
1472
- responses: {
1473
- '200': {
1474
- description: 'OK',
1475
- content: {
1476
- 'application/json': {
1477
- schema: {
1478
- type: 'object',
1479
- properties: {
1480
- oauth: {
1481
- type: 'boolean',
1482
- },
1483
- token: {
1484
- type: 'boolean',
1485
- },
1486
- ghImport: {
1487
- type: 'object',
1488
- properties: {
1489
- available: {
1490
- type: 'boolean',
1491
- },
1492
- authenticated: {
1493
- type: 'boolean',
1494
- },
1495
- reason: {
1496
- type: 'string',
1497
- },
1498
- },
1499
- required: ['available', 'authenticated'],
1500
- },
1501
- },
1502
- required: ['oauth', 'token', 'ghImport'],
1503
- },
1504
- },
1505
- },
1506
- },
1507
- },
1508
- },
1509
- async (c) => {
1510
- const ghImport = getGhImportCapability();
1511
- return c.json({
1512
- oauth: true,
1513
- token: true,
1514
- ghImport,
1515
- });
1516
- },
1517
- );
1518
-
1519
- openApiRoute(
1520
- app,
1521
- {
1522
- method: 'post',
1523
- path: '/v1/auth/copilot/token',
1524
- tags: ['auth'],
1525
- operationId: 'saveCopilotToken',
1526
- summary: 'Save Copilot token after validating model access',
1527
- requestBody: {
1528
- required: true,
1529
- content: {
1530
- 'application/json': {
1531
- schema: {
1532
- type: 'object',
1533
- properties: {
1534
- token: {
1535
- type: 'string',
1536
- },
1537
- },
1538
- required: ['token'],
1539
- },
1540
- },
1541
- },
1542
- },
1543
- responses: {
1544
- '200': {
1545
- description: 'OK',
1546
- content: {
1547
- 'application/json': {
1548
- schema: {
1549
- type: 'object',
1550
- properties: {
1551
- success: {
1552
- type: 'boolean',
1553
- },
1554
- provider: {
1555
- type: 'string',
1556
- },
1557
- source: {
1558
- type: 'string',
1559
- enum: ['token'],
1560
- },
1561
- modelCount: {
1562
- type: 'integer',
1563
- },
1564
- hasGpt52Codex: {
1565
- type: 'boolean',
1566
- },
1567
- sampleModels: {
1568
- type: 'array',
1569
- items: {
1570
- type: 'string',
1571
- },
1572
- },
1573
- },
1574
- required: [
1575
- 'success',
1576
- 'provider',
1577
- 'source',
1578
- 'modelCount',
1579
- 'hasGpt52Codex',
1580
- 'sampleModels',
1581
- ],
1582
- },
1583
- },
1584
- },
1585
- },
1586
- '400': {
1587
- description: 'Bad Request',
1588
- content: {
1589
- 'application/json': {
1590
- schema: {
1591
- type: 'object',
1592
- properties: {
1593
- error: {
1594
- type: 'string',
1595
- },
1596
- },
1597
- required: ['error'],
1598
- },
1599
- },
1600
- },
1601
- },
1602
- },
1603
- },
1604
- async (c) => {
1605
- try {
1606
- const { token } = await c.req.json<{ token: string }>();
1607
- const sanitized = token?.trim();
1608
- if (!sanitized) {
1609
- return c.json({ error: 'Copilot token is required' }, 400);
1610
- }
1611
-
1612
- const modelsResult = await fetchCopilotModels(sanitized);
1613
- if (!modelsResult.ok) {
1614
- return c.json(
1615
- {
1616
- error: `Invalid Copilot token: ${modelsResult.message}`,
1617
- },
1618
- 400,
1619
- );
1620
- }
1621
-
1622
- await setAuth(
1623
- 'copilot',
1624
- {
1625
- type: 'oauth',
1626
- refresh: sanitized,
1627
- access: sanitized,
1628
- expires: 0,
1629
- },
1630
- undefined,
1631
- 'global',
1632
- );
1633
-
1634
- const models = Array.from(modelsResult.models).sort();
1635
- return c.json({
1636
- success: true,
1637
- provider: 'copilot',
1638
- source: 'token',
1639
- modelCount: models.length,
1640
- hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
1641
- sampleModels: models.slice(0, 25),
1642
- });
1643
- } catch (error) {
1644
- const message =
1645
- error instanceof Error
1646
- ? error.message
1647
- : 'Failed to save Copilot token';
1648
- logger.error('Failed to save Copilot token', error);
1649
- return c.json({ error: message }, 500);
1650
- }
1651
- },
1652
- );
1653
-
1654
- openApiRoute(
1655
- app,
1656
- {
1657
- method: 'post',
1658
- path: '/v1/auth/copilot/gh/import',
1659
- tags: ['auth'],
1660
- operationId: 'importCopilotTokenFromGh',
1661
- summary: 'Import Copilot token from GitHub CLI (gh)',
1662
- responses: {
1663
- '200': {
1664
- description: 'OK',
1665
- content: {
1666
- 'application/json': {
1667
- schema: {
1668
- type: 'object',
1669
- properties: {
1670
- success: {
1671
- type: 'boolean',
1672
- },
1673
- provider: {
1674
- type: 'string',
1675
- },
1676
- source: {
1677
- type: 'string',
1678
- enum: ['gh'],
1679
- },
1680
- modelCount: {
1681
- type: 'integer',
1682
- },
1683
- hasGpt52Codex: {
1684
- type: 'boolean',
1685
- },
1686
- sampleModels: {
1687
- type: 'array',
1688
- items: {
1689
- type: 'string',
1690
- },
1691
- },
1692
- },
1693
- required: [
1694
- 'success',
1695
- 'provider',
1696
- 'source',
1697
- 'modelCount',
1698
- 'hasGpt52Codex',
1699
- 'sampleModels',
1700
- ],
1701
- },
1702
- },
1703
- },
1704
- },
1705
- '400': {
1706
- description: 'Bad Request',
1707
- content: {
1708
- 'application/json': {
1709
- schema: {
1710
- type: 'object',
1711
- properties: {
1712
- error: {
1713
- type: 'string',
1714
- },
1715
- },
1716
- required: ['error'],
1717
- },
1718
- },
1719
- },
1720
- },
1721
- },
1722
- },
1723
- async (c) => {
1724
- try {
1725
- const ghImport = getGhImportCapability();
1726
- if (!ghImport.available) {
1727
- return c.json(
1728
- {
1729
- error: ghImport.reason || 'GitHub CLI is not available',
1730
- },
1731
- 400,
1732
- );
1733
- }
1734
- if (!ghImport.authenticated) {
1735
- return c.json(
1736
- {
1737
- error: ghImport.reason || 'GitHub CLI is not authenticated',
1738
- },
1739
- 400,
1740
- );
1741
- }
1742
-
1743
- const ghToken = execFileSync('gh', ['auth', 'token'], {
1744
- encoding: 'utf8',
1745
- stdio: ['ignore', 'pipe', 'pipe'],
1746
- }).trim();
1747
- if (!ghToken) {
1748
- return c.json({ error: 'GitHub CLI returned an empty token' }, 400);
1749
- }
1750
-
1751
- const modelsResult = await fetchCopilotModels(ghToken);
1752
- if (!modelsResult.ok) {
1753
- return c.json(
1754
- {
1755
- error: `Imported gh token is not valid for Copilot: ${modelsResult.message}`,
1756
- },
1757
- 400,
1758
- );
1759
- }
1760
-
1761
- await setAuth(
1762
- 'copilot',
1763
- {
1764
- type: 'oauth',
1765
- refresh: ghToken,
1766
- access: ghToken,
1767
- expires: 0,
1768
- },
1769
- undefined,
1770
- 'global',
1771
- );
1772
-
1773
- const models = Array.from(modelsResult.models).sort();
1774
- return c.json({
1775
- success: true,
1776
- provider: 'copilot',
1777
- source: 'gh',
1778
- modelCount: models.length,
1779
- hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
1780
- sampleModels: models.slice(0, 25),
1781
- });
1782
- } catch (error) {
1783
- const message =
1784
- error instanceof Error
1785
- ? error.message
1786
- : 'Failed to import GitHub CLI token';
1787
- logger.error('Failed to import Copilot token from GitHub CLI', error);
1788
- return c.json({ error: message }, 500);
1789
- }
1790
- },
1791
- );
1792
-
1793
- openApiRoute(
1794
- app,
1795
- {
1796
- method: 'get',
1797
- path: '/v1/auth/copilot/diagnostics',
1798
- tags: ['auth'],
1799
- operationId: 'getCopilotDiagnostics',
1800
- summary: 'Get Copilot token diagnostics and model visibility',
1801
- responses: {
1802
- '200': {
1803
- description: 'OK',
1804
- content: {
1805
- 'application/json': {
1806
- schema: {
1807
- type: 'object',
1808
- properties: {
1809
- tokenSources: {
1810
- type: 'array',
1811
- items: {
1812
- type: 'object',
1813
- properties: {
1814
- source: {
1815
- type: 'string',
1816
- enum: ['env', 'stored'],
1817
- },
1818
- configured: {
1819
- type: 'boolean',
1820
- },
1821
- modelCount: {
1822
- type: 'integer',
1823
- },
1824
- hasGpt52Codex: {
1825
- type: 'boolean',
1826
- },
1827
- sampleModels: {
1828
- type: 'array',
1829
- items: {
1830
- type: 'string',
1831
- },
1832
- },
1833
- restrictedByOrgPolicy: {
1834
- type: 'boolean',
1835
- },
1836
- restrictedOrg: {
1837
- type: 'string',
1838
- },
1839
- restrictionMessage: {
1840
- type: 'string',
1841
- },
1842
- error: {
1843
- type: 'string',
1844
- },
1845
- },
1846
- required: ['source', 'configured'],
1847
- },
1848
- },
1849
- methods: {
1850
- type: 'object',
1851
- properties: {
1852
- oauth: {
1853
- type: 'boolean',
1854
- },
1855
- token: {
1856
- type: 'boolean',
1857
- },
1858
- ghImport: {
1859
- type: 'object',
1860
- properties: {
1861
- available: {
1862
- type: 'boolean',
1863
- },
1864
- authenticated: {
1865
- type: 'boolean',
1866
- },
1867
- reason: {
1868
- type: 'string',
1869
- },
1870
- },
1871
- required: ['available', 'authenticated'],
1872
- },
1873
- },
1874
- required: ['oauth', 'token', 'ghImport'],
1875
- },
1876
- },
1877
- required: ['tokenSources', 'methods'],
1878
- },
1879
- },
1880
- },
1881
- },
1882
- },
1883
- },
1884
- async (c) => {
1885
- try {
1886
- const projectRoot = process.cwd();
1887
- const entries: Array<{
1888
- source: 'env' | 'stored';
1889
- configured: boolean;
1890
- modelCount?: number;
1891
- hasGpt52Codex?: boolean;
1892
- sampleModels?: string[];
1893
- restrictedByOrgPolicy?: boolean;
1894
- restrictedOrg?: string;
1895
- restrictionMessage?: string;
1896
- error?: string;
1897
- }> = [];
1898
-
1899
- const envToken = readEnvKey('copilot');
1900
- if (envToken) {
1901
- const modelsResult = await fetchCopilotModels(envToken);
1902
- if (modelsResult.ok) {
1903
- const models = Array.from(modelsResult.models).sort();
1904
- entries.push({
1905
- source: 'env',
1906
- configured: true,
1907
- modelCount: models.length,
1908
- hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
1909
- sampleModels: models.slice(0, 25),
1910
- });
1911
- } else {
1912
- entries.push({
1913
- source: 'env',
1914
- configured: true,
1915
- error: modelsResult.message,
1916
- });
1917
- }
1918
- } else {
1919
- entries.push({ source: 'env', configured: false });
1920
- }
1921
-
1922
- const storedAuth = await getAuth('copilot', projectRoot);
1923
- if (storedAuth?.type === 'oauth') {
1924
- const modelsResult = await fetchCopilotModels(storedAuth.refresh);
1925
- const restriction = await detectOAuthOrgRestriction(
1926
- storedAuth.refresh,
1927
- );
1928
- if (modelsResult.ok) {
1929
- const models = Array.from(modelsResult.models).sort();
1930
- entries.push({
1931
- source: 'stored',
1932
- configured: true,
1933
- modelCount: models.length,
1934
- hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
1935
- sampleModels: models.slice(0, 25),
1936
- restrictedByOrgPolicy: restriction.restricted,
1937
- restrictedOrg: restriction.org,
1938
- restrictionMessage: restriction.message,
1939
- });
1940
- } else {
1941
- entries.push({
1942
- source: 'stored',
1943
- configured: true,
1944
- error: modelsResult.message,
1945
- restrictedByOrgPolicy: restriction.restricted,
1946
- restrictedOrg: restriction.org,
1947
- restrictionMessage: restriction.message,
1948
- });
1949
- }
1950
- } else {
1951
- entries.push({ source: 'stored', configured: false });
1952
- }
1953
-
1954
- return c.json({
1955
- tokenSources: entries,
1956
- methods: {
1957
- oauth: true,
1958
- token: true,
1959
- ghImport: getGhImportCapability(),
1960
- },
1961
- });
1962
- } catch (error) {
1963
- const message =
1964
- error instanceof Error ? error.message : 'Failed to inspect Copilot';
1965
- logger.error('Failed to build Copilot diagnostics', error);
1966
- return c.json({ error: message }, 500);
1967
- }
1968
- },
1969
- );
1970
-
1971
- openApiRoute(
1972
- app,
1973
- {
1974
- method: 'post',
1975
- path: '/v1/auth/onboarding/complete',
1976
- tags: ['auth'],
1977
- operationId: 'completeOnboarding',
1978
- summary: 'Mark onboarding as complete',
1979
- responses: {
1980
- '200': {
1981
- description: 'OK',
1982
- content: {
1983
- 'application/json': {
1984
- schema: {
1985
- type: 'object',
1986
- properties: {
1987
- success: {
1988
- type: 'boolean',
1989
- },
1990
- },
1991
- required: ['success'],
1992
- },
1993
- },
1994
- },
1995
- },
1996
- },
1997
- },
1998
- async (c) => {
1999
- try {
2000
- await setOnboardingComplete();
2001
- return c.json({ success: true });
2002
- } catch (error) {
2003
- logger.error('Failed to complete onboarding', error);
2004
- const errorResponse = serializeError(error);
2005
- return c.json(errorResponse, errorResponse.error.status || 500);
2006
- }
2007
- },
2008
- );
2009
-
2010
- openApiRoute(
2011
- app,
2012
- {
2013
- method: 'delete',
2014
- path: '/v1/auth/{provider}',
2015
- tags: ['auth'],
2016
- operationId: 'removeProvider',
2017
- summary: 'Remove auth for a provider',
2018
- parameters: [
2019
- {
2020
- in: 'path',
2021
- name: 'provider',
2022
- required: true,
2023
- schema: {
2024
- type: 'string',
2025
- },
2026
- },
2027
- ],
2028
- responses: {
2029
- '200': {
2030
- description: 'OK',
2031
- content: {
2032
- 'application/json': {
2033
- schema: {
2034
- type: 'object',
2035
- properties: {
2036
- success: {
2037
- type: 'boolean',
2038
- },
2039
- provider: {
2040
- type: 'string',
2041
- },
2042
- },
2043
- required: ['success', 'provider'],
2044
- },
2045
- },
2046
- },
2047
- },
2048
- '400': {
2049
- description: 'Bad Request',
2050
- content: {
2051
- 'application/json': {
2052
- schema: {
2053
- type: 'object',
2054
- properties: {
2055
- error: {
2056
- type: 'string',
2057
- },
2058
- },
2059
- required: ['error'],
2060
- },
2061
- },
2062
- },
2063
- },
2064
- },
2065
- },
2066
- async (c) => {
2067
- try {
2068
- const provider = c.req.param('provider') as ProviderId;
2069
-
2070
- if (!isBuiltInProviderId(provider) || !catalog[provider]) {
2071
- return c.json({ error: 'Unknown provider' }, 400);
2072
- }
2073
-
2074
- await removeAuth(provider, undefined, 'global');
2075
-
2076
- return c.json({ success: true, provider });
2077
- } catch (error) {
2078
- logger.error('Failed to remove provider', error);
2079
- const errorResponse = serializeError(error);
2080
- return c.json(errorResponse, errorResponse.error.status || 500);
2081
- }
2082
- },
2083
- );
10
+ registerAuthStatusRoutes(app);
11
+ registerAuthWalletRoutes(app);
12
+ registerAuthProviderRoutes(app);
13
+ registerAuthOAuthRoutes(app);
14
+ registerAuthCopilotRoutes(app);
15
+ registerAuthOnboardingRoutes(app);
2084
16
  }