@xtr-dev/rondevu-server 0.2.3 → 0.3.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.
@@ -0,0 +1,83 @@
1
+ -- V2 Migration: Add offers, usernames, and services tables
2
+
3
+ -- Offers table (replaces sessions)
4
+ CREATE TABLE IF NOT EXISTS offers (
5
+ id TEXT PRIMARY KEY,
6
+ peer_id TEXT NOT NULL,
7
+ sdp TEXT NOT NULL,
8
+ created_at INTEGER NOT NULL,
9
+ expires_at INTEGER NOT NULL,
10
+ last_seen INTEGER NOT NULL,
11
+ secret TEXT,
12
+ answerer_peer_id TEXT,
13
+ answer_sdp TEXT,
14
+ answered_at INTEGER
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);
18
+ CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
19
+ CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
20
+ CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
21
+
22
+ -- ICE candidates table
23
+ CREATE TABLE IF NOT EXISTS ice_candidates (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ offer_id TEXT NOT NULL,
26
+ peer_id TEXT NOT NULL,
27
+ role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
28
+ candidate TEXT NOT NULL,
29
+ created_at INTEGER NOT NULL,
30
+ FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
34
+ CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);
35
+ CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
36
+
37
+ -- Usernames table
38
+ CREATE TABLE IF NOT EXISTS usernames (
39
+ username TEXT PRIMARY KEY,
40
+ public_key TEXT NOT NULL UNIQUE,
41
+ claimed_at INTEGER NOT NULL,
42
+ expires_at INTEGER NOT NULL,
43
+ last_used INTEGER NOT NULL,
44
+ metadata TEXT,
45
+ CHECK(length(username) >= 3 AND length(username) <= 32)
46
+ );
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
49
+ CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
50
+
51
+ -- Services table
52
+ CREATE TABLE IF NOT EXISTS services (
53
+ id TEXT PRIMARY KEY,
54
+ username TEXT NOT NULL,
55
+ service_fqn TEXT NOT NULL,
56
+ offer_id TEXT NOT NULL,
57
+ created_at INTEGER NOT NULL,
58
+ expires_at INTEGER NOT NULL,
59
+ is_public INTEGER NOT NULL DEFAULT 0,
60
+ metadata TEXT,
61
+ FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
62
+ FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE,
63
+ UNIQUE(username, service_fqn)
64
+ );
65
+
66
+ CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
67
+ CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
68
+ CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
69
+ CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
70
+
71
+ -- Service index table (privacy layer)
72
+ CREATE TABLE IF NOT EXISTS service_index (
73
+ uuid TEXT PRIMARY KEY,
74
+ service_id TEXT NOT NULL,
75
+ username TEXT NOT NULL,
76
+ service_fqn TEXT NOT NULL,
77
+ created_at INTEGER NOT NULL,
78
+ expires_at INTEGER NOT NULL,
79
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS idx_service_index_username ON service_index(username);
83
+ CREATE INDEX IF NOT EXISTS idx_service_index_expires ON service_index(expires_at);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-server",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "DNS-like WebRTC signaling server with username claiming and service discovery",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/app.ts CHANGED
@@ -3,11 +3,12 @@ import { cors } from 'hono/cors';
3
3
  import { Storage } from './storage/types.ts';
4
4
  import { Config } from './config.ts';
5
5
  import { createAuthMiddleware, getAuthenticatedPeerId } from './middleware/auth.ts';
6
- import { generatePeerId, encryptPeerId, validateUsernameClaim, validateServiceFqn } from './crypto.ts';
6
+ import { generatePeerId, encryptPeerId, validateUsernameClaim, validateServicePublish, validateServiceFqn, parseServiceFqn, isVersionCompatible } from './crypto.ts';
7
7
  import type { Context } from 'hono';
8
8
 
9
9
  /**
10
10
  * Creates the Hono application with username and service-based WebRTC signaling
11
+ * RESTful API design - v0.11.0
11
12
  */
12
13
  export function createApp(storage: Storage, config: Config) {
13
14
  const app = new Hono();
@@ -61,7 +62,7 @@ export function createApp(storage: Storage, config: Config) {
61
62
 
62
63
  /**
63
64
  * POST /register
64
- * Register a new peer (still needed for peer ID generation)
65
+ * Register a new peer
65
66
  */
66
67
  app.post('/register', async (c) => {
67
68
  try {
@@ -78,19 +79,50 @@ export function createApp(storage: Storage, config: Config) {
78
79
  }
79
80
  });
80
81
 
81
- // ===== Username Management =====
82
+ // ===== User Management (RESTful) =====
82
83
 
83
84
  /**
84
- * POST /usernames/claim
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
85
116
  * Claim a username with cryptographic proof
86
117
  */
87
- app.post('/usernames/claim', async (c) => {
118
+ app.post('/users/:username', async (c) => {
88
119
  try {
120
+ const username = c.req.param('username');
89
121
  const body = await c.req.json();
90
- const { username, publicKey, signature, message } = body;
122
+ const { publicKey, signature, message } = body;
91
123
 
92
- if (!username || !publicKey || !signature || !message) {
93
- return c.json({ error: 'Missing required parameters: username, publicKey, signature, message' }, 400);
124
+ if (!publicKey || !signature || !message) {
125
+ return c.json({ error: 'Missing required parameters: publicKey, signature, message' }, 400);
94
126
  }
95
127
 
96
128
  // Validate claim
@@ -112,7 +144,7 @@ export function createApp(storage: Storage, config: Config) {
112
144
  username: claimed.username,
113
145
  claimedAt: claimed.claimedAt,
114
146
  expiresAt: claimed.expiresAt
115
- }, 200);
147
+ }, 201);
116
148
  } catch (err: any) {
117
149
  if (err.message?.includes('already claimed')) {
118
150
  return c.json({ error: 'Username already claimed by different public key' }, 409);
@@ -126,68 +158,104 @@ export function createApp(storage: Storage, config: Config) {
126
158
  });
127
159
 
128
160
  /**
129
- * GET /usernames/:username
130
- * Check if username is available or get claim info
161
+ * GET /users/:username/services/:fqn
162
+ * Get service by username and FQN with semver-compatible matching
131
163
  */
132
- app.get('/usernames/:username', async (c) => {
164
+ app.get('/users/:username/services/:fqn', async (c) => {
133
165
  try {
134
166
  const username = c.req.param('username');
167
+ const serviceFqn = decodeURIComponent(c.req.param('fqn'));
135
168
 
136
- const claimed = await storage.getUsername(username);
169
+ // Parse the requested FQN
170
+ const parsed = parseServiceFqn(serviceFqn);
171
+ if (!parsed) {
172
+ return c.json({ error: 'Invalid service FQN format' }, 400);
173
+ }
137
174
 
138
- if (!claimed) {
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) {
139
192
  return c.json({
140
- username,
141
- available: true
142
- }, 200);
193
+ error: 'No compatible version found',
194
+ message: `Requested ${serviceFqn}, but no compatible versions available`
195
+ }, 404);
143
196
  }
144
197
 
145
- return c.json({
146
- username: claimed.username,
147
- available: false,
148
- claimedAt: claimed.claimedAt,
149
- expiresAt: claimed.expiresAt,
150
- publicKey: claimed.publicKey
151
- }, 200);
152
- } catch (err) {
153
- console.error('Error checking username:', err);
154
- return c.json({ error: 'Internal server error' }, 500);
155
- }
156
- });
198
+ // Use the first compatible service (most recently created)
199
+ const service = compatibleServices[0];
157
200
 
158
- /**
159
- * GET /usernames/:username/services
160
- * List services for a username (privacy-preserving)
161
- */
162
- app.get('/usernames/:username/services', async (c) => {
163
- try {
164
- const username = c.req.param('username');
201
+ // Get the UUID for this service
202
+ const uuid = await storage.queryService(username, service.serviceFqn);
165
203
 
166
- const services = await storage.listServicesForUsername(username);
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
+ }
167
224
 
168
225
  return c.json({
169
- username,
170
- services
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
171
236
  }, 200);
172
237
  } catch (err) {
173
- console.error('Error listing services:', err);
238
+ console.error('Error getting service:', err);
174
239
  return c.json({ error: 'Internal server error' }, 500);
175
240
  }
176
241
  });
177
242
 
178
- // ===== Service Management =====
179
-
180
243
  /**
181
- * POST /services
182
- * Publish a service
244
+ * POST /users/:username/services
245
+ * Publish a service with one or more offers (RESTful endpoint)
183
246
  */
184
- app.post('/services', authMiddleware, async (c) => {
247
+ app.post('/users/:username/services', authMiddleware, async (c) => {
248
+ let serviceFqn: string | undefined;
249
+ let createdOffers: any[] = [];
250
+
185
251
  try {
252
+ const username = c.req.param('username');
186
253
  const body = await c.req.json();
187
- const { username, serviceFqn, sdp, ttl, isPublic, metadata, signature, message } = body;
254
+ serviceFqn = body.serviceFqn;
255
+ const { offers, ttl, isPublic, metadata, signature, message } = body;
188
256
 
189
- if (!username || !serviceFqn || !sdp) {
190
- return c.json({ error: 'Missing required parameters: username, serviceFqn, sdp' }, 400);
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);
191
259
  }
192
260
 
193
261
  // Validate service FQN
@@ -207,18 +275,29 @@ export function createApp(storage: Storage, config: Config) {
207
275
  }
208
276
 
209
277
  // Verify signature matches username's public key
210
- const signatureValidation = await validateUsernameClaim(username, usernameRecord.publicKey, signature, message);
278
+ const signatureValidation = await validateServicePublish(username, serviceFqn, usernameRecord.publicKey, signature, message);
211
279
  if (!signatureValidation.valid) {
212
280
  return c.json({ error: 'Invalid signature for username' }, 403);
213
281
  }
214
282
 
215
- // Validate SDP
216
- if (typeof sdp !== 'string' || sdp.length === 0) {
217
- return c.json({ error: 'Invalid SDP' }, 400);
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
+ }
218
290
  }
219
291
 
220
- if (sdp.length > 64 * 1024) {
221
- return c.json({ error: 'SDP too large (max 64KB)' }, 400);
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
+ }
222
301
  }
223
302
 
224
303
  // Calculate expiry
@@ -229,94 +308,79 @@ export function createApp(storage: Storage, config: Config) {
229
308
  );
230
309
  const expiresAt = Date.now() + offerTtl;
231
310
 
232
- // Create offer first
233
- const offers = await storage.createOffers([{
311
+ // Prepare offer requests
312
+ const offerRequests = offers.map(offer => ({
234
313
  peerId,
235
- sdp,
314
+ sdp: offer.sdp,
236
315
  expiresAt
237
- }]);
238
-
239
- if (offers.length === 0) {
240
- return c.json({ error: 'Failed to create offer' }, 500);
241
- }
316
+ }));
242
317
 
243
- const offer = offers[0];
244
-
245
- // Create service
318
+ // Create service with offers
246
319
  const result = await storage.createService({
247
320
  username,
248
321
  serviceFqn,
249
- offerId: offer.id,
250
322
  expiresAt,
251
323
  isPublic: isPublic || false,
252
- metadata: metadata ? JSON.stringify(metadata) : undefined
324
+ metadata: metadata ? JSON.stringify(metadata) : undefined,
325
+ offers: offerRequests
253
326
  });
254
327
 
328
+ createdOffers = result.offers;
329
+
330
+ // Return full service details with all offers
255
331
  return c.json({
256
- serviceId: result.service.id,
257
332
  uuid: result.indexUuid,
258
- offerId: offer.id,
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,
259
345
  expiresAt: result.service.expiresAt
260
346
  }, 201);
261
347
  } catch (err) {
262
348
  console.error('Error creating service:', err);
263
- return c.json({ error: 'Internal server error' }, 500);
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);
264
360
  }
265
361
  });
266
362
 
267
363
  /**
268
- * GET /services/:uuid
269
- * Get service details by index UUID
364
+ * DELETE /users/:username/services/:fqn
365
+ * Delete a service by username and FQN (RESTful)
270
366
  */
271
- app.get('/services/:uuid', async (c) => {
367
+ app.delete('/users/:username/services/:fqn', authMiddleware, async (c) => {
272
368
  try {
273
- const uuid = c.req.param('uuid');
274
-
275
- const service = await storage.getServiceByUuid(uuid);
369
+ const username = c.req.param('username');
370
+ const serviceFqn = decodeURIComponent(c.req.param('fqn'));
276
371
 
277
- if (!service) {
372
+ // Find service by username and FQN
373
+ const uuid = await storage.queryService(username, serviceFqn);
374
+ if (!uuid) {
278
375
  return c.json({ error: 'Service not found' }, 404);
279
376
  }
280
377
 
281
- // Get associated offer
282
- const offer = await storage.getOfferById(service.offerId);
283
-
284
- if (!offer) {
285
- return c.json({ error: 'Associated offer not found' }, 404);
286
- }
287
-
288
- return c.json({
289
- serviceId: service.id,
290
- username: service.username,
291
- serviceFqn: service.serviceFqn,
292
- offerId: service.offerId,
293
- sdp: offer.sdp,
294
- isPublic: service.isPublic,
295
- metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
296
- createdAt: service.createdAt,
297
- expiresAt: service.expiresAt
298
- }, 200);
299
- } catch (err) {
300
- console.error('Error getting service:', err);
301
- return c.json({ error: 'Internal server error' }, 500);
302
- }
303
- });
304
-
305
- /**
306
- * DELETE /services/:serviceId
307
- * Delete a service (requires ownership)
308
- */
309
- app.delete('/services/:serviceId', authMiddleware, async (c) => {
310
- try {
311
- const serviceId = c.req.param('serviceId');
312
- const body = await c.req.json();
313
- const { username } = body;
314
-
315
- if (!username) {
316
- return c.json({ error: 'Missing required parameter: username' }, 400);
378
+ const service = await storage.getServiceByUuid(uuid);
379
+ if (!service) {
380
+ return c.json({ error: 'Service not found' }, 404);
317
381
  }
318
382
 
319
- const deleted = await storage.deleteService(serviceId, username);
383
+ const deleted = await storage.deleteService(service.id, username);
320
384
 
321
385
  if (!deleted) {
322
386
  return c.json({ error: 'Service not found or not owned by this username' }, 404);
@@ -329,32 +393,53 @@ export function createApp(storage: Storage, config: Config) {
329
393
  }
330
394
  });
331
395
 
396
+ // ===== Service Management (Legacy - for UUID-based access) =====
397
+
332
398
  /**
333
- * POST /index/:username/query
334
- * Query service by FQN (returns UUID)
399
+ * GET /services/:uuid
400
+ * Get service details by index UUID (kept for privacy)
335
401
  */
336
- app.post('/index/:username/query', async (c) => {
402
+ app.get('/services/:uuid', async (c) => {
337
403
  try {
338
- const username = c.req.param('username');
339
- const body = await c.req.json();
340
- const { serviceFqn } = body;
404
+ const uuid = c.req.param('uuid');
405
+
406
+ const service = await storage.getServiceByUuid(uuid);
341
407
 
342
- if (!serviceFqn) {
343
- return c.json({ error: 'Missing required parameter: serviceFqn' }, 400);
408
+ if (!service) {
409
+ return c.json({ error: 'Service not found' }, 404);
344
410
  }
345
411
 
346
- const uuid = await storage.queryService(username, serviceFqn);
412
+ // Get all offers for this service
413
+ const serviceOffers = await storage.getOffersForService(service.id);
347
414
 
348
- if (!uuid) {
349
- return c.json({ error: 'Service not found' }, 404);
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);
350
427
  }
351
428
 
352
429
  return c.json({
353
- uuid,
354
- allowed: true
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
355
440
  }, 200);
356
441
  } catch (err) {
357
- console.error('Error querying service:', err);
442
+ console.error('Error getting service:', err);
358
443
  return c.json({ error: 'Internal server error' }, 500);
359
444
  }
360
445
  });
@@ -449,6 +534,35 @@ export function createApp(storage: Storage, config: Config) {
449
534
  }
450
535
  });
451
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
+
452
566
  /**
453
567
  * DELETE /offers/:offerId
454
568
  * Delete an offer
@@ -509,24 +623,38 @@ export function createApp(storage: Storage, config: Config) {
509
623
  });
510
624
 
511
625
  /**
512
- * GET /offers/answers
513
- * Get answers for authenticated peer's offers
626
+ * GET /offers/:offerId/answer
627
+ * Get answer for a specific offer (RESTful endpoint)
514
628
  */
515
- app.get('/offers/answers', authMiddleware, async (c) => {
629
+ app.get('/offers/:offerId/answer', authMiddleware, async (c) => {
516
630
  try {
631
+ const offerId = c.req.param('offerId');
517
632
  const peerId = getAuthenticatedPeerId(c);
518
- const offers = await storage.getAnsweredOffers(peerId);
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
+ }
519
649
 
520
650
  return c.json({
521
- answers: offers.map(offer => ({
522
- offerId: offer.id,
523
- answererPeerId: offer.answererPeerId,
524
- answerSdp: offer.answerSdp,
525
- answeredAt: offer.answeredAt
526
- }))
651
+ offerId: offer.id,
652
+ answererId: offer.answererPeerId,
653
+ sdp: offer.answerSdp,
654
+ answeredAt: offer.answeredAt
527
655
  }, 200);
528
656
  } catch (err) {
529
- console.error('Error getting answers:', err);
657
+ console.error('Error getting answer:', err);
530
658
  return c.json({ error: 'Internal server error' }, 500);
531
659
  }
532
660
  });
package/src/config.ts CHANGED
@@ -16,7 +16,6 @@ export interface Config {
16
16
  offerMinTtl: number;
17
17
  cleanupInterval: number;
18
18
  maxOffersPerRequest: number;
19
- maxTopicsPerOffer: number;
20
19
  }
21
20
 
22
21
  /**
@@ -45,7 +44,6 @@ export function loadConfig(): Config {
45
44
  offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || '86400000', 10),
46
45
  offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || '60000', 10),
47
46
  cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || '60000', 10),
48
- maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || '100', 10),
49
- maxTopicsPerOffer: parseInt(process.env.MAX_TOPICS_PER_OFFER || '50', 10),
47
+ maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || '100', 10)
50
48
  };
51
49
  }