@xtr-dev/rondevu-server 0.4.0 → 0.5.1
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/ADVANCED.md +502 -0
- package/README.md +136 -282
- package/dist/index.js +694 -733
- package/dist/index.js.map +4 -4
- package/migrations/0006_service_offer_refactor.sql +40 -0
- package/migrations/0007_simplify_schema.sql +54 -0
- package/migrations/0008_peer_id_to_username.sql +67 -0
- package/migrations/fresh_schema.sql +81 -0
- package/package.json +2 -1
- package/src/app.ts +38 -591
- package/src/config.ts +0 -13
- package/src/crypto.ts +103 -139
- package/src/rpc.ts +725 -0
- package/src/storage/d1.ts +169 -182
- package/src/storage/sqlite.ts +142 -168
- package/src/storage/types.ts +51 -95
- package/src/worker.ts +0 -6
- package/wrangler.toml +3 -3
- package/src/middleware/auth.ts +0 -51
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 {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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,620 +24,70 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
27
24
|
}
|
|
28
25
|
return config.corsOrigins[0];
|
|
29
26
|
},
|
|
30
|
-
allowMethods: ['GET', 'POST', '
|
|
31
|
-
allowHeaders: ['Content-Type', 'Origin'
|
|
27
|
+
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
|
28
|
+
allowHeaders: ['Content-Type', 'Origin'],
|
|
32
29
|
exposeHeaders: ['Content-Type'],
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
credentials: false,
|
|
31
|
+
maxAge: 86400,
|
|
35
32
|
}));
|
|
36
33
|
|
|
37
|
-
//
|
|
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: '
|
|
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
|
-
}
|
|
48
|
+
version: config.version,
|
|
49
|
+
}, 200);
|
|
445
50
|
});
|
|
446
51
|
|
|
447
|
-
// ===== Service-Based WebRTC Signaling =====
|
|
448
|
-
|
|
449
52
|
/**
|
|
450
|
-
* POST /
|
|
451
|
-
*
|
|
53
|
+
* POST /rpc
|
|
54
|
+
* RPC endpoint - accepts single or batch method calls
|
|
452
55
|
*/
|
|
453
|
-
app.post('/
|
|
56
|
+
app.post('/rpc', async (c) => {
|
|
454
57
|
try {
|
|
455
|
-
const uuid = c.req.param('uuid');
|
|
456
58
|
const body = await c.req.json();
|
|
457
|
-
const { sdp } = body;
|
|
458
|
-
|
|
459
|
-
if (!sdp) {
|
|
460
|
-
return c.json({ error: 'Missing required parameter: sdp' }, 400);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (typeof sdp !== 'string' || sdp.length === 0) {
|
|
464
|
-
return c.json({ error: 'Invalid SDP' }, 400);
|
|
465
|
-
}
|
|
466
59
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
60
|
+
// Support both single request and batch array
|
|
61
|
+
const requests: RpcRequest[] = Array.isArray(body) ? body : [body];
|
|
470
62
|
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
return c.json({ error: 'Service not found' }, 404);
|
|
63
|
+
// Validate requests
|
|
64
|
+
if (requests.length === 0) {
|
|
65
|
+
return c.json({ error: 'Empty request array' }, 400);
|
|
475
66
|
}
|
|
476
67
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const availableOffer = serviceOffers.find(offer => !offer.answererPeerId);
|
|
480
|
-
|
|
481
|
-
if (!availableOffer) {
|
|
482
|
-
return c.json({ error: 'No available offers' }, 503);
|
|
68
|
+
if (requests.length > MAX_BATCH_SIZE) {
|
|
69
|
+
return c.json({ error: `Too many requests in batch (max ${MAX_BATCH_SIZE})` }, 400);
|
|
483
70
|
}
|
|
484
71
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const result = await storage.answerOffer(availableOffer.id, answererPeerId, sdp);
|
|
72
|
+
// Handle RPC
|
|
73
|
+
const responses = await handleRpc(requests, storage, config);
|
|
488
74
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return c.json({
|
|
494
|
-
success: true,
|
|
495
|
-
offerId: availableOffer.id
|
|
496
|
-
}, 200);
|
|
75
|
+
// Return single response or array based on input
|
|
76
|
+
return c.json(Array.isArray(body) ? responses : responses[0], 200);
|
|
497
77
|
} catch (err) {
|
|
498
|
-
console.error('
|
|
499
|
-
return c.json({ error: 'Internal server error' }, 500);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* GET /services/:uuid/answer
|
|
505
|
-
* Get answer for a service (offerer polls this)
|
|
506
|
-
*/
|
|
507
|
-
app.get('/services/:uuid/answer', authMiddleware, async (c) => {
|
|
508
|
-
try {
|
|
509
|
-
const uuid = c.req.param('uuid');
|
|
510
|
-
const peerId = getAuthenticatedPeerId(c);
|
|
511
|
-
|
|
512
|
-
// Get the service by UUID
|
|
513
|
-
const service = await storage.getServiceByUuid(uuid);
|
|
514
|
-
if (!service) {
|
|
515
|
-
return c.json({ error: 'Service not found' }, 404);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Get offers for this service owned by the requesting peer
|
|
519
|
-
const serviceOffers = await storage.getOffersForService(service.id);
|
|
520
|
-
const myOffer = serviceOffers.find(offer => offer.peerId === peerId && offer.answererPeerId);
|
|
521
|
-
|
|
522
|
-
if (!myOffer || !myOffer.answerSdp) {
|
|
523
|
-
return c.json({ error: 'Offer not yet answered' }, 404);
|
|
524
|
-
}
|
|
525
|
-
|
|
78
|
+
console.error('RPC error:', err);
|
|
526
79
|
return c.json({
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
answeredAt: myOffer.answeredAt
|
|
531
|
-
}, 200);
|
|
532
|
-
} catch (err) {
|
|
533
|
-
console.error('Error getting service answer:', err);
|
|
534
|
-
return c.json({ error: 'Internal server error' }, 500);
|
|
80
|
+
success: false,
|
|
81
|
+
error: 'Invalid request format',
|
|
82
|
+
}, 400);
|
|
535
83
|
}
|
|
536
84
|
});
|
|
537
85
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
const uuid = c.req.param('uuid');
|
|
545
|
-
const body = await c.req.json();
|
|
546
|
-
const { candidates, offerId } = body;
|
|
547
|
-
|
|
548
|
-
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
549
|
-
return c.json({ error: 'Missing or invalid required parameter: candidates' }, 400);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const peerId = getAuthenticatedPeerId(c);
|
|
553
|
-
|
|
554
|
-
// Get the service by UUID
|
|
555
|
-
const service = await storage.getServiceByUuid(uuid);
|
|
556
|
-
if (!service) {
|
|
557
|
-
return c.json({ error: 'Service not found' }, 404);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// If offerId is provided, use it; otherwise find the peer's offer
|
|
561
|
-
let targetOfferId = offerId;
|
|
562
|
-
if (!targetOfferId) {
|
|
563
|
-
const serviceOffers = await storage.getOffersForService(service.id);
|
|
564
|
-
const myOffer = serviceOffers.find(offer =>
|
|
565
|
-
offer.peerId === peerId || offer.answererPeerId === peerId
|
|
566
|
-
);
|
|
567
|
-
if (!myOffer) {
|
|
568
|
-
return c.json({ error: 'No offer found for this peer' }, 404);
|
|
569
|
-
}
|
|
570
|
-
targetOfferId = myOffer.id;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Get offer to determine role
|
|
574
|
-
const offer = await storage.getOfferById(targetOfferId);
|
|
575
|
-
if (!offer) {
|
|
576
|
-
return c.json({ error: 'Offer not found' }, 404);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Determine role
|
|
580
|
-
const role = offer.peerId === peerId ? 'offerer' : 'answerer';
|
|
581
|
-
|
|
582
|
-
const count = await storage.addIceCandidates(targetOfferId, peerId, role, candidates);
|
|
583
|
-
|
|
584
|
-
return c.json({ count, offerId: targetOfferId }, 200);
|
|
585
|
-
} catch (err) {
|
|
586
|
-
console.error('Error adding ICE candidates to service:', err);
|
|
587
|
-
return c.json({ error: 'Internal server error' }, 500);
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* GET /services/:uuid/ice-candidates
|
|
593
|
-
* Get ICE candidates for a service
|
|
594
|
-
*/
|
|
595
|
-
app.get('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
|
|
596
|
-
try {
|
|
597
|
-
const uuid = c.req.param('uuid');
|
|
598
|
-
const since = c.req.query('since');
|
|
599
|
-
const offerId = c.req.query('offerId');
|
|
600
|
-
const peerId = getAuthenticatedPeerId(c);
|
|
601
|
-
|
|
602
|
-
// Get the service by UUID
|
|
603
|
-
const service = await storage.getServiceByUuid(uuid);
|
|
604
|
-
if (!service) {
|
|
605
|
-
return c.json({ error: 'Service not found' }, 404);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// If offerId is provided, use it; otherwise find the peer's offer
|
|
609
|
-
let targetOfferId = offerId;
|
|
610
|
-
if (!targetOfferId) {
|
|
611
|
-
const serviceOffers = await storage.getOffersForService(service.id);
|
|
612
|
-
const myOffer = serviceOffers.find(offer =>
|
|
613
|
-
offer.peerId === peerId || offer.answererPeerId === peerId
|
|
614
|
-
);
|
|
615
|
-
if (!myOffer) {
|
|
616
|
-
return c.json({ error: 'No offer found for this peer' }, 404);
|
|
617
|
-
}
|
|
618
|
-
targetOfferId = myOffer.id;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Get offer to determine role
|
|
622
|
-
const offer = await storage.getOfferById(targetOfferId);
|
|
623
|
-
if (!offer) {
|
|
624
|
-
return c.json({ error: 'Offer not found' }, 404);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Get candidates for opposite role
|
|
628
|
-
const targetRole = offer.peerId === peerId ? 'answerer' : 'offerer';
|
|
629
|
-
const sinceTimestamp = since ? parseInt(since, 10) : undefined;
|
|
630
|
-
|
|
631
|
-
const candidates = await storage.getIceCandidates(targetOfferId, targetRole, sinceTimestamp);
|
|
632
|
-
|
|
633
|
-
return c.json({
|
|
634
|
-
candidates: candidates.map(c => ({
|
|
635
|
-
candidate: c.candidate,
|
|
636
|
-
createdAt: c.createdAt
|
|
637
|
-
})),
|
|
638
|
-
offerId: targetOfferId
|
|
639
|
-
}, 200);
|
|
640
|
-
} catch (err) {
|
|
641
|
-
console.error('Error getting ICE candidates for service:', err);
|
|
642
|
-
return c.json({ error: 'Internal server error' }, 500);
|
|
643
|
-
}
|
|
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);
|
|
644
91
|
});
|
|
645
92
|
|
|
646
93
|
return app;
|