@xtr-dev/rondevu-server 0.2.4 → 0.4.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/README.md +121 -78
- package/dist/index.js +318 -220
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/src/app.ts +314 -272
- package/src/config.ts +1 -3
- package/src/crypto.ts +54 -0
- package/src/index.ts +0 -1
- package/src/storage/d1.ts +54 -7
- package/src/storage/hash-id.ts +4 -9
- package/src/storage/sqlite.ts +66 -15
- package/src/storage/types.ts +43 -10
- package/src/worker.ts +1 -3
- package/src/bloom.ts +0 -66
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, validateServicePublish, 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
|
|
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
|
-
// =====
|
|
82
|
+
// ===== User Management (RESTful) =====
|
|
82
83
|
|
|
83
84
|
/**
|
|
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
|
|
85
116
|
* Claim a username with cryptographic proof
|
|
86
117
|
*/
|
|
87
|
-
app.post('/
|
|
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 {
|
|
122
|
+
const { publicKey, signature, message } = body;
|
|
91
123
|
|
|
92
|
-
if (!
|
|
93
|
-
return c.json({ error: 'Missing required parameters:
|
|
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
|
-
},
|
|
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 /
|
|
130
|
-
*
|
|
161
|
+
* GET /users/:username/services/:fqn
|
|
162
|
+
* Get service by username and FQN with semver-compatible matching
|
|
131
163
|
*/
|
|
132
|
-
app.get('/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
},
|
|
193
|
+
error: 'No compatible version found',
|
|
194
|
+
message: `Requested ${serviceFqn}, but no compatible versions available`
|
|
195
|
+
}, 404);
|
|
143
196
|
}
|
|
144
197
|
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const username = c.req.param('username');
|
|
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
|
+
}
|
|
165
207
|
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
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
|
-
|
|
254
|
+
serviceFqn = body.serviceFqn;
|
|
255
|
+
const { offers, ttl, isPublic, metadata, signature, message } = body;
|
|
188
256
|
|
|
189
|
-
if (!
|
|
190
|
-
return c.json({ error: 'Missing required parameters:
|
|
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
|
|
@@ -212,13 +280,24 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
212
280
|
return c.json({ error: 'Invalid signature for username' }, 403);
|
|
213
281
|
}
|
|
214
282
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
//
|
|
233
|
-
const
|
|
311
|
+
// Prepare offer requests
|
|
312
|
+
const offerRequests = offers.map(offer => ({
|
|
234
313
|
peerId,
|
|
235
|
-
sdp,
|
|
314
|
+
sdp: offer.sdp,
|
|
236
315
|
expiresAt
|
|
237
|
-
}
|
|
316
|
+
}));
|
|
238
317
|
|
|
239
|
-
|
|
240
|
-
return c.json({ error: 'Failed to create offer' }, 500);
|
|
241
|
-
}
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
269
|
-
*
|
|
364
|
+
* DELETE /users/:username/services/:fqn
|
|
365
|
+
* Delete a service by username and FQN (RESTful)
|
|
270
366
|
*/
|
|
271
|
-
app.
|
|
367
|
+
app.delete('/users/:username/services/:fqn', authMiddleware, async (c) => {
|
|
272
368
|
try {
|
|
273
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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(
|
|
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,157 +393,68 @@ 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
|
-
*
|
|
334
|
-
*
|
|
399
|
+
* GET /services/:uuid
|
|
400
|
+
* Get service details by index UUID (kept for privacy)
|
|
335
401
|
*/
|
|
336
|
-
app.
|
|
402
|
+
app.get('/services/:uuid', async (c) => {
|
|
337
403
|
try {
|
|
338
|
-
const
|
|
339
|
-
const body = await c.req.json();
|
|
340
|
-
const { serviceFqn } = body;
|
|
341
|
-
|
|
342
|
-
if (!serviceFqn) {
|
|
343
|
-
return c.json({ error: 'Missing required parameter: serviceFqn' }, 400);
|
|
344
|
-
}
|
|
404
|
+
const uuid = c.req.param('uuid');
|
|
345
405
|
|
|
346
|
-
const
|
|
406
|
+
const service = await storage.getServiceByUuid(uuid);
|
|
347
407
|
|
|
348
|
-
if (!
|
|
408
|
+
if (!service) {
|
|
349
409
|
return c.json({ error: 'Service not found' }, 404);
|
|
350
410
|
}
|
|
351
411
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
allowed: true
|
|
355
|
-
}, 200);
|
|
356
|
-
} catch (err) {
|
|
357
|
-
console.error('Error querying service:', err);
|
|
358
|
-
return c.json({ error: 'Internal server error' }, 500);
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// ===== Offer Management (Core WebRTC) =====
|
|
412
|
+
// Get all offers for this service
|
|
413
|
+
const serviceOffers = await storage.getOffersForService(service.id);
|
|
363
414
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
* Create offers (direct, no service - for testing/advanced users)
|
|
367
|
-
*/
|
|
368
|
-
app.post('/offers', authMiddleware, async (c) => {
|
|
369
|
-
try {
|
|
370
|
-
const body = await c.req.json();
|
|
371
|
-
const { offers } = body;
|
|
372
|
-
|
|
373
|
-
if (!Array.isArray(offers) || offers.length === 0) {
|
|
374
|
-
return c.json({ error: 'Missing or invalid required parameter: offers (must be non-empty array)' }, 400);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (offers.length > config.maxOffersPerRequest) {
|
|
378
|
-
return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
|
|
415
|
+
if (serviceOffers.length === 0) {
|
|
416
|
+
return c.json({ error: 'No offers found for this service' }, 404);
|
|
379
417
|
}
|
|
380
418
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// Validate and prepare offers
|
|
384
|
-
const validated = offers.map((offer: any) => {
|
|
385
|
-
const { sdp, ttl, secret } = offer;
|
|
386
|
-
|
|
387
|
-
if (typeof sdp !== 'string' || sdp.length === 0) {
|
|
388
|
-
throw new Error('Invalid SDP in offer');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (sdp.length > 64 * 1024) {
|
|
392
|
-
throw new Error('SDP too large (max 64KB)');
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const offerTtl = Math.min(
|
|
396
|
-
Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
|
|
397
|
-
config.offerMaxTtl
|
|
398
|
-
);
|
|
419
|
+
// Find an unanswered offer
|
|
420
|
+
const availableOffer = serviceOffers.find(offer => !offer.answererPeerId);
|
|
399
421
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
const created = await storage.createOffers(validated);
|
|
409
|
-
|
|
410
|
-
return c.json({
|
|
411
|
-
offers: created.map(offer => ({
|
|
412
|
-
id: offer.id,
|
|
413
|
-
peerId: offer.peerId,
|
|
414
|
-
expiresAt: offer.expiresAt,
|
|
415
|
-
createdAt: offer.createdAt,
|
|
416
|
-
hasSecret: !!offer.secret
|
|
417
|
-
}))
|
|
418
|
-
}, 201);
|
|
419
|
-
} catch (err: any) {
|
|
420
|
-
console.error('Error creating offers:', err);
|
|
421
|
-
return c.json({ error: err.message || 'Internal server error' }, 500);
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* GET /offers/mine
|
|
427
|
-
* Get authenticated peer's offers
|
|
428
|
-
*/
|
|
429
|
-
app.get('/offers/mine', authMiddleware, async (c) => {
|
|
430
|
-
try {
|
|
431
|
-
const peerId = getAuthenticatedPeerId(c);
|
|
432
|
-
const offers = await storage.getOffersByPeerId(peerId);
|
|
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
|
+
}
|
|
433
428
|
|
|
434
429
|
return c.json({
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
445
440
|
}, 200);
|
|
446
441
|
} catch (err) {
|
|
447
|
-
console.error('Error getting
|
|
442
|
+
console.error('Error getting service:', err);
|
|
448
443
|
return c.json({ error: 'Internal server error' }, 500);
|
|
449
444
|
}
|
|
450
445
|
});
|
|
451
446
|
|
|
452
|
-
|
|
453
|
-
* DELETE /offers/:offerId
|
|
454
|
-
* Delete an offer
|
|
455
|
-
*/
|
|
456
|
-
app.delete('/offers/:offerId', authMiddleware, async (c) => {
|
|
457
|
-
try {
|
|
458
|
-
const offerId = c.req.param('offerId');
|
|
459
|
-
const peerId = getAuthenticatedPeerId(c);
|
|
460
|
-
|
|
461
|
-
const deleted = await storage.deleteOffer(offerId, peerId);
|
|
462
|
-
|
|
463
|
-
if (!deleted) {
|
|
464
|
-
return c.json({ error: 'Offer not found or not owned by this peer' }, 404);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return c.json({ success: true }, 200);
|
|
468
|
-
} catch (err) {
|
|
469
|
-
console.error('Error deleting offer:', err);
|
|
470
|
-
return c.json({ error: 'Internal server error' }, 500);
|
|
471
|
-
}
|
|
472
|
-
});
|
|
447
|
+
// ===== Service-Based WebRTC Signaling =====
|
|
473
448
|
|
|
474
449
|
/**
|
|
475
|
-
* POST /
|
|
476
|
-
* Answer
|
|
450
|
+
* POST /services/:uuid/answer
|
|
451
|
+
* Answer a service offer
|
|
477
452
|
*/
|
|
478
|
-
app.post('/
|
|
453
|
+
app.post('/services/:uuid/answer', authMiddleware, async (c) => {
|
|
479
454
|
try {
|
|
480
|
-
const
|
|
455
|
+
const uuid = c.req.param('uuid');
|
|
481
456
|
const body = await c.req.json();
|
|
482
|
-
const { sdp
|
|
457
|
+
const { sdp } = body;
|
|
483
458
|
|
|
484
459
|
if (!sdp) {
|
|
485
460
|
return c.json({ error: 'Missing required parameter: sdp' }, 400);
|
|
@@ -493,55 +468,82 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
493
468
|
return c.json({ error: 'SDP too large (max 64KB)' }, 400);
|
|
494
469
|
}
|
|
495
470
|
|
|
471
|
+
// Get the service by UUID
|
|
472
|
+
const service = await storage.getServiceByUuid(uuid);
|
|
473
|
+
if (!service) {
|
|
474
|
+
return c.json({ error: 'Service not found' }, 404);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Get available offer from service
|
|
478
|
+
const serviceOffers = await storage.getOffersForService(service.id);
|
|
479
|
+
const availableOffer = serviceOffers.find(offer => !offer.answererPeerId);
|
|
480
|
+
|
|
481
|
+
if (!availableOffer) {
|
|
482
|
+
return c.json({ error: 'No available offers' }, 503);
|
|
483
|
+
}
|
|
484
|
+
|
|
496
485
|
const answererPeerId = getAuthenticatedPeerId(c);
|
|
497
486
|
|
|
498
|
-
const result = await storage.answerOffer(
|
|
487
|
+
const result = await storage.answerOffer(availableOffer.id, answererPeerId, sdp);
|
|
499
488
|
|
|
500
489
|
if (!result.success) {
|
|
501
490
|
return c.json({ error: result.error }, 400);
|
|
502
491
|
}
|
|
503
492
|
|
|
504
|
-
return c.json({
|
|
493
|
+
return c.json({
|
|
494
|
+
success: true,
|
|
495
|
+
offerId: availableOffer.id
|
|
496
|
+
}, 200);
|
|
505
497
|
} catch (err) {
|
|
506
|
-
console.error('Error answering
|
|
498
|
+
console.error('Error answering service:', err);
|
|
507
499
|
return c.json({ error: 'Internal server error' }, 500);
|
|
508
500
|
}
|
|
509
501
|
});
|
|
510
502
|
|
|
511
503
|
/**
|
|
512
|
-
* GET /
|
|
513
|
-
* Get
|
|
504
|
+
* GET /services/:uuid/answer
|
|
505
|
+
* Get answer for a service (offerer polls this)
|
|
514
506
|
*/
|
|
515
|
-
app.get('/
|
|
507
|
+
app.get('/services/:uuid/answer', authMiddleware, async (c) => {
|
|
516
508
|
try {
|
|
509
|
+
const uuid = c.req.param('uuid');
|
|
517
510
|
const peerId = getAuthenticatedPeerId(c);
|
|
518
|
-
|
|
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
|
+
}
|
|
519
525
|
|
|
520
526
|
return c.json({
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
answeredAt: offer.answeredAt
|
|
526
|
-
}))
|
|
527
|
+
offerId: myOffer.id,
|
|
528
|
+
answererId: myOffer.answererPeerId,
|
|
529
|
+
sdp: myOffer.answerSdp,
|
|
530
|
+
answeredAt: myOffer.answeredAt
|
|
527
531
|
}, 200);
|
|
528
532
|
} catch (err) {
|
|
529
|
-
console.error('Error getting
|
|
533
|
+
console.error('Error getting service answer:', err);
|
|
530
534
|
return c.json({ error: 'Internal server error' }, 500);
|
|
531
535
|
}
|
|
532
536
|
});
|
|
533
537
|
|
|
534
|
-
// ===== ICE Candidate Exchange =====
|
|
535
|
-
|
|
536
538
|
/**
|
|
537
|
-
* POST /
|
|
538
|
-
* Add ICE candidates for
|
|
539
|
+
* POST /services/:uuid/ice-candidates
|
|
540
|
+
* Add ICE candidates for a service
|
|
539
541
|
*/
|
|
540
|
-
app.post('/
|
|
542
|
+
app.post('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
|
|
541
543
|
try {
|
|
542
|
-
const
|
|
544
|
+
const uuid = c.req.param('uuid');
|
|
543
545
|
const body = await c.req.json();
|
|
544
|
-
const { candidates } = body;
|
|
546
|
+
const { candidates, offerId } = body;
|
|
545
547
|
|
|
546
548
|
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
547
549
|
return c.json({ error: 'Missing or invalid required parameter: candidates' }, 400);
|
|
@@ -549,8 +551,27 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
549
551
|
|
|
550
552
|
const peerId = getAuthenticatedPeerId(c);
|
|
551
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
|
+
|
|
552
573
|
// Get offer to determine role
|
|
553
|
-
const offer = await storage.getOfferById(
|
|
574
|
+
const offer = await storage.getOfferById(targetOfferId);
|
|
554
575
|
if (!offer) {
|
|
555
576
|
return c.json({ error: 'Offer not found' }, 404);
|
|
556
577
|
}
|
|
@@ -558,27 +579,47 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
558
579
|
// Determine role
|
|
559
580
|
const role = offer.peerId === peerId ? 'offerer' : 'answerer';
|
|
560
581
|
|
|
561
|
-
const count = await storage.addIceCandidates(
|
|
582
|
+
const count = await storage.addIceCandidates(targetOfferId, peerId, role, candidates);
|
|
562
583
|
|
|
563
|
-
return c.json({ count }, 200);
|
|
584
|
+
return c.json({ count, offerId: targetOfferId }, 200);
|
|
564
585
|
} catch (err) {
|
|
565
|
-
console.error('Error adding ICE candidates:', err);
|
|
586
|
+
console.error('Error adding ICE candidates to service:', err);
|
|
566
587
|
return c.json({ error: 'Internal server error' }, 500);
|
|
567
588
|
}
|
|
568
589
|
});
|
|
569
590
|
|
|
570
591
|
/**
|
|
571
|
-
* GET /
|
|
572
|
-
* Get ICE candidates for
|
|
592
|
+
* GET /services/:uuid/ice-candidates
|
|
593
|
+
* Get ICE candidates for a service
|
|
573
594
|
*/
|
|
574
|
-
app.get('/
|
|
595
|
+
app.get('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
|
|
575
596
|
try {
|
|
576
|
-
const
|
|
597
|
+
const uuid = c.req.param('uuid');
|
|
577
598
|
const since = c.req.query('since');
|
|
599
|
+
const offerId = c.req.query('offerId');
|
|
578
600
|
const peerId = getAuthenticatedPeerId(c);
|
|
579
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
|
+
|
|
580
621
|
// Get offer to determine role
|
|
581
|
-
const offer = await storage.getOfferById(
|
|
622
|
+
const offer = await storage.getOfferById(targetOfferId);
|
|
582
623
|
if (!offer) {
|
|
583
624
|
return c.json({ error: 'Offer not found' }, 404);
|
|
584
625
|
}
|
|
@@ -587,16 +628,17 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
587
628
|
const targetRole = offer.peerId === peerId ? 'answerer' : 'offerer';
|
|
588
629
|
const sinceTimestamp = since ? parseInt(since, 10) : undefined;
|
|
589
630
|
|
|
590
|
-
const candidates = await storage.getIceCandidates(
|
|
631
|
+
const candidates = await storage.getIceCandidates(targetOfferId, targetRole, sinceTimestamp);
|
|
591
632
|
|
|
592
633
|
return c.json({
|
|
593
634
|
candidates: candidates.map(c => ({
|
|
594
635
|
candidate: c.candidate,
|
|
595
636
|
createdAt: c.createdAt
|
|
596
|
-
}))
|
|
637
|
+
})),
|
|
638
|
+
offerId: targetOfferId
|
|
597
639
|
}, 200);
|
|
598
640
|
} catch (err) {
|
|
599
|
-
console.error('Error getting ICE candidates:', err);
|
|
641
|
+
console.error('Error getting ICE candidates for service:', err);
|
|
600
642
|
return c.json({ error: 'Internal server error' }, 500);
|
|
601
643
|
}
|
|
602
644
|
});
|