@xtr-dev/rondevu-server 0.2.4 → 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.
- package/README.md +77 -68
- package/dist/index.js +218 -104
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/src/app.ts +266 -138
- 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/package.json
CHANGED
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
|
-
* 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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
238
|
-
|
|
239
|
-
if (offers.length === 0) {
|
|
240
|
-
return c.json({ error: 'Failed to create offer' }, 500);
|
|
241
|
-
}
|
|
316
|
+
}));
|
|
242
317
|
|
|
243
|
-
|
|
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,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
|
-
*
|
|
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
|
-
|
|
340
|
-
const
|
|
404
|
+
const uuid = c.req.param('uuid');
|
|
405
|
+
|
|
406
|
+
const service = await storage.getServiceByUuid(uuid);
|
|
341
407
|
|
|
342
|
-
if (!
|
|
343
|
-
return c.json({ error: '
|
|
408
|
+
if (!service) {
|
|
409
|
+
return c.json({ error: 'Service not found' }, 404);
|
|
344
410
|
}
|
|
345
411
|
|
|
346
|
-
|
|
412
|
+
// Get all offers for this service
|
|
413
|
+
const serviceOffers = await storage.getOffersForService(service.id);
|
|
347
414
|
|
|
348
|
-
if (
|
|
349
|
-
return c.json({ error: '
|
|
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
|
-
|
|
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
|
|
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/
|
|
513
|
-
* Get
|
|
626
|
+
* GET /offers/:offerId/answer
|
|
627
|
+
* Get answer for a specific offer (RESTful endpoint)
|
|
514
628
|
*/
|
|
515
|
-
app.get('/offers/
|
|
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
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
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
|
}
|
package/src/crypto.ts
CHANGED
|
@@ -228,6 +228,60 @@ export function validateServiceFqn(fqn: string): { valid: boolean; error?: strin
|
|
|
228
228
|
return { valid: true };
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Parse semantic version string into components
|
|
233
|
+
*/
|
|
234
|
+
export function parseVersion(version: string): { major: number; minor: number; patch: number; prerelease?: string } | null {
|
|
235
|
+
const match = version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-z0-9.-]+)?$/);
|
|
236
|
+
if (!match) return null;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
major: parseInt(match[1], 10),
|
|
240
|
+
minor: parseInt(match[2], 10),
|
|
241
|
+
patch: parseInt(match[3], 10),
|
|
242
|
+
prerelease: match[4]?.substring(1), // Remove leading dash
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if two versions are compatible (same major version)
|
|
248
|
+
* Following semver rules: ^1.0.0 matches 1.x.x but not 2.x.x
|
|
249
|
+
*/
|
|
250
|
+
export function isVersionCompatible(requested: string, available: string): boolean {
|
|
251
|
+
const req = parseVersion(requested);
|
|
252
|
+
const avail = parseVersion(available);
|
|
253
|
+
|
|
254
|
+
if (!req || !avail) return false;
|
|
255
|
+
|
|
256
|
+
// Major version must match
|
|
257
|
+
if (req.major !== avail.major) return false;
|
|
258
|
+
|
|
259
|
+
// If major is 0, minor must also match (0.x.y is unstable)
|
|
260
|
+
if (req.major === 0 && req.minor !== avail.minor) return false;
|
|
261
|
+
|
|
262
|
+
// Available version must be >= requested version
|
|
263
|
+
if (avail.minor < req.minor) return false;
|
|
264
|
+
if (avail.minor === req.minor && avail.patch < req.patch) return false;
|
|
265
|
+
|
|
266
|
+
// Prerelease versions are only compatible with exact matches
|
|
267
|
+
if (req.prerelease && req.prerelease !== avail.prerelease) return false;
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Parse service FQN into service name and version
|
|
274
|
+
*/
|
|
275
|
+
export function parseServiceFqn(fqn: string): { serviceName: string; version: string } | null {
|
|
276
|
+
const parts = fqn.split('@');
|
|
277
|
+
if (parts.length !== 2) return null;
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
serviceName: parts[0],
|
|
281
|
+
version: parts[1],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
231
285
|
/**
|
|
232
286
|
* Validates timestamp is within acceptable range (prevents replay attacks)
|
|
233
287
|
*/
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,6 @@ async function main() {
|
|
|
20
20
|
offerMinTtl: `${config.offerMinTtl}ms`,
|
|
21
21
|
cleanupInterval: `${config.cleanupInterval}ms`,
|
|
22
22
|
maxOffersPerRequest: config.maxOffersPerRequest,
|
|
23
|
-
maxTopicsPerOffer: config.maxTopicsPerOffer,
|
|
24
23
|
corsOrigins: config.corsOrigins,
|
|
25
24
|
version: config.version,
|
|
26
25
|
});
|