@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/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
|
});
|
package/src/storage/d1.ts
CHANGED
|
@@ -34,7 +34,7 @@ export class D1Storage implements Storage {
|
|
|
34
34
|
*/
|
|
35
35
|
async initializeDatabase(): Promise<void> {
|
|
36
36
|
await this.db.exec(`
|
|
37
|
-
--
|
|
37
|
+
-- WebRTC signaling offers
|
|
38
38
|
CREATE TABLE IF NOT EXISTS offers (
|
|
39
39
|
id TEXT PRIMARY KEY,
|
|
40
40
|
peer_id TEXT NOT NULL,
|
|
@@ -125,7 +125,7 @@ export class D1Storage implements Storage {
|
|
|
125
125
|
|
|
126
126
|
// D1 doesn't support true transactions yet, so we do this sequentially
|
|
127
127
|
for (const offer of offers) {
|
|
128
|
-
const id = offer.id || await generateOfferHash(offer.sdp
|
|
128
|
+
const id = offer.id || await generateOfferHash(offer.sdp);
|
|
129
129
|
const now = Date.now();
|
|
130
130
|
|
|
131
131
|
await this.db.prepare(`
|
|
@@ -401,6 +401,7 @@ export class D1Storage implements Storage {
|
|
|
401
401
|
async createService(request: CreateServiceRequest): Promise<{
|
|
402
402
|
service: Service;
|
|
403
403
|
indexUuid: string;
|
|
404
|
+
offers: Offer[];
|
|
404
405
|
}> {
|
|
405
406
|
const serviceId = crypto.randomUUID();
|
|
406
407
|
const indexUuid = crypto.randomUUID();
|
|
@@ -408,13 +409,12 @@ export class D1Storage implements Storage {
|
|
|
408
409
|
|
|
409
410
|
// Insert service
|
|
410
411
|
await this.db.prepare(`
|
|
411
|
-
INSERT INTO services (id, username, service_fqn,
|
|
412
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
412
|
+
INSERT INTO services (id, username, service_fqn, created_at, expires_at, is_public, metadata)
|
|
413
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
413
414
|
`).bind(
|
|
414
415
|
serviceId,
|
|
415
416
|
request.username,
|
|
416
417
|
request.serviceFqn,
|
|
417
|
-
request.offerId,
|
|
418
418
|
now,
|
|
419
419
|
request.expiresAt,
|
|
420
420
|
request.isPublic ? 1 : 0,
|
|
@@ -434,6 +434,13 @@ export class D1Storage implements Storage {
|
|
|
434
434
|
request.expiresAt
|
|
435
435
|
).run();
|
|
436
436
|
|
|
437
|
+
// Create offers with serviceId
|
|
438
|
+
const offerRequests = request.offers.map(offer => ({
|
|
439
|
+
...offer,
|
|
440
|
+
serviceId,
|
|
441
|
+
}));
|
|
442
|
+
const offers = await this.createOffers(offerRequests);
|
|
443
|
+
|
|
437
444
|
// Touch username to extend expiry
|
|
438
445
|
await this.touchUsername(request.username);
|
|
439
446
|
|
|
@@ -442,16 +449,43 @@ export class D1Storage implements Storage {
|
|
|
442
449
|
id: serviceId,
|
|
443
450
|
username: request.username,
|
|
444
451
|
serviceFqn: request.serviceFqn,
|
|
445
|
-
offerId: request.offerId,
|
|
446
452
|
createdAt: now,
|
|
447
453
|
expiresAt: request.expiresAt,
|
|
448
454
|
isPublic: request.isPublic || false,
|
|
449
455
|
metadata: request.metadata,
|
|
450
456
|
},
|
|
451
457
|
indexUuid,
|
|
458
|
+
offers,
|
|
452
459
|
};
|
|
453
460
|
}
|
|
454
461
|
|
|
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
|
+
|
|
475
|
+
async getOffersForService(serviceId: string): Promise<Offer[]> {
|
|
476
|
+
const result = await this.db.prepare(`
|
|
477
|
+
SELECT * FROM offers
|
|
478
|
+
WHERE service_id = ? AND expires_at > ?
|
|
479
|
+
ORDER BY created_at ASC
|
|
480
|
+
`).bind(serviceId, Date.now()).all();
|
|
481
|
+
|
|
482
|
+
if (!result.results) {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return result.results.map(row => this.rowToOffer(row as any));
|
|
487
|
+
}
|
|
488
|
+
|
|
455
489
|
async getServiceById(serviceId: string): Promise<Service | null> {
|
|
456
490
|
const result = await this.db.prepare(`
|
|
457
491
|
SELECT * FROM services
|
|
@@ -510,6 +544,20 @@ export class D1Storage implements Storage {
|
|
|
510
544
|
return result ? (result as any).uuid : null;
|
|
511
545
|
}
|
|
512
546
|
|
|
547
|
+
async findServicesByName(username: string, serviceName: string): Promise<Service[]> {
|
|
548
|
+
const result = await this.db.prepare(`
|
|
549
|
+
SELECT * FROM services
|
|
550
|
+
WHERE username = ? AND service_fqn LIKE ? AND expires_at > ?
|
|
551
|
+
ORDER BY created_at DESC
|
|
552
|
+
`).bind(username, `${serviceName}@%`, Date.now()).all();
|
|
553
|
+
|
|
554
|
+
if (!result.results) {
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return result.results.map(row => this.rowToService(row as any));
|
|
559
|
+
}
|
|
560
|
+
|
|
513
561
|
async deleteService(serviceId: string, username: string): Promise<boolean> {
|
|
514
562
|
const result = await this.db.prepare(`
|
|
515
563
|
DELETE FROM services
|
|
@@ -560,7 +608,6 @@ export class D1Storage implements Storage {
|
|
|
560
608
|
id: row.id,
|
|
561
609
|
username: row.username,
|
|
562
610
|
serviceFqn: row.service_fqn,
|
|
563
|
-
offerId: row.offer_id,
|
|
564
611
|
createdAt: row.created_at,
|
|
565
612
|
expiresAt: row.expires_at,
|
|
566
613
|
isPublic: row.is_public === 1,
|
package/src/storage/hash-id.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generates a content-based offer ID using SHA-256 hash
|
|
3
|
-
* Creates deterministic IDs based on offer content
|
|
3
|
+
* Creates deterministic IDs based on offer SDP content
|
|
4
4
|
* PeerID is not included as it's inferred from authentication
|
|
5
5
|
* Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers
|
|
6
6
|
*
|
|
7
7
|
* @param sdp - The WebRTC SDP offer
|
|
8
|
-
* @
|
|
9
|
-
* @returns SHA-256 hash of the sanitized offer content
|
|
8
|
+
* @returns SHA-256 hash of the SDP content
|
|
10
9
|
*/
|
|
11
|
-
export async function generateOfferHash(
|
|
12
|
-
sdp: string,
|
|
13
|
-
topics: string[]
|
|
14
|
-
): Promise<string> {
|
|
10
|
+
export async function generateOfferHash(sdp: string): Promise<string> {
|
|
15
11
|
// Sanitize and normalize the offer content
|
|
16
12
|
// Only include core offer content (not peerId - that's inferred from auth)
|
|
17
13
|
const sanitizedOffer = {
|
|
18
|
-
sdp
|
|
19
|
-
topics: [...topics].sort(), // Sort topics for consistency
|
|
14
|
+
sdp
|
|
20
15
|
};
|
|
21
16
|
|
|
22
17
|
// Create non-prettified JSON string
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -36,10 +36,11 @@ export class SQLiteStorage implements Storage {
|
|
|
36
36
|
*/
|
|
37
37
|
private initializeDatabase(): void {
|
|
38
38
|
this.db.exec(`
|
|
39
|
-
--
|
|
39
|
+
-- WebRTC signaling offers
|
|
40
40
|
CREATE TABLE IF NOT EXISTS offers (
|
|
41
41
|
id TEXT PRIMARY KEY,
|
|
42
42
|
peer_id TEXT NOT NULL,
|
|
43
|
+
service_id TEXT,
|
|
43
44
|
sdp TEXT NOT NULL,
|
|
44
45
|
created_at INTEGER NOT NULL,
|
|
45
46
|
expires_at INTEGER NOT NULL,
|
|
@@ -47,10 +48,12 @@ export class SQLiteStorage implements Storage {
|
|
|
47
48
|
secret TEXT,
|
|
48
49
|
answerer_peer_id TEXT,
|
|
49
50
|
answer_sdp TEXT,
|
|
50
|
-
answered_at INTEGER
|
|
51
|
+
answered_at INTEGER,
|
|
52
|
+
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
51
53
|
);
|
|
52
54
|
|
|
53
55
|
CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_offers_service ON offers(service_id);
|
|
54
57
|
CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
|
|
55
58
|
CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
|
|
56
59
|
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
|
|
@@ -84,25 +87,22 @@ export class SQLiteStorage implements Storage {
|
|
|
84
87
|
CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
|
|
85
88
|
CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
|
|
86
89
|
|
|
87
|
-
-- Services table
|
|
90
|
+
-- Services table (one service can have multiple offers)
|
|
88
91
|
CREATE TABLE IF NOT EXISTS services (
|
|
89
92
|
id TEXT PRIMARY KEY,
|
|
90
93
|
username TEXT NOT NULL,
|
|
91
94
|
service_fqn TEXT NOT NULL,
|
|
92
|
-
offer_id TEXT NOT NULL,
|
|
93
95
|
created_at INTEGER NOT NULL,
|
|
94
96
|
expires_at INTEGER NOT NULL,
|
|
95
97
|
is_public INTEGER NOT NULL DEFAULT 0,
|
|
96
98
|
metadata TEXT,
|
|
97
99
|
FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
|
|
98
|
-
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE,
|
|
99
100
|
UNIQUE(username, service_fqn)
|
|
100
101
|
);
|
|
101
102
|
|
|
102
103
|
CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
|
|
103
104
|
CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
|
|
104
105
|
CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
|
|
105
|
-
CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
|
|
106
106
|
|
|
107
107
|
-- Service index table (privacy layer)
|
|
108
108
|
CREATE TABLE IF NOT EXISTS service_index (
|
|
@@ -132,15 +132,15 @@ export class SQLiteStorage implements Storage {
|
|
|
132
132
|
const offersWithIds = await Promise.all(
|
|
133
133
|
offers.map(async (offer) => ({
|
|
134
134
|
...offer,
|
|
135
|
-
id: offer.id || await generateOfferHash(offer.sdp
|
|
135
|
+
id: offer.id || await generateOfferHash(offer.sdp),
|
|
136
136
|
}))
|
|
137
137
|
);
|
|
138
138
|
|
|
139
139
|
// Use transaction for atomic creation
|
|
140
140
|
const transaction = this.db.transaction((offersWithIds: (CreateOfferRequest & { id: string })[]) => {
|
|
141
141
|
const offerStmt = this.db.prepare(`
|
|
142
|
-
INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
|
|
143
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
142
|
+
INSERT INTO offers (id, peer_id, service_id, sdp, created_at, expires_at, last_seen, secret)
|
|
143
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
144
144
|
`);
|
|
145
145
|
|
|
146
146
|
for (const offer of offersWithIds) {
|
|
@@ -150,6 +150,7 @@ export class SQLiteStorage implements Storage {
|
|
|
150
150
|
offerStmt.run(
|
|
151
151
|
offer.id,
|
|
152
152
|
offer.peerId,
|
|
153
|
+
offer.serviceId || null,
|
|
153
154
|
offer.sdp,
|
|
154
155
|
now,
|
|
155
156
|
offer.expiresAt,
|
|
@@ -160,6 +161,7 @@ export class SQLiteStorage implements Storage {
|
|
|
160
161
|
created.push({
|
|
161
162
|
id: offer.id,
|
|
162
163
|
peerId: offer.peerId,
|
|
164
|
+
serviceId: offer.serviceId || undefined,
|
|
163
165
|
sdp: offer.sdp,
|
|
164
166
|
createdAt: now,
|
|
165
167
|
expiresAt: offer.expiresAt,
|
|
@@ -426,23 +428,31 @@ export class SQLiteStorage implements Storage {
|
|
|
426
428
|
async createService(request: CreateServiceRequest): Promise<{
|
|
427
429
|
service: Service;
|
|
428
430
|
indexUuid: string;
|
|
431
|
+
offers: Offer[];
|
|
429
432
|
}> {
|
|
430
433
|
const serviceId = randomUUID();
|
|
431
434
|
const indexUuid = randomUUID();
|
|
432
435
|
const now = Date.now();
|
|
433
436
|
|
|
437
|
+
// Create offers with serviceId
|
|
438
|
+
const offerRequests: CreateOfferRequest[] = request.offers.map(offer => ({
|
|
439
|
+
...offer,
|
|
440
|
+
serviceId,
|
|
441
|
+
}));
|
|
442
|
+
|
|
443
|
+
const offers = await this.createOffers(offerRequests);
|
|
444
|
+
|
|
434
445
|
const transaction = this.db.transaction(() => {
|
|
435
|
-
// Insert service
|
|
446
|
+
// Insert service (no offer_id column anymore)
|
|
436
447
|
const serviceStmt = this.db.prepare(`
|
|
437
|
-
INSERT INTO services (id, username, service_fqn,
|
|
438
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
448
|
+
INSERT INTO services (id, username, service_fqn, created_at, expires_at, is_public, metadata)
|
|
449
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
439
450
|
`);
|
|
440
451
|
|
|
441
452
|
serviceStmt.run(
|
|
442
453
|
serviceId,
|
|
443
454
|
request.username,
|
|
444
455
|
request.serviceFqn,
|
|
445
|
-
request.offerId,
|
|
446
456
|
now,
|
|
447
457
|
request.expiresAt,
|
|
448
458
|
request.isPublic ? 1 : 0,
|
|
@@ -475,16 +485,31 @@ export class SQLiteStorage implements Storage {
|
|
|
475
485
|
id: serviceId,
|
|
476
486
|
username: request.username,
|
|
477
487
|
serviceFqn: request.serviceFqn,
|
|
478
|
-
offerId: request.offerId,
|
|
479
488
|
createdAt: now,
|
|
480
489
|
expiresAt: request.expiresAt,
|
|
481
490
|
isPublic: request.isPublic || false,
|
|
482
491
|
metadata: request.metadata,
|
|
483
492
|
},
|
|
484
493
|
indexUuid,
|
|
494
|
+
offers,
|
|
485
495
|
};
|
|
486
496
|
}
|
|
487
497
|
|
|
498
|
+
async batchCreateServices(requests: CreateServiceRequest[]): Promise<Array<{
|
|
499
|
+
service: Service;
|
|
500
|
+
indexUuid: string;
|
|
501
|
+
offers: Offer[];
|
|
502
|
+
}>> {
|
|
503
|
+
const results = [];
|
|
504
|
+
|
|
505
|
+
for (const request of requests) {
|
|
506
|
+
const result = await this.createService(request);
|
|
507
|
+
results.push(result);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return results;
|
|
511
|
+
}
|
|
512
|
+
|
|
488
513
|
async getServiceById(serviceId: string): Promise<Service | null> {
|
|
489
514
|
const stmt = this.db.prepare(`
|
|
490
515
|
SELECT * FROM services
|
|
@@ -547,6 +572,18 @@ export class SQLiteStorage implements Storage {
|
|
|
547
572
|
return row ? row.uuid : null;
|
|
548
573
|
}
|
|
549
574
|
|
|
575
|
+
async findServicesByName(username: string, serviceName: string): Promise<Service[]> {
|
|
576
|
+
const stmt = this.db.prepare(`
|
|
577
|
+
SELECT * FROM services
|
|
578
|
+
WHERE username = ? AND service_fqn LIKE ? AND expires_at > ?
|
|
579
|
+
ORDER BY created_at DESC
|
|
580
|
+
`);
|
|
581
|
+
|
|
582
|
+
const rows = stmt.all(username, `${serviceName}@%`, Date.now()) as any[];
|
|
583
|
+
|
|
584
|
+
return rows.map(row => this.rowToService(row));
|
|
585
|
+
}
|
|
586
|
+
|
|
550
587
|
async deleteService(serviceId: string, username: string): Promise<boolean> {
|
|
551
588
|
const stmt = this.db.prepare(`
|
|
552
589
|
DELETE FROM services
|
|
@@ -576,6 +613,7 @@ export class SQLiteStorage implements Storage {
|
|
|
576
613
|
return {
|
|
577
614
|
id: row.id,
|
|
578
615
|
peerId: row.peer_id,
|
|
616
|
+
serviceId: row.service_id || undefined,
|
|
579
617
|
sdp: row.sdp,
|
|
580
618
|
createdAt: row.created_at,
|
|
581
619
|
expiresAt: row.expires_at,
|
|
@@ -595,11 +633,24 @@ export class SQLiteStorage implements Storage {
|
|
|
595
633
|
id: row.id,
|
|
596
634
|
username: row.username,
|
|
597
635
|
serviceFqn: row.service_fqn,
|
|
598
|
-
offerId: row.offer_id,
|
|
599
636
|
createdAt: row.created_at,
|
|
600
637
|
expiresAt: row.expires_at,
|
|
601
638
|
isPublic: row.is_public === 1,
|
|
602
639
|
metadata: row.metadata || undefined,
|
|
603
640
|
};
|
|
604
641
|
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Get all offers for a service
|
|
645
|
+
*/
|
|
646
|
+
async getOffersForService(serviceId: string): Promise<Offer[]> {
|
|
647
|
+
const stmt = this.db.prepare(`
|
|
648
|
+
SELECT * FROM offers
|
|
649
|
+
WHERE service_id = ? AND expires_at > ?
|
|
650
|
+
ORDER BY created_at ASC
|
|
651
|
+
`);
|
|
652
|
+
|
|
653
|
+
const rows = stmt.all(serviceId, Date.now()) as any[];
|
|
654
|
+
return rows.map(row => this.rowToOffer(row));
|
|
655
|
+
}
|
|
605
656
|
}
|
package/src/storage/types.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Represents a WebRTC signaling offer
|
|
2
|
+
* Represents a WebRTC signaling offer
|
|
3
3
|
*/
|
|
4
4
|
export interface Offer {
|
|
5
5
|
id: string;
|
|
6
6
|
peerId: string;
|
|
7
|
+
serviceId?: string; // Optional link to service (null for standalone offers)
|
|
7
8
|
sdp: string;
|
|
8
9
|
createdAt: number;
|
|
9
10
|
expiresAt: number;
|
|
10
11
|
lastSeen: number;
|
|
11
12
|
secret?: string;
|
|
12
|
-
info?: string;
|
|
13
13
|
answererPeerId?: string;
|
|
14
14
|
answerSdp?: string;
|
|
15
15
|
answeredAt?: number;
|
|
@@ -34,10 +34,10 @@ export interface IceCandidate {
|
|
|
34
34
|
export interface CreateOfferRequest {
|
|
35
35
|
id?: string;
|
|
36
36
|
peerId: string;
|
|
37
|
+
serviceId?: string; // Optional link to service
|
|
37
38
|
sdp: string;
|
|
38
39
|
expiresAt: number;
|
|
39
40
|
secret?: string;
|
|
40
|
-
info?: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -63,13 +63,12 @@ export interface ClaimUsernameRequest {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Represents a published service
|
|
66
|
+
* Represents a published service (can have multiple offers)
|
|
67
67
|
*/
|
|
68
68
|
export interface Service {
|
|
69
69
|
id: string; // UUID v4
|
|
70
70
|
username: string;
|
|
71
71
|
serviceFqn: string; // com.example.chat@1.0.0
|
|
72
|
-
offerId: string; // Links to offers table
|
|
73
72
|
createdAt: number;
|
|
74
73
|
expiresAt: number;
|
|
75
74
|
isPublic: boolean;
|
|
@@ -77,15 +76,22 @@ export interface Service {
|
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/**
|
|
80
|
-
* Request to create a service
|
|
79
|
+
* Request to create a single service
|
|
81
80
|
*/
|
|
82
81
|
export interface CreateServiceRequest {
|
|
83
82
|
username: string;
|
|
84
83
|
serviceFqn: string;
|
|
85
|
-
offerId: string;
|
|
86
84
|
expiresAt: number;
|
|
87
85
|
isPublic?: boolean;
|
|
88
86
|
metadata?: string;
|
|
87
|
+
offers: CreateOfferRequest[]; // Multiple offers per service
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Request to create multiple services in batch
|
|
92
|
+
*/
|
|
93
|
+
export interface BatchCreateServicesRequest {
|
|
94
|
+
services: CreateServiceRequest[];
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
/**
|
|
@@ -236,15 +242,34 @@ export interface Storage {
|
|
|
236
242
|
// ===== Service Management =====
|
|
237
243
|
|
|
238
244
|
/**
|
|
239
|
-
* Creates a new service
|
|
240
|
-
* @param request Service creation request
|
|
241
|
-
* @returns Created service with generated ID and
|
|
245
|
+
* Creates a new service with offers
|
|
246
|
+
* @param request Service creation request (includes offers)
|
|
247
|
+
* @returns Created service with generated ID, index UUID, and created offers
|
|
242
248
|
*/
|
|
243
249
|
createService(request: CreateServiceRequest): Promise<{
|
|
244
250
|
service: Service;
|
|
245
251
|
indexUuid: string;
|
|
252
|
+
offers: Offer[];
|
|
246
253
|
}>;
|
|
247
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Creates multiple services with offers in batch
|
|
257
|
+
* @param requests Array of service creation requests
|
|
258
|
+
* @returns Array of created services with IDs, UUIDs, and offers
|
|
259
|
+
*/
|
|
260
|
+
batchCreateServices(requests: CreateServiceRequest[]): Promise<Array<{
|
|
261
|
+
service: Service;
|
|
262
|
+
indexUuid: string;
|
|
263
|
+
offers: Offer[];
|
|
264
|
+
}>>;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Gets all offers for a service
|
|
268
|
+
* @param serviceId Service ID
|
|
269
|
+
* @returns Array of offers for the service
|
|
270
|
+
*/
|
|
271
|
+
getOffersForService(serviceId: string): Promise<Offer[]>;
|
|
272
|
+
|
|
248
273
|
/**
|
|
249
274
|
* Gets a service by its service ID
|
|
250
275
|
* @param serviceId Service ID
|
|
@@ -274,6 +299,14 @@ export interface Storage {
|
|
|
274
299
|
*/
|
|
275
300
|
queryService(username: string, serviceFqn: string): Promise<string | null>;
|
|
276
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Finds all services by username and service name (without version)
|
|
304
|
+
* @param username Username
|
|
305
|
+
* @param serviceName Service name (e.g., 'com.example.chat')
|
|
306
|
+
* @returns Array of services with matching service name
|
|
307
|
+
*/
|
|
308
|
+
findServicesByName(username: string, serviceName: string): Promise<Service[]>;
|
|
309
|
+
|
|
277
310
|
/**
|
|
278
311
|
* Deletes a service (with ownership verification)
|
|
279
312
|
* @param serviceId Service ID
|
package/src/worker.ts
CHANGED
|
@@ -13,7 +13,6 @@ export interface Env {
|
|
|
13
13
|
OFFER_MAX_TTL?: string;
|
|
14
14
|
OFFER_MIN_TTL?: string;
|
|
15
15
|
MAX_OFFERS_PER_REQUEST?: string;
|
|
16
|
-
MAX_TOPICS_PER_OFFER?: string;
|
|
17
16
|
CORS_ORIGINS?: string;
|
|
18
17
|
VERSION?: string;
|
|
19
18
|
}
|
|
@@ -43,8 +42,7 @@ export default {
|
|
|
43
42
|
offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : 86400000,
|
|
44
43
|
offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : 60000,
|
|
45
44
|
cleanupInterval: 60000, // Not used in Workers (scheduled handler instead)
|
|
46
|
-
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100
|
|
47
|
-
maxTopicsPerOffer: env.MAX_TOPICS_PER_OFFER ? parseInt(env.MAX_TOPICS_PER_OFFER, 10) : 50,
|
|
45
|
+
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100
|
|
48
46
|
};
|
|
49
47
|
|
|
50
48
|
// Create Hono app
|
package/src/bloom.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bloom filter utility for testing if peer IDs might be in a set
|
|
3
|
-
* Used to filter out known peers from discovery results
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export class BloomFilter {
|
|
7
|
-
private bits: Uint8Array;
|
|
8
|
-
private size: number;
|
|
9
|
-
private numHashes: number;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Creates a bloom filter from a base64 encoded bit array
|
|
13
|
-
*/
|
|
14
|
-
constructor(base64Data: string, numHashes: number = 3) {
|
|
15
|
-
// Decode base64 to Uint8Array (works in both Node.js and Workers)
|
|
16
|
-
const binaryString = atob(base64Data);
|
|
17
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
18
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
19
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
20
|
-
}
|
|
21
|
-
this.bits = bytes;
|
|
22
|
-
this.size = this.bits.length * 8;
|
|
23
|
-
this.numHashes = numHashes;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Test if a peer ID might be in the filter
|
|
28
|
-
* Returns true if possibly in set, false if definitely not in set
|
|
29
|
-
*/
|
|
30
|
-
test(peerId: string): boolean {
|
|
31
|
-
for (let i = 0; i < this.numHashes; i++) {
|
|
32
|
-
const hash = this.hash(peerId, i);
|
|
33
|
-
const index = hash % this.size;
|
|
34
|
-
const byteIndex = Math.floor(index / 8);
|
|
35
|
-
const bitIndex = index % 8;
|
|
36
|
-
|
|
37
|
-
if (!(this.bits[byteIndex] & (1 << bitIndex))) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Simple hash function (FNV-1a variant)
|
|
46
|
-
*/
|
|
47
|
-
private hash(str: string, seed: number): number {
|
|
48
|
-
let hash = 2166136261 ^ seed;
|
|
49
|
-
for (let i = 0; i < str.length; i++) {
|
|
50
|
-
hash ^= str.charCodeAt(i);
|
|
51
|
-
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
52
|
-
}
|
|
53
|
-
return hash >>> 0;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Helper to parse bloom filter from base64 string
|
|
59
|
-
*/
|
|
60
|
-
export function parseBloomFilter(base64: string): BloomFilter | null {
|
|
61
|
-
try {
|
|
62
|
-
return new BloomFilter(base64);
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|