@xtr-dev/rondevu-server 0.3.0 → 0.5.0

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.
package/src/app.ts CHANGED
@@ -2,20 +2,17 @@ import { Hono } from 'hono';
2
2
  import { cors } from 'hono/cors';
3
3
  import { Storage } from './storage/types.ts';
4
4
  import { Config } from './config.ts';
5
- import { createAuthMiddleware, getAuthenticatedPeerId } from './middleware/auth.ts';
6
- import { generatePeerId, encryptPeerId, validateUsernameClaim, validateServicePublish, validateServiceFqn, parseServiceFqn, isVersionCompatible } from './crypto.ts';
7
- import type { Context } from 'hono';
5
+ import { handleRpc, RpcRequest } from './rpc.ts';
6
+
7
+ // Constants
8
+ const MAX_BATCH_SIZE = 100;
8
9
 
9
10
  /**
10
- * Creates the Hono application with username and service-based WebRTC signaling
11
- * RESTful API design - v0.11.0
11
+ * Creates the Hono application with RPC interface
12
12
  */
13
13
  export function createApp(storage: Storage, config: Config) {
14
14
  const app = new Hono();
15
15
 
16
- // Create auth middleware
17
- const authMiddleware = createAuthMiddleware(config.authSecret);
18
-
19
16
  // Enable CORS
20
17
  app.use('/*', cors({
21
18
  origin: (origin) => {
@@ -27,706 +24,70 @@ export function createApp(storage: Storage, config: Config) {
27
24
  }
28
25
  return config.corsOrigins[0];
29
26
  },
30
- allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
31
- allowHeaders: ['Content-Type', 'Origin', 'Authorization'],
27
+ allowMethods: ['GET', 'POST', 'OPTIONS'],
28
+ allowHeaders: ['Content-Type', 'Origin'],
32
29
  exposeHeaders: ['Content-Type'],
33
- maxAge: 600,
34
- credentials: true,
30
+ credentials: false,
31
+ maxAge: 86400,
35
32
  }));
36
33
 
37
- // ===== General Endpoints =====
38
-
39
- /**
40
- * GET /
41
- * Returns server information
42
- */
34
+ // Root endpoint - server info
43
35
  app.get('/', (c) => {
44
36
  return c.json({
45
37
  version: config.version,
46
38
  name: 'Rondevu',
47
- description: 'DNS-like WebRTC signaling with username claiming and service discovery'
48
- });
39
+ description: 'WebRTC signaling with RPC interface and Ed25519 authentication',
40
+ }, 200);
49
41
  });
50
42
 
51
- /**
52
- * GET /health
53
- * Health check endpoint
54
- */
43
+ // Health check
55
44
  app.get('/health', (c) => {
56
45
  return c.json({
57
46
  status: 'ok',
58
47
  timestamp: Date.now(),
59
- version: config.version
60
- });
61
- });
62
-
63
- /**
64
- * POST /register
65
- * Register a new peer
66
- */
67
- app.post('/register', async (c) => {
68
- try {
69
- const peerId = generatePeerId();
70
- const secret = await encryptPeerId(peerId, config.authSecret);
71
-
72
- return c.json({
73
- peerId,
74
- secret
75
- }, 200);
76
- } catch (err) {
77
- console.error('Error registering peer:', err);
78
- return c.json({ error: 'Internal server error' }, 500);
79
- }
80
- });
81
-
82
- // ===== User Management (RESTful) =====
83
-
84
- /**
85
- * GET /users/:username
86
- * Check if username is available or get claim info
87
- */
88
- app.get('/users/:username', async (c) => {
89
- try {
90
- const username = c.req.param('username');
91
-
92
- const claimed = await storage.getUsername(username);
93
-
94
- if (!claimed) {
95
- return c.json({
96
- username,
97
- available: true
98
- }, 200);
99
- }
100
-
101
- return c.json({
102
- username: claimed.username,
103
- available: false,
104
- claimedAt: claimed.claimedAt,
105
- expiresAt: claimed.expiresAt,
106
- publicKey: claimed.publicKey
107
- }, 200);
108
- } catch (err) {
109
- console.error('Error checking username:', err);
110
- return c.json({ error: 'Internal server error' }, 500);
111
- }
112
- });
113
-
114
- /**
115
- * POST /users/:username
116
- * Claim a username with cryptographic proof
117
- */
118
- app.post('/users/:username', async (c) => {
119
- try {
120
- const username = c.req.param('username');
121
- const body = await c.req.json();
122
- const { publicKey, signature, message } = body;
123
-
124
- if (!publicKey || !signature || !message) {
125
- return c.json({ error: 'Missing required parameters: publicKey, signature, message' }, 400);
126
- }
127
-
128
- // Validate claim
129
- const validation = await validateUsernameClaim(username, publicKey, signature, message);
130
- if (!validation.valid) {
131
- return c.json({ error: validation.error }, 400);
132
- }
133
-
134
- // Attempt to claim username
135
- try {
136
- const claimed = await storage.claimUsername({
137
- username,
138
- publicKey,
139
- signature,
140
- message
141
- });
142
-
143
- return c.json({
144
- username: claimed.username,
145
- claimedAt: claimed.claimedAt,
146
- expiresAt: claimed.expiresAt
147
- }, 201);
148
- } catch (err: any) {
149
- if (err.message?.includes('already claimed')) {
150
- return c.json({ error: 'Username already claimed by different public key' }, 409);
151
- }
152
- throw err;
153
- }
154
- } catch (err) {
155
- console.error('Error claiming username:', err);
156
- return c.json({ error: 'Internal server error' }, 500);
157
- }
158
- });
159
-
160
- /**
161
- * GET /users/:username/services/:fqn
162
- * Get service by username and FQN with semver-compatible matching
163
- */
164
- app.get('/users/:username/services/:fqn', async (c) => {
165
- try {
166
- const username = c.req.param('username');
167
- const serviceFqn = decodeURIComponent(c.req.param('fqn'));
168
-
169
- // Parse the requested FQN
170
- const parsed = parseServiceFqn(serviceFqn);
171
- if (!parsed) {
172
- return c.json({ error: 'Invalid service FQN format' }, 400);
173
- }
174
-
175
- const { serviceName, version: requestedVersion } = parsed;
176
-
177
- // Find all services with matching service name
178
- const matchingServices = await storage.findServicesByName(username, serviceName);
179
-
180
- if (matchingServices.length === 0) {
181
- return c.json({ error: 'Service not found' }, 404);
182
- }
183
-
184
- // Filter to compatible versions
185
- const compatibleServices = matchingServices.filter(service => {
186
- const serviceParsed = parseServiceFqn(service.serviceFqn);
187
- if (!serviceParsed) return false;
188
- return isVersionCompatible(requestedVersion, serviceParsed.version);
189
- });
190
-
191
- if (compatibleServices.length === 0) {
192
- return c.json({
193
- error: 'No compatible version found',
194
- message: `Requested ${serviceFqn}, but no compatible versions available`
195
- }, 404);
196
- }
197
-
198
- // Use the first compatible service (most recently created)
199
- const service = compatibleServices[0];
200
-
201
- // Get the UUID for this service
202
- const uuid = await storage.queryService(username, service.serviceFqn);
203
-
204
- if (!uuid) {
205
- return c.json({ error: 'Service index not found' }, 500);
206
- }
207
-
208
- // Get all offers for this service
209
- const serviceOffers = await storage.getOffersForService(service.id);
210
-
211
- if (serviceOffers.length === 0) {
212
- return c.json({ error: 'No offers found for this service' }, 404);
213
- }
214
-
215
- // Find an unanswered offer
216
- const availableOffer = serviceOffers.find(offer => !offer.answererPeerId);
217
-
218
- if (!availableOffer) {
219
- return c.json({
220
- error: 'No available offers',
221
- message: 'All offers from this service are currently in use. Please try again later.'
222
- }, 503);
223
- }
224
-
225
- return c.json({
226
- uuid: uuid,
227
- serviceId: service.id,
228
- username: service.username,
229
- serviceFqn: service.serviceFqn,
230
- offerId: availableOffer.id,
231
- sdp: availableOffer.sdp,
232
- isPublic: service.isPublic,
233
- metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
234
- createdAt: service.createdAt,
235
- expiresAt: service.expiresAt
236
- }, 200);
237
- } catch (err) {
238
- console.error('Error getting service:', err);
239
- return c.json({ error: 'Internal server error' }, 500);
240
- }
241
- });
242
-
243
- /**
244
- * POST /users/:username/services
245
- * Publish a service with one or more offers (RESTful endpoint)
246
- */
247
- app.post('/users/:username/services', authMiddleware, async (c) => {
248
- let serviceFqn: string | undefined;
249
- let createdOffers: any[] = [];
250
-
251
- try {
252
- const username = c.req.param('username');
253
- const body = await c.req.json();
254
- serviceFqn = body.serviceFqn;
255
- const { offers, ttl, isPublic, metadata, signature, message } = body;
256
-
257
- if (!serviceFqn || !offers || !Array.isArray(offers) || offers.length === 0) {
258
- return c.json({ error: 'Missing required parameters: serviceFqn, offers (must be non-empty array)' }, 400);
259
- }
260
-
261
- // Validate service FQN
262
- const fqnValidation = validateServiceFqn(serviceFqn);
263
- if (!fqnValidation.valid) {
264
- return c.json({ error: fqnValidation.error }, 400);
265
- }
266
-
267
- // Verify username ownership (signature required)
268
- if (!signature || !message) {
269
- return c.json({ error: 'Missing signature or message for username verification' }, 400);
270
- }
271
-
272
- const usernameRecord = await storage.getUsername(username);
273
- if (!usernameRecord) {
274
- return c.json({ error: 'Username not claimed' }, 404);
275
- }
276
-
277
- // Verify signature matches username's public key
278
- const signatureValidation = await validateServicePublish(username, serviceFqn, usernameRecord.publicKey, signature, message);
279
- if (!signatureValidation.valid) {
280
- return c.json({ error: 'Invalid signature for username' }, 403);
281
- }
282
-
283
- // Delete existing service if one exists (upsert behavior)
284
- const existingUuid = await storage.queryService(username, serviceFqn);
285
- if (existingUuid) {
286
- const existingService = await storage.getServiceByUuid(existingUuid);
287
- if (existingService) {
288
- await storage.deleteService(existingService.id, username);
289
- }
290
- }
291
-
292
- // Validate all offers
293
- for (const offer of offers) {
294
- if (!offer.sdp || typeof offer.sdp !== 'string' || offer.sdp.length === 0) {
295
- return c.json({ error: 'Invalid SDP in offers array' }, 400);
296
- }
297
-
298
- if (offer.sdp.length > 64 * 1024) {
299
- return c.json({ error: 'SDP too large (max 64KB)' }, 400);
300
- }
301
- }
302
-
303
- // Calculate expiry
304
- const peerId = getAuthenticatedPeerId(c);
305
- const offerTtl = Math.min(
306
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
307
- config.offerMaxTtl
308
- );
309
- const expiresAt = Date.now() + offerTtl;
310
-
311
- // Prepare offer requests
312
- const offerRequests = offers.map(offer => ({
313
- peerId,
314
- sdp: offer.sdp,
315
- expiresAt
316
- }));
317
-
318
- // Create service with offers
319
- const result = await storage.createService({
320
- username,
321
- serviceFqn,
322
- expiresAt,
323
- isPublic: isPublic || false,
324
- metadata: metadata ? JSON.stringify(metadata) : undefined,
325
- offers: offerRequests
326
- });
327
-
328
- createdOffers = result.offers;
329
-
330
- // Return full service details with all offers
331
- return c.json({
332
- uuid: result.indexUuid,
333
- serviceFqn: serviceFqn,
334
- username: username,
335
- serviceId: result.service.id,
336
- offers: result.offers.map(o => ({
337
- offerId: o.id,
338
- sdp: o.sdp,
339
- createdAt: o.createdAt,
340
- expiresAt: o.expiresAt
341
- })),
342
- isPublic: result.service.isPublic,
343
- metadata: metadata,
344
- createdAt: result.service.createdAt,
345
- expiresAt: result.service.expiresAt
346
- }, 201);
347
- } catch (err) {
348
- console.error('Error creating service:', err);
349
- console.error('Error details:', {
350
- message: (err as Error).message,
351
- stack: (err as Error).stack,
352
- username: c.req.param('username'),
353
- serviceFqn,
354
- offerIds: createdOffers.map(o => o.id)
355
- });
356
- return c.json({
357
- error: 'Internal server error',
358
- details: (err as Error).message
359
- }, 500);
360
- }
361
- });
362
-
363
- /**
364
- * DELETE /users/:username/services/:fqn
365
- * Delete a service by username and FQN (RESTful)
366
- */
367
- app.delete('/users/:username/services/:fqn', authMiddleware, async (c) => {
368
- try {
369
- const username = c.req.param('username');
370
- const serviceFqn = decodeURIComponent(c.req.param('fqn'));
371
-
372
- // Find service by username and FQN
373
- const uuid = await storage.queryService(username, serviceFqn);
374
- if (!uuid) {
375
- return c.json({ error: 'Service not found' }, 404);
376
- }
377
-
378
- const service = await storage.getServiceByUuid(uuid);
379
- if (!service) {
380
- return c.json({ error: 'Service not found' }, 404);
381
- }
382
-
383
- const deleted = await storage.deleteService(service.id, username);
384
-
385
- if (!deleted) {
386
- return c.json({ error: 'Service not found or not owned by this username' }, 404);
387
- }
388
-
389
- return c.json({ success: true }, 200);
390
- } catch (err) {
391
- console.error('Error deleting service:', err);
392
- return c.json({ error: 'Internal server error' }, 500);
393
- }
394
- });
395
-
396
- // ===== Service Management (Legacy - for UUID-based access) =====
397
-
398
- /**
399
- * GET /services/:uuid
400
- * Get service details by index UUID (kept for privacy)
401
- */
402
- app.get('/services/:uuid', async (c) => {
403
- try {
404
- const uuid = c.req.param('uuid');
405
-
406
- const service = await storage.getServiceByUuid(uuid);
407
-
408
- if (!service) {
409
- return c.json({ error: 'Service not found' }, 404);
410
- }
411
-
412
- // Get all offers for this service
413
- const serviceOffers = await storage.getOffersForService(service.id);
414
-
415
- if (serviceOffers.length === 0) {
416
- return c.json({ error: 'No offers found for this service' }, 404);
417
- }
418
-
419
- // Find an unanswered offer
420
- const availableOffer = serviceOffers.find(offer => !offer.answererPeerId);
421
-
422
- if (!availableOffer) {
423
- return c.json({
424
- error: 'No available offers',
425
- message: 'All offers from this service are currently in use. Please try again later.'
426
- }, 503);
427
- }
428
-
429
- return c.json({
430
- uuid: uuid,
431
- serviceId: service.id,
432
- username: service.username,
433
- serviceFqn: service.serviceFqn,
434
- offerId: availableOffer.id,
435
- sdp: availableOffer.sdp,
436
- isPublic: service.isPublic,
437
- metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
438
- createdAt: service.createdAt,
439
- expiresAt: service.expiresAt
440
- }, 200);
441
- } catch (err) {
442
- console.error('Error getting service:', err);
443
- return c.json({ error: 'Internal server error' }, 500);
444
- }
445
- });
446
-
447
- // ===== Offer Management (Core WebRTC) =====
448
-
449
- /**
450
- * POST /offers
451
- * Create offers (direct, no service - for testing/advanced users)
452
- */
453
- app.post('/offers', authMiddleware, async (c) => {
454
- try {
455
- const body = await c.req.json();
456
- const { offers } = body;
457
-
458
- if (!Array.isArray(offers) || offers.length === 0) {
459
- return c.json({ error: 'Missing or invalid required parameter: offers (must be non-empty array)' }, 400);
460
- }
461
-
462
- if (offers.length > config.maxOffersPerRequest) {
463
- return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
464
- }
465
-
466
- const peerId = getAuthenticatedPeerId(c);
467
-
468
- // Validate and prepare offers
469
- const validated = offers.map((offer: any) => {
470
- const { sdp, ttl, secret } = offer;
471
-
472
- if (typeof sdp !== 'string' || sdp.length === 0) {
473
- throw new Error('Invalid SDP in offer');
474
- }
475
-
476
- if (sdp.length > 64 * 1024) {
477
- throw new Error('SDP too large (max 64KB)');
478
- }
479
-
480
- const offerTtl = Math.min(
481
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
482
- config.offerMaxTtl
483
- );
484
-
485
- return {
486
- peerId,
487
- sdp,
488
- expiresAt: Date.now() + offerTtl,
489
- secret: secret ? String(secret).substring(0, 128) : undefined
490
- };
491
- });
492
-
493
- const created = await storage.createOffers(validated);
494
-
495
- return c.json({
496
- offers: created.map(offer => ({
497
- id: offer.id,
498
- peerId: offer.peerId,
499
- expiresAt: offer.expiresAt,
500
- createdAt: offer.createdAt,
501
- hasSecret: !!offer.secret
502
- }))
503
- }, 201);
504
- } catch (err: any) {
505
- console.error('Error creating offers:', err);
506
- return c.json({ error: err.message || 'Internal server error' }, 500);
507
- }
508
- });
509
-
510
- /**
511
- * GET /offers/mine
512
- * Get authenticated peer's offers
513
- */
514
- app.get('/offers/mine', authMiddleware, async (c) => {
515
- try {
516
- const peerId = getAuthenticatedPeerId(c);
517
- const offers = await storage.getOffersByPeerId(peerId);
518
-
519
- return c.json({
520
- offers: offers.map(offer => ({
521
- id: offer.id,
522
- sdp: offer.sdp,
523
- createdAt: offer.createdAt,
524
- expiresAt: offer.expiresAt,
525
- lastSeen: offer.lastSeen,
526
- hasSecret: !!offer.secret,
527
- answererPeerId: offer.answererPeerId,
528
- answered: !!offer.answererPeerId
529
- }))
530
- }, 200);
531
- } catch (err) {
532
- console.error('Error getting offers:', err);
533
- return c.json({ error: 'Internal server error' }, 500);
534
- }
535
- });
536
-
537
- /**
538
- * GET /offers/:offerId
539
- * Get offer details (added for completeness)
540
- */
541
- app.get('/offers/:offerId', authMiddleware, async (c) => {
542
- try {
543
- const offerId = c.req.param('offerId');
544
- const offer = await storage.getOfferById(offerId);
545
-
546
- if (!offer) {
547
- return c.json({ error: 'Offer not found' }, 404);
548
- }
549
-
550
- return c.json({
551
- id: offer.id,
552
- peerId: offer.peerId,
553
- sdp: offer.sdp,
554
- createdAt: offer.createdAt,
555
- expiresAt: offer.expiresAt,
556
- answererPeerId: offer.answererPeerId,
557
- answered: !!offer.answererPeerId,
558
- answerSdp: offer.answerSdp
559
- }, 200);
560
- } catch (err) {
561
- console.error('Error getting offer:', err);
562
- return c.json({ error: 'Internal server error' }, 500);
563
- }
564
- });
565
-
566
- /**
567
- * DELETE /offers/:offerId
568
- * Delete an offer
569
- */
570
- app.delete('/offers/:offerId', authMiddleware, async (c) => {
571
- try {
572
- const offerId = c.req.param('offerId');
573
- const peerId = getAuthenticatedPeerId(c);
574
-
575
- const deleted = await storage.deleteOffer(offerId, peerId);
576
-
577
- if (!deleted) {
578
- return c.json({ error: 'Offer not found or not owned by this peer' }, 404);
579
- }
580
-
581
- return c.json({ success: true }, 200);
582
- } catch (err) {
583
- console.error('Error deleting offer:', err);
584
- return c.json({ error: 'Internal server error' }, 500);
585
- }
48
+ version: config.version,
49
+ }, 200);
586
50
  });
587
51
 
588
52
  /**
589
- * POST /offers/:offerId/answer
590
- * Answer an offer
53
+ * POST /rpc
54
+ * RPC endpoint - accepts single or batch method calls
591
55
  */
592
- app.post('/offers/:offerId/answer', authMiddleware, async (c) => {
56
+ app.post('/rpc', async (c) => {
593
57
  try {
594
- const offerId = c.req.param('offerId');
595
58
  const body = await c.req.json();
596
- const { sdp, secret } = body;
597
59
 
598
- if (!sdp) {
599
- return c.json({ error: 'Missing required parameter: sdp' }, 400);
600
- }
60
+ // Support both single request and batch array
61
+ const requests: RpcRequest[] = Array.isArray(body) ? body : [body];
601
62
 
602
- if (typeof sdp !== 'string' || sdp.length === 0) {
603
- return c.json({ error: 'Invalid SDP' }, 400);
63
+ // Validate requests
64
+ if (requests.length === 0) {
65
+ return c.json({ error: 'Empty request array' }, 400);
604
66
  }
605
67
 
606
- if (sdp.length > 64 * 1024) {
607
- return c.json({ error: 'SDP too large (max 64KB)' }, 400);
68
+ if (requests.length > MAX_BATCH_SIZE) {
69
+ return c.json({ error: `Too many requests in batch (max ${MAX_BATCH_SIZE})` }, 400);
608
70
  }
609
71
 
610
- const answererPeerId = getAuthenticatedPeerId(c);
611
-
612
- const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
613
-
614
- if (!result.success) {
615
- return c.json({ error: result.error }, 400);
616
- }
72
+ // Handle RPC
73
+ const responses = await handleRpc(requests, storage, config);
617
74
 
618
- return c.json({ success: true }, 200);
75
+ // Return single response or array based on input
76
+ return c.json(Array.isArray(body) ? responses : responses[0], 200);
619
77
  } catch (err) {
620
- console.error('Error answering offer:', err);
621
- return c.json({ error: 'Internal server error' }, 500);
622
- }
623
- });
624
-
625
- /**
626
- * GET /offers/:offerId/answer
627
- * Get answer for a specific offer (RESTful endpoint)
628
- */
629
- app.get('/offers/:offerId/answer', authMiddleware, async (c) => {
630
- try {
631
- const offerId = c.req.param('offerId');
632
- const peerId = getAuthenticatedPeerId(c);
633
-
634
- const offer = await storage.getOfferById(offerId);
635
-
636
- if (!offer) {
637
- return c.json({ error: 'Offer not found' }, 404);
638
- }
639
-
640
- // Verify ownership
641
- if (offer.peerId !== peerId) {
642
- return c.json({ error: 'Not authorized to view this answer' }, 403);
643
- }
644
-
645
- // Check if answered
646
- if (!offer.answererPeerId || !offer.answerSdp) {
647
- return c.json({ error: 'Offer not yet answered' }, 404);
648
- }
649
-
78
+ console.error('RPC error:', err);
650
79
  return c.json({
651
- offerId: offer.id,
652
- answererId: offer.answererPeerId,
653
- sdp: offer.answerSdp,
654
- answeredAt: offer.answeredAt
655
- }, 200);
656
- } catch (err) {
657
- console.error('Error getting answer:', err);
658
- return c.json({ error: 'Internal server error' }, 500);
80
+ success: false,
81
+ error: 'Invalid request format',
82
+ }, 400);
659
83
  }
660
84
  });
661
85
 
662
- // ===== ICE Candidate Exchange =====
663
-
664
- /**
665
- * POST /offers/:offerId/ice-candidates
666
- * Add ICE candidates for an offer
667
- */
668
- app.post('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {
669
- try {
670
- const offerId = c.req.param('offerId');
671
- const body = await c.req.json();
672
- const { candidates } = body;
673
-
674
- if (!Array.isArray(candidates) || candidates.length === 0) {
675
- return c.json({ error: 'Missing or invalid required parameter: candidates' }, 400);
676
- }
677
-
678
- const peerId = getAuthenticatedPeerId(c);
679
-
680
- // Get offer to determine role
681
- const offer = await storage.getOfferById(offerId);
682
- if (!offer) {
683
- return c.json({ error: 'Offer not found' }, 404);
684
- }
685
-
686
- // Determine role
687
- const role = offer.peerId === peerId ? 'offerer' : 'answerer';
688
-
689
- const count = await storage.addIceCandidates(offerId, peerId, role, candidates);
690
-
691
- return c.json({ count }, 200);
692
- } catch (err) {
693
- console.error('Error adding ICE candidates:', err);
694
- return c.json({ error: 'Internal server error' }, 500);
695
- }
696
- });
697
-
698
- /**
699
- * GET /offers/:offerId/ice-candidates
700
- * Get ICE candidates for an offer
701
- */
702
- app.get('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {
703
- try {
704
- const offerId = c.req.param('offerId');
705
- const since = c.req.query('since');
706
- const peerId = getAuthenticatedPeerId(c);
707
-
708
- // Get offer to determine role
709
- const offer = await storage.getOfferById(offerId);
710
- if (!offer) {
711
- return c.json({ error: 'Offer not found' }, 404);
712
- }
713
-
714
- // Get candidates for opposite role
715
- const targetRole = offer.peerId === peerId ? 'answerer' : 'offerer';
716
- const sinceTimestamp = since ? parseInt(since, 10) : undefined;
717
-
718
- const candidates = await storage.getIceCandidates(offerId, targetRole, sinceTimestamp);
719
-
720
- return c.json({
721
- candidates: candidates.map(c => ({
722
- candidate: c.candidate,
723
- createdAt: c.createdAt
724
- }))
725
- }, 200);
726
- } catch (err) {
727
- console.error('Error getting ICE candidates:', err);
728
- return c.json({ error: 'Internal server error' }, 500);
729
- }
86
+ // 404 for all other routes
87
+ app.all('*', (c) => {
88
+ return c.json({
89
+ error: 'Not found. Use POST /rpc for all API calls.',
90
+ }, 404);
730
91
  });
731
92
 
732
93
  return app;