@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/postgres.ts
CHANGED
|
@@ -1,40 +1,29 @@
|
|
|
1
|
-
import { Pool
|
|
1
|
+
import { Pool } from 'pg';
|
|
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
|
* PostgreSQL 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 PostgreSQLStorage 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 PostgreSQL storage instance with connection pooling
|
|
29
23
|
* @param connectionString PostgreSQL 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<PostgreSQLStorage> {
|
|
26
|
+
static async create(connectionString: string, poolSize: number = 10): Promise<PostgreSQLStorage> {
|
|
38
27
|
const pool = new Pool({
|
|
39
28
|
connectionString,
|
|
40
29
|
max: poolSize,
|
|
@@ -42,7 +31,7 @@ export class PostgreSQLStorage implements Storage {
|
|
|
42
31
|
connectionTimeoutMillis: 5000,
|
|
43
32
|
});
|
|
44
33
|
|
|
45
|
-
const storage = new PostgreSQLStorage(pool
|
|
34
|
+
const storage = new PostgreSQLStorage(pool);
|
|
46
35
|
await storage.initializeDatabase();
|
|
47
36
|
return storage;
|
|
48
37
|
}
|
|
@@ -50,33 +39,44 @@ export class PostgreSQLStorage implements Storage {
|
|
|
50
39
|
private async initializeDatabase(): Promise<void> {
|
|
51
40
|
const client = await this.pool.connect();
|
|
52
41
|
try {
|
|
42
|
+
await client.query(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS identities (
|
|
44
|
+
public_key CHAR(64) PRIMARY KEY,
|
|
45
|
+
created_at BIGINT NOT NULL,
|
|
46
|
+
expires_at BIGINT NOT NULL,
|
|
47
|
+
last_used BIGINT NOT NULL
|
|
48
|
+
)
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_identities_expires ON identities(expires_at)`);
|
|
52
|
+
|
|
53
53
|
await client.query(`
|
|
54
54
|
CREATE TABLE IF NOT EXISTS offers (
|
|
55
55
|
id VARCHAR(64) PRIMARY KEY,
|
|
56
|
-
|
|
56
|
+
public_key CHAR(64) NOT NULL REFERENCES identities(public_key) ON DELETE CASCADE,
|
|
57
57
|
tags JSONB NOT NULL,
|
|
58
58
|
sdp TEXT NOT NULL,
|
|
59
59
|
created_at BIGINT NOT NULL,
|
|
60
60
|
expires_at BIGINT NOT NULL,
|
|
61
61
|
last_seen BIGINT NOT NULL,
|
|
62
|
-
|
|
62
|
+
answerer_public_key CHAR(64),
|
|
63
63
|
answer_sdp TEXT,
|
|
64
64
|
answered_at BIGINT,
|
|
65
65
|
matched_tags JSONB
|
|
66
66
|
)
|
|
67
67
|
`);
|
|
68
68
|
|
|
69
|
-
await client.query(`CREATE INDEX IF NOT EXISTS
|
|
69
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_public_key ON offers(public_key)`);
|
|
70
70
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at)`);
|
|
71
71
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen)`);
|
|
72
|
-
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(
|
|
72
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_public_key)`);
|
|
73
73
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_tags ON offers USING GIN(tags)`);
|
|
74
74
|
|
|
75
75
|
await client.query(`
|
|
76
76
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
77
77
|
id BIGSERIAL PRIMARY KEY,
|
|
78
78
|
offer_id VARCHAR(64) NOT NULL REFERENCES offers(id) ON DELETE CASCADE,
|
|
79
|
-
|
|
79
|
+
public_key CHAR(64) NOT NULL,
|
|
80
80
|
role VARCHAR(8) NOT NULL CHECK (role IN ('offerer', 'answerer')),
|
|
81
81
|
candidate JSONB NOT NULL,
|
|
82
82
|
created_at BIGINT NOT NULL
|
|
@@ -84,21 +84,9 @@ export class PostgreSQLStorage implements Storage {
|
|
|
84
84
|
`);
|
|
85
85
|
|
|
86
86
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id)`);
|
|
87
|
-
await client.query(`CREATE INDEX IF NOT EXISTS
|
|
87
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_ice_public_key ON ice_candidates(public_key)`);
|
|
88
88
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at)`);
|
|
89
89
|
|
|
90
|
-
await client.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
|
-
)
|
|
98
|
-
`);
|
|
99
|
-
|
|
100
|
-
await client.query(`CREATE INDEX IF NOT EXISTS idx_credentials_expires ON credentials(expires_at)`);
|
|
101
|
-
|
|
102
90
|
await client.query(`
|
|
103
91
|
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
104
92
|
identifier VARCHAR(255) PRIMARY KEY,
|
|
@@ -138,14 +126,14 @@ export class PostgreSQLStorage implements Storage {
|
|
|
138
126
|
const id = request.id || await generateOfferHash(request.sdp);
|
|
139
127
|
|
|
140
128
|
await client.query(
|
|
141
|
-
`INSERT INTO offers (id,
|
|
129
|
+
`INSERT INTO offers (id, public_key, tags, sdp, created_at, expires_at, last_seen)
|
|
142
130
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
143
|
-
[id, request.
|
|
131
|
+
[id, request.publicKey, JSON.stringify(request.tags), request.sdp, now, request.expiresAt, now]
|
|
144
132
|
);
|
|
145
133
|
|
|
146
134
|
created.push({
|
|
147
135
|
id,
|
|
148
|
-
|
|
136
|
+
publicKey: request.publicKey,
|
|
149
137
|
tags: request.tags,
|
|
150
138
|
sdp: request.sdp,
|
|
151
139
|
createdAt: now,
|
|
@@ -165,10 +153,10 @@ export class PostgreSQLStorage implements Storage {
|
|
|
165
153
|
return created;
|
|
166
154
|
}
|
|
167
155
|
|
|
168
|
-
async
|
|
156
|
+
async getOffersByPublicKey(publicKey: string): Promise<Offer[]> {
|
|
169
157
|
const result = await this.pool.query(
|
|
170
|
-
`SELECT * FROM offers WHERE
|
|
171
|
-
[
|
|
158
|
+
`SELECT * FROM offers WHERE public_key = $1 AND expires_at > $2 ORDER BY last_seen DESC`,
|
|
159
|
+
[publicKey, Date.now()]
|
|
172
160
|
);
|
|
173
161
|
return result.rows.map(row => this.rowToOffer(row));
|
|
174
162
|
}
|
|
@@ -181,10 +169,10 @@ export class PostgreSQLStorage implements Storage {
|
|
|
181
169
|
return result.rows.length > 0 ? this.rowToOffer(result.rows[0]) : null;
|
|
182
170
|
}
|
|
183
171
|
|
|
184
|
-
async deleteOffer(offerId: string,
|
|
172
|
+
async deleteOffer(offerId: string, ownerPublicKey: string): Promise<boolean> {
|
|
185
173
|
const result = await this.pool.query(
|
|
186
|
-
`DELETE FROM offers WHERE id = $1 AND
|
|
187
|
-
[offerId,
|
|
174
|
+
`DELETE FROM offers WHERE id = $1 AND public_key = $2`,
|
|
175
|
+
[offerId, ownerPublicKey]
|
|
188
176
|
);
|
|
189
177
|
return (result.rowCount ?? 0) > 0;
|
|
190
178
|
}
|
|
@@ -199,7 +187,7 @@ export class PostgreSQLStorage implements Storage {
|
|
|
199
187
|
|
|
200
188
|
async answerOffer(
|
|
201
189
|
offerId: string,
|
|
202
|
-
|
|
190
|
+
answererPublicKey: string,
|
|
203
191
|
answerSdp: string,
|
|
204
192
|
matchedTags?: string[]
|
|
205
193
|
): Promise<{ success: boolean; error?: string }> {
|
|
@@ -209,15 +197,15 @@ export class PostgreSQLStorage implements Storage {
|
|
|
209
197
|
return { success: false, error: 'Offer not found or expired' };
|
|
210
198
|
}
|
|
211
199
|
|
|
212
|
-
if (offer.
|
|
200
|
+
if (offer.answererPublicKey) {
|
|
213
201
|
return { success: false, error: 'Offer already answered' };
|
|
214
202
|
}
|
|
215
203
|
|
|
216
204
|
const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
|
|
217
205
|
const result = await this.pool.query(
|
|
218
|
-
`UPDATE offers SET
|
|
219
|
-
WHERE id = $5 AND
|
|
220
|
-
[
|
|
206
|
+
`UPDATE offers SET answerer_public_key = $1, answer_sdp = $2, answered_at = $3, matched_tags = $4
|
|
207
|
+
WHERE id = $5 AND answerer_public_key IS NULL`,
|
|
208
|
+
[answererPublicKey, answerSdp, Date.now(), matchedTagsJson, offerId]
|
|
221
209
|
);
|
|
222
210
|
|
|
223
211
|
if ((result.rowCount ?? 0) === 0) {
|
|
@@ -227,22 +215,22 @@ export class PostgreSQLStorage implements Storage {
|
|
|
227
215
|
return { success: true };
|
|
228
216
|
}
|
|
229
217
|
|
|
230
|
-
async getAnsweredOffers(
|
|
218
|
+
async getAnsweredOffers(offererPublicKey: string): Promise<Offer[]> {
|
|
231
219
|
const result = await this.pool.query(
|
|
232
220
|
`SELECT * FROM offers
|
|
233
|
-
WHERE
|
|
221
|
+
WHERE public_key = $1 AND answerer_public_key IS NOT NULL AND expires_at > $2
|
|
234
222
|
ORDER BY answered_at DESC`,
|
|
235
|
-
[
|
|
223
|
+
[offererPublicKey, Date.now()]
|
|
236
224
|
);
|
|
237
225
|
return result.rows.map(row => this.rowToOffer(row));
|
|
238
226
|
}
|
|
239
227
|
|
|
240
|
-
async getOffersAnsweredBy(
|
|
228
|
+
async getOffersAnsweredBy(answererPublicKey: string): Promise<Offer[]> {
|
|
241
229
|
const result = await this.pool.query(
|
|
242
230
|
`SELECT * FROM offers
|
|
243
|
-
WHERE
|
|
231
|
+
WHERE answerer_public_key = $1 AND expires_at > $2
|
|
244
232
|
ORDER BY answered_at DESC`,
|
|
245
|
-
[
|
|
233
|
+
[answererPublicKey, Date.now()]
|
|
246
234
|
);
|
|
247
235
|
return result.rows.map(row => this.rowToOffer(row));
|
|
248
236
|
}
|
|
@@ -251,25 +239,24 @@ export class PostgreSQLStorage implements Storage {
|
|
|
251
239
|
|
|
252
240
|
async discoverOffers(
|
|
253
241
|
tags: string[],
|
|
254
|
-
|
|
242
|
+
excludePublicKey: string | null,
|
|
255
243
|
limit: number,
|
|
256
244
|
offset: number
|
|
257
245
|
): Promise<Offer[]> {
|
|
258
246
|
if (tags.length === 0) return [];
|
|
259
247
|
|
|
260
|
-
// Use PostgreSQL's ?| operator for JSONB array overlap
|
|
261
248
|
let query = `
|
|
262
249
|
SELECT DISTINCT o.* FROM offers o
|
|
263
250
|
WHERE o.tags ?| $1
|
|
264
251
|
AND o.expires_at > $2
|
|
265
|
-
AND o.
|
|
252
|
+
AND o.answerer_public_key IS NULL
|
|
266
253
|
`;
|
|
267
254
|
const params: any[] = [tags, Date.now()];
|
|
268
255
|
let paramIndex = 3;
|
|
269
256
|
|
|
270
|
-
if (
|
|
271
|
-
query += ` AND o.
|
|
272
|
-
params.push(
|
|
257
|
+
if (excludePublicKey) {
|
|
258
|
+
query += ` AND o.public_key != $${paramIndex}`;
|
|
259
|
+
params.push(excludePublicKey);
|
|
273
260
|
paramIndex++;
|
|
274
261
|
}
|
|
275
262
|
|
|
@@ -282,7 +269,7 @@ export class PostgreSQLStorage implements Storage {
|
|
|
282
269
|
|
|
283
270
|
async getRandomOffer(
|
|
284
271
|
tags: string[],
|
|
285
|
-
|
|
272
|
+
excludePublicKey: string | null
|
|
286
273
|
): Promise<Offer | null> {
|
|
287
274
|
if (tags.length === 0) return null;
|
|
288
275
|
|
|
@@ -290,14 +277,14 @@ export class PostgreSQLStorage implements Storage {
|
|
|
290
277
|
SELECT DISTINCT o.* FROM offers o
|
|
291
278
|
WHERE o.tags ?| $1
|
|
292
279
|
AND o.expires_at > $2
|
|
293
|
-
AND o.
|
|
280
|
+
AND o.answerer_public_key IS NULL
|
|
294
281
|
`;
|
|
295
282
|
const params: any[] = [tags, Date.now()];
|
|
296
283
|
let paramIndex = 3;
|
|
297
284
|
|
|
298
|
-
if (
|
|
299
|
-
query += ` AND o.
|
|
300
|
-
params.push(
|
|
285
|
+
if (excludePublicKey) {
|
|
286
|
+
query += ` AND o.public_key != $${paramIndex}`;
|
|
287
|
+
params.push(excludePublicKey);
|
|
301
288
|
}
|
|
302
289
|
|
|
303
290
|
query += ' ORDER BY RANDOM() LIMIT 1';
|
|
@@ -310,7 +297,7 @@ export class PostgreSQLStorage implements Storage {
|
|
|
310
297
|
|
|
311
298
|
async addIceCandidates(
|
|
312
299
|
offerId: string,
|
|
313
|
-
|
|
300
|
+
publicKey: string,
|
|
314
301
|
role: 'offerer' | 'answerer',
|
|
315
302
|
candidates: any[]
|
|
316
303
|
): Promise<number> {
|
|
@@ -324,9 +311,9 @@ export class PostgreSQLStorage implements Storage {
|
|
|
324
311
|
|
|
325
312
|
for (let i = 0; i < candidates.length; i++) {
|
|
326
313
|
await client.query(
|
|
327
|
-
`INSERT INTO ice_candidates (offer_id,
|
|
314
|
+
`INSERT INTO ice_candidates (offer_id, public_key, role, candidate, created_at)
|
|
328
315
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
329
|
-
[offerId,
|
|
316
|
+
[offerId, publicKey, role, JSON.stringify(candidates[i]), baseTimestamp + i]
|
|
330
317
|
);
|
|
331
318
|
}
|
|
332
319
|
|
|
@@ -362,7 +349,7 @@ export class PostgreSQLStorage implements Storage {
|
|
|
362
349
|
|
|
363
350
|
async getIceCandidatesForMultipleOffers(
|
|
364
351
|
offerIds: string[],
|
|
365
|
-
|
|
352
|
+
publicKey: string,
|
|
366
353
|
since?: number
|
|
367
354
|
): Promise<Map<string, IceCandidate[]>> {
|
|
368
355
|
const resultMap = new Map<string, IceCandidate[]>();
|
|
@@ -373,16 +360,16 @@ export class PostgreSQLStorage implements Storage {
|
|
|
373
360
|
}
|
|
374
361
|
|
|
375
362
|
let query = `
|
|
376
|
-
SELECT ic.*, o.
|
|
363
|
+
SELECT ic.*, o.public_key as offer_public_key
|
|
377
364
|
FROM ice_candidates ic
|
|
378
365
|
INNER JOIN offers o ON o.id = ic.offer_id
|
|
379
366
|
WHERE ic.offer_id = ANY($1)
|
|
380
367
|
AND (
|
|
381
|
-
(o.
|
|
382
|
-
OR (o.
|
|
368
|
+
(o.public_key = $2 AND ic.role = 'answerer')
|
|
369
|
+
OR (o.answerer_public_key = $2 AND ic.role = 'offerer')
|
|
383
370
|
)
|
|
384
371
|
`;
|
|
385
|
-
const params: any[] = [offerIds,
|
|
372
|
+
const params: any[] = [offerIds, publicKey];
|
|
386
373
|
|
|
387
374
|
if (since !== undefined) {
|
|
388
375
|
query += ' AND ic.created_at > $3';
|
|
@@ -404,113 +391,12 @@ export class PostgreSQLStorage implements Storage {
|
|
|
404
391
|
return resultMap;
|
|
405
392
|
}
|
|
406
393
|
|
|
407
|
-
// ===== Credential Management =====
|
|
408
|
-
|
|
409
|
-
async generateCredentials(request: GenerateCredentialsRequest): Promise<Credential> {
|
|
410
|
-
const now = Date.now();
|
|
411
|
-
const expiresAt = request.expiresAt || (now + YEAR_IN_MS);
|
|
412
|
-
|
|
413
|
-
const { generateCredentialName, generateSecret, encryptSecret } = await import('../crypto.ts');
|
|
414
|
-
|
|
415
|
-
let name: string;
|
|
416
|
-
|
|
417
|
-
if (request.name) {
|
|
418
|
-
const existing = await this.pool.query(
|
|
419
|
-
`SELECT name FROM credentials WHERE name = $1`,
|
|
420
|
-
[request.name]
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
if (existing.rows.length > 0) {
|
|
424
|
-
throw new Error('Username already taken');
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
name = request.name;
|
|
428
|
-
} else {
|
|
429
|
-
let attempts = 0;
|
|
430
|
-
const maxAttempts = 100;
|
|
431
|
-
|
|
432
|
-
while (attempts < maxAttempts) {
|
|
433
|
-
name = generateCredentialName();
|
|
434
|
-
|
|
435
|
-
const existing = await this.pool.query(
|
|
436
|
-
`SELECT name FROM credentials WHERE name = $1`,
|
|
437
|
-
[name]
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
if (existing.rows.length === 0) break;
|
|
441
|
-
attempts++;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (attempts >= maxAttempts) {
|
|
445
|
-
throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const secret = generateSecret();
|
|
450
|
-
const encryptedSecret = await encryptSecret(secret, this.masterEncryptionKey);
|
|
451
|
-
|
|
452
|
-
await this.pool.query(
|
|
453
|
-
`INSERT INTO credentials (name, secret, created_at, expires_at, last_used)
|
|
454
|
-
VALUES ($1, $2, $3, $4, $5)`,
|
|
455
|
-
[name!, encryptedSecret, now, expiresAt, now]
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
name: name!,
|
|
460
|
-
secret,
|
|
461
|
-
createdAt: now,
|
|
462
|
-
expiresAt,
|
|
463
|
-
lastUsed: now,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
async getCredential(name: string): Promise<Credential | null> {
|
|
468
|
-
const result = await this.pool.query(
|
|
469
|
-
`SELECT * FROM credentials WHERE name = $1 AND expires_at > $2`,
|
|
470
|
-
[name, Date.now()]
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
if (result.rows.length === 0) return null;
|
|
474
|
-
|
|
475
|
-
try {
|
|
476
|
-
const { decryptSecret } = await import('../crypto.ts');
|
|
477
|
-
const decryptedSecret = await decryptSecret(result.rows[0].secret, this.masterEncryptionKey);
|
|
478
|
-
|
|
479
|
-
return {
|
|
480
|
-
name: result.rows[0].name,
|
|
481
|
-
secret: decryptedSecret,
|
|
482
|
-
createdAt: Number(result.rows[0].created_at),
|
|
483
|
-
expiresAt: Number(result.rows[0].expires_at),
|
|
484
|
-
lastUsed: Number(result.rows[0].last_used),
|
|
485
|
-
};
|
|
486
|
-
} catch (error) {
|
|
487
|
-
console.error(`Failed to decrypt secret for credential '${name}':`, error);
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void> {
|
|
493
|
-
await this.pool.query(
|
|
494
|
-
`UPDATE credentials SET last_used = $1, expires_at = $2 WHERE name = $3`,
|
|
495
|
-
[lastUsed, expiresAt, name]
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
async deleteExpiredCredentials(now: number): Promise<number> {
|
|
500
|
-
const result = await this.pool.query(
|
|
501
|
-
`DELETE FROM credentials WHERE expires_at < $1`,
|
|
502
|
-
[now]
|
|
503
|
-
);
|
|
504
|
-
return result.rowCount ?? 0;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
394
|
// ===== Rate Limiting =====
|
|
508
395
|
|
|
509
396
|
async checkRateLimit(identifier: string, limit: number, windowMs: number): Promise<boolean> {
|
|
510
397
|
const now = Date.now();
|
|
511
398
|
const resetTime = now + windowMs;
|
|
512
399
|
|
|
513
|
-
// Use INSERT ... ON CONFLICT for atomic upsert
|
|
514
400
|
const result = await this.pool.query(
|
|
515
401
|
`INSERT INTO rate_limits (identifier, count, reset_time)
|
|
516
402
|
VALUES ($1, 1, $2)
|
|
@@ -548,7 +434,6 @@ export class PostgreSQLStorage implements Storage {
|
|
|
548
434
|
);
|
|
549
435
|
return true;
|
|
550
436
|
} catch (error: any) {
|
|
551
|
-
// PostgreSQL unique violation error code
|
|
552
437
|
if (error.code === '23505') {
|
|
553
438
|
return false;
|
|
554
439
|
}
|
|
@@ -575,19 +460,14 @@ export class PostgreSQLStorage implements Storage {
|
|
|
575
460
|
return Number(result.rows[0].count);
|
|
576
461
|
}
|
|
577
462
|
|
|
578
|
-
async
|
|
463
|
+
async getOfferCountByPublicKey(publicKey: string): Promise<number> {
|
|
579
464
|
const result = await this.pool.query(
|
|
580
|
-
'SELECT COUNT(*) as count FROM offers WHERE
|
|
581
|
-
[
|
|
465
|
+
'SELECT COUNT(*) as count FROM offers WHERE public_key = $1',
|
|
466
|
+
[publicKey]
|
|
582
467
|
);
|
|
583
468
|
return Number(result.rows[0].count);
|
|
584
469
|
}
|
|
585
470
|
|
|
586
|
-
async getCredentialCount(): Promise<number> {
|
|
587
|
-
const result = await this.pool.query('SELECT COUNT(*) as count FROM credentials');
|
|
588
|
-
return Number(result.rows[0].count);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
471
|
async getIceCandidateCount(offerId: string): Promise<number> {
|
|
592
472
|
const result = await this.pool.query(
|
|
593
473
|
'SELECT COUNT(*) as count FROM ice_candidates WHERE offer_id = $1',
|
|
@@ -601,13 +481,13 @@ export class PostgreSQLStorage implements Storage {
|
|
|
601
481
|
private rowToOffer(row: any): Offer {
|
|
602
482
|
return {
|
|
603
483
|
id: row.id,
|
|
604
|
-
|
|
484
|
+
publicKey: row.public_key.trim(),
|
|
605
485
|
tags: typeof row.tags === 'string' ? JSON.parse(row.tags) : row.tags,
|
|
606
486
|
sdp: row.sdp,
|
|
607
487
|
createdAt: Number(row.created_at),
|
|
608
488
|
expiresAt: Number(row.expires_at),
|
|
609
489
|
lastSeen: Number(row.last_seen),
|
|
610
|
-
|
|
490
|
+
answererPublicKey: row.answerer_public_key?.trim() || undefined,
|
|
611
491
|
answerSdp: row.answer_sdp || undefined,
|
|
612
492
|
answeredAt: row.answered_at ? Number(row.answered_at) : undefined,
|
|
613
493
|
matchedTags: row.matched_tags || undefined,
|
|
@@ -618,7 +498,7 @@ export class PostgreSQLStorage implements Storage {
|
|
|
618
498
|
return {
|
|
619
499
|
id: Number(row.id),
|
|
620
500
|
offerId: row.offer_id,
|
|
621
|
-
|
|
501
|
+
publicKey: row.public_key.trim(),
|
|
622
502
|
role: row.role as 'offerer' | 'answerer',
|
|
623
503
|
candidate: typeof row.candidate === 'string' ? JSON.parse(row.candidate) : row.candidate,
|
|
624
504
|
createdAt: Number(row.created_at),
|