@ottocode/server 0.1.260 → 0.1.261

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 (67) hide show
  1. package/package.json +4 -3
  2. package/src/index.ts +5 -4
  3. package/src/openapi/register.ts +92 -0
  4. package/src/openapi/route.ts +22 -0
  5. package/src/routes/ask.ts +210 -99
  6. package/src/routes/auth.ts +1701 -626
  7. package/src/routes/branch.ts +281 -90
  8. package/src/routes/config/agents.ts +79 -32
  9. package/src/routes/config/cwd.ts +46 -14
  10. package/src/routes/config/debug.ts +159 -30
  11. package/src/routes/config/defaults.ts +182 -64
  12. package/src/routes/config/main.ts +109 -73
  13. package/src/routes/config/models.ts +304 -137
  14. package/src/routes/config/providers.ts +462 -166
  15. package/src/routes/config/utils.ts +2 -2
  16. package/src/routes/doctor.ts +395 -161
  17. package/src/routes/files.ts +650 -260
  18. package/src/routes/git/branch.ts +143 -52
  19. package/src/routes/git/commit.ts +347 -141
  20. package/src/routes/git/diff.ts +239 -116
  21. package/src/routes/git/init.ts +103 -23
  22. package/src/routes/git/pull.ts +167 -65
  23. package/src/routes/git/push.ts +222 -117
  24. package/src/routes/git/remote.ts +401 -100
  25. package/src/routes/git/staging.ts +502 -141
  26. package/src/routes/git/status.ts +171 -78
  27. package/src/routes/mcp.ts +1129 -404
  28. package/src/routes/openapi.ts +27 -4
  29. package/src/routes/ottorouter.ts +1221 -389
  30. package/src/routes/provider-usage.ts +153 -36
  31. package/src/routes/research.ts +817 -370
  32. package/src/routes/root.ts +50 -6
  33. package/src/routes/session-approval.ts +228 -54
  34. package/src/routes/session-files.ts +265 -134
  35. package/src/routes/session-messages.ts +330 -150
  36. package/src/routes/session-stream.ts +83 -2
  37. package/src/routes/sessions.ts +1830 -780
  38. package/src/routes/skills.ts +849 -161
  39. package/src/routes/terminals.ts +469 -103
  40. package/src/routes/tunnel.ts +394 -118
  41. package/src/runtime/ask/service.ts +1 -0
  42. package/src/runtime/message/compaction-limits.ts +3 -3
  43. package/src/runtime/provider/reasoning.ts +2 -1
  44. package/src/runtime/session/db-operations.ts +4 -3
  45. package/src/runtime/utils/token.ts +7 -2
  46. package/src/tools/adapter.ts +21 -0
  47. package/src/openapi/paths/ask.ts +0 -81
  48. package/src/openapi/paths/auth.ts +0 -687
  49. package/src/openapi/paths/branch.ts +0 -102
  50. package/src/openapi/paths/config.ts +0 -485
  51. package/src/openapi/paths/doctor.ts +0 -165
  52. package/src/openapi/paths/files.ts +0 -236
  53. package/src/openapi/paths/git.ts +0 -690
  54. package/src/openapi/paths/mcp.ts +0 -339
  55. package/src/openapi/paths/messages.ts +0 -103
  56. package/src/openapi/paths/ottorouter.ts +0 -594
  57. package/src/openapi/paths/provider-usage.ts +0 -59
  58. package/src/openapi/paths/research.ts +0 -227
  59. package/src/openapi/paths/session-approval.ts +0 -93
  60. package/src/openapi/paths/session-extras.ts +0 -336
  61. package/src/openapi/paths/session-files.ts +0 -91
  62. package/src/openapi/paths/sessions.ts +0 -210
  63. package/src/openapi/paths/skills.ts +0 -377
  64. package/src/openapi/paths/stream.ts +0 -26
  65. package/src/openapi/paths/terminals.ts +0 -226
  66. package/src/openapi/paths/tunnel.ts +0 -163
  67. package/src/openapi/spec.ts +0 -73
@@ -9,6 +9,7 @@ import {
9
9
  importWallet,
10
10
  loadConfig,
11
11
  catalog,
12
+ isBuiltInProviderId,
12
13
  readEnvKey,
13
14
  getOnboardingComplete,
14
15
  setOnboardingComplete,
@@ -26,6 +27,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
26
27
  import { logger } from '@ottocode/sdk';
27
28
  import { serializeError } from '../runtime/errors/api-error.ts';
28
29
  import { getProviderDetails } from './config/utils.ts';
30
+ import { openApiRoute } from '../openapi/route.ts';
29
31
 
30
32
  const oauthVerifiers = new Map<
31
33
  string,
@@ -218,397 +220,980 @@ setInterval(() => {
218
220
  }, 60 * 1000);
219
221
 
220
222
  export function registerAuthRoutes(app: Hono) {
221
- app.get('/v1/auth/status', async (c) => {
222
- try {
223
- const projectRoot = process.cwd();
224
- const auth = await getAllAuth(projectRoot);
225
- const cfg = await loadConfig(projectRoot);
226
- const onboardingComplete = await getOnboardingComplete(projectRoot);
227
- const ottorouterWallet = await getOttoRouterWallet(projectRoot);
228
- const ghImportCapability = getGhImportCapability();
229
-
230
- const providers: Record<
231
- string,
232
- {
233
- configured: boolean;
234
- type?: 'api' | 'oauth' | 'wallet';
235
- label: string;
236
- supportsOAuth: boolean;
237
- supportsToken?: boolean;
238
- supportsGhImport?: boolean;
239
- custom?: boolean;
240
- modelCount: number;
241
- costRange?: { min: number; max: number };
242
- }
243
- > = {};
244
-
245
- for (const [id, entry] of Object.entries(catalog)) {
246
- const providerAuth = auth[id as ProviderId];
247
- const models = entry.models || [];
248
- const costs = models
249
- .map((m) => m.cost?.input)
250
- .filter((c): c is number => c !== undefined);
251
-
252
- providers[id] = {
253
- configured: !!providerAuth,
254
- type: providerAuth?.type,
255
- label: entry.label || id,
256
- supportsOAuth:
257
- id === 'anthropic' || id === 'openai' || id === 'copilot',
258
- supportsToken: id === 'copilot',
259
- supportsGhImport:
260
- id === 'copilot' ? ghImportCapability.available : false,
261
- modelCount: models.length,
262
- costRange:
263
- costs.length > 0
264
- ? {
265
- min: Math.min(...costs),
266
- max: Math.max(...costs),
267
- }
268
- : undefined,
269
- };
270
- }
271
-
272
- const providerDetails = await getProviderDetails(undefined, cfg);
273
- for (const detail of providerDetails) {
274
- if (!detail.custom || providers[detail.id]) continue;
275
- providers[detail.id] = {
276
- configured: detail.authorized,
277
- type: detail.authType,
278
- label: detail.label,
279
- supportsOAuth: false,
280
- custom: true,
281
- modelCount: detail.modelCount,
282
- };
283
- }
284
-
285
- return c.json({
286
- onboardingComplete,
287
- ottorouter: ottorouterWallet
288
- ? {
289
- configured: true,
290
- publicKey: ottorouterWallet.publicKey,
291
- }
292
- : {
293
- configured: false,
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
+ },
294
320
  },
295
- providers,
296
- defaults: cfg.defaults,
297
- });
298
- } catch (error) {
299
- logger.error('Failed to get auth status', error);
300
- const errorResponse = serializeError(error);
301
- return c.json(errorResponse, errorResponse.error.status || 500);
302
- }
303
- });
304
-
305
- app.post('/v1/auth/ottorouter/setup', async (c) => {
306
- try {
307
- const projectRoot = process.cwd();
308
- const existing = await getOttoRouterWallet(projectRoot);
309
- const wallet = await ensureOttoRouterWallet(projectRoot);
310
-
311
- return c.json({
312
- success: true,
313
- publicKey: wallet.publicKey,
314
- isNew: !existing,
315
- });
316
- } catch (error) {
317
- logger.error('Failed to setup OttoRouter wallet', error);
318
- const errorResponse = serializeError(error);
319
- return c.json(errorResponse, errorResponse.error.status || 500);
320
- }
321
- });
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
+ }
322
375
 
323
- app.post('/v1/auth/ottorouter/import', async (c) => {
324
- try {
325
- const { privateKey } = await c.req.json<{ privateKey: string }>();
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
+ }
326
388
 
327
- if (!privateKey) {
328
- return c.json({ error: 'Private key required' }, 400);
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);
329
406
  }
330
-
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) => {
331
444
  try {
332
- const wallet = importWallet(privateKey);
333
- await setAuth(
334
- 'ottorouter',
335
- { type: 'wallet', secret: privateKey },
336
- undefined,
337
- 'global',
338
- );
445
+ const projectRoot = process.cwd();
446
+ const existing = await getOttoRouterWallet(projectRoot);
447
+ const wallet = await ensureOttoRouterWallet(projectRoot);
339
448
 
340
449
  return c.json({
341
450
  success: true,
342
451
  publicKey: wallet.publicKey,
452
+ isNew: !existing,
343
453
  });
344
- } catch {
345
- return c.json({ error: 'Invalid private key format' }, 400);
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);
346
458
  }
347
- } catch (error) {
348
- logger.error('Failed to import OttoRouter wallet', error);
349
- const errorResponse = serializeError(error);
350
- return c.json(errorResponse, errorResponse.error.status || 500);
351
- }
352
- });
353
-
354
- app.get('/v1/auth/ottorouter/export', async (c) => {
355
- try {
356
- const projectRoot = process.cwd();
357
- const wallet = await getOttoRouterWallet(projectRoot);
358
-
359
- if (!wallet) {
360
- return c.json({ error: 'OttoRouter wallet not configured' }, 404);
361
- }
362
-
363
- return c.json({
364
- success: true,
365
- publicKey: wallet.publicKey,
366
- privateKey: wallet.privateKey,
367
- });
368
- } catch (error) {
369
- logger.error('Failed to export OttoRouter wallet', error);
370
- const errorResponse = serializeError(error);
371
- return c.json(errorResponse, errorResponse.error.status || 500);
372
- }
373
- });
374
-
375
- app.post('/v1/auth/:provider', async (c) => {
376
- try {
377
- const provider = c.req.param('provider') as ProviderId;
378
- const { apiKey } = await c.req.json<{ apiKey: string }>();
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 }>();
379
527
 
380
- if (!catalog[provider]) {
381
- return c.json({ error: 'Unknown provider' }, 400);
382
- }
528
+ if (!privateKey) {
529
+ return c.json({ error: 'Private key required' }, 400);
530
+ }
383
531
 
384
- if (!apiKey) {
385
- return c.json({ error: 'API key required' }, 400);
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);
386
552
  }
387
-
388
- await setAuth(
389
- provider,
390
- { type: 'api', key: apiKey },
391
- undefined,
392
- 'global',
393
- );
394
-
395
- return c.json({ success: true, provider });
396
- } catch (error) {
397
- logger.error('Failed to add provider', error);
398
- const errorResponse = serializeError(error);
399
- return c.json(errorResponse, errorResponse.error.status || 500);
400
- }
401
- });
402
-
403
- app.post('/v1/auth/:provider/oauth/url', async (c) => {
404
- try {
405
- const provider = c.req.param('provider');
406
- const body = await c.req.json<{ mode?: string }>().catch(() => undefined);
407
- const mode: 'max' | 'console' =
408
- body?.mode === 'console' ? 'console' : 'max';
409
-
410
- let url: string;
411
- let verifier: string;
412
-
413
- if (provider === 'anthropic') {
414
- const result = await authorize(mode);
415
- url = result.url;
416
- verifier = result.verifier;
417
- } else if (provider === 'openai') {
418
- return c.json(
419
- {
420
- error:
421
- 'OpenAI OAuth requires localhost callback. Use the redirect flow instead.',
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
+ },
422
585
  },
423
- 400,
424
- );
425
- } else {
426
- return c.json(
427
- {
428
- error: `OAuth not supported for provider: ${provider}. Copilot uses device flow — use /v1/auth/copilot/device/start instead.`,
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
+ },
429
601
  },
430
- 400,
431
- );
432
- }
433
-
434
- const sessionId = crypto.randomUUID();
435
- oauthVerifiers.set(sessionId, {
436
- verifier,
437
- provider,
438
- createdAt: Date.now(),
439
- callbackUrl: '',
440
- });
441
-
442
- return c.json({ url, sessionId, provider });
443
- } catch (error) {
444
- const message =
445
- error instanceof Error ? error.message : 'OAuth initialization failed';
446
- logger.error('OAuth URL generation failed', error);
447
- return c.json({ error: message }, 500);
448
- }
449
- });
602
+ },
603
+ },
604
+ },
605
+ async (c) => {
606
+ try {
607
+ const projectRoot = process.cwd();
608
+ const wallet = await getOttoRouterWallet(projectRoot);
450
609
 
451
- app.post('/v1/auth/:provider/oauth/exchange', async (c) => {
452
- try {
453
- const provider = c.req.param('provider');
454
- const { code, sessionId } = await c.req.json<{
455
- code: string;
456
- sessionId: string;
457
- }>();
610
+ if (!wallet) {
611
+ return c.json({ error: 'OttoRouter wallet not configured' }, 404);
612
+ }
458
613
 
459
- if (!code || !sessionId) {
460
- return c.json({ error: 'Code and sessionId required' }, 400);
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);
461
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 }>();
462
703
 
463
- if (!oauthVerifiers.has(sessionId)) {
464
- return c.json({ error: 'Session expired or invalid' }, 400);
465
- }
704
+ if (!isBuiltInProviderId(provider) || !catalog[provider]) {
705
+ return c.json({ error: 'Unknown provider' }, 400);
706
+ }
466
707
 
467
- const verifierEntry = oauthVerifiers.get(sessionId);
468
- if (!verifierEntry) {
469
- return c.json({ error: 'Session expired or invalid' }, 400);
470
- }
471
- const { verifier } = verifierEntry;
472
- oauthVerifiers.delete(sessionId);
708
+ if (!apiKey) {
709
+ return c.json({ error: 'API key required' }, 400);
710
+ }
473
711
 
474
- if (provider === 'anthropic') {
475
- const tokens = await exchange(code, verifier);
476
712
  await setAuth(
477
- 'anthropic',
478
- {
479
- type: 'oauth',
480
- refresh: tokens.refresh,
481
- access: tokens.access,
482
- expires: tokens.expires,
483
- },
713
+ provider,
714
+ { type: 'api', key: apiKey },
484
715
  undefined,
485
716
  'global',
486
717
  );
487
- } else if (provider === 'openai') {
488
- return c.json({ error: 'Use redirect flow for OpenAI' }, 400);
489
- } else {
490
- return c.json({ error: 'Unknown provider' }, 400);
491
- }
492
718
 
493
- return c.json({ success: true, provider });
494
- } catch (error) {
495
- const message =
496
- error instanceof Error ? error.message : 'Token exchange failed';
497
- logger.error('OAuth exchange failed', error);
498
- return c.json({ error: message }, 500);
499
- }
500
- });
501
-
502
- app.get('/v1/auth/:provider/oauth/start', async (c) => {
503
- try {
504
- const provider = c.req.param('provider');
505
- const mode = c.req.query('mode') || 'max';
506
- const host = c.req.header('host') || 'localhost:3000';
507
- const protocol = c.req.header('x-forwarded-proto') || 'http';
508
-
509
- let url: string;
510
- let verifier: string;
511
- let callbackUrl = '';
512
-
513
- if (provider === 'anthropic') {
514
- callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
515
- const result = authorizeWeb(mode as 'max' | 'console', callbackUrl);
516
- url = result.url;
517
- verifier = result.verifier;
518
- } else if (provider === 'openai') {
519
- callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
520
- const result = authorizeOpenAIWeb(callbackUrl);
521
- url = result.url;
522
- verifier = result.verifier;
523
- } else {
524
- return c.json({ error: 'OAuth not supported for this provider' }, 400);
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);
525
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
+ }
526
836
 
527
- const sessionId = crypto.randomUUID();
528
- oauthVerifiers.set(sessionId, {
529
- verifier,
530
- provider,
531
- createdAt: Date.now(),
532
- callbackUrl,
533
- });
534
-
535
- c.header(
536
- 'Set-Cookie',
537
- `oauth_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
538
- );
539
-
540
- return c.redirect(url);
541
- } catch (error) {
542
- const message =
543
- error instanceof Error ? error.message : 'OAuth initialization failed';
544
- logger.error('OAuth start failed', error);
545
- return c.json({ error: message }, 500);
546
- }
547
- });
548
-
549
- app.get('/v1/auth/:provider/oauth/callback', async (c) => {
550
- try {
551
- const provider = c.req.param('provider');
552
- const code = c.req.query('code');
553
- const fragment = c.req.query('fragment');
554
-
555
- const cookies = c.req.header('Cookie') || '';
556
- const sessionMatch = cookies.match(/oauth_session=([^;]+)/);
557
- const sessionId = sessionMatch?.[1];
837
+ const sessionId = crypto.randomUUID();
838
+ oauthVerifiers.set(sessionId, {
839
+ verifier,
840
+ provider,
841
+ createdAt: Date.now(),
842
+ callbackUrl: '',
843
+ });
558
844
 
559
- if (!sessionId || !oauthVerifiers.has(sessionId)) {
560
- return c.html(
561
- '<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
562
- );
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);
563
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
+ }
564
943
 
565
- const callbackEntry = oauthVerifiers.get(sessionId);
566
- if (!callbackEntry) {
567
- return c.html(
568
- '<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
569
- );
570
- }
571
- const { verifier, callbackUrl } = callbackEntry;
572
- oauthVerifiers.delete(sessionId);
944
+ if (!oauthVerifiers.has(sessionId)) {
945
+ return c.json({ error: 'Session expired or invalid' }, 400);
946
+ }
573
947
 
574
- if (provider === 'anthropic') {
575
- const fullCode = fragment ? `${code}#${fragment}` : (code ?? '');
576
- const tokens = await exchangeWeb(fullCode, verifier, callbackUrl);
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
+ }
577
973
 
578
- await setAuth(
579
- 'anthropic',
580
- {
581
- type: 'oauth',
582
- refresh: tokens.refresh,
583
- access: tokens.access,
584
- expires: tokens.expires,
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',
585
999
  },
586
- undefined,
587
- 'global',
588
- );
589
- } else if (provider === 'openai') {
590
- const tokens = await exchangeOpenAIWeb(
591
- code ?? '',
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, {
592
1064
  verifier,
1065
+ provider,
1066
+ createdAt: Date.now(),
593
1067
  callbackUrl,
594
- );
1068
+ });
595
1069
 
596
- await setAuth(
597
- 'openai',
598
- {
599
- type: 'oauth',
600
- refresh: tokens.refresh,
601
- access: tokens.access,
602
- expires: tokens.expires,
603
- accountId: tokens.accountId,
604
- idToken: tokens.idToken,
605
- },
606
- undefined,
607
- 'global',
1070
+ c.header(
1071
+ 'Set-Cookie',
1072
+ `oauth_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
608
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);
609
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
+ }
610
1149
 
611
- return c.html(`
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(`
612
1197
  <html>
613
1198
  <head>
614
1199
  <title>Connected!</title>
@@ -653,11 +1238,11 @@ export function registerAuthRoutes(app: Hono) {
653
1238
  </body>
654
1239
  </html>
655
1240
  `);
656
- } catch (error) {
657
- const message =
658
- error instanceof Error ? error.message : 'Authentication failed';
659
- logger.error('OAuth callback failed', error);
660
- return c.html(`
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(`
661
1246
  <html>
662
1247
  <head>
663
1248
  <title>Error</title>
@@ -699,311 +1284,801 @@ export function registerAuthRoutes(app: Hono) {
699
1284
  </body>
700
1285
  </html>
701
1286
  `);
702
- }
703
- });
704
-
705
- app.post('/v1/auth/copilot/device/start', async (c) => {
706
- try {
707
- const deviceData = await authorizeCopilot();
708
- const sessionId = crypto.randomUUID();
709
- copilotDeviceSessions.set(sessionId, {
710
- deviceCode: deviceData.deviceCode,
711
- interval: deviceData.interval,
712
- provider: 'copilot',
713
- createdAt: Date.now(),
714
- });
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();
715
1511
  return c.json({
716
- sessionId,
717
- userCode: deviceData.userCode,
718
- verificationUri: deviceData.verificationUri,
719
- interval: deviceData.interval,
1512
+ oauth: true,
1513
+ token: true,
1514
+ ghImport,
720
1515
  });
721
- } catch (error) {
722
- const message =
723
- error instanceof Error
724
- ? error.message
725
- : 'Failed to start Copilot device flow';
726
- logger.error('Copilot device flow start failed', error);
727
- return c.json({ error: message }, 500);
728
- }
729
- });
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
+ }
730
1621
 
731
- app.post('/v1/auth/copilot/device/poll', async (c) => {
732
- try {
733
- const { sessionId } = await c.req.json<{ sessionId: string }>();
734
- if (!sessionId || !copilotDeviceSessions.has(sessionId)) {
735
- return c.json({ error: 'Session expired or invalid' }, 400);
736
- }
737
- const session = copilotDeviceSessions.get(sessionId);
738
- if (!session) {
739
- return c.json({ error: 'Session expired or invalid' }, 400);
740
- }
741
- const result = await pollForCopilotTokenOnce(session.deviceCode);
742
- if (result.status === 'complete') {
743
- copilotDeviceSessions.delete(sessionId);
744
1622
  await setAuth(
745
1623
  'copilot',
746
1624
  {
747
1625
  type: 'oauth',
748
- refresh: result.accessToken,
749
- access: result.accessToken,
1626
+ refresh: sanitized,
1627
+ access: sanitized,
750
1628
  expires: 0,
751
1629
  },
752
1630
  undefined,
753
1631
  'global',
754
1632
  );
755
- return c.json({ status: 'complete' });
756
- }
757
- if (result.status === 'pending') {
758
- return c.json({ status: 'pending' });
759
- }
760
- if (result.status === 'error') {
761
- copilotDeviceSessions.delete(sessionId);
762
- return c.json({ status: 'error', error: result.error });
763
- }
764
- return c.json({ status: 'pending' });
765
- } catch (error) {
766
- const message = error instanceof Error ? error.message : 'Poll failed';
767
- logger.error('Copilot device poll failed', error);
768
- return c.json({ error: message }, 500);
769
- }
770
- });
771
1633
 
772
- app.get('/v1/auth/copilot/methods', async (c) => {
773
- const ghImport = getGhImportCapability();
774
- return c.json({
775
- oauth: true,
776
- token: true,
777
- ghImport,
778
- });
779
- });
780
-
781
- app.post('/v1/auth/copilot/token', async (c) => {
782
- try {
783
- const { token } = await c.req.json<{ token: string }>();
784
- const sanitized = token?.trim();
785
- if (!sanitized) {
786
- return c.json({ error: 'Copilot token is required' }, 400);
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);
787
1650
  }
788
-
789
- const modelsResult = await fetchCopilotModels(sanitized);
790
- if (!modelsResult.ok) {
791
- return c.json(
792
- {
793
- error: `Invalid Copilot token: ${modelsResult.message}`,
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
+ },
794
1703
  },
795
- 400,
796
- );
797
- }
798
-
799
- await setAuth(
800
- 'copilot',
801
- {
802
- type: 'oauth',
803
- refresh: sanitized,
804
- access: sanitized,
805
- expires: 0,
806
1704
  },
807
- undefined,
808
- 'global',
809
- );
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
+ }
810
1742
 
811
- const models = Array.from(modelsResult.models).sort();
812
- return c.json({
813
- success: true,
814
- provider: 'copilot',
815
- source: 'token',
816
- modelCount: models.length,
817
- hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
818
- sampleModels: models.slice(0, 25),
819
- });
820
- } catch (error) {
821
- const message =
822
- error instanceof Error ? error.message : 'Failed to save Copilot token';
823
- logger.error('Failed to save Copilot token', error);
824
- return c.json({ error: message }, 500);
825
- }
826
- });
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
+ }
827
1750
 
828
- app.post('/v1/auth/copilot/gh/import', async (c) => {
829
- try {
830
- const ghImport = getGhImportCapability();
831
- if (!ghImport.available) {
832
- return c.json(
833
- {
834
- error: ghImport.reason || 'GitHub CLI is not available',
835
- },
836
- 400,
837
- );
838
- }
839
- if (!ghImport.authenticated) {
840
- return c.json(
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',
841
1763
  {
842
- error: ghImport.reason || 'GitHub CLI is not authenticated',
1764
+ type: 'oauth',
1765
+ refresh: ghToken,
1766
+ access: ghToken,
1767
+ expires: 0,
843
1768
  },
844
- 400,
1769
+ undefined,
1770
+ 'global',
845
1771
  );
846
- }
847
1772
 
848
- const ghToken = execFileSync('gh', ['auth', 'token'], {
849
- encoding: 'utf8',
850
- stdio: ['ignore', 'pipe', 'pipe'],
851
- }).trim();
852
- if (!ghToken) {
853
- return c.json({ error: 'GitHub CLI returned an empty token' }, 400);
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);
854
1789
  }
855
-
856
- const modelsResult = await fetchCopilotModels(ghToken);
857
- if (!modelsResult.ok) {
858
- return c.json(
859
- {
860
- error: `Imported gh token is not valid for Copilot: ${modelsResult.message}`,
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
+ },
861
1880
  },
862
- 400,
863
- );
864
- }
865
-
866
- await setAuth(
867
- 'copilot',
868
- {
869
- type: 'oauth',
870
- refresh: ghToken,
871
- access: ghToken,
872
- expires: 0,
873
1881
  },
874
- undefined,
875
- 'global',
876
- );
877
-
878
- const models = Array.from(modelsResult.models).sort();
879
- return c.json({
880
- success: true,
881
- provider: 'copilot',
882
- source: 'gh',
883
- modelCount: models.length,
884
- hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
885
- sampleModels: models.slice(0, 25),
886
- });
887
- } catch (error) {
888
- const message =
889
- error instanceof Error
890
- ? error.message
891
- : 'Failed to import GitHub CLI token';
892
- logger.error('Failed to import Copilot token from GitHub CLI', error);
893
- return c.json({ error: message }, 500);
894
- }
895
- });
896
-
897
- app.get('/v1/auth/copilot/diagnostics', async (c) => {
898
- try {
899
- const projectRoot = process.cwd();
900
- const entries: Array<{
901
- source: 'env' | 'stored';
902
- configured: boolean;
903
- modelCount?: number;
904
- hasGpt52Codex?: boolean;
905
- sampleModels?: string[];
906
- restrictedByOrgPolicy?: boolean;
907
- restrictedOrg?: string;
908
- restrictionMessage?: string;
909
- error?: string;
910
- }> = [];
911
-
912
- const envToken = readEnvKey('copilot');
913
- if (envToken) {
914
- const modelsResult = await fetchCopilotModels(envToken);
915
- if (modelsResult.ok) {
916
- const models = Array.from(modelsResult.models).sort();
917
- entries.push({
918
- source: 'env',
919
- configured: true,
920
- modelCount: models.length,
921
- hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
922
- sampleModels: models.slice(0, 25),
923
- });
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
+ }
924
1918
  } else {
925
- entries.push({
926
- source: 'env',
927
- configured: true,
928
- error: modelsResult.message,
929
- });
1919
+ entries.push({ source: 'env', configured: false });
930
1920
  }
931
- } else {
932
- entries.push({ source: 'env', configured: false });
933
- }
934
1921
 
935
- const storedAuth = await getAuth('copilot', projectRoot);
936
- if (storedAuth?.type === 'oauth') {
937
- const modelsResult = await fetchCopilotModels(storedAuth.refresh);
938
- const restriction = await detectOAuthOrgRestriction(storedAuth.refresh);
939
- if (modelsResult.ok) {
940
- const models = Array.from(modelsResult.models).sort();
941
- entries.push({
942
- source: 'stored',
943
- configured: true,
944
- modelCount: models.length,
945
- hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
946
- sampleModels: models.slice(0, 25),
947
- restrictedByOrgPolicy: restriction.restricted,
948
- restrictedOrg: restriction.org,
949
- restrictionMessage: restriction.message,
950
- });
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
+ }
951
1950
  } else {
952
- entries.push({
953
- source: 'stored',
954
- configured: true,
955
- error: modelsResult.message,
956
- restrictedByOrgPolicy: restriction.restricted,
957
- restrictedOrg: restriction.org,
958
- restrictionMessage: restriction.message,
959
- });
1951
+ entries.push({ source: 'stored', configured: false });
960
1952
  }
961
- } else {
962
- entries.push({ source: 'stored', configured: false });
963
- }
964
1953
 
965
- return c.json({
966
- tokenSources: entries,
967
- methods: {
968
- oauth: true,
969
- token: true,
970
- ghImport: getGhImportCapability(),
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
+ },
971
1995
  },
972
- });
973
- } catch (error) {
974
- const message =
975
- error instanceof Error ? error.message : 'Failed to inspect Copilot';
976
- logger.error('Failed to build Copilot diagnostics', error);
977
- return c.json({ error: message }, 500);
978
- }
979
- });
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;
980
2069
 
981
- app.post('/v1/auth/onboarding/complete', async (c) => {
982
- try {
983
- await setOnboardingComplete();
984
- return c.json({ success: true });
985
- } catch (error) {
986
- logger.error('Failed to complete onboarding', error);
987
- const errorResponse = serializeError(error);
988
- return c.json(errorResponse, errorResponse.error.status || 500);
989
- }
990
- });
2070
+ if (!isBuiltInProviderId(provider) || !catalog[provider]) {
2071
+ return c.json({ error: 'Unknown provider' }, 400);
2072
+ }
991
2073
 
992
- app.delete('/v1/auth/:provider', async (c) => {
993
- try {
994
- const provider = c.req.param('provider') as ProviderId;
2074
+ await removeAuth(provider, undefined, 'global');
995
2075
 
996
- if (!catalog[provider]) {
997
- return c.json({ error: 'Unknown provider' }, 400);
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);
998
2081
  }
999
-
1000
- await removeAuth(provider, undefined, 'global');
1001
-
1002
- return c.json({ success: true, provider });
1003
- } catch (error) {
1004
- logger.error('Failed to remove provider', error);
1005
- const errorResponse = serializeError(error);
1006
- return c.json(errorResponse, errorResponse.error.status || 500);
1007
- }
1008
- });
2082
+ },
2083
+ );
1009
2084
  }