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