@ottocode/server 0.1.265 → 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 (72) 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/provider/custom.ts +73 -0
  64. package/src/runtime/provider/index.ts +2 -85
  65. package/src/runtime/provider/reasoning-builders.ts +280 -0
  66. package/src/runtime/provider/reasoning.ts +67 -264
  67. package/src/tools/adapter/events.ts +116 -0
  68. package/src/tools/adapter/execution.ts +160 -0
  69. package/src/tools/adapter/pending.ts +37 -0
  70. package/src/tools/adapter/persistence.ts +166 -0
  71. package/src/tools/adapter/results.ts +97 -0
  72. package/src/tools/adapter.ts +124 -451
@@ -1,1323 +1,10 @@
1
1
  import type { Hono } from 'hono';
2
- import {
3
- fetchOttoRouterBalance,
4
- getPublicKeyFromPrivate,
5
- getAuth,
6
- loadConfig,
7
- } from '@ottocode/sdk';
8
- import { logger } from '@ottocode/sdk';
9
- import { serializeError } from '../runtime/errors/api-error.ts';
10
- import { Keypair } from '@solana/web3.js';
11
- import bs58 from 'bs58';
12
- import nacl from 'tweetnacl';
13
- import { publish } from '../events/bus.ts';
14
- import {
15
- resolveTopupMethodSelection,
16
- rejectTopupSelection,
17
- getPendingTopup,
18
- type TopupMethod,
19
- } from '../runtime/topup/manager.ts';
20
- import { openApiRoute } from '../openapi/route.ts';
21
-
22
- const OTTOROUTER_BASE_URL =
23
- process.env.OTTOROUTER_BASE_URL || 'https://api.ottorouter.org';
24
-
25
- function getOttoRouterBaseUrl(): string {
26
- return OTTOROUTER_BASE_URL.endsWith('/')
27
- ? OTTOROUTER_BASE_URL.slice(0, -1)
28
- : OTTOROUTER_BASE_URL;
29
- }
30
-
31
- async function getOttoRouterPrivateKey(): Promise<string | null> {
32
- if (process.env.OTTOROUTER_PRIVATE_KEY) {
33
- return process.env.OTTOROUTER_PRIVATE_KEY;
34
- }
35
-
36
- try {
37
- const cfg = await loadConfig(process.cwd());
38
- const auth = await getAuth('ottorouter', cfg.projectRoot);
39
- if (auth?.type === 'wallet' && auth.secret) {
40
- return auth.secret;
41
- }
42
- } catch {}
43
-
44
- return null;
45
- }
46
-
47
- function signNonce(nonce: string, privateKeyBytes: Uint8Array): string {
48
- const data = new TextEncoder().encode(nonce);
49
- const signature = nacl.sign.detached(data, privateKeyBytes);
50
- return bs58.encode(signature);
51
- }
52
-
53
- function buildWalletHeaders(privateKey: string): Record<string, string> {
54
- const privateKeyBytes = bs58.decode(privateKey);
55
- const keypair = Keypair.fromSecretKey(privateKeyBytes);
56
- const walletAddress = keypair.publicKey.toBase58();
57
- const nonce = Date.now().toString();
58
- const signature = signNonce(nonce, privateKeyBytes);
59
- return {
60
- 'x-wallet-address': walletAddress,
61
- 'x-wallet-nonce': nonce,
62
- 'x-wallet-signature': signature,
63
- };
64
- }
2
+ import { registerOttoRouterBillingRoutes } from './ottorouter/billing.ts';
3
+ import { registerOttoRouterTopupRoutes } from './ottorouter/topup.ts';
4
+ import { registerOttoRouterWalletRoutes } from './ottorouter/wallet.ts';
65
5
 
66
6
  export function registerOttoRouterRoutes(app: Hono) {
67
- openApiRoute(
68
- app,
69
- {
70
- method: 'get',
71
- path: '/v1/ottorouter/balance',
72
- tags: ['ottorouter'],
73
- operationId: 'getOttoRouterBalance',
74
- summary: 'Get OttoRouter account balance',
75
- description:
76
- 'Returns wallet balance, subscription, account info, limits, and usage data',
77
- responses: {
78
- '200': {
79
- description: 'OK',
80
- content: {
81
- 'application/json': {
82
- schema: {
83
- type: 'object',
84
- properties: {
85
- walletAddress: {
86
- type: 'string',
87
- },
88
- balance: {
89
- type: 'number',
90
- },
91
- totalSpent: {
92
- type: 'number',
93
- },
94
- totalTopups: {
95
- type: 'number',
96
- },
97
- requestCount: {
98
- type: 'number',
99
- },
100
- scope: {
101
- type: 'string',
102
- enum: ['wallet', 'account'],
103
- },
104
- payg: {
105
- type: 'object',
106
- properties: {
107
- walletBalanceUsd: {
108
- type: 'number',
109
- },
110
- accountBalanceUsd: {
111
- type: 'number',
112
- },
113
- rawPoolUsd: {
114
- type: 'number',
115
- },
116
- effectiveSpendableUsd: {
117
- type: 'number',
118
- },
119
- },
120
- },
121
- limits: {
122
- type: 'object',
123
- nullable: true,
124
- properties: {
125
- enabled: {
126
- type: 'boolean',
127
- },
128
- dailyLimitUsd: {
129
- type: 'number',
130
- nullable: true,
131
- },
132
- dailySpentUsd: {
133
- type: 'number',
134
- },
135
- dailyRemainingUsd: {
136
- type: 'number',
137
- nullable: true,
138
- },
139
- monthlyLimitUsd: {
140
- type: 'number',
141
- nullable: true,
142
- },
143
- monthlySpentUsd: {
144
- type: 'number',
145
- },
146
- monthlyRemainingUsd: {
147
- type: 'number',
148
- nullable: true,
149
- },
150
- capRemainingUsd: {
151
- type: 'number',
152
- nullable: true,
153
- },
154
- },
155
- },
156
- subscription: {
157
- type: 'object',
158
- nullable: true,
159
- properties: {
160
- active: {
161
- type: 'boolean',
162
- },
163
- tierId: {
164
- type: 'string',
165
- },
166
- tierName: {
167
- type: 'string',
168
- },
169
- creditsIncluded: {
170
- type: 'number',
171
- },
172
- creditsUsed: {
173
- type: 'number',
174
- },
175
- creditsRemaining: {
176
- type: 'number',
177
- },
178
- periodStart: {
179
- type: 'string',
180
- },
181
- periodEnd: {
182
- type: 'string',
183
- },
184
- },
185
- },
186
- },
187
- required: [
188
- 'walletAddress',
189
- 'balance',
190
- 'totalSpent',
191
- 'totalTopups',
192
- 'requestCount',
193
- ],
194
- },
195
- },
196
- },
197
- },
198
- '401': {
199
- description: 'Wallet not configured',
200
- content: {
201
- 'application/json': {
202
- schema: {
203
- type: 'object',
204
- properties: {
205
- error: {
206
- type: 'string',
207
- },
208
- },
209
- required: ['error'],
210
- },
211
- },
212
- },
213
- },
214
- '502': {
215
- description: 'Failed to fetch balance from OttoRouter',
216
- content: {
217
- 'application/json': {
218
- schema: {
219
- type: 'object',
220
- properties: {
221
- error: {
222
- type: 'string',
223
- },
224
- },
225
- required: ['error'],
226
- },
227
- },
228
- },
229
- },
230
- },
231
- },
232
- async (c) => {
233
- try {
234
- const privateKey = await getOttoRouterPrivateKey();
235
- if (!privateKey) {
236
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
237
- }
238
-
239
- const balance = await fetchOttoRouterBalance({ privateKey });
240
- if (!balance) {
241
- return c.json(
242
- { error: 'Failed to fetch balance from OttoRouter' },
243
- 502,
244
- );
245
- }
246
-
247
- return c.json(balance);
248
- } catch (error) {
249
- logger.error('Failed to fetch OttoRouter balance', error);
250
- const errorResponse = serializeError(error);
251
- return c.json(errorResponse, errorResponse.error.status || 500);
252
- }
253
- },
254
- );
255
-
256
- openApiRoute(
257
- app,
258
- {
259
- method: 'get',
260
- path: '/v1/ottorouter/wallet',
261
- tags: ['ottorouter'],
262
- operationId: 'getOttoRouterWallet',
263
- summary: 'Get OttoRouter wallet info',
264
- description:
265
- 'Returns whether the wallet is configured and its public key',
266
- responses: {
267
- '200': {
268
- description: 'OK',
269
- content: {
270
- 'application/json': {
271
- schema: {
272
- type: 'object',
273
- properties: {
274
- configured: {
275
- type: 'boolean',
276
- },
277
- publicKey: {
278
- type: 'string',
279
- },
280
- error: {
281
- type: 'string',
282
- },
283
- },
284
- required: ['configured'],
285
- },
286
- },
287
- },
288
- },
289
- },
290
- },
291
- async (c) => {
292
- try {
293
- const privateKey = await getOttoRouterPrivateKey();
294
- if (!privateKey) {
295
- return c.json(
296
- { error: 'OttoRouter wallet not configured', configured: false },
297
- 200,
298
- );
299
- }
300
-
301
- const publicKey = getPublicKeyFromPrivate(privateKey);
302
- if (!publicKey) {
303
- return c.json(
304
- { error: 'Invalid private key', configured: false },
305
- 200,
306
- );
307
- }
308
-
309
- return c.json({
310
- configured: true,
311
- publicKey,
312
- });
313
- } catch (error) {
314
- logger.error('Failed to get OttoRouter wallet info', error);
315
- const errorResponse = serializeError(error);
316
- return c.json(errorResponse, errorResponse.error.status || 500);
317
- }
318
- },
319
- );
320
-
321
- openApiRoute(
322
- app,
323
- {
324
- method: 'get',
325
- path: '/v1/ottorouter/usdc-balance',
326
- tags: ['ottorouter'],
327
- operationId: 'getOttoRouterUsdcBalance',
328
- summary: 'Get USDC token balance',
329
- description:
330
- 'Fetches USDC balance from Solana blockchain for the configured wallet',
331
- parameters: [
332
- {
333
- in: 'query',
334
- name: 'network',
335
- schema: {
336
- type: 'string',
337
- enum: ['mainnet', 'devnet'],
338
- default: 'mainnet',
339
- },
340
- description: 'Solana network to query',
341
- },
342
- ],
343
- responses: {
344
- '200': {
345
- description: 'OK',
346
- content: {
347
- 'application/json': {
348
- schema: {
349
- type: 'object',
350
- properties: {
351
- walletAddress: {
352
- type: 'string',
353
- },
354
- usdcBalance: {
355
- type: 'number',
356
- },
357
- network: {
358
- type: 'string',
359
- enum: ['mainnet', 'devnet'],
360
- },
361
- },
362
- required: ['walletAddress', 'usdcBalance', 'network'],
363
- },
364
- },
365
- },
366
- },
367
- '401': {
368
- description: 'Wallet not configured',
369
- content: {
370
- 'application/json': {
371
- schema: {
372
- type: 'object',
373
- properties: {
374
- error: {
375
- type: 'string',
376
- },
377
- },
378
- required: ['error'],
379
- },
380
- },
381
- },
382
- },
383
- '502': {
384
- description: 'Failed to fetch USDC balance from Solana',
385
- content: {
386
- 'application/json': {
387
- schema: {
388
- type: 'object',
389
- properties: {
390
- error: {
391
- type: 'string',
392
- },
393
- },
394
- required: ['error'],
395
- },
396
- },
397
- },
398
- },
399
- },
400
- },
401
- async (c) => {
402
- try {
403
- const privateKey = await getOttoRouterPrivateKey();
404
- if (!privateKey) {
405
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
406
- }
407
-
408
- const publicKey = getPublicKeyFromPrivate(privateKey);
409
- if (!publicKey) {
410
- return c.json({ error: 'Invalid private key' }, 400);
411
- }
412
-
413
- const baseUrl = getOttoRouterBaseUrl();
414
- const response = await fetch(
415
- `${baseUrl}/v1/wallet/${publicKey}/balances?limit=100&showNative=false&showNfts=false&showZeroBalance=false`,
416
- {
417
- method: 'GET',
418
- headers: { 'Content-Type': 'application/json' },
419
- },
420
- );
421
-
422
- if (!response.ok) {
423
- return c.json({ error: 'Failed to fetch wallet balances' }, 502);
424
- }
425
-
426
- const data = (await response.json()) as {
427
- balances: Array<{
428
- mint: string;
429
- symbol: string;
430
- name: string;
431
- balance: number;
432
- decimals: number;
433
- pricePerToken: number | null;
434
- usdValue: number | null;
435
- }>;
436
- totalUsdValue: number;
437
- };
438
-
439
- const usdcEntry = data.balances.find((b) => b.symbol === 'USDC');
440
-
441
- return c.json({
442
- walletAddress: publicKey,
443
- usdcBalance: usdcEntry?.balance ?? 0,
444
- network: 'mainnet' as const,
445
- });
446
- } catch (error) {
447
- logger.error('Failed to fetch USDC balance', error);
448
- const errorResponse = serializeError(error);
449
- return c.json(errorResponse, errorResponse.error.status || 500);
450
- }
451
- },
452
- );
453
-
454
- openApiRoute(
455
- app,
456
- {
457
- method: 'get',
458
- path: '/v1/ottorouter/topup/polar/estimate',
459
- tags: ['ottorouter'],
460
- operationId: 'getPolarTopupEstimate',
461
- summary: 'Get estimated fees for a Polar topup',
462
- parameters: [
463
- {
464
- in: 'query',
465
- name: 'amount',
466
- required: true,
467
- schema: {
468
- type: 'number',
469
- },
470
- description: 'Amount in USD',
471
- },
472
- ],
473
- responses: {
474
- '200': {
475
- description: 'OK',
476
- content: {
477
- 'application/json': {
478
- schema: {
479
- type: 'object',
480
- properties: {
481
- creditAmount: {
482
- type: 'number',
483
- },
484
- chargeAmount: {
485
- type: 'number',
486
- },
487
- feeAmount: {
488
- type: 'number',
489
- },
490
- feeBreakdown: {
491
- type: 'object',
492
- properties: {
493
- basePercent: {
494
- type: 'number',
495
- },
496
- internationalPercent: {
497
- type: 'number',
498
- },
499
- fixedCents: {
500
- type: 'number',
501
- },
502
- },
503
- },
504
- },
505
- },
506
- },
507
- },
508
- },
509
- },
510
- },
511
- async (c) => {
512
- try {
513
- const amount = c.req.query('amount');
514
- if (!amount) {
515
- return c.json({ error: 'Missing amount parameter' }, 400);
516
- }
517
-
518
- const baseUrl = getOttoRouterBaseUrl();
519
- const response = await fetch(
520
- `${baseUrl}/v1/topup/polar/estimate?amount=${amount}`,
521
- {
522
- method: 'GET',
523
- headers: { 'Content-Type': 'application/json' },
524
- },
525
- );
526
-
527
- const data = await response.json();
528
- if (!response.ok) {
529
- return c.json(data, response.status as 400 | 500);
530
- }
531
-
532
- return c.json(data);
533
- } catch (error) {
534
- logger.error('Failed to get Polar estimate', error);
535
- const errorResponse = serializeError(error);
536
- return c.json(errorResponse, errorResponse.error.status || 500);
537
- }
538
- },
539
- );
540
-
541
- openApiRoute(
542
- app,
543
- {
544
- method: 'post',
545
- path: '/v1/ottorouter/topup/polar',
546
- tags: ['ottorouter'],
547
- operationId: 'createPolarCheckout',
548
- summary: 'Create a Polar checkout for topping up',
549
- requestBody: {
550
- required: true,
551
- content: {
552
- 'application/json': {
553
- schema: {
554
- type: 'object',
555
- properties: {
556
- amount: {
557
- type: 'number',
558
- },
559
- successUrl: {
560
- type: 'string',
561
- },
562
- },
563
- required: ['amount', 'successUrl'],
564
- },
565
- },
566
- },
567
- },
568
- responses: {
569
- '200': {
570
- description: 'OK',
571
- content: {
572
- 'application/json': {
573
- schema: {
574
- type: 'object',
575
- },
576
- },
577
- },
578
- },
579
- '401': {
580
- description: 'Wallet not configured',
581
- content: {
582
- 'application/json': {
583
- schema: {
584
- type: 'object',
585
- properties: {
586
- error: {
587
- type: 'string',
588
- },
589
- },
590
- required: ['error'],
591
- },
592
- },
593
- },
594
- },
595
- },
596
- },
597
- async (c) => {
598
- try {
599
- const privateKey = await getOttoRouterPrivateKey();
600
- if (!privateKey) {
601
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
602
- }
603
-
604
- const body = await c.req.json();
605
- const { amount, successUrl } = body as {
606
- amount: number;
607
- successUrl: string;
608
- };
609
-
610
- if (!amount || typeof amount !== 'number') {
611
- return c.json({ error: 'Invalid amount' }, 400);
612
- }
613
-
614
- if (!successUrl || typeof successUrl !== 'string') {
615
- return c.json({ error: 'Missing successUrl' }, 400);
616
- }
617
-
618
- const walletHeaders = buildWalletHeaders(privateKey);
619
- const baseUrl = getOttoRouterBaseUrl();
620
-
621
- const response = await fetch(`${baseUrl}/v1/topup/polar`, {
622
- method: 'POST',
623
- headers: {
624
- 'Content-Type': 'application/json',
625
- ...walletHeaders,
626
- },
627
- body: JSON.stringify({ amount, successUrl }),
628
- });
629
-
630
- const data = await response.json();
631
- if (!response.ok) {
632
- return c.json(data, response.status as 400 | 500);
633
- }
634
-
635
- return c.json(data);
636
- } catch (error) {
637
- logger.error('Failed to create Polar checkout', error);
638
- const errorResponse = serializeError(error);
639
- return c.json(errorResponse, errorResponse.error.status || 500);
640
- }
641
- },
642
- );
643
-
644
- openApiRoute(
645
- app,
646
- {
647
- method: 'post',
648
- path: '/v1/ottorouter/topup/select',
649
- tags: ['ottorouter'],
650
- operationId: 'selectTopupMethod',
651
- summary: 'Select topup method for pending request',
652
- requestBody: {
653
- required: true,
654
- content: {
655
- 'application/json': {
656
- schema: {
657
- type: 'object',
658
- properties: {
659
- sessionId: {
660
- type: 'string',
661
- },
662
- method: {
663
- type: 'string',
664
- enum: ['crypto', 'fiat'],
665
- },
666
- },
667
- required: ['sessionId', 'method'],
668
- },
669
- },
670
- },
671
- },
672
- responses: {
673
- '200': {
674
- description: 'OK',
675
- content: {
676
- 'application/json': {
677
- schema: {
678
- type: 'object',
679
- properties: {
680
- success: {
681
- type: 'boolean',
682
- },
683
- method: {
684
- type: 'string',
685
- },
686
- },
687
- required: ['success', 'method'],
688
- },
689
- },
690
- },
691
- },
692
- '404': {
693
- description: 'No pending topup',
694
- content: {
695
- 'application/json': {
696
- schema: {
697
- type: 'object',
698
- properties: {
699
- error: {
700
- type: 'string',
701
- },
702
- },
703
- required: ['error'],
704
- },
705
- },
706
- },
707
- },
708
- },
709
- },
710
- async (c) => {
711
- try {
712
- const body = await c.req.json();
713
- const { sessionId, method } = body as {
714
- sessionId: string;
715
- method: TopupMethod;
716
- };
717
-
718
- if (!sessionId || typeof sessionId !== 'string') {
719
- return c.json({ error: 'Missing sessionId' }, 400);
720
- }
721
-
722
- if (!method || !['crypto', 'fiat'].includes(method)) {
723
- return c.json(
724
- { error: 'Invalid method, must be "crypto" or "fiat"' },
725
- 400,
726
- );
727
- }
728
-
729
- const resolved = resolveTopupMethodSelection(sessionId, method);
730
- if (!resolved) {
731
- return c.json(
732
- { error: 'No pending topup request found for this session' },
733
- 404,
734
- );
735
- }
736
-
737
- publish({
738
- type: 'ottorouter.topup.method_selected',
739
- sessionId,
740
- payload: { method },
741
- });
742
-
743
- return c.json({ success: true, method });
744
- } catch (error) {
745
- logger.error('Failed to select topup method', error);
746
- const errorResponse = serializeError(error);
747
- return c.json(errorResponse, errorResponse.error.status || 500);
748
- }
749
- },
750
- );
751
-
752
- openApiRoute(
753
- app,
754
- {
755
- method: 'post',
756
- path: '/v1/ottorouter/topup/cancel',
757
- tags: ['ottorouter'],
758
- operationId: 'cancelTopup',
759
- summary: 'Cancel pending topup',
760
- requestBody: {
761
- required: true,
762
- content: {
763
- 'application/json': {
764
- schema: {
765
- type: 'object',
766
- properties: {
767
- sessionId: {
768
- type: 'string',
769
- },
770
- reason: {
771
- type: 'string',
772
- },
773
- },
774
- required: ['sessionId'],
775
- },
776
- },
777
- },
778
- },
779
- responses: {
780
- '200': {
781
- description: 'OK',
782
- content: {
783
- 'application/json': {
784
- schema: {
785
- type: 'object',
786
- properties: {
787
- success: {
788
- type: 'boolean',
789
- },
790
- },
791
- required: ['success'],
792
- },
793
- },
794
- },
795
- },
796
- '404': {
797
- description: 'No pending topup',
798
- content: {
799
- 'application/json': {
800
- schema: {
801
- type: 'object',
802
- properties: {
803
- error: {
804
- type: 'string',
805
- },
806
- },
807
- required: ['error'],
808
- },
809
- },
810
- },
811
- },
812
- },
813
- },
814
- async (c) => {
815
- try {
816
- const body = await c.req.json();
817
- const { sessionId, reason } = body as {
818
- sessionId: string;
819
- reason?: string;
820
- };
821
-
822
- if (!sessionId || typeof sessionId !== 'string') {
823
- return c.json({ error: 'Missing sessionId' }, 400);
824
- }
825
-
826
- const rejected = rejectTopupSelection(
827
- sessionId,
828
- reason ?? 'User cancelled',
829
- );
830
- if (!rejected) {
831
- return c.json(
832
- { error: 'No pending topup request found for this session' },
833
- 404,
834
- );
835
- }
836
-
837
- publish({
838
- type: 'ottorouter.topup.cancelled',
839
- sessionId,
840
- payload: { reason: reason ?? 'User cancelled' },
841
- });
842
-
843
- return c.json({ success: true });
844
- } catch (error) {
845
- logger.error('Failed to cancel topup', error);
846
- const errorResponse = serializeError(error);
847
- return c.json(errorResponse, errorResponse.error.status || 500);
848
- }
849
- },
850
- );
851
-
852
- openApiRoute(
853
- app,
854
- {
855
- method: 'get',
856
- path: '/v1/ottorouter/topup/pending',
857
- tags: ['ottorouter'],
858
- operationId: 'getPendingTopup',
859
- summary: 'Get pending topup for a session',
860
- parameters: [
861
- {
862
- in: 'query',
863
- name: 'sessionId',
864
- required: true,
865
- schema: {
866
- type: 'string',
867
- },
868
- },
869
- ],
870
- responses: {
871
- '200': {
872
- description: 'OK',
873
- content: {
874
- 'application/json': {
875
- schema: {
876
- type: 'object',
877
- properties: {
878
- hasPending: {
879
- type: 'boolean',
880
- },
881
- sessionId: {
882
- type: 'string',
883
- },
884
- messageId: {
885
- type: 'string',
886
- },
887
- amountUsd: {
888
- type: 'number',
889
- },
890
- currentBalance: {
891
- type: 'number',
892
- },
893
- createdAt: {
894
- type: 'integer',
895
- },
896
- },
897
- required: ['hasPending'],
898
- },
899
- },
900
- },
901
- },
902
- },
903
- },
904
- async (c) => {
905
- try {
906
- const sessionId = c.req.query('sessionId');
907
- if (!sessionId) {
908
- return c.json({ error: 'Missing sessionId parameter' }, 400);
909
- }
910
-
911
- const pending = getPendingTopup(sessionId);
912
- if (!pending) {
913
- return c.json({ hasPending: false });
914
- }
915
-
916
- return c.json({
917
- hasPending: true,
918
- sessionId: pending.sessionId,
919
- messageId: pending.messageId,
920
- amountUsd: pending.amountUsd,
921
- currentBalance: pending.currentBalance,
922
- createdAt: pending.createdAt,
923
- });
924
- } catch (error) {
925
- logger.error('Failed to get pending topup', error);
926
- const errorResponse = serializeError(error);
927
- return c.json(errorResponse, errorResponse.error.status || 500);
928
- }
929
- },
930
- );
931
-
932
- openApiRoute(
933
- app,
934
- {
935
- method: 'get',
936
- path: '/v1/ottorouter/topup/polar/status',
937
- tags: ['ottorouter'],
938
- operationId: 'getPolarTopupStatus',
939
- summary: 'Get status of a Polar checkout',
940
- parameters: [
941
- {
942
- in: 'query',
943
- name: 'checkoutId',
944
- required: true,
945
- schema: {
946
- type: 'string',
947
- },
948
- },
949
- ],
950
- responses: {
951
- '200': {
952
- description: 'OK',
953
- content: {
954
- 'application/json': {
955
- schema: {
956
- type: 'object',
957
- properties: {
958
- checkoutId: {
959
- type: 'string',
960
- },
961
- confirmed: {
962
- type: 'boolean',
963
- },
964
- amountUsd: {
965
- type: 'number',
966
- nullable: true,
967
- },
968
- confirmedAt: {
969
- type: 'string',
970
- nullable: true,
971
- },
972
- },
973
- },
974
- },
975
- },
976
- },
977
- },
978
- },
979
- async (c) => {
980
- try {
981
- const checkoutId = c.req.query('checkoutId');
982
- if (!checkoutId) {
983
- return c.json({ error: 'Missing checkoutId parameter' }, 400);
984
- }
985
-
986
- const baseUrl = getOttoRouterBaseUrl();
987
- const response = await fetch(
988
- `${baseUrl}/v1/topup/polar/status?checkoutId=${checkoutId}`,
989
- {
990
- method: 'GET',
991
- headers: { 'Content-Type': 'application/json' },
992
- },
993
- );
994
-
995
- const data = await response.json();
996
- if (!response.ok) {
997
- return c.json(data, response.status as 400 | 500);
998
- }
999
-
1000
- return c.json(data);
1001
- } catch (error) {
1002
- logger.error('Failed to check Polar status', error);
1003
- const errorResponse = serializeError(error);
1004
- return c.json(errorResponse, errorResponse.error.status || 500);
1005
- }
1006
- },
1007
- );
1008
-
1009
- openApiRoute(
1010
- app,
1011
- {
1012
- method: 'get',
1013
- path: '/v1/ottorouter/topup/razorpay/estimate',
1014
- tags: ['ottorouter'],
1015
- operationId: 'getRazorpayTopupEstimate',
1016
- summary: 'Get estimated fees for a Razorpay topup',
1017
- parameters: [
1018
- {
1019
- in: 'query',
1020
- name: 'amount',
1021
- required: true,
1022
- schema: {
1023
- type: 'number',
1024
- },
1025
- description: 'Amount in USD',
1026
- },
1027
- ],
1028
- responses: {
1029
- '200': {
1030
- description: 'OK',
1031
- content: {
1032
- 'application/json': {
1033
- schema: {
1034
- type: 'object',
1035
- properties: {
1036
- creditAmountUsd: {
1037
- type: 'number',
1038
- },
1039
- chargeAmountInr: {
1040
- type: 'number',
1041
- },
1042
- feeAmountInr: {
1043
- type: 'number',
1044
- },
1045
- currency: {
1046
- type: 'string',
1047
- },
1048
- exchangeRate: {
1049
- type: 'number',
1050
- },
1051
- },
1052
- },
1053
- },
1054
- },
1055
- },
1056
- },
1057
- },
1058
- async (c) => {
1059
- try {
1060
- const amount = c.req.query('amount');
1061
- if (!amount) {
1062
- return c.json({ error: 'Missing amount parameter' }, 400);
1063
- }
1064
-
1065
- const baseUrl = getOttoRouterBaseUrl();
1066
- const response = await fetch(
1067
- `${baseUrl}/v1/topup/razorpay/estimate?amount=${amount}`,
1068
- {
1069
- method: 'GET',
1070
- headers: { 'Content-Type': 'application/json' },
1071
- },
1072
- );
1073
-
1074
- const data = await response.json();
1075
- if (!response.ok) {
1076
- return c.json(data, response.status as 400 | 500);
1077
- }
1078
-
1079
- return c.json(data);
1080
- } catch (error) {
1081
- logger.error('Failed to get Razorpay estimate', error);
1082
- const errorResponse = serializeError(error);
1083
- return c.json(errorResponse, errorResponse.error.status || 500);
1084
- }
1085
- },
1086
- );
1087
-
1088
- openApiRoute(
1089
- app,
1090
- {
1091
- method: 'post',
1092
- path: '/v1/ottorouter/topup/razorpay',
1093
- tags: ['ottorouter'],
1094
- operationId: 'createRazorpayOrder',
1095
- summary: 'Create a Razorpay order for topping up',
1096
- requestBody: {
1097
- required: true,
1098
- content: {
1099
- 'application/json': {
1100
- schema: {
1101
- type: 'object',
1102
- properties: {
1103
- amount: {
1104
- type: 'number',
1105
- },
1106
- },
1107
- required: ['amount'],
1108
- },
1109
- },
1110
- },
1111
- },
1112
- responses: {
1113
- '200': {
1114
- description: 'OK',
1115
- content: {
1116
- 'application/json': {
1117
- schema: {
1118
- type: 'object',
1119
- properties: {
1120
- success: {
1121
- type: 'boolean',
1122
- },
1123
- orderId: {
1124
- type: 'string',
1125
- },
1126
- amount: {
1127
- type: 'number',
1128
- },
1129
- currency: {
1130
- type: 'string',
1131
- },
1132
- creditAmountUsd: {
1133
- type: 'number',
1134
- },
1135
- keyId: {
1136
- type: 'string',
1137
- },
1138
- },
1139
- },
1140
- },
1141
- },
1142
- },
1143
- '401': {
1144
- description: 'Wallet not configured',
1145
- content: {
1146
- 'application/json': {
1147
- schema: {
1148
- type: 'object',
1149
- properties: {
1150
- error: {
1151
- type: 'string',
1152
- },
1153
- },
1154
- required: ['error'],
1155
- },
1156
- },
1157
- },
1158
- },
1159
- },
1160
- },
1161
- async (c) => {
1162
- try {
1163
- const privateKey = await getOttoRouterPrivateKey();
1164
- if (!privateKey) {
1165
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
1166
- }
1167
-
1168
- const body = await c.req.json();
1169
- const { amount } = body as { amount: number };
1170
-
1171
- if (!amount || typeof amount !== 'number') {
1172
- return c.json({ error: 'Invalid amount' }, 400);
1173
- }
1174
-
1175
- const walletHeaders = buildWalletHeaders(privateKey);
1176
- const baseUrl = getOttoRouterBaseUrl();
1177
-
1178
- const response = await fetch(`${baseUrl}/v1/topup/razorpay`, {
1179
- method: 'POST',
1180
- headers: {
1181
- 'Content-Type': 'application/json',
1182
- ...walletHeaders,
1183
- },
1184
- body: JSON.stringify({ amount }),
1185
- });
1186
-
1187
- const data = await response.json();
1188
- if (!response.ok) {
1189
- return c.json(data, response.status as 400 | 500);
1190
- }
1191
-
1192
- return c.json(data);
1193
- } catch (error) {
1194
- logger.error('Failed to create Razorpay order', error);
1195
- const errorResponse = serializeError(error);
1196
- return c.json(errorResponse, errorResponse.error.status || 500);
1197
- }
1198
- },
1199
- );
1200
-
1201
- openApiRoute(
1202
- app,
1203
- {
1204
- method: 'post',
1205
- path: '/v1/ottorouter/topup/razorpay/verify',
1206
- tags: ['ottorouter'],
1207
- operationId: 'verifyRazorpayPayment',
1208
- summary: 'Verify Razorpay payment and credit balance',
1209
- requestBody: {
1210
- required: true,
1211
- content: {
1212
- 'application/json': {
1213
- schema: {
1214
- type: 'object',
1215
- properties: {
1216
- razorpay_order_id: {
1217
- type: 'string',
1218
- },
1219
- razorpay_payment_id: {
1220
- type: 'string',
1221
- },
1222
- razorpay_signature: {
1223
- type: 'string',
1224
- },
1225
- },
1226
- required: [
1227
- 'razorpay_order_id',
1228
- 'razorpay_payment_id',
1229
- 'razorpay_signature',
1230
- ],
1231
- },
1232
- },
1233
- },
1234
- },
1235
- responses: {
1236
- '200': {
1237
- description: 'OK',
1238
- content: {
1239
- 'application/json': {
1240
- schema: {
1241
- type: 'object',
1242
- properties: {
1243
- success: {
1244
- type: 'boolean',
1245
- },
1246
- credited: {
1247
- type: 'number',
1248
- },
1249
- newBalance: {
1250
- type: 'number',
1251
- },
1252
- },
1253
- },
1254
- },
1255
- },
1256
- },
1257
- '401': {
1258
- description: 'Wallet not configured',
1259
- content: {
1260
- 'application/json': {
1261
- schema: {
1262
- type: 'object',
1263
- properties: {
1264
- error: {
1265
- type: 'string',
1266
- },
1267
- },
1268
- required: ['error'],
1269
- },
1270
- },
1271
- },
1272
- },
1273
- },
1274
- },
1275
- async (c) => {
1276
- try {
1277
- const privateKey = await getOttoRouterPrivateKey();
1278
- if (!privateKey) {
1279
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
1280
- }
1281
-
1282
- const body = await c.req.json();
1283
- const { razorpay_order_id, razorpay_payment_id, razorpay_signature } =
1284
- body as {
1285
- razorpay_order_id: string;
1286
- razorpay_payment_id: string;
1287
- razorpay_signature: string;
1288
- };
1289
-
1290
- if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) {
1291
- return c.json({ error: 'Missing payment details' }, 400);
1292
- }
1293
-
1294
- const walletHeaders = buildWalletHeaders(privateKey);
1295
- const baseUrl = getOttoRouterBaseUrl();
1296
-
1297
- const response = await fetch(`${baseUrl}/v1/topup/razorpay/verify`, {
1298
- method: 'POST',
1299
- headers: {
1300
- 'Content-Type': 'application/json',
1301
- ...walletHeaders,
1302
- },
1303
- body: JSON.stringify({
1304
- razorpay_order_id,
1305
- razorpay_payment_id,
1306
- razorpay_signature,
1307
- }),
1308
- });
1309
-
1310
- const data = await response.json();
1311
- if (!response.ok) {
1312
- return c.json(data, response.status as 400 | 500);
1313
- }
1314
-
1315
- return c.json(data);
1316
- } catch (error) {
1317
- logger.error('Failed to verify Razorpay payment', error);
1318
- const errorResponse = serializeError(error);
1319
- return c.json(errorResponse, errorResponse.error.status || 500);
1320
- }
1321
- },
1322
- );
7
+ registerOttoRouterWalletRoutes(app);
8
+ registerOttoRouterBillingRoutes(app);
9
+ registerOttoRouterTopupRoutes(app);
1323
10
  }