@xtr-dev/rondevu-server 0.5.12 → 0.5.14
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 +9 -21
- package/dist/index.js +939 -1110
- package/dist/index.js.map +4 -4
- package/migrations/0009_public_key_auth.sql +74 -0
- package/migrations/fresh_schema.sql +20 -21
- package/package.json +2 -1
- package/src/config.ts +1 -47
- package/src/crypto.ts +70 -304
- package/src/index.ts +2 -3
- package/src/rpc.ts +90 -272
- package/src/storage/d1.ts +72 -235
- package/src/storage/factory.ts +4 -17
- package/src/storage/memory.ts +46 -151
- package/src/storage/mysql.ts +66 -187
- package/src/storage/postgres.ts +66 -186
- package/src/storage/sqlite.ts +65 -194
- package/src/storage/types.ts +30 -88
- package/src/worker.ts +4 -9
- package/wrangler.toml +1 -1
package/src/storage/memory.ts
CHANGED
|
@@ -3,13 +3,9 @@ import {
|
|
|
3
3
|
Offer,
|
|
4
4
|
IceCandidate,
|
|
5
5
|
CreateOfferRequest,
|
|
6
|
-
Credential,
|
|
7
|
-
GenerateCredentialsRequest,
|
|
8
6
|
} from './types.ts';
|
|
9
7
|
import { generateOfferHash } from './hash-id.ts';
|
|
10
8
|
|
|
11
|
-
const YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000;
|
|
12
|
-
|
|
13
9
|
interface RateLimit {
|
|
14
10
|
count: number;
|
|
15
11
|
resetTime: number;
|
|
@@ -25,26 +21,21 @@ interface NonceEntry {
|
|
|
25
21
|
* Best for development, testing, or ephemeral deployments
|
|
26
22
|
*/
|
|
27
23
|
export class MemoryStorage implements Storage {
|
|
28
|
-
private masterEncryptionKey: string;
|
|
29
|
-
|
|
30
24
|
// Primary storage
|
|
31
|
-
private credentials = new Map<string, Credential>();
|
|
32
25
|
private offers = new Map<string, Offer>();
|
|
33
26
|
private iceCandidates = new Map<string, IceCandidate[]>(); // offerId → candidates
|
|
34
27
|
private rateLimits = new Map<string, RateLimit>();
|
|
35
28
|
private nonces = new Map<string, NonceEntry>();
|
|
36
29
|
|
|
37
30
|
// Secondary indexes for efficient lookups
|
|
38
|
-
private
|
|
31
|
+
private offersByPublicKey = new Map<string, Set<string>>(); // publicKey → offer IDs
|
|
39
32
|
private offersByTag = new Map<string, Set<string>>(); // tag → offer IDs
|
|
40
|
-
private offersByAnswerer = new Map<string, Set<string>>(); // answerer
|
|
33
|
+
private offersByAnswerer = new Map<string, Set<string>>(); // answerer publicKey → offer IDs
|
|
41
34
|
|
|
42
35
|
// Auto-increment counter for ICE candidates
|
|
43
36
|
private iceCandidateIdCounter = 0;
|
|
44
37
|
|
|
45
|
-
constructor(
|
|
46
|
-
this.masterEncryptionKey = masterEncryptionKey;
|
|
47
|
-
}
|
|
38
|
+
constructor() {}
|
|
48
39
|
|
|
49
40
|
// ===== Offer Management =====
|
|
50
41
|
|
|
@@ -57,7 +48,7 @@ export class MemoryStorage implements Storage {
|
|
|
57
48
|
|
|
58
49
|
const offer: Offer = {
|
|
59
50
|
id,
|
|
60
|
-
|
|
51
|
+
publicKey: request.publicKey,
|
|
61
52
|
tags: request.tags,
|
|
62
53
|
sdp: request.sdp,
|
|
63
54
|
createdAt: now,
|
|
@@ -68,11 +59,11 @@ export class MemoryStorage implements Storage {
|
|
|
68
59
|
// Store offer
|
|
69
60
|
this.offers.set(id, offer);
|
|
70
61
|
|
|
71
|
-
// Update
|
|
72
|
-
if (!this.
|
|
73
|
-
this.
|
|
62
|
+
// Update publicKey index
|
|
63
|
+
if (!this.offersByPublicKey.has(request.publicKey)) {
|
|
64
|
+
this.offersByPublicKey.set(request.publicKey, new Set());
|
|
74
65
|
}
|
|
75
|
-
this.
|
|
66
|
+
this.offersByPublicKey.get(request.publicKey)!.add(id);
|
|
76
67
|
|
|
77
68
|
// Update tag indexes
|
|
78
69
|
for (const tag of request.tags) {
|
|
@@ -88,9 +79,9 @@ export class MemoryStorage implements Storage {
|
|
|
88
79
|
return created;
|
|
89
80
|
}
|
|
90
81
|
|
|
91
|
-
async
|
|
82
|
+
async getOffersByPublicKey(publicKey: string): Promise<Offer[]> {
|
|
92
83
|
const now = Date.now();
|
|
93
|
-
const offerIds = this.
|
|
84
|
+
const offerIds = this.offersByPublicKey.get(publicKey);
|
|
94
85
|
if (!offerIds) return [];
|
|
95
86
|
|
|
96
87
|
const offers: Offer[] = [];
|
|
@@ -112,9 +103,9 @@ export class MemoryStorage implements Storage {
|
|
|
112
103
|
return offer;
|
|
113
104
|
}
|
|
114
105
|
|
|
115
|
-
async deleteOffer(offerId: string,
|
|
106
|
+
async deleteOffer(offerId: string, ownerPublicKey: string): Promise<boolean> {
|
|
116
107
|
const offer = this.offers.get(offerId);
|
|
117
|
-
if (!offer || offer.
|
|
108
|
+
if (!offer || offer.publicKey !== ownerPublicKey) {
|
|
118
109
|
return false;
|
|
119
110
|
}
|
|
120
111
|
|
|
@@ -142,7 +133,7 @@ export class MemoryStorage implements Storage {
|
|
|
142
133
|
|
|
143
134
|
async answerOffer(
|
|
144
135
|
offerId: string,
|
|
145
|
-
|
|
136
|
+
answererPublicKey: string,
|
|
146
137
|
answerSdp: string,
|
|
147
138
|
matchedTags?: string[]
|
|
148
139
|
): Promise<{ success: boolean; error?: string }> {
|
|
@@ -152,35 +143,35 @@ export class MemoryStorage implements Storage {
|
|
|
152
143
|
return { success: false, error: 'Offer not found or expired' };
|
|
153
144
|
}
|
|
154
145
|
|
|
155
|
-
if (offer.
|
|
146
|
+
if (offer.answererPublicKey) {
|
|
156
147
|
return { success: false, error: 'Offer already answered' };
|
|
157
148
|
}
|
|
158
149
|
|
|
159
150
|
// Update offer with answer
|
|
160
151
|
const now = Date.now();
|
|
161
|
-
offer.
|
|
152
|
+
offer.answererPublicKey = answererPublicKey;
|
|
162
153
|
offer.answerSdp = answerSdp;
|
|
163
154
|
offer.answeredAt = now;
|
|
164
155
|
offer.matchedTags = matchedTags;
|
|
165
156
|
|
|
166
157
|
// Update answerer index
|
|
167
|
-
if (!this.offersByAnswerer.has(
|
|
168
|
-
this.offersByAnswerer.set(
|
|
158
|
+
if (!this.offersByAnswerer.has(answererPublicKey)) {
|
|
159
|
+
this.offersByAnswerer.set(answererPublicKey, new Set());
|
|
169
160
|
}
|
|
170
|
-
this.offersByAnswerer.get(
|
|
161
|
+
this.offersByAnswerer.get(answererPublicKey)!.add(offerId);
|
|
171
162
|
|
|
172
163
|
return { success: true };
|
|
173
164
|
}
|
|
174
165
|
|
|
175
|
-
async getAnsweredOffers(
|
|
166
|
+
async getAnsweredOffers(offererPublicKey: string): Promise<Offer[]> {
|
|
176
167
|
const now = Date.now();
|
|
177
|
-
const offerIds = this.
|
|
168
|
+
const offerIds = this.offersByPublicKey.get(offererPublicKey);
|
|
178
169
|
if (!offerIds) return [];
|
|
179
170
|
|
|
180
171
|
const offers: Offer[] = [];
|
|
181
172
|
for (const id of offerIds) {
|
|
182
173
|
const offer = this.offers.get(id);
|
|
183
|
-
if (offer && offer.
|
|
174
|
+
if (offer && offer.answererPublicKey && offer.expiresAt > now) {
|
|
184
175
|
offers.push(offer);
|
|
185
176
|
}
|
|
186
177
|
}
|
|
@@ -188,9 +179,9 @@ export class MemoryStorage implements Storage {
|
|
|
188
179
|
return offers.sort((a, b) => (b.answeredAt || 0) - (a.answeredAt || 0));
|
|
189
180
|
}
|
|
190
181
|
|
|
191
|
-
async getOffersAnsweredBy(
|
|
182
|
+
async getOffersAnsweredBy(answererPublicKey: string): Promise<Offer[]> {
|
|
192
183
|
const now = Date.now();
|
|
193
|
-
const offerIds = this.offersByAnswerer.get(
|
|
184
|
+
const offerIds = this.offersByAnswerer.get(answererPublicKey);
|
|
194
185
|
if (!offerIds) return [];
|
|
195
186
|
|
|
196
187
|
const offers: Offer[] = [];
|
|
@@ -208,7 +199,7 @@ export class MemoryStorage implements Storage {
|
|
|
208
199
|
|
|
209
200
|
async discoverOffers(
|
|
210
201
|
tags: string[],
|
|
211
|
-
|
|
202
|
+
excludePublicKey: string | null,
|
|
212
203
|
limit: number,
|
|
213
204
|
offset: number
|
|
214
205
|
): Promise<Offer[]> {
|
|
@@ -234,8 +225,8 @@ export class MemoryStorage implements Storage {
|
|
|
234
225
|
if (
|
|
235
226
|
offer &&
|
|
236
227
|
offer.expiresAt > now &&
|
|
237
|
-
!offer.
|
|
238
|
-
(!
|
|
228
|
+
!offer.answererPublicKey &&
|
|
229
|
+
(!excludePublicKey || offer.publicKey !== excludePublicKey)
|
|
239
230
|
) {
|
|
240
231
|
offers.push(offer);
|
|
241
232
|
}
|
|
@@ -248,7 +239,7 @@ export class MemoryStorage implements Storage {
|
|
|
248
239
|
|
|
249
240
|
async getRandomOffer(
|
|
250
241
|
tags: string[],
|
|
251
|
-
|
|
242
|
+
excludePublicKey: string | null
|
|
252
243
|
): Promise<Offer | null> {
|
|
253
244
|
if (tags.length === 0) return null;
|
|
254
245
|
|
|
@@ -272,8 +263,8 @@ export class MemoryStorage implements Storage {
|
|
|
272
263
|
if (
|
|
273
264
|
offer &&
|
|
274
265
|
offer.expiresAt > now &&
|
|
275
|
-
!offer.
|
|
276
|
-
(!
|
|
266
|
+
!offer.answererPublicKey &&
|
|
267
|
+
(!excludePublicKey || offer.publicKey !== excludePublicKey)
|
|
277
268
|
) {
|
|
278
269
|
matchingOffers.push(offer);
|
|
279
270
|
}
|
|
@@ -290,7 +281,7 @@ export class MemoryStorage implements Storage {
|
|
|
290
281
|
|
|
291
282
|
async addIceCandidates(
|
|
292
283
|
offerId: string,
|
|
293
|
-
|
|
284
|
+
publicKey: string,
|
|
294
285
|
role: 'offerer' | 'answerer',
|
|
295
286
|
candidates: any[]
|
|
296
287
|
): Promise<number> {
|
|
@@ -306,7 +297,7 @@ export class MemoryStorage implements Storage {
|
|
|
306
297
|
const candidate: IceCandidate = {
|
|
307
298
|
id: ++this.iceCandidateIdCounter,
|
|
308
299
|
offerId,
|
|
309
|
-
|
|
300
|
+
publicKey,
|
|
310
301
|
role,
|
|
311
302
|
candidate: candidates[i],
|
|
312
303
|
createdAt: baseTimestamp + i,
|
|
@@ -331,7 +322,7 @@ export class MemoryStorage implements Storage {
|
|
|
331
322
|
|
|
332
323
|
async getIceCandidatesForMultipleOffers(
|
|
333
324
|
offerIds: string[],
|
|
334
|
-
|
|
325
|
+
publicKey: string,
|
|
335
326
|
since?: number
|
|
336
327
|
): Promise<Map<string, IceCandidate[]>> {
|
|
337
328
|
const result = new Map<string, IceCandidate[]>();
|
|
@@ -349,8 +340,8 @@ export class MemoryStorage implements Storage {
|
|
|
349
340
|
|
|
350
341
|
// Determine which role's candidates to return
|
|
351
342
|
// If user is offerer, return answerer candidates and vice versa
|
|
352
|
-
const isOfferer = offer.
|
|
353
|
-
const isAnswerer = offer.
|
|
343
|
+
const isOfferer = offer.publicKey === publicKey;
|
|
344
|
+
const isAnswerer = offer.answererPublicKey === publicKey;
|
|
354
345
|
|
|
355
346
|
if (!isOfferer && !isAnswerer) continue;
|
|
356
347
|
|
|
@@ -368,97 +359,6 @@ export class MemoryStorage implements Storage {
|
|
|
368
359
|
return result;
|
|
369
360
|
}
|
|
370
361
|
|
|
371
|
-
// ===== Credential Management =====
|
|
372
|
-
|
|
373
|
-
async generateCredentials(request: GenerateCredentialsRequest): Promise<Credential> {
|
|
374
|
-
const now = Date.now();
|
|
375
|
-
const expiresAt = request.expiresAt || (now + YEAR_IN_MS);
|
|
376
|
-
|
|
377
|
-
const { generateCredentialName, generateSecret, encryptSecret } = await import('../crypto.ts');
|
|
378
|
-
|
|
379
|
-
let name: string;
|
|
380
|
-
|
|
381
|
-
if (request.name) {
|
|
382
|
-
if (this.credentials.has(request.name)) {
|
|
383
|
-
throw new Error('Username already taken');
|
|
384
|
-
}
|
|
385
|
-
name = request.name;
|
|
386
|
-
} else {
|
|
387
|
-
let attempts = 0;
|
|
388
|
-
const maxAttempts = 100;
|
|
389
|
-
|
|
390
|
-
while (attempts < maxAttempts) {
|
|
391
|
-
name = generateCredentialName();
|
|
392
|
-
if (!this.credentials.has(name)) break;
|
|
393
|
-
attempts++;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (attempts >= maxAttempts) {
|
|
397
|
-
throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const secret = generateSecret();
|
|
402
|
-
|
|
403
|
-
// Encrypt secret before storing
|
|
404
|
-
const encryptedSecret = await encryptSecret(secret, this.masterEncryptionKey);
|
|
405
|
-
|
|
406
|
-
const credential: Credential = {
|
|
407
|
-
name: name!,
|
|
408
|
-
secret: encryptedSecret,
|
|
409
|
-
createdAt: now,
|
|
410
|
-
expiresAt,
|
|
411
|
-
lastUsed: now,
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
this.credentials.set(name!, credential);
|
|
415
|
-
|
|
416
|
-
// Return plaintext secret to user
|
|
417
|
-
return {
|
|
418
|
-
...credential,
|
|
419
|
-
secret, // Return plaintext, not encrypted
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async getCredential(name: string): Promise<Credential | null> {
|
|
424
|
-
const credential = this.credentials.get(name);
|
|
425
|
-
if (!credential || credential.expiresAt <= Date.now()) {
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
try {
|
|
430
|
-
const { decryptSecret } = await import('../crypto.ts');
|
|
431
|
-
const decryptedSecret = await decryptSecret(credential.secret, this.masterEncryptionKey);
|
|
432
|
-
|
|
433
|
-
return {
|
|
434
|
-
...credential,
|
|
435
|
-
secret: decryptedSecret,
|
|
436
|
-
};
|
|
437
|
-
} catch (error) {
|
|
438
|
-
console.error(`Failed to decrypt secret for credential '${name}':`, error);
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void> {
|
|
444
|
-
const credential = this.credentials.get(name);
|
|
445
|
-
if (credential) {
|
|
446
|
-
credential.lastUsed = lastUsed;
|
|
447
|
-
credential.expiresAt = expiresAt;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async deleteExpiredCredentials(now: number): Promise<number> {
|
|
452
|
-
let count = 0;
|
|
453
|
-
for (const [name, credential] of this.credentials) {
|
|
454
|
-
if (credential.expiresAt < now) {
|
|
455
|
-
this.credentials.delete(name);
|
|
456
|
-
count++;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return count;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
362
|
// ===== Rate Limiting =====
|
|
463
363
|
|
|
464
364
|
async checkRateLimit(identifier: string, limit: number, windowMs: number): Promise<boolean> {
|
|
@@ -514,12 +414,11 @@ export class MemoryStorage implements Storage {
|
|
|
514
414
|
|
|
515
415
|
async close(): Promise<void> {
|
|
516
416
|
// Clear all data
|
|
517
|
-
this.credentials.clear();
|
|
518
417
|
this.offers.clear();
|
|
519
418
|
this.iceCandidates.clear();
|
|
520
419
|
this.rateLimits.clear();
|
|
521
420
|
this.nonces.clear();
|
|
522
|
-
this.
|
|
421
|
+
this.offersByPublicKey.clear();
|
|
523
422
|
this.offersByTag.clear();
|
|
524
423
|
this.offersByAnswerer.clear();
|
|
525
424
|
}
|
|
@@ -530,15 +429,11 @@ export class MemoryStorage implements Storage {
|
|
|
530
429
|
return this.offers.size;
|
|
531
430
|
}
|
|
532
431
|
|
|
533
|
-
async
|
|
534
|
-
const offerIds = this.
|
|
432
|
+
async getOfferCountByPublicKey(publicKey: string): Promise<number> {
|
|
433
|
+
const offerIds = this.offersByPublicKey.get(publicKey);
|
|
535
434
|
return offerIds ? offerIds.size : 0;
|
|
536
435
|
}
|
|
537
436
|
|
|
538
|
-
async getCredentialCount(): Promise<number> {
|
|
539
|
-
return this.credentials.size;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
437
|
async getIceCandidateCount(offerId: string): Promise<number> {
|
|
543
438
|
const candidates = this.iceCandidates.get(offerId);
|
|
544
439
|
return candidates ? candidates.length : 0;
|
|
@@ -547,12 +442,12 @@ export class MemoryStorage implements Storage {
|
|
|
547
442
|
// ===== Helper Methods =====
|
|
548
443
|
|
|
549
444
|
private removeOfferFromIndexes(offer: Offer): void {
|
|
550
|
-
// Remove from
|
|
551
|
-
const
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
if (
|
|
555
|
-
this.
|
|
445
|
+
// Remove from publicKey index
|
|
446
|
+
const publicKeyOffers = this.offersByPublicKey.get(offer.publicKey);
|
|
447
|
+
if (publicKeyOffers) {
|
|
448
|
+
publicKeyOffers.delete(offer.id);
|
|
449
|
+
if (publicKeyOffers.size === 0) {
|
|
450
|
+
this.offersByPublicKey.delete(offer.publicKey);
|
|
556
451
|
}
|
|
557
452
|
}
|
|
558
453
|
|
|
@@ -568,12 +463,12 @@ export class MemoryStorage implements Storage {
|
|
|
568
463
|
}
|
|
569
464
|
|
|
570
465
|
// Remove from answerer index
|
|
571
|
-
if (offer.
|
|
572
|
-
const answererOffers = this.offersByAnswerer.get(offer.
|
|
466
|
+
if (offer.answererPublicKey) {
|
|
467
|
+
const answererOffers = this.offersByAnswerer.get(offer.answererPublicKey);
|
|
573
468
|
if (answererOffers) {
|
|
574
469
|
answererOffers.delete(offer.id);
|
|
575
470
|
if (answererOffers.size === 0) {
|
|
576
|
-
this.offersByAnswerer.delete(offer.
|
|
471
|
+
this.offersByAnswerer.delete(offer.answererPublicKey);
|
|
577
472
|
}
|
|
578
473
|
}
|
|
579
474
|
}
|