@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.
- package/README.md +77 -68
- package/dist/index.js +242 -105
- package/dist/index.js.map +2 -2
- package/migrations/0005_v2_schema.sql +83 -0
- package/package.json +1 -1
- package/src/app.ts +267 -139
- package/src/config.ts +1 -3
- package/src/crypto.ts +97 -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
|
@@ -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
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
|
|
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
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
}
|