@ottocode/server 0.1.259 → 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 (69) 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/agent/runner-reasoning.ts +38 -3
  42. package/src/runtime/agent/runner.ts +1 -0
  43. package/src/runtime/ask/service.ts +1 -0
  44. package/src/runtime/message/compaction-limits.ts +3 -3
  45. package/src/runtime/provider/reasoning.ts +18 -7
  46. package/src/runtime/session/db-operations.ts +4 -3
  47. package/src/runtime/utils/token.ts +7 -2
  48. package/src/tools/adapter.ts +21 -0
  49. package/src/openapi/paths/ask.ts +0 -81
  50. package/src/openapi/paths/auth.ts +0 -687
  51. package/src/openapi/paths/branch.ts +0 -102
  52. package/src/openapi/paths/config.ts +0 -485
  53. package/src/openapi/paths/doctor.ts +0 -165
  54. package/src/openapi/paths/files.ts +0 -236
  55. package/src/openapi/paths/git.ts +0 -690
  56. package/src/openapi/paths/mcp.ts +0 -339
  57. package/src/openapi/paths/messages.ts +0 -103
  58. package/src/openapi/paths/ottorouter.ts +0 -594
  59. package/src/openapi/paths/provider-usage.ts +0 -59
  60. package/src/openapi/paths/research.ts +0 -227
  61. package/src/openapi/paths/session-approval.ts +0 -93
  62. package/src/openapi/paths/session-extras.ts +0 -336
  63. package/src/openapi/paths/session-files.ts +0 -91
  64. package/src/openapi/paths/sessions.ts +0 -210
  65. package/src/openapi/paths/skills.ts +0 -377
  66. package/src/openapi/paths/stream.ts +0 -26
  67. package/src/openapi/paths/terminals.ts +0 -226
  68. package/src/openapi/paths/tunnel.ts +0 -163
  69. package/src/openapi/spec.ts +0 -73
@@ -17,6 +17,7 @@ import {
17
17
  getPendingTopup,
18
18
  type TopupMethod,
19
19
  } from '../runtime/topup/manager.ts';
20
+ import { openApiRoute } from '../openapi/route.ts';
20
21
 
21
22
  const OTTOROUTER_BASE_URL =
22
23
  process.env.OTTOROUTER_BASE_URL || 'https://api.ottorouter.org';
@@ -63,429 +64,1260 @@ function buildWalletHeaders(privateKey: string): Record<string, string> {
63
64
  }
64
65
 
65
66
  export function registerOttoRouterRoutes(app: Hono) {
66
- app.get('/v1/ottorouter/balance', async (c) => {
67
- try {
68
- const privateKey = await getOttoRouterPrivateKey();
69
- if (!privateKey) {
70
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
71
- }
72
-
73
- const balance = await fetchOttoRouterBalance({ privateKey });
74
- if (!balance) {
75
- return c.json(
76
- { error: 'Failed to fetch balance from OttoRouter' },
77
- 502,
78
- );
79
- }
80
-
81
- return c.json(balance);
82
- } catch (error) {
83
- logger.error('Failed to fetch OttoRouter balance', error);
84
- const errorResponse = serializeError(error);
85
- return c.json(errorResponse, errorResponse.error.status || 500);
86
- }
87
- });
88
-
89
- app.get('/v1/ottorouter/wallet', async (c) => {
90
- try {
91
- const privateKey = await getOttoRouterPrivateKey();
92
- if (!privateKey) {
93
- return c.json(
94
- { error: 'OttoRouter wallet not configured', configured: false },
95
- 200,
96
- );
97
- }
98
-
99
- const publicKey = getPublicKeyFromPrivate(privateKey);
100
- if (!publicKey) {
101
- return c.json({ error: 'Invalid private key', configured: false }, 200);
102
- }
103
-
104
- return c.json({
105
- configured: true,
106
- publicKey,
107
- });
108
- } catch (error) {
109
- logger.error('Failed to get OttoRouter wallet info', error);
110
- const errorResponse = serializeError(error);
111
- return c.json(errorResponse, errorResponse.error.status || 500);
112
- }
113
- });
114
-
115
- app.get('/v1/ottorouter/usdc-balance', async (c) => {
116
- try {
117
- const privateKey = await getOttoRouterPrivateKey();
118
- if (!privateKey) {
119
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
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);
120
252
  }
121
-
122
- const publicKey = getPublicKeyFromPrivate(privateKey);
123
- if (!publicKey) {
124
- return c.json({ error: 'Invalid private key' }, 400);
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);
125
317
  }
126
-
127
- const baseUrl = getOttoRouterBaseUrl();
128
- const response = await fetch(
129
- `${baseUrl}/v1/wallet/${publicKey}/balances?limit=100&showNative=false&showNfts=false&showZeroBalance=false`,
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: [
130
332
  {
131
- method: 'GET',
132
- headers: { 'Content-Type': 'application/json' },
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',
133
341
  },
134
- );
135
-
136
- if (!response.ok) {
137
- return c.json({ error: 'Failed to fetch wallet balances' }, 502);
138
- }
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
+ );
139
421
 
140
- const data = (await response.json()) as {
141
- balances: Array<{
142
- mint: string;
143
- symbol: string;
144
- name: string;
145
- balance: number;
146
- decimals: number;
147
- pricePerToken: number | null;
148
- usdValue: number | null;
149
- }>;
150
- totalUsdValue: number;
151
- };
152
-
153
- const usdcEntry = data.balances.find((b) => b.symbol === 'USDC');
154
-
155
- return c.json({
156
- walletAddress: publicKey,
157
- usdcBalance: usdcEntry?.balance ?? 0,
158
- network: 'mainnet' as const,
159
- });
160
- } catch (error) {
161
- logger.error('Failed to fetch USDC balance', error);
162
- const errorResponse = serializeError(error);
163
- return c.json(errorResponse, errorResponse.error.status || 500);
164
- }
165
- });
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
+ };
166
438
 
167
- app.get('/v1/ottorouter/topup/polar/estimate', async (c) => {
168
- try {
169
- const amount = c.req.query('amount');
170
- if (!amount) {
171
- return c.json({ error: 'Missing amount parameter' }, 400);
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);
172
450
  }
173
-
174
- const baseUrl = getOttoRouterBaseUrl();
175
- const response = await fetch(
176
- `${baseUrl}/v1/topup/polar/estimate?amount=${amount}`,
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: [
177
463
  {
178
- method: 'GET',
179
- headers: { 'Content-Type': 'application/json' },
464
+ in: 'query',
465
+ name: 'amount',
466
+ required: true,
467
+ schema: {
468
+ type: 'number',
469
+ },
470
+ description: 'Amount in USD',
180
471
  },
181
- );
182
-
183
- const data = await response.json();
184
- if (!response.ok) {
185
- return c.json(data, response.status as 400 | 500);
186
- }
187
-
188
- return c.json(data);
189
- } catch (error) {
190
- logger.error('Failed to get Polar estimate', error);
191
- const errorResponse = serializeError(error);
192
- return c.json(errorResponse, errorResponse.error.status || 500);
193
- }
194
- });
195
-
196
- app.post('/v1/ottorouter/topup/polar', async (c) => {
197
- try {
198
- const privateKey = await getOttoRouterPrivateKey();
199
- if (!privateKey) {
200
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
201
- }
202
-
203
- const body = await c.req.json();
204
- const { amount, successUrl } = body as {
205
- amount: number;
206
- successUrl: string;
207
- };
208
-
209
- if (!amount || typeof amount !== 'number') {
210
- return c.json({ error: 'Invalid amount' }, 400);
211
- }
212
-
213
- if (!successUrl || typeof successUrl !== 'string') {
214
- return c.json({ error: 'Missing successUrl' }, 400);
215
- }
216
-
217
- const walletHeaders = buildWalletHeaders(privateKey);
218
- const baseUrl = getOttoRouterBaseUrl();
219
-
220
- const response = await fetch(`${baseUrl}/v1/topup/polar`, {
221
- method: 'POST',
222
- headers: {
223
- 'Content-Type': 'application/json',
224
- ...walletHeaders,
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
+ },
225
508
  },
226
- body: JSON.stringify({ amount, successUrl }),
227
- });
228
-
229
- const data = await response.json();
230
- if (!response.ok) {
231
- return c.json(data, response.status as 400 | 500);
232
- }
233
-
234
- return c.json(data);
235
- } catch (error) {
236
- logger.error('Failed to create Polar checkout', error);
237
- const errorResponse = serializeError(error);
238
- return c.json(errorResponse, errorResponse.error.status || 500);
239
- }
240
- });
241
-
242
- app.post('/v1/ottorouter/topup/select', async (c) => {
243
- try {
244
- const body = await c.req.json();
245
- const { sessionId, method } = body as {
246
- sessionId: string;
247
- method: TopupMethod;
248
- };
249
-
250
- if (!sessionId || typeof sessionId !== 'string') {
251
- return c.json({ error: 'Missing sessionId' }, 400);
252
- }
253
-
254
- if (!method || !['crypto', 'fiat'].includes(method)) {
255
- return c.json(
256
- { error: 'Invalid method, must be "crypto" or "fiat"' },
257
- 400,
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
+ },
258
525
  );
259
- }
260
-
261
- const resolved = resolveTopupMethodSelection(sessionId, method);
262
- if (!resolved) {
263
- return c.json(
264
- { error: 'No pending topup request found for this session' },
265
- 404,
266
- );
267
- }
268
526
 
269
- publish({
270
- type: 'ottorouter.topup.method_selected',
271
- sessionId,
272
- payload: { method },
273
- });
274
-
275
- return c.json({ success: true, method });
276
- } catch (error) {
277
- logger.error('Failed to select topup method', error);
278
- const errorResponse = serializeError(error);
279
- return c.json(errorResponse, errorResponse.error.status || 500);
280
- }
281
- });
282
-
283
- app.post('/v1/ottorouter/topup/cancel', async (c) => {
284
- try {
285
- const body = await c.req.json();
286
- const { sessionId, reason } = body as {
287
- sessionId: string;
288
- reason?: string;
289
- };
290
-
291
- if (!sessionId || typeof sessionId !== 'string') {
292
- return c.json({ error: 'Missing sessionId' }, 400);
293
- }
527
+ const data = await response.json();
528
+ if (!response.ok) {
529
+ return c.json(data, response.status as 400 | 500);
530
+ }
294
531
 
295
- const rejected = rejectTopupSelection(
296
- sessionId,
297
- reason ?? 'User cancelled',
298
- );
299
- if (!rejected) {
300
- return c.json(
301
- { error: 'No pending topup request found for this session' },
302
- 404,
303
- );
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);
304
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
+ };
305
609
 
306
- publish({
307
- type: 'ottorouter.topup.cancelled',
308
- sessionId,
309
- payload: { reason: reason ?? 'User cancelled' },
310
- });
311
-
312
- return c.json({ success: true });
313
- } catch (error) {
314
- logger.error('Failed to cancel topup', error);
315
- const errorResponse = serializeError(error);
316
- return c.json(errorResponse, errorResponse.error.status || 500);
317
- }
318
- });
319
-
320
- app.get('/v1/ottorouter/topup/pending', async (c) => {
321
- try {
322
- const sessionId = c.req.query('sessionId');
323
- if (!sessionId) {
324
- return c.json({ error: 'Missing sessionId parameter' }, 400);
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);
325
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
+ };
326
717
 
327
- const pending = getPendingTopup(sessionId);
328
- if (!pending) {
329
- return c.json({ hasPending: false });
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);
330
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
+ };
331
821
 
332
- return c.json({
333
- hasPending: true,
334
- sessionId: pending.sessionId,
335
- messageId: pending.messageId,
336
- amountUsd: pending.amountUsd,
337
- currentBalance: pending.currentBalance,
338
- createdAt: pending.createdAt,
339
- });
340
- } catch (error) {
341
- logger.error('Failed to get pending topup', error);
342
- const errorResponse = serializeError(error);
343
- return c.json(errorResponse, errorResponse.error.status || 500);
344
- }
345
- });
822
+ if (!sessionId || typeof sessionId !== 'string') {
823
+ return c.json({ error: 'Missing sessionId' }, 400);
824
+ }
346
825
 
347
- app.get('/v1/ottorouter/topup/polar/status', async (c) => {
348
- try {
349
- const checkoutId = c.req.query('checkoutId');
350
- if (!checkoutId) {
351
- return c.json({ error: 'Missing checkoutId parameter' }, 400);
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);
352
848
  }
353
-
354
- const baseUrl = getOttoRouterBaseUrl();
355
- const response = await fetch(
356
- `${baseUrl}/v1/topup/polar/status?checkoutId=${checkoutId}`,
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: [
357
861
  {
358
- method: 'GET',
359
- headers: { 'Content-Type': 'application/json' },
862
+ in: 'query',
863
+ name: 'sessionId',
864
+ required: true,
865
+ schema: {
866
+ type: 'string',
867
+ },
360
868
  },
361
- );
362
-
363
- const data = await response.json();
364
- if (!response.ok) {
365
- return c.json(data, response.status as 400 | 500);
366
- }
367
-
368
- return c.json(data);
369
- } catch (error) {
370
- logger.error('Failed to check Polar status', error);
371
- const errorResponse = serializeError(error);
372
- return c.json(errorResponse, errorResponse.error.status || 500);
373
- }
374
- });
375
-
376
- app.get('/v1/ottorouter/topup/razorpay/estimate', async (c) => {
377
- try {
378
- const amount = c.req.query('amount');
379
- if (!amount) {
380
- return c.json({ error: 'Missing amount parameter' }, 400);
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);
381
928
  }
382
-
383
- const baseUrl = getOttoRouterBaseUrl();
384
- const response = await fetch(
385
- `${baseUrl}/v1/topup/razorpay/estimate?amount=${amount}`,
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: [
386
941
  {
387
- method: 'GET',
388
- headers: { 'Content-Type': 'application/json' },
942
+ in: 'query',
943
+ name: 'checkoutId',
944
+ required: true,
945
+ schema: {
946
+ type: 'string',
947
+ },
389
948
  },
390
- );
391
-
392
- const data = await response.json();
393
- if (!response.ok) {
394
- return c.json(data, response.status as 400 | 500);
395
- }
396
-
397
- return c.json(data);
398
- } catch (error) {
399
- logger.error('Failed to get Razorpay estimate', error);
400
- const errorResponse = serializeError(error);
401
- return c.json(errorResponse, errorResponse.error.status || 500);
402
- }
403
- });
404
-
405
- app.post('/v1/ottorouter/topup/razorpay', async (c) => {
406
- try {
407
- const privateKey = await getOttoRouterPrivateKey();
408
- if (!privateKey) {
409
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
410
- }
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
+ );
411
994
 
412
- const body = await c.req.json();
413
- const { amount } = body as { amount: number };
995
+ const data = await response.json();
996
+ if (!response.ok) {
997
+ return c.json(data, response.status as 400 | 500);
998
+ }
414
999
 
415
- if (!amount || typeof amount !== 'number') {
416
- return c.json({ error: 'Invalid amount' }, 400);
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);
417
1005
  }
418
-
419
- const walletHeaders = buildWalletHeaders(privateKey);
420
- const baseUrl = getOttoRouterBaseUrl();
421
-
422
- const response = await fetch(`${baseUrl}/v1/topup/razorpay`, {
423
- method: 'POST',
424
- headers: {
425
- 'Content-Type': 'application/json',
426
- ...walletHeaders,
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',
427
1026
  },
428
- body: JSON.stringify({ amount }),
429
- });
430
-
431
- const data = await response.json();
432
- if (!response.ok) {
433
- return c.json(data, response.status as 400 | 500);
434
- }
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
+ );
435
1073
 
436
- return c.json(data);
437
- } catch (error) {
438
- logger.error('Failed to create Razorpay order', error);
439
- const errorResponse = serializeError(error);
440
- return c.json(errorResponse, errorResponse.error.status || 500);
441
- }
442
- });
1074
+ const data = await response.json();
1075
+ if (!response.ok) {
1076
+ return c.json(data, response.status as 400 | 500);
1077
+ }
443
1078
 
444
- app.post('/v1/ottorouter/topup/razorpay/verify', async (c) => {
445
- try {
446
- const privateKey = await getOttoRouterPrivateKey();
447
- if (!privateKey) {
448
- return c.json({ error: 'OttoRouter wallet not configured' }, 401);
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);
449
1084
  }
450
-
451
- const body = await c.req.json();
452
- const { razorpay_order_id, razorpay_payment_id, razorpay_signature } =
453
- body as {
454
- razorpay_order_id: string;
455
- razorpay_payment_id: string;
456
- razorpay_signature: string;
457
- };
458
-
459
- if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) {
460
- return c.json({ error: 'Missing payment details' }, 400);
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);
461
1197
  }
462
-
463
- const walletHeaders = buildWalletHeaders(privateKey);
464
- const baseUrl = getOttoRouterBaseUrl();
465
-
466
- const response = await fetch(`${baseUrl}/v1/topup/razorpay/verify`, {
467
- method: 'POST',
468
- headers: {
469
- 'Content-Type': 'application/json',
470
- ...walletHeaders,
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
+ },
471
1256
  },
472
- body: JSON.stringify({
473
- razorpay_order_id,
474
- razorpay_payment_id,
475
- razorpay_signature,
476
- }),
477
- });
478
-
479
- const data = await response.json();
480
- if (!response.ok) {
481
- return c.json(data, response.status as 400 | 500);
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);
482
1320
  }
483
-
484
- return c.json(data);
485
- } catch (error) {
486
- logger.error('Failed to verify Razorpay payment', error);
487
- const errorResponse = serializeError(error);
488
- return c.json(errorResponse, errorResponse.error.status || 500);
489
- }
490
- });
1321
+ },
1322
+ );
491
1323
  }