@xtr-dev/rondevu-server 0.5.12 → 0.5.13
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 +8 -1
- package/dist/index.js.map +2 -2
- 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/src/storage/mysql.ts
CHANGED
|
@@ -1,40 +1,29 @@
|
|
|
1
|
-
import mysql, { Pool,
|
|
1
|
+
import mysql, { Pool, RowDataPacket, ResultSetHeader } from 'mysql2/promise';
|
|
2
2
|
import {
|
|
3
3
|
Storage,
|
|
4
4
|
Offer,
|
|
5
5
|
IceCandidate,
|
|
6
6
|
CreateOfferRequest,
|
|
7
|
-
Credential,
|
|
8
|
-
GenerateCredentialsRequest,
|
|
9
7
|
} from './types.ts';
|
|
10
8
|
import { generateOfferHash } from './hash-id.ts';
|
|
11
9
|
|
|
12
|
-
const YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000;
|
|
13
|
-
|
|
14
10
|
/**
|
|
15
11
|
* MySQL storage adapter for rondevu signaling system
|
|
16
|
-
* Uses
|
|
12
|
+
* Uses Ed25519 public key as identity (no usernames, no secrets)
|
|
17
13
|
*/
|
|
18
14
|
export class MySQLStorage implements Storage {
|
|
19
15
|
private pool: Pool;
|
|
20
|
-
private masterEncryptionKey: string;
|
|
21
16
|
|
|
22
|
-
private constructor(pool: Pool
|
|
17
|
+
private constructor(pool: Pool) {
|
|
23
18
|
this.pool = pool;
|
|
24
|
-
this.masterEncryptionKey = masterEncryptionKey;
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
/**
|
|
28
22
|
* Creates a new MySQL storage instance with connection pooling
|
|
29
23
|
* @param connectionString MySQL connection URL
|
|
30
|
-
* @param masterEncryptionKey 64-char hex string for encrypting secrets
|
|
31
24
|
* @param poolSize Maximum number of connections in the pool
|
|
32
25
|
*/
|
|
33
|
-
static async create(
|
|
34
|
-
connectionString: string,
|
|
35
|
-
masterEncryptionKey: string,
|
|
36
|
-
poolSize: number = 10
|
|
37
|
-
): Promise<MySQLStorage> {
|
|
26
|
+
static async create(connectionString: string, poolSize: number = 10): Promise<MySQLStorage> {
|
|
38
27
|
const pool = mysql.createPool({
|
|
39
28
|
uri: connectionString,
|
|
40
29
|
waitForConnections: true,
|
|
@@ -44,7 +33,7 @@ export class MySQLStorage implements Storage {
|
|
|
44
33
|
keepAliveInitialDelay: 10000,
|
|
45
34
|
});
|
|
46
35
|
|
|
47
|
-
const storage = new MySQLStorage(pool
|
|
36
|
+
const storage = new MySQLStorage(pool);
|
|
48
37
|
await storage.initializeDatabase();
|
|
49
38
|
return storage;
|
|
50
39
|
}
|
|
@@ -52,23 +41,34 @@ export class MySQLStorage implements Storage {
|
|
|
52
41
|
private async initializeDatabase(): Promise<void> {
|
|
53
42
|
const conn = await this.pool.getConnection();
|
|
54
43
|
try {
|
|
44
|
+
await conn.query(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS identities (
|
|
46
|
+
public_key CHAR(64) PRIMARY KEY,
|
|
47
|
+
created_at BIGINT NOT NULL,
|
|
48
|
+
expires_at BIGINT NOT NULL,
|
|
49
|
+
last_used BIGINT NOT NULL,
|
|
50
|
+
INDEX idx_identities_expires (expires_at)
|
|
51
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
52
|
+
`);
|
|
53
|
+
|
|
55
54
|
await conn.query(`
|
|
56
55
|
CREATE TABLE IF NOT EXISTS offers (
|
|
57
56
|
id VARCHAR(64) PRIMARY KEY,
|
|
58
|
-
|
|
57
|
+
public_key CHAR(64) NOT NULL,
|
|
59
58
|
tags JSON NOT NULL,
|
|
60
59
|
sdp MEDIUMTEXT NOT NULL,
|
|
61
60
|
created_at BIGINT NOT NULL,
|
|
62
61
|
expires_at BIGINT NOT NULL,
|
|
63
62
|
last_seen BIGINT NOT NULL,
|
|
64
|
-
|
|
63
|
+
answerer_public_key CHAR(64),
|
|
65
64
|
answer_sdp MEDIUMTEXT,
|
|
66
65
|
answered_at BIGINT,
|
|
67
66
|
matched_tags JSON,
|
|
68
|
-
INDEX
|
|
67
|
+
INDEX idx_offers_public_key (public_key),
|
|
69
68
|
INDEX idx_offers_expires (expires_at),
|
|
70
69
|
INDEX idx_offers_last_seen (last_seen),
|
|
71
|
-
INDEX idx_offers_answerer (
|
|
70
|
+
INDEX idx_offers_answerer (answerer_public_key),
|
|
71
|
+
FOREIGN KEY (public_key) REFERENCES identities(public_key) ON DELETE CASCADE
|
|
72
72
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
73
73
|
`);
|
|
74
74
|
|
|
@@ -76,28 +76,17 @@ export class MySQLStorage implements Storage {
|
|
|
76
76
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
77
77
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
78
78
|
offer_id VARCHAR(64) NOT NULL,
|
|
79
|
-
|
|
79
|
+
public_key CHAR(64) NOT NULL,
|
|
80
80
|
role ENUM('offerer', 'answerer') NOT NULL,
|
|
81
81
|
candidate JSON NOT NULL,
|
|
82
82
|
created_at BIGINT NOT NULL,
|
|
83
83
|
INDEX idx_ice_offer (offer_id),
|
|
84
|
-
INDEX
|
|
84
|
+
INDEX idx_ice_public_key (public_key),
|
|
85
85
|
INDEX idx_ice_created (created_at),
|
|
86
86
|
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
87
87
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
88
88
|
`);
|
|
89
89
|
|
|
90
|
-
await conn.query(`
|
|
91
|
-
CREATE TABLE IF NOT EXISTS credentials (
|
|
92
|
-
name VARCHAR(32) PRIMARY KEY,
|
|
93
|
-
secret VARCHAR(512) NOT NULL UNIQUE,
|
|
94
|
-
created_at BIGINT NOT NULL,
|
|
95
|
-
expires_at BIGINT NOT NULL,
|
|
96
|
-
last_used BIGINT NOT NULL,
|
|
97
|
-
INDEX idx_credentials_expires (expires_at)
|
|
98
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
99
|
-
`);
|
|
100
|
-
|
|
101
90
|
await conn.query(`
|
|
102
91
|
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
103
92
|
identifier VARCHAR(255) PRIMARY KEY,
|
|
@@ -135,14 +124,14 @@ export class MySQLStorage implements Storage {
|
|
|
135
124
|
const id = request.id || await generateOfferHash(request.sdp);
|
|
136
125
|
|
|
137
126
|
await conn.query(
|
|
138
|
-
`INSERT INTO offers (id,
|
|
127
|
+
`INSERT INTO offers (id, public_key, tags, sdp, created_at, expires_at, last_seen)
|
|
139
128
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
140
|
-
[id, request.
|
|
129
|
+
[id, request.publicKey, JSON.stringify(request.tags), request.sdp, now, request.expiresAt, now]
|
|
141
130
|
);
|
|
142
131
|
|
|
143
132
|
created.push({
|
|
144
133
|
id,
|
|
145
|
-
|
|
134
|
+
publicKey: request.publicKey,
|
|
146
135
|
tags: request.tags,
|
|
147
136
|
sdp: request.sdp,
|
|
148
137
|
createdAt: now,
|
|
@@ -162,10 +151,10 @@ export class MySQLStorage implements Storage {
|
|
|
162
151
|
return created;
|
|
163
152
|
}
|
|
164
153
|
|
|
165
|
-
async
|
|
154
|
+
async getOffersByPublicKey(publicKey: string): Promise<Offer[]> {
|
|
166
155
|
const [rows] = await this.pool.query<RowDataPacket[]>(
|
|
167
|
-
`SELECT * FROM offers WHERE
|
|
168
|
-
[
|
|
156
|
+
`SELECT * FROM offers WHERE public_key = ? AND expires_at > ? ORDER BY last_seen DESC`,
|
|
157
|
+
[publicKey, Date.now()]
|
|
169
158
|
);
|
|
170
159
|
return rows.map(row => this.rowToOffer(row));
|
|
171
160
|
}
|
|
@@ -178,10 +167,10 @@ export class MySQLStorage implements Storage {
|
|
|
178
167
|
return rows.length > 0 ? this.rowToOffer(rows[0]) : null;
|
|
179
168
|
}
|
|
180
169
|
|
|
181
|
-
async deleteOffer(offerId: string,
|
|
170
|
+
async deleteOffer(offerId: string, ownerPublicKey: string): Promise<boolean> {
|
|
182
171
|
const [result] = await this.pool.query<ResultSetHeader>(
|
|
183
|
-
`DELETE FROM offers WHERE id = ? AND
|
|
184
|
-
[offerId,
|
|
172
|
+
`DELETE FROM offers WHERE id = ? AND public_key = ?`,
|
|
173
|
+
[offerId, ownerPublicKey]
|
|
185
174
|
);
|
|
186
175
|
return result.affectedRows > 0;
|
|
187
176
|
}
|
|
@@ -196,7 +185,7 @@ export class MySQLStorage implements Storage {
|
|
|
196
185
|
|
|
197
186
|
async answerOffer(
|
|
198
187
|
offerId: string,
|
|
199
|
-
|
|
188
|
+
answererPublicKey: string,
|
|
200
189
|
answerSdp: string,
|
|
201
190
|
matchedTags?: string[]
|
|
202
191
|
): Promise<{ success: boolean; error?: string }> {
|
|
@@ -206,15 +195,15 @@ export class MySQLStorage implements Storage {
|
|
|
206
195
|
return { success: false, error: 'Offer not found or expired' };
|
|
207
196
|
}
|
|
208
197
|
|
|
209
|
-
if (offer.
|
|
198
|
+
if (offer.answererPublicKey) {
|
|
210
199
|
return { success: false, error: 'Offer already answered' };
|
|
211
200
|
}
|
|
212
201
|
|
|
213
202
|
const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
|
|
214
203
|
const [result] = await this.pool.query<ResultSetHeader>(
|
|
215
|
-
`UPDATE offers SET
|
|
216
|
-
WHERE id = ? AND
|
|
217
|
-
[
|
|
204
|
+
`UPDATE offers SET answerer_public_key = ?, answer_sdp = ?, answered_at = ?, matched_tags = ?
|
|
205
|
+
WHERE id = ? AND answerer_public_key IS NULL`,
|
|
206
|
+
[answererPublicKey, answerSdp, Date.now(), matchedTagsJson, offerId]
|
|
218
207
|
);
|
|
219
208
|
|
|
220
209
|
if (result.affectedRows === 0) {
|
|
@@ -224,22 +213,22 @@ export class MySQLStorage implements Storage {
|
|
|
224
213
|
return { success: true };
|
|
225
214
|
}
|
|
226
215
|
|
|
227
|
-
async getAnsweredOffers(
|
|
216
|
+
async getAnsweredOffers(offererPublicKey: string): Promise<Offer[]> {
|
|
228
217
|
const [rows] = await this.pool.query<RowDataPacket[]>(
|
|
229
218
|
`SELECT * FROM offers
|
|
230
|
-
WHERE
|
|
219
|
+
WHERE public_key = ? AND answerer_public_key IS NOT NULL AND expires_at > ?
|
|
231
220
|
ORDER BY answered_at DESC`,
|
|
232
|
-
[
|
|
221
|
+
[offererPublicKey, Date.now()]
|
|
233
222
|
);
|
|
234
223
|
return rows.map(row => this.rowToOffer(row));
|
|
235
224
|
}
|
|
236
225
|
|
|
237
|
-
async getOffersAnsweredBy(
|
|
226
|
+
async getOffersAnsweredBy(answererPublicKey: string): Promise<Offer[]> {
|
|
238
227
|
const [rows] = await this.pool.query<RowDataPacket[]>(
|
|
239
228
|
`SELECT * FROM offers
|
|
240
|
-
WHERE
|
|
229
|
+
WHERE answerer_public_key = ? AND expires_at > ?
|
|
241
230
|
ORDER BY answered_at DESC`,
|
|
242
|
-
[
|
|
231
|
+
[answererPublicKey, Date.now()]
|
|
243
232
|
);
|
|
244
233
|
return rows.map(row => this.rowToOffer(row));
|
|
245
234
|
}
|
|
@@ -248,27 +237,25 @@ export class MySQLStorage implements Storage {
|
|
|
248
237
|
|
|
249
238
|
async discoverOffers(
|
|
250
239
|
tags: string[],
|
|
251
|
-
|
|
240
|
+
excludePublicKey: string | null,
|
|
252
241
|
limit: number,
|
|
253
242
|
offset: number
|
|
254
243
|
): Promise<Offer[]> {
|
|
255
244
|
if (tags.length === 0) return [];
|
|
256
245
|
|
|
257
|
-
// Use JSON_OVERLAPS for efficient tag matching (MySQL 8.0.17+)
|
|
258
|
-
// Falls back to JSON_CONTAINS for each tag with OR logic
|
|
259
246
|
const tagArray = JSON.stringify(tags);
|
|
260
247
|
|
|
261
248
|
let query = `
|
|
262
249
|
SELECT DISTINCT o.* FROM offers o
|
|
263
250
|
WHERE JSON_OVERLAPS(o.tags, ?)
|
|
264
251
|
AND o.expires_at > ?
|
|
265
|
-
AND o.
|
|
252
|
+
AND o.answerer_public_key IS NULL
|
|
266
253
|
`;
|
|
267
254
|
const params: any[] = [tagArray, Date.now()];
|
|
268
255
|
|
|
269
|
-
if (
|
|
270
|
-
query += ' AND o.
|
|
271
|
-
params.push(
|
|
256
|
+
if (excludePublicKey) {
|
|
257
|
+
query += ' AND o.public_key != ?';
|
|
258
|
+
params.push(excludePublicKey);
|
|
272
259
|
}
|
|
273
260
|
|
|
274
261
|
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
|
|
@@ -280,7 +267,7 @@ export class MySQLStorage implements Storage {
|
|
|
280
267
|
|
|
281
268
|
async getRandomOffer(
|
|
282
269
|
tags: string[],
|
|
283
|
-
|
|
270
|
+
excludePublicKey: string | null
|
|
284
271
|
): Promise<Offer | null> {
|
|
285
272
|
if (tags.length === 0) return null;
|
|
286
273
|
|
|
@@ -290,13 +277,13 @@ export class MySQLStorage implements Storage {
|
|
|
290
277
|
SELECT DISTINCT o.* FROM offers o
|
|
291
278
|
WHERE JSON_OVERLAPS(o.tags, ?)
|
|
292
279
|
AND o.expires_at > ?
|
|
293
|
-
AND o.
|
|
280
|
+
AND o.answerer_public_key IS NULL
|
|
294
281
|
`;
|
|
295
282
|
const params: any[] = [tagArray, Date.now()];
|
|
296
283
|
|
|
297
|
-
if (
|
|
298
|
-
query += ' AND o.
|
|
299
|
-
params.push(
|
|
284
|
+
if (excludePublicKey) {
|
|
285
|
+
query += ' AND o.public_key != ?';
|
|
286
|
+
params.push(excludePublicKey);
|
|
300
287
|
}
|
|
301
288
|
|
|
302
289
|
query += ' ORDER BY RAND() LIMIT 1';
|
|
@@ -309,7 +296,7 @@ export class MySQLStorage implements Storage {
|
|
|
309
296
|
|
|
310
297
|
async addIceCandidates(
|
|
311
298
|
offerId: string,
|
|
312
|
-
|
|
299
|
+
publicKey: string,
|
|
313
300
|
role: 'offerer' | 'answerer',
|
|
314
301
|
candidates: any[]
|
|
315
302
|
): Promise<number> {
|
|
@@ -318,14 +305,14 @@ export class MySQLStorage implements Storage {
|
|
|
318
305
|
const baseTimestamp = Date.now();
|
|
319
306
|
const values = candidates.map((c, i) => [
|
|
320
307
|
offerId,
|
|
321
|
-
|
|
308
|
+
publicKey,
|
|
322
309
|
role,
|
|
323
310
|
JSON.stringify(c),
|
|
324
311
|
baseTimestamp + i,
|
|
325
312
|
]);
|
|
326
313
|
|
|
327
314
|
await this.pool.query(
|
|
328
|
-
`INSERT INTO ice_candidates (offer_id,
|
|
315
|
+
`INSERT INTO ice_candidates (offer_id, public_key, role, candidate, created_at)
|
|
329
316
|
VALUES ?`,
|
|
330
317
|
[values]
|
|
331
318
|
);
|
|
@@ -354,7 +341,7 @@ export class MySQLStorage implements Storage {
|
|
|
354
341
|
|
|
355
342
|
async getIceCandidatesForMultipleOffers(
|
|
356
343
|
offerIds: string[],
|
|
357
|
-
|
|
344
|
+
publicKey: string,
|
|
358
345
|
since?: number
|
|
359
346
|
): Promise<Map<string, IceCandidate[]>> {
|
|
360
347
|
const result = new Map<string, IceCandidate[]>();
|
|
@@ -367,16 +354,16 @@ export class MySQLStorage implements Storage {
|
|
|
367
354
|
const placeholders = offerIds.map(() => '?').join(',');
|
|
368
355
|
|
|
369
356
|
let query = `
|
|
370
|
-
SELECT ic.*, o.
|
|
357
|
+
SELECT ic.*, o.public_key as offer_public_key
|
|
371
358
|
FROM ice_candidates ic
|
|
372
359
|
INNER JOIN offers o ON o.id = ic.offer_id
|
|
373
360
|
WHERE ic.offer_id IN (${placeholders})
|
|
374
361
|
AND (
|
|
375
|
-
(o.
|
|
376
|
-
OR (o.
|
|
362
|
+
(o.public_key = ? AND ic.role = 'answerer')
|
|
363
|
+
OR (o.answerer_public_key = ? AND ic.role = 'offerer')
|
|
377
364
|
)
|
|
378
365
|
`;
|
|
379
|
-
const params: any[] = [...offerIds,
|
|
366
|
+
const params: any[] = [...offerIds, publicKey, publicKey];
|
|
380
367
|
|
|
381
368
|
if (since !== undefined) {
|
|
382
369
|
query += ' AND ic.created_at > ?';
|
|
@@ -398,113 +385,12 @@ export class MySQLStorage implements Storage {
|
|
|
398
385
|
return result;
|
|
399
386
|
}
|
|
400
387
|
|
|
401
|
-
// ===== Credential Management =====
|
|
402
|
-
|
|
403
|
-
async generateCredentials(request: GenerateCredentialsRequest): Promise<Credential> {
|
|
404
|
-
const now = Date.now();
|
|
405
|
-
const expiresAt = request.expiresAt || (now + YEAR_IN_MS);
|
|
406
|
-
|
|
407
|
-
const { generateCredentialName, generateSecret, encryptSecret } = await import('../crypto.ts');
|
|
408
|
-
|
|
409
|
-
let name: string;
|
|
410
|
-
|
|
411
|
-
if (request.name) {
|
|
412
|
-
const [existing] = await this.pool.query<RowDataPacket[]>(
|
|
413
|
-
`SELECT name FROM credentials WHERE name = ?`,
|
|
414
|
-
[request.name]
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
if (existing.length > 0) {
|
|
418
|
-
throw new Error('Username already taken');
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
name = request.name;
|
|
422
|
-
} else {
|
|
423
|
-
let attempts = 0;
|
|
424
|
-
const maxAttempts = 100;
|
|
425
|
-
|
|
426
|
-
while (attempts < maxAttempts) {
|
|
427
|
-
name = generateCredentialName();
|
|
428
|
-
|
|
429
|
-
const [existing] = await this.pool.query<RowDataPacket[]>(
|
|
430
|
-
`SELECT name FROM credentials WHERE name = ?`,
|
|
431
|
-
[name]
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
if (existing.length === 0) break;
|
|
435
|
-
attempts++;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (attempts >= maxAttempts) {
|
|
439
|
-
throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const secret = generateSecret();
|
|
444
|
-
const encryptedSecret = await encryptSecret(secret, this.masterEncryptionKey);
|
|
445
|
-
|
|
446
|
-
await this.pool.query(
|
|
447
|
-
`INSERT INTO credentials (name, secret, created_at, expires_at, last_used)
|
|
448
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
449
|
-
[name!, encryptedSecret, now, expiresAt, now]
|
|
450
|
-
);
|
|
451
|
-
|
|
452
|
-
return {
|
|
453
|
-
name: name!,
|
|
454
|
-
secret,
|
|
455
|
-
createdAt: now,
|
|
456
|
-
expiresAt,
|
|
457
|
-
lastUsed: now,
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
async getCredential(name: string): Promise<Credential | null> {
|
|
462
|
-
const [rows] = await this.pool.query<RowDataPacket[]>(
|
|
463
|
-
`SELECT * FROM credentials WHERE name = ? AND expires_at > ?`,
|
|
464
|
-
[name, Date.now()]
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
if (rows.length === 0) return null;
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
const { decryptSecret } = await import('../crypto.ts');
|
|
471
|
-
const decryptedSecret = await decryptSecret(rows[0].secret, this.masterEncryptionKey);
|
|
472
|
-
|
|
473
|
-
return {
|
|
474
|
-
name: rows[0].name,
|
|
475
|
-
secret: decryptedSecret,
|
|
476
|
-
createdAt: Number(rows[0].created_at),
|
|
477
|
-
expiresAt: Number(rows[0].expires_at),
|
|
478
|
-
lastUsed: Number(rows[0].last_used),
|
|
479
|
-
};
|
|
480
|
-
} catch (error) {
|
|
481
|
-
console.error(`Failed to decrypt secret for credential '${name}':`, error);
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
async updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void> {
|
|
487
|
-
await this.pool.query(
|
|
488
|
-
`UPDATE credentials SET last_used = ?, expires_at = ? WHERE name = ?`,
|
|
489
|
-
[lastUsed, expiresAt, name]
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async deleteExpiredCredentials(now: number): Promise<number> {
|
|
494
|
-
const [result] = await this.pool.query<ResultSetHeader>(
|
|
495
|
-
`DELETE FROM credentials WHERE expires_at < ?`,
|
|
496
|
-
[now]
|
|
497
|
-
);
|
|
498
|
-
return result.affectedRows;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
388
|
// ===== Rate Limiting =====
|
|
502
389
|
|
|
503
390
|
async checkRateLimit(identifier: string, limit: number, windowMs: number): Promise<boolean> {
|
|
504
391
|
const now = Date.now();
|
|
505
392
|
const resetTime = now + windowMs;
|
|
506
393
|
|
|
507
|
-
// Use INSERT ... ON DUPLICATE KEY UPDATE for atomic upsert
|
|
508
394
|
await this.pool.query(
|
|
509
395
|
`INSERT INTO rate_limits (identifier, count, reset_time)
|
|
510
396
|
VALUES (?, 1, ?)
|
|
@@ -514,7 +400,6 @@ export class MySQLStorage implements Storage {
|
|
|
514
400
|
[identifier, resetTime, now, now, resetTime]
|
|
515
401
|
);
|
|
516
402
|
|
|
517
|
-
// Get current count
|
|
518
403
|
const [rows] = await this.pool.query<RowDataPacket[]>(
|
|
519
404
|
`SELECT count FROM rate_limits WHERE identifier = ?`,
|
|
520
405
|
[identifier]
|
|
@@ -541,7 +426,6 @@ export class MySQLStorage implements Storage {
|
|
|
541
426
|
);
|
|
542
427
|
return true;
|
|
543
428
|
} catch (error: any) {
|
|
544
|
-
// MySQL duplicate key error code
|
|
545
429
|
if (error.code === 'ER_DUP_ENTRY') {
|
|
546
430
|
return false;
|
|
547
431
|
}
|
|
@@ -568,19 +452,14 @@ export class MySQLStorage implements Storage {
|
|
|
568
452
|
return Number(rows[0].count);
|
|
569
453
|
}
|
|
570
454
|
|
|
571
|
-
async
|
|
455
|
+
async getOfferCountByPublicKey(publicKey: string): Promise<number> {
|
|
572
456
|
const [rows] = await this.pool.query<RowDataPacket[]>(
|
|
573
|
-
'SELECT COUNT(*) as count FROM offers WHERE
|
|
574
|
-
[
|
|
457
|
+
'SELECT COUNT(*) as count FROM offers WHERE public_key = ?',
|
|
458
|
+
[publicKey]
|
|
575
459
|
);
|
|
576
460
|
return Number(rows[0].count);
|
|
577
461
|
}
|
|
578
462
|
|
|
579
|
-
async getCredentialCount(): Promise<number> {
|
|
580
|
-
const [rows] = await this.pool.query<RowDataPacket[]>('SELECT COUNT(*) as count FROM credentials');
|
|
581
|
-
return Number(rows[0].count);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
463
|
async getIceCandidateCount(offerId: string): Promise<number> {
|
|
585
464
|
const [rows] = await this.pool.query<RowDataPacket[]>(
|
|
586
465
|
'SELECT COUNT(*) as count FROM ice_candidates WHERE offer_id = ?',
|
|
@@ -594,13 +473,13 @@ export class MySQLStorage implements Storage {
|
|
|
594
473
|
private rowToOffer(row: RowDataPacket): Offer {
|
|
595
474
|
return {
|
|
596
475
|
id: row.id,
|
|
597
|
-
|
|
476
|
+
publicKey: row.public_key,
|
|
598
477
|
tags: typeof row.tags === 'string' ? JSON.parse(row.tags) : row.tags,
|
|
599
478
|
sdp: row.sdp,
|
|
600
479
|
createdAt: Number(row.created_at),
|
|
601
480
|
expiresAt: Number(row.expires_at),
|
|
602
481
|
lastSeen: Number(row.last_seen),
|
|
603
|
-
|
|
482
|
+
answererPublicKey: row.answerer_public_key || undefined,
|
|
604
483
|
answerSdp: row.answer_sdp || undefined,
|
|
605
484
|
answeredAt: row.answered_at ? Number(row.answered_at) : undefined,
|
|
606
485
|
matchedTags: row.matched_tags ? (typeof row.matched_tags === 'string' ? JSON.parse(row.matched_tags) : row.matched_tags) : undefined,
|
|
@@ -611,7 +490,7 @@ export class MySQLStorage implements Storage {
|
|
|
611
490
|
return {
|
|
612
491
|
id: Number(row.id),
|
|
613
492
|
offerId: row.offer_id,
|
|
614
|
-
|
|
493
|
+
publicKey: row.public_key,
|
|
615
494
|
role: row.role as 'offerer' | 'answerer',
|
|
616
495
|
candidate: typeof row.candidate === 'string' ? JSON.parse(row.candidate) : row.candidate,
|
|
617
496
|
createdAt: Number(row.created_at),
|