@xtr-dev/rondevu-server 0.3.0 → 0.5.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/ADVANCED.md +502 -0
- package/README.md +139 -251
- package/dist/index.js +715 -770
- 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 -677
- package/src/config.ts +0 -13
- package/src/crypto.ts +98 -133
- 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/storage/d1.ts
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
ClaimUsernameRequest,
|
|
9
9
|
Service,
|
|
10
10
|
CreateServiceRequest,
|
|
11
|
-
ServiceInfo,
|
|
12
11
|
} from './types.ts';
|
|
13
12
|
import { generateOfferHash } from './hash-id.ts';
|
|
13
|
+
import { parseServiceFqn } from '../crypto.ts';
|
|
14
14
|
|
|
15
15
|
const YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000; // 365 days
|
|
16
16
|
|
|
@@ -37,27 +37,28 @@ export class D1Storage implements Storage {
|
|
|
37
37
|
-- WebRTC signaling offers
|
|
38
38
|
CREATE TABLE IF NOT EXISTS offers (
|
|
39
39
|
id TEXT PRIMARY KEY,
|
|
40
|
-
|
|
40
|
+
username TEXT NOT NULL,
|
|
41
|
+
service_id TEXT,
|
|
41
42
|
sdp TEXT NOT NULL,
|
|
42
43
|
created_at INTEGER NOT NULL,
|
|
43
44
|
expires_at INTEGER NOT NULL,
|
|
44
45
|
last_seen INTEGER NOT NULL,
|
|
45
|
-
|
|
46
|
-
answerer_peer_id TEXT,
|
|
46
|
+
answerer_username TEXT,
|
|
47
47
|
answer_sdp TEXT,
|
|
48
48
|
answered_at INTEGER
|
|
49
49
|
);
|
|
50
50
|
|
|
51
|
-
CREATE INDEX IF NOT EXISTS
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_offers_username ON offers(username);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_offers_service ON offers(service_id);
|
|
52
53
|
CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
|
|
53
54
|
CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_username);
|
|
55
56
|
|
|
56
57
|
-- ICE candidates table
|
|
57
58
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
58
59
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
60
|
offer_id TEXT NOT NULL,
|
|
60
|
-
|
|
61
|
+
username TEXT NOT NULL,
|
|
61
62
|
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
|
|
62
63
|
candidate TEXT NOT NULL,
|
|
63
64
|
created_at INTEGER NOT NULL,
|
|
@@ -65,7 +66,7 @@ export class D1Storage implements Storage {
|
|
|
65
66
|
);
|
|
66
67
|
|
|
67
68
|
CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
|
|
68
|
-
CREATE INDEX IF NOT EXISTS
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_ice_username ON ice_candidates(username);
|
|
69
70
|
CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
|
|
70
71
|
|
|
71
72
|
-- Usernames table
|
|
@@ -82,39 +83,23 @@ export class D1Storage implements Storage {
|
|
|
82
83
|
CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
|
|
83
84
|
CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
|
|
84
85
|
|
|
85
|
-
-- Services table
|
|
86
|
+
-- Services table (new schema with extracted fields for discovery)
|
|
86
87
|
CREATE TABLE IF NOT EXISTS services (
|
|
87
88
|
id TEXT PRIMARY KEY,
|
|
88
|
-
username TEXT NOT NULL,
|
|
89
89
|
service_fqn TEXT NOT NULL,
|
|
90
|
-
|
|
90
|
+
service_name TEXT NOT NULL,
|
|
91
|
+
version TEXT NOT NULL,
|
|
92
|
+
username TEXT NOT NULL,
|
|
91
93
|
created_at INTEGER NOT NULL,
|
|
92
94
|
expires_at INTEGER NOT NULL,
|
|
93
|
-
is_public INTEGER NOT NULL DEFAULT 0,
|
|
94
|
-
metadata TEXT,
|
|
95
95
|
FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
|
|
96
|
-
|
|
97
|
-
UNIQUE(username, service_fqn)
|
|
96
|
+
UNIQUE(service_fqn)
|
|
98
97
|
);
|
|
99
98
|
|
|
100
|
-
CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
|
|
101
99
|
CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_services_discovery ON services(service_name, version);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
|
|
102
102
|
CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
|
|
103
|
-
CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
|
|
104
|
-
|
|
105
|
-
-- Service index table (privacy layer)
|
|
106
|
-
CREATE TABLE IF NOT EXISTS service_index (
|
|
107
|
-
uuid TEXT PRIMARY KEY,
|
|
108
|
-
service_id TEXT NOT NULL,
|
|
109
|
-
username TEXT NOT NULL,
|
|
110
|
-
service_fqn TEXT NOT NULL,
|
|
111
|
-
created_at INTEGER NOT NULL,
|
|
112
|
-
expires_at INTEGER NOT NULL,
|
|
113
|
-
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
CREATE INDEX IF NOT EXISTS idx_service_index_username ON service_index(username);
|
|
117
|
-
CREATE INDEX IF NOT EXISTS idx_service_index_expires ON service_index(expires_at);
|
|
118
103
|
`);
|
|
119
104
|
}
|
|
120
105
|
|
|
@@ -129,30 +114,31 @@ export class D1Storage implements Storage {
|
|
|
129
114
|
const now = Date.now();
|
|
130
115
|
|
|
131
116
|
await this.db.prepare(`
|
|
132
|
-
INSERT INTO offers (id,
|
|
117
|
+
INSERT INTO offers (id, username, service_id, sdp, created_at, expires_at, last_seen)
|
|
133
118
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
134
|
-
`).bind(id, offer.
|
|
119
|
+
`).bind(id, offer.username, offer.serviceId || null, offer.sdp, now, offer.expiresAt, now).run();
|
|
135
120
|
|
|
136
121
|
created.push({
|
|
137
122
|
id,
|
|
138
|
-
|
|
123
|
+
username: offer.username,
|
|
124
|
+
serviceId: offer.serviceId,
|
|
125
|
+
serviceFqn: offer.serviceFqn,
|
|
139
126
|
sdp: offer.sdp,
|
|
140
127
|
createdAt: now,
|
|
141
128
|
expiresAt: offer.expiresAt,
|
|
142
129
|
lastSeen: now,
|
|
143
|
-
secret: offer.secret,
|
|
144
130
|
});
|
|
145
131
|
}
|
|
146
132
|
|
|
147
133
|
return created;
|
|
148
134
|
}
|
|
149
135
|
|
|
150
|
-
async
|
|
136
|
+
async getOffersByUsername(username: string): Promise<Offer[]> {
|
|
151
137
|
const result = await this.db.prepare(`
|
|
152
138
|
SELECT * FROM offers
|
|
153
|
-
WHERE
|
|
139
|
+
WHERE username = ? AND expires_at > ?
|
|
154
140
|
ORDER BY last_seen DESC
|
|
155
|
-
`).bind(
|
|
141
|
+
`).bind(username, Date.now()).all();
|
|
156
142
|
|
|
157
143
|
if (!result.results) {
|
|
158
144
|
return [];
|
|
@@ -174,11 +160,11 @@ export class D1Storage implements Storage {
|
|
|
174
160
|
return this.rowToOffer(result as any);
|
|
175
161
|
}
|
|
176
162
|
|
|
177
|
-
async deleteOffer(offerId: string,
|
|
163
|
+
async deleteOffer(offerId: string, ownerUsername: string): Promise<boolean> {
|
|
178
164
|
const result = await this.db.prepare(`
|
|
179
165
|
DELETE FROM offers
|
|
180
|
-
WHERE id = ? AND
|
|
181
|
-
`).bind(offerId,
|
|
166
|
+
WHERE id = ? AND username = ?
|
|
167
|
+
`).bind(offerId, ownerUsername).run();
|
|
182
168
|
|
|
183
169
|
return (result.meta.changes || 0) > 0;
|
|
184
170
|
}
|
|
@@ -193,9 +179,8 @@ export class D1Storage implements Storage {
|
|
|
193
179
|
|
|
194
180
|
async answerOffer(
|
|
195
181
|
offerId: string,
|
|
196
|
-
|
|
197
|
-
answerSdp: string
|
|
198
|
-
secret?: string
|
|
182
|
+
answererUsername: string,
|
|
183
|
+
answerSdp: string
|
|
199
184
|
): Promise<{ success: boolean; error?: string }> {
|
|
200
185
|
// Check if offer exists and is not expired
|
|
201
186
|
const offer = await this.getOfferById(offerId);
|
|
@@ -207,16 +192,8 @@ export class D1Storage implements Storage {
|
|
|
207
192
|
};
|
|
208
193
|
}
|
|
209
194
|
|
|
210
|
-
// Verify secret if offer is protected
|
|
211
|
-
if (offer.secret && offer.secret !== secret) {
|
|
212
|
-
return {
|
|
213
|
-
success: false,
|
|
214
|
-
error: 'Invalid or missing secret'
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
195
|
// Check if offer already has an answerer
|
|
219
|
-
if (offer.
|
|
196
|
+
if (offer.answererUsername) {
|
|
220
197
|
return {
|
|
221
198
|
success: false,
|
|
222
199
|
error: 'Offer already answered'
|
|
@@ -226,9 +203,9 @@ export class D1Storage implements Storage {
|
|
|
226
203
|
// Update offer with answer
|
|
227
204
|
const result = await this.db.prepare(`
|
|
228
205
|
UPDATE offers
|
|
229
|
-
SET
|
|
230
|
-
WHERE id = ? AND
|
|
231
|
-
`).bind(
|
|
206
|
+
SET answerer_username = ?, answer_sdp = ?, answered_at = ?
|
|
207
|
+
WHERE id = ? AND answerer_username IS NULL
|
|
208
|
+
`).bind(answererUsername, answerSdp, Date.now(), offerId).run();
|
|
232
209
|
|
|
233
210
|
if ((result.meta.changes || 0) === 0) {
|
|
234
211
|
return {
|
|
@@ -240,12 +217,12 @@ export class D1Storage implements Storage {
|
|
|
240
217
|
return { success: true };
|
|
241
218
|
}
|
|
242
219
|
|
|
243
|
-
async getAnsweredOffers(
|
|
220
|
+
async getAnsweredOffers(offererUsername: string): Promise<Offer[]> {
|
|
244
221
|
const result = await this.db.prepare(`
|
|
245
222
|
SELECT * FROM offers
|
|
246
|
-
WHERE
|
|
223
|
+
WHERE username = ? AND answerer_username IS NOT NULL AND expires_at > ?
|
|
247
224
|
ORDER BY answered_at DESC
|
|
248
|
-
`).bind(
|
|
225
|
+
`).bind(offererUsername, Date.now()).all();
|
|
249
226
|
|
|
250
227
|
if (!result.results) {
|
|
251
228
|
return [];
|
|
@@ -258,7 +235,7 @@ export class D1Storage implements Storage {
|
|
|
258
235
|
|
|
259
236
|
async addIceCandidates(
|
|
260
237
|
offerId: string,
|
|
261
|
-
|
|
238
|
+
username: string,
|
|
262
239
|
role: 'offerer' | 'answerer',
|
|
263
240
|
candidates: any[]
|
|
264
241
|
): Promise<number> {
|
|
@@ -266,11 +243,11 @@ export class D1Storage implements Storage {
|
|
|
266
243
|
for (let i = 0; i < candidates.length; i++) {
|
|
267
244
|
const timestamp = Date.now() + i;
|
|
268
245
|
await this.db.prepare(`
|
|
269
|
-
INSERT INTO ice_candidates (offer_id,
|
|
246
|
+
INSERT INTO ice_candidates (offer_id, username, role, candidate, created_at)
|
|
270
247
|
VALUES (?, ?, ?, ?, ?)
|
|
271
248
|
`).bind(
|
|
272
249
|
offerId,
|
|
273
|
-
|
|
250
|
+
username,
|
|
274
251
|
role,
|
|
275
252
|
JSON.stringify(candidates[i]),
|
|
276
253
|
timestamp
|
|
@@ -308,7 +285,7 @@ export class D1Storage implements Storage {
|
|
|
308
285
|
return result.results.map((row: any) => ({
|
|
309
286
|
id: row.id,
|
|
310
287
|
offerId: row.offer_id,
|
|
311
|
-
|
|
288
|
+
username: row.username,
|
|
312
289
|
role: row.role,
|
|
313
290
|
candidate: JSON.parse(row.candidate),
|
|
314
291
|
createdAt: row.created_at,
|
|
@@ -321,36 +298,44 @@ export class D1Storage implements Storage {
|
|
|
321
298
|
const now = Date.now();
|
|
322
299
|
const expiresAt = now + YEAR_IN_MS;
|
|
323
300
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
301
|
+
try {
|
|
302
|
+
// Try to insert or update
|
|
303
|
+
const result = await this.db.prepare(`
|
|
304
|
+
INSERT INTO usernames (username, public_key, claimed_at, expires_at, last_used, metadata)
|
|
305
|
+
VALUES (?, ?, ?, ?, ?, NULL)
|
|
306
|
+
ON CONFLICT(username) DO UPDATE SET
|
|
307
|
+
expires_at = ?,
|
|
308
|
+
last_used = ?
|
|
309
|
+
WHERE public_key = ?
|
|
310
|
+
`).bind(
|
|
311
|
+
request.username,
|
|
312
|
+
request.publicKey,
|
|
313
|
+
now,
|
|
314
|
+
expiresAt,
|
|
315
|
+
now,
|
|
316
|
+
expiresAt,
|
|
317
|
+
now,
|
|
318
|
+
request.publicKey
|
|
319
|
+
).run();
|
|
342
320
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
321
|
+
if ((result.meta.changes || 0) === 0) {
|
|
322
|
+
throw new Error('Username already claimed by different public key');
|
|
323
|
+
}
|
|
346
324
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
325
|
+
return {
|
|
326
|
+
username: request.username,
|
|
327
|
+
publicKey: request.publicKey,
|
|
328
|
+
claimedAt: now,
|
|
329
|
+
expiresAt,
|
|
330
|
+
lastUsed: now,
|
|
331
|
+
};
|
|
332
|
+
} catch (err: any) {
|
|
333
|
+
// Handle UNIQUE constraint on public_key
|
|
334
|
+
if (err.message?.includes('UNIQUE constraint failed: usernames.public_key')) {
|
|
335
|
+
throw new Error('This public key has already claimed a different username');
|
|
336
|
+
}
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
354
339
|
}
|
|
355
340
|
|
|
356
341
|
async getUsername(username: string): Promise<Username | null> {
|
|
@@ -375,18 +360,6 @@ export class D1Storage implements Storage {
|
|
|
375
360
|
};
|
|
376
361
|
}
|
|
377
362
|
|
|
378
|
-
async touchUsername(username: string): Promise<boolean> {
|
|
379
|
-
const now = Date.now();
|
|
380
|
-
const expiresAt = now + YEAR_IN_MS;
|
|
381
|
-
|
|
382
|
-
const result = await this.db.prepare(`
|
|
383
|
-
UPDATE usernames
|
|
384
|
-
SET last_used = ?, expires_at = ?
|
|
385
|
-
WHERE username = ? AND expires_at > ?
|
|
386
|
-
`).bind(now, expiresAt, username, now).run();
|
|
387
|
-
|
|
388
|
-
return (result.meta.changes || 0) > 0;
|
|
389
|
-
}
|
|
390
363
|
|
|
391
364
|
async deleteExpiredUsernames(now: number): Promise<number> {
|
|
392
365
|
const result = await this.db.prepare(`
|
|
@@ -400,36 +373,51 @@ export class D1Storage implements Storage {
|
|
|
400
373
|
|
|
401
374
|
async createService(request: CreateServiceRequest): Promise<{
|
|
402
375
|
service: Service;
|
|
403
|
-
indexUuid: string;
|
|
404
376
|
offers: Offer[];
|
|
405
377
|
}> {
|
|
406
378
|
const serviceId = crypto.randomUUID();
|
|
407
|
-
const indexUuid = crypto.randomUUID();
|
|
408
379
|
const now = Date.now();
|
|
409
380
|
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
request.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
381
|
+
// Parse FQN to extract components
|
|
382
|
+
const parsed = parseServiceFqn(request.serviceFqn);
|
|
383
|
+
if (!parsed) {
|
|
384
|
+
throw new Error(`Invalid service FQN: ${request.serviceFqn}`);
|
|
385
|
+
}
|
|
386
|
+
if (!parsed.username) {
|
|
387
|
+
throw new Error(`Service FQN must include username: ${request.serviceFqn}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const { serviceName, version, username } = parsed;
|
|
391
|
+
|
|
392
|
+
// Delete existing service with same (service_name, version, username) and its related offers (upsert behavior)
|
|
393
|
+
// First get the existing service
|
|
394
|
+
const existingService = await this.db.prepare(`
|
|
395
|
+
SELECT id FROM services
|
|
396
|
+
WHERE service_name = ? AND version = ? AND username = ?
|
|
397
|
+
`).bind(serviceName, version, username).first();
|
|
398
|
+
|
|
399
|
+
if (existingService) {
|
|
400
|
+
// Delete related offers first (no FK cascade from offers to services)
|
|
401
|
+
await this.db.prepare(`
|
|
402
|
+
DELETE FROM offers WHERE service_id = ?
|
|
403
|
+
`).bind(existingService.id).run();
|
|
404
|
+
|
|
405
|
+
// Delete the service
|
|
406
|
+
await this.db.prepare(`
|
|
407
|
+
DELETE FROM services WHERE id = ?
|
|
408
|
+
`).bind(existingService.id).run();
|
|
409
|
+
}
|
|
423
410
|
|
|
424
|
-
// Insert service
|
|
411
|
+
// Insert new service with extracted fields
|
|
425
412
|
await this.db.prepare(`
|
|
426
|
-
INSERT INTO
|
|
427
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
413
|
+
INSERT INTO services (id, service_fqn, service_name, version, username, created_at, expires_at)
|
|
414
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
428
415
|
`).bind(
|
|
429
|
-
indexUuid,
|
|
430
416
|
serviceId,
|
|
431
|
-
request.username,
|
|
432
417
|
request.serviceFqn,
|
|
418
|
+
serviceName,
|
|
419
|
+
version,
|
|
420
|
+
username,
|
|
433
421
|
now,
|
|
434
422
|
request.expiresAt
|
|
435
423
|
).run();
|
|
@@ -441,36 +429,28 @@ export class D1Storage implements Storage {
|
|
|
441
429
|
}));
|
|
442
430
|
const offers = await this.createOffers(offerRequests);
|
|
443
431
|
|
|
444
|
-
// Touch username to extend expiry
|
|
445
|
-
|
|
432
|
+
// Touch username to extend expiry (inline logic)
|
|
433
|
+
const expiresAt = now + YEAR_IN_MS;
|
|
434
|
+
await this.db.prepare(`
|
|
435
|
+
UPDATE usernames
|
|
436
|
+
SET last_used = ?, expires_at = ?
|
|
437
|
+
WHERE username = ? AND expires_at > ?
|
|
438
|
+
`).bind(now, expiresAt, username, now).run();
|
|
446
439
|
|
|
447
440
|
return {
|
|
448
441
|
service: {
|
|
449
442
|
id: serviceId,
|
|
450
|
-
username: request.username,
|
|
451
443
|
serviceFqn: request.serviceFqn,
|
|
444
|
+
serviceName,
|
|
445
|
+
version,
|
|
446
|
+
username,
|
|
452
447
|
createdAt: now,
|
|
453
448
|
expiresAt: request.expiresAt,
|
|
454
|
-
isPublic: request.isPublic || false,
|
|
455
|
-
metadata: request.metadata,
|
|
456
449
|
},
|
|
457
|
-
indexUuid,
|
|
458
450
|
offers,
|
|
459
451
|
};
|
|
460
452
|
}
|
|
461
453
|
|
|
462
|
-
async batchCreateServices(requests: CreateServiceRequest[]): Promise<Array<{
|
|
463
|
-
service: Service;
|
|
464
|
-
indexUuid: string;
|
|
465
|
-
offers: Offer[];
|
|
466
|
-
}>> {
|
|
467
|
-
const results = [];
|
|
468
|
-
for (const request of requests) {
|
|
469
|
-
const result = await this.createService(request);
|
|
470
|
-
results.push(result);
|
|
471
|
-
}
|
|
472
|
-
return results;
|
|
473
|
-
}
|
|
474
454
|
|
|
475
455
|
async getOffersForService(serviceId: string): Promise<Offer[]> {
|
|
476
456
|
const result = await this.db.prepare(`
|
|
@@ -499,12 +479,11 @@ export class D1Storage implements Storage {
|
|
|
499
479
|
return this.rowToService(result as any);
|
|
500
480
|
}
|
|
501
481
|
|
|
502
|
-
async
|
|
482
|
+
async getServiceByFqn(serviceFqn: string): Promise<Service | null> {
|
|
503
483
|
const result = await this.db.prepare(`
|
|
504
|
-
SELECT
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
`).bind(uuid, Date.now()).first();
|
|
484
|
+
SELECT * FROM services
|
|
485
|
+
WHERE service_fqn = ? AND expires_at > ?
|
|
486
|
+
`).bind(serviceFqn, Date.now()).first();
|
|
508
487
|
|
|
509
488
|
if (!result) {
|
|
510
489
|
return null;
|
|
@@ -513,49 +492,56 @@ export class D1Storage implements Storage {
|
|
|
513
492
|
return this.rowToService(result as any);
|
|
514
493
|
}
|
|
515
494
|
|
|
516
|
-
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
async discoverServices(
|
|
500
|
+
serviceName: string,
|
|
501
|
+
version: string,
|
|
502
|
+
limit: number,
|
|
503
|
+
offset: number
|
|
504
|
+
): Promise<Service[]> {
|
|
505
|
+
// Query for unique services with available offers
|
|
506
|
+
// We join with offers and filter for available ones (answerer_username IS NULL)
|
|
517
507
|
const result = await this.db.prepare(`
|
|
518
|
-
SELECT
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
508
|
+
SELECT DISTINCT s.* FROM services s
|
|
509
|
+
INNER JOIN offers o ON o.service_id = s.id
|
|
510
|
+
WHERE s.service_name = ?
|
|
511
|
+
AND s.version = ?
|
|
512
|
+
AND s.expires_at > ?
|
|
513
|
+
AND o.answerer_username IS NULL
|
|
514
|
+
AND o.expires_at > ?
|
|
522
515
|
ORDER BY s.created_at DESC
|
|
523
|
-
|
|
516
|
+
LIMIT ? OFFSET ?
|
|
517
|
+
`).bind(serviceName, version, Date.now(), Date.now(), limit, offset).all();
|
|
524
518
|
|
|
525
519
|
if (!result.results) {
|
|
526
520
|
return [];
|
|
527
521
|
}
|
|
528
522
|
|
|
529
|
-
return result.results.map((row
|
|
530
|
-
uuid: row.uuid,
|
|
531
|
-
isPublic: row.is_public === 1,
|
|
532
|
-
serviceFqn: row.is_public === 1 ? row.service_fqn : undefined,
|
|
533
|
-
metadata: row.is_public === 1 ? row.metadata || undefined : undefined,
|
|
534
|
-
}));
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
async queryService(username: string, serviceFqn: string): Promise<string | null> {
|
|
538
|
-
const result = await this.db.prepare(`
|
|
539
|
-
SELECT si.uuid FROM service_index si
|
|
540
|
-
INNER JOIN services s ON si.service_id = s.id
|
|
541
|
-
WHERE si.username = ? AND si.service_fqn = ? AND si.expires_at > ?
|
|
542
|
-
`).bind(username, serviceFqn, Date.now()).first();
|
|
543
|
-
|
|
544
|
-
return result ? (result as any).uuid : null;
|
|
523
|
+
return result.results.map(row => this.rowToService(row as any));
|
|
545
524
|
}
|
|
546
525
|
|
|
547
|
-
async
|
|
526
|
+
async getRandomService(serviceName: string, version: string): Promise<Service | null> {
|
|
527
|
+
// Get a random service with an available offer
|
|
548
528
|
const result = await this.db.prepare(`
|
|
549
|
-
SELECT
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
529
|
+
SELECT s.* FROM services s
|
|
530
|
+
INNER JOIN offers o ON o.service_id = s.id
|
|
531
|
+
WHERE s.service_name = ?
|
|
532
|
+
AND s.version = ?
|
|
533
|
+
AND s.expires_at > ?
|
|
534
|
+
AND o.answerer_username IS NULL
|
|
535
|
+
AND o.expires_at > ?
|
|
536
|
+
ORDER BY RANDOM()
|
|
537
|
+
LIMIT 1
|
|
538
|
+
`).bind(serviceName, version, Date.now(), Date.now()).first();
|
|
553
539
|
|
|
554
|
-
if (!result
|
|
555
|
-
return
|
|
540
|
+
if (!result) {
|
|
541
|
+
return null;
|
|
556
542
|
}
|
|
557
543
|
|
|
558
|
-
return
|
|
544
|
+
return this.rowToService(result as any);
|
|
559
545
|
}
|
|
560
546
|
|
|
561
547
|
async deleteService(serviceId: string, username: string): Promise<boolean> {
|
|
@@ -588,13 +574,14 @@ export class D1Storage implements Storage {
|
|
|
588
574
|
private rowToOffer(row: any): Offer {
|
|
589
575
|
return {
|
|
590
576
|
id: row.id,
|
|
591
|
-
|
|
577
|
+
username: row.username,
|
|
578
|
+
serviceId: row.service_id || undefined,
|
|
579
|
+
serviceFqn: row.service_fqn || undefined,
|
|
592
580
|
sdp: row.sdp,
|
|
593
581
|
createdAt: row.created_at,
|
|
594
582
|
expiresAt: row.expires_at,
|
|
595
583
|
lastSeen: row.last_seen,
|
|
596
|
-
|
|
597
|
-
answererPeerId: row.answerer_peer_id || undefined,
|
|
584
|
+
answererUsername: row.answerer_username || undefined,
|
|
598
585
|
answerSdp: row.answer_sdp || undefined,
|
|
599
586
|
answeredAt: row.answered_at || undefined,
|
|
600
587
|
};
|
|
@@ -606,12 +593,12 @@ export class D1Storage implements Storage {
|
|
|
606
593
|
private rowToService(row: any): Service {
|
|
607
594
|
return {
|
|
608
595
|
id: row.id,
|
|
609
|
-
username: row.username,
|
|
610
596
|
serviceFqn: row.service_fqn,
|
|
597
|
+
serviceName: row.service_name,
|
|
598
|
+
version: row.version,
|
|
599
|
+
username: row.username,
|
|
611
600
|
createdAt: row.created_at,
|
|
612
601
|
expiresAt: row.expires_at,
|
|
613
|
-
isPublic: row.is_public === 1,
|
|
614
|
-
metadata: row.metadata || undefined,
|
|
615
602
|
};
|
|
616
603
|
}
|
|
617
604
|
}
|