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