@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/rpc.ts
CHANGED
|
@@ -3,45 +3,25 @@ import { Storage } from './storage/types.ts';
|
|
|
3
3
|
import { Config } from './config.ts';
|
|
4
4
|
import {
|
|
5
5
|
validateTags,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
validatePublicKey,
|
|
7
|
+
verifyEd25519Signature,
|
|
8
8
|
buildSignatureMessage,
|
|
9
9
|
} from './crypto.ts';
|
|
10
10
|
|
|
11
11
|
// Constants (non-configurable)
|
|
12
12
|
const MAX_PAGE_SIZE = 100;
|
|
13
|
-
|
|
14
|
-
// NOTE: MAX_SDP_SIZE, MAX_CANDIDATE_SIZE, MAX_CANDIDATE_DEPTH, and MAX_CANDIDATES_PER_REQUEST
|
|
15
|
-
// are now configurable via environment variables (see config.ts)
|
|
16
|
-
|
|
17
|
-
// ===== Rate Limiting =====
|
|
18
|
-
|
|
19
|
-
// Rate limiting windows (these are fixed, limits come from config)
|
|
20
|
-
// NOTE: Uses fixed-window rate limiting with full window reset on expiry
|
|
21
|
-
// - Window starts on first request and expires after window duration
|
|
22
|
-
// - When window expires, counter resets to 0 and new window starts
|
|
23
|
-
// - This is simpler than sliding windows but may allow bursts at window boundaries
|
|
24
|
-
const CREDENTIAL_RATE_WINDOW = 1000; // 1 second in milliseconds
|
|
25
13
|
const REQUEST_RATE_WINDOW = 1000; // 1 second in milliseconds
|
|
26
14
|
|
|
27
15
|
/**
|
|
28
16
|
* Check JSON object depth to prevent stack overflow from deeply nested objects
|
|
29
|
-
* CRITICAL: Checks depth BEFORE recursing to prevent stack overflow
|
|
30
|
-
* @param obj Object to check
|
|
31
|
-
* @param maxDepth Maximum allowed depth
|
|
32
|
-
* @param currentDepth Current recursion depth
|
|
33
|
-
* @returns Actual depth of the object (returns maxDepth + 1 if exceeded)
|
|
34
17
|
*/
|
|
35
18
|
function getJsonDepth(obj: any, maxDepth: number, currentDepth = 0): number {
|
|
36
|
-
// Check for primitives/null first
|
|
37
19
|
if (obj === null || typeof obj !== 'object') {
|
|
38
20
|
return currentDepth;
|
|
39
21
|
}
|
|
40
22
|
|
|
41
|
-
// CRITICAL: Check depth BEFORE recursing to prevent stack overflow
|
|
42
|
-
// If we're already at max depth, don't recurse further
|
|
43
23
|
if (currentDepth >= maxDepth) {
|
|
44
|
-
return currentDepth + 1;
|
|
24
|
+
return currentDepth + 1;
|
|
45
25
|
}
|
|
46
26
|
|
|
47
27
|
let maxChildDepth = currentDepth;
|
|
@@ -50,7 +30,6 @@ function getJsonDepth(obj: any, maxDepth: number, currentDepth = 0): number {
|
|
|
50
30
|
const childDepth = getJsonDepth(obj[key], maxDepth, currentDepth + 1);
|
|
51
31
|
maxChildDepth = Math.max(maxChildDepth, childDepth);
|
|
52
32
|
|
|
53
|
-
// Early exit if exceeded
|
|
54
33
|
if (maxChildDepth > maxDepth) {
|
|
55
34
|
return maxChildDepth;
|
|
56
35
|
}
|
|
@@ -62,7 +41,6 @@ function getJsonDepth(obj: any, maxDepth: number, currentDepth = 0): number {
|
|
|
62
41
|
|
|
63
42
|
/**
|
|
64
43
|
* Validate parameter is a non-empty string
|
|
65
|
-
* Prevents type coercion issues and injection attacks
|
|
66
44
|
*/
|
|
67
45
|
function validateStringParam(value: any, paramName: string): void {
|
|
68
46
|
if (typeof value !== 'string' || value.length === 0) {
|
|
@@ -79,7 +57,7 @@ export const ErrorCodes = {
|
|
|
79
57
|
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
|
|
80
58
|
|
|
81
59
|
// Validation errors
|
|
82
|
-
|
|
60
|
+
INVALID_PUBLIC_KEY: 'INVALID_PUBLIC_KEY',
|
|
83
61
|
INVALID_TAG: 'INVALID_TAG',
|
|
84
62
|
INVALID_SDP: 'INVALID_SDP',
|
|
85
63
|
INVALID_PARAMS: 'INVALID_PARAMS',
|
|
@@ -144,11 +122,6 @@ export interface RpcResponse {
|
|
|
144
122
|
/**
|
|
145
123
|
* RPC Method Parameter Interfaces
|
|
146
124
|
*/
|
|
147
|
-
export interface GenerateCredentialsParams {
|
|
148
|
-
name?: string; // Optional: claim specific username (4-32 chars, alphanumeric + dashes + periods)
|
|
149
|
-
expiresAt?: number;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
125
|
export interface DiscoverParams {
|
|
153
126
|
tags: string[];
|
|
154
127
|
limit?: number;
|
|
@@ -168,6 +141,7 @@ export interface DeleteOfferParams {
|
|
|
168
141
|
export interface AnswerOfferParams {
|
|
169
142
|
offerId: string;
|
|
170
143
|
sdp: string;
|
|
144
|
+
matchedTags?: string[];
|
|
171
145
|
}
|
|
172
146
|
|
|
173
147
|
export interface GetOfferAnswerParams {
|
|
@@ -190,11 +164,10 @@ export interface GetIceCandidatesParams {
|
|
|
190
164
|
|
|
191
165
|
/**
|
|
192
166
|
* RPC method handler
|
|
193
|
-
* Generic type parameter allows individual handlers to specify their param types
|
|
194
167
|
*/
|
|
195
168
|
type RpcHandler<TParams = any> = (
|
|
196
169
|
params: TParams,
|
|
197
|
-
|
|
170
|
+
publicKey: string,
|
|
198
171
|
timestamp: number,
|
|
199
172
|
signature: string,
|
|
200
173
|
storage: Storage,
|
|
@@ -204,28 +177,25 @@ type RpcHandler<TParams = any> = (
|
|
|
204
177
|
|
|
205
178
|
/**
|
|
206
179
|
* Validate timestamp for replay attack prevention
|
|
207
|
-
* Throws RpcError if timestamp is invalid
|
|
208
180
|
*/
|
|
209
181
|
function validateTimestamp(timestamp: number, config: Config): void {
|
|
210
182
|
const now = Date.now();
|
|
211
183
|
|
|
212
|
-
// Check if timestamp is too old (replay attack)
|
|
213
184
|
if (now - timestamp > config.timestampMaxAge) {
|
|
214
185
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'Timestamp too old');
|
|
215
186
|
}
|
|
216
187
|
|
|
217
|
-
// Check if timestamp is too far in future (clock skew)
|
|
218
188
|
if (timestamp - now > config.timestampMaxFuture) {
|
|
219
189
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'Timestamp too far in future');
|
|
220
190
|
}
|
|
221
191
|
}
|
|
222
192
|
|
|
223
193
|
/**
|
|
224
|
-
* Verify request signature
|
|
225
|
-
*
|
|
194
|
+
* Verify request signature using Ed25519
|
|
195
|
+
* Stateless verification - no identity registration required
|
|
226
196
|
*/
|
|
227
197
|
async function verifyRequestSignature(
|
|
228
|
-
|
|
198
|
+
publicKey: string,
|
|
229
199
|
timestamp: number,
|
|
230
200
|
nonce: string,
|
|
231
201
|
signature: string,
|
|
@@ -237,143 +207,40 @@ async function verifyRequestSignature(
|
|
|
237
207
|
// Validate timestamp first
|
|
238
208
|
validateTimestamp(timestamp, config);
|
|
239
209
|
|
|
240
|
-
//
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
243
|
-
throw new RpcError(ErrorCodes.
|
|
210
|
+
// Validate public key format
|
|
211
|
+
const pkValidation = validatePublicKey(publicKey);
|
|
212
|
+
if (!pkValidation.valid) {
|
|
213
|
+
throw new RpcError(ErrorCodes.INVALID_PUBLIC_KEY, pkValidation.error || 'Invalid public key');
|
|
244
214
|
}
|
|
245
215
|
|
|
246
|
-
// Build message and verify
|
|
216
|
+
// Build message and verify Ed25519 signature
|
|
247
217
|
const message = buildSignatureMessage(timestamp, nonce, method, params);
|
|
248
|
-
const isValid = await
|
|
218
|
+
const isValid = await verifyEd25519Signature(publicKey, message, signature);
|
|
249
219
|
|
|
250
220
|
if (!isValid) {
|
|
251
221
|
throw new RpcError(ErrorCodes.INVALID_CREDENTIALS, 'Invalid signature');
|
|
252
222
|
}
|
|
253
223
|
|
|
254
224
|
// Check nonce uniqueness AFTER successful signature verification
|
|
255
|
-
|
|
256
|
-
// Only valid authenticated requests can mark nonces as used
|
|
257
|
-
const nonceKey = `nonce:${name}:${nonce}`;
|
|
225
|
+
const nonceKey = `nonce:${publicKey}:${nonce}`;
|
|
258
226
|
const nonceExpiresAt = timestamp + config.timestampMaxAge;
|
|
259
227
|
const nonceIsNew = await storage.checkAndMarkNonce(nonceKey, nonceExpiresAt);
|
|
260
228
|
|
|
261
229
|
if (!nonceIsNew) {
|
|
262
230
|
throw new RpcError(ErrorCodes.INVALID_CREDENTIALS, 'Nonce already used (replay attack detected)');
|
|
263
231
|
}
|
|
264
|
-
|
|
265
|
-
// Update last used timestamp
|
|
266
|
-
const now = Date.now();
|
|
267
|
-
const credentialExpiresAt = now + (365 * 24 * 60 * 60 * 1000); // 1 year
|
|
268
|
-
await storage.updateCredentialUsage(name, now, credentialExpiresAt);
|
|
269
232
|
}
|
|
270
233
|
|
|
271
234
|
/**
|
|
272
235
|
* RPC Method Handlers
|
|
273
236
|
*/
|
|
274
|
-
|
|
275
237
|
const handlers: Record<string, RpcHandler> = {
|
|
276
238
|
/**
|
|
277
|
-
*
|
|
278
|
-
* No authentication required - this is how users get started
|
|
279
|
-
* SECURITY: Rate limited per IP to prevent abuse (database-backed for multi-instance support)
|
|
280
|
-
*/
|
|
281
|
-
async generateCredentials(params: GenerateCredentialsParams, name, timestamp, signature, storage, config, request: RpcRequest & { clientIp?: string }) {
|
|
282
|
-
// Check total credentials limit
|
|
283
|
-
const credentialCount = await storage.getCredentialCount();
|
|
284
|
-
if (credentialCount >= config.maxTotalCredentials) {
|
|
285
|
-
throw new RpcError(
|
|
286
|
-
ErrorCodes.STORAGE_FULL,
|
|
287
|
-
`Server credential limit reached (${config.maxTotalCredentials}). Try again later.`
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Rate limiting check (IP-based, stored in database)
|
|
292
|
-
// SECURITY: Use stricter global rate limit for requests without identifiable IP
|
|
293
|
-
let rateLimitKey: string;
|
|
294
|
-
let rateLimit: number;
|
|
295
|
-
|
|
296
|
-
if (!request.clientIp) {
|
|
297
|
-
// Warn about missing IP (suggests proxy misconfiguration)
|
|
298
|
-
console.warn('⚠️ WARNING: Unable to determine client IP for credential generation. Using global rate limit.');
|
|
299
|
-
// Use global rate limit with much stricter limit (prevents DoS while allowing basic function)
|
|
300
|
-
rateLimitKey = 'cred_gen:global_unknown';
|
|
301
|
-
rateLimit = 2; // Only 2 credentials per second globally for all unknown IPs combined
|
|
302
|
-
} else {
|
|
303
|
-
rateLimitKey = `cred_gen:${request.clientIp}`;
|
|
304
|
-
rateLimit = config.credentialsPerIpPerSecond;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const allowed = await storage.checkRateLimit(
|
|
308
|
-
rateLimitKey,
|
|
309
|
-
rateLimit,
|
|
310
|
-
CREDENTIAL_RATE_WINDOW
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
if (!allowed) {
|
|
314
|
-
throw new RpcError(
|
|
315
|
-
ErrorCodes.RATE_LIMIT_EXCEEDED,
|
|
316
|
-
`Rate limit exceeded. Maximum ${rateLimit} credentials per second${request.clientIp ? ' per IP' : ' (global limit for unidentified IPs)'}.`
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Validate username if provided
|
|
321
|
-
if (params.name !== undefined) {
|
|
322
|
-
if (typeof params.name !== 'string') {
|
|
323
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'name must be a string');
|
|
324
|
-
}
|
|
325
|
-
const usernameValidation = validateUsername(params.name);
|
|
326
|
-
if (!usernameValidation.valid) {
|
|
327
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, usernameValidation.error || 'Invalid username');
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Validate expiresAt if provided
|
|
332
|
-
if (params.expiresAt !== undefined) {
|
|
333
|
-
if (typeof params.expiresAt !== 'number' || isNaN(params.expiresAt) || !Number.isFinite(params.expiresAt)) {
|
|
334
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'expiresAt must be a valid timestamp');
|
|
335
|
-
}
|
|
336
|
-
// Prevent setting expiry in the past (with 1 minute tolerance for clock skew)
|
|
337
|
-
const now = Date.now();
|
|
338
|
-
if (params.expiresAt < now - 60000) {
|
|
339
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'expiresAt cannot be in the past');
|
|
340
|
-
}
|
|
341
|
-
// Prevent unreasonably far future expiry (max 10 years)
|
|
342
|
-
const maxFuture = now + (10 * 365 * 24 * 60 * 60 * 1000);
|
|
343
|
-
if (params.expiresAt > maxFuture) {
|
|
344
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'expiresAt cannot be more than 10 years in the future');
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
const credential = await storage.generateCredentials({
|
|
350
|
-
name: params.name,
|
|
351
|
-
expiresAt: params.expiresAt,
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
name: credential.name,
|
|
356
|
-
secret: credential.secret,
|
|
357
|
-
createdAt: credential.createdAt,
|
|
358
|
-
expiresAt: credential.expiresAt,
|
|
359
|
-
};
|
|
360
|
-
} catch (error: any) {
|
|
361
|
-
if (error.message === 'Username already taken') {
|
|
362
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'Username already taken');
|
|
363
|
-
}
|
|
364
|
-
throw error;
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Discover offers by tags - Supports 2 modes:
|
|
370
|
-
* 1. Paginated discovery: tags array with limit/offset
|
|
371
|
-
* 2. Random discovery: tags array without limit (returns single random offer)
|
|
239
|
+
* Discover offers by tags
|
|
372
240
|
*/
|
|
373
|
-
async discover(params: DiscoverParams,
|
|
241
|
+
async discover(params: DiscoverParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
374
242
|
const { tags, limit, offset } = params;
|
|
375
243
|
|
|
376
|
-
// Validate tags
|
|
377
244
|
const tagsValidation = validateTags(tags);
|
|
378
245
|
if (!tagsValidation.valid) {
|
|
379
246
|
throw new RpcError(ErrorCodes.INVALID_TAG, tagsValidation.error || 'Invalid tags');
|
|
@@ -381,7 +248,6 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
381
248
|
|
|
382
249
|
// Mode 1: Paginated discovery
|
|
383
250
|
if (limit !== undefined) {
|
|
384
|
-
// Validate numeric parameters
|
|
385
251
|
if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 0) {
|
|
386
252
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'limit must be a non-negative integer');
|
|
387
253
|
}
|
|
@@ -392,12 +258,11 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
392
258
|
const pageLimit = Math.min(Math.max(1, limit), MAX_PAGE_SIZE);
|
|
393
259
|
const pageOffset = Math.max(0, offset || 0);
|
|
394
260
|
|
|
395
|
-
|
|
396
|
-
const excludeUsername = name || null;
|
|
261
|
+
const excludePublicKey = publicKey || null;
|
|
397
262
|
|
|
398
263
|
const offers = await storage.discoverOffers(
|
|
399
264
|
tags,
|
|
400
|
-
|
|
265
|
+
excludePublicKey,
|
|
401
266
|
pageLimit,
|
|
402
267
|
pageOffset
|
|
403
268
|
);
|
|
@@ -405,7 +270,7 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
405
270
|
return {
|
|
406
271
|
offers: offers.map(offer => ({
|
|
407
272
|
offerId: offer.id,
|
|
408
|
-
|
|
273
|
+
publicKey: offer.publicKey,
|
|
409
274
|
tags: offer.tags,
|
|
410
275
|
sdp: offer.sdp,
|
|
411
276
|
createdAt: offer.createdAt,
|
|
@@ -417,11 +282,9 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
417
282
|
};
|
|
418
283
|
}
|
|
419
284
|
|
|
420
|
-
// Mode 2: Random discovery
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
const offer = await storage.getRandomOffer(tags, excludeUsername);
|
|
285
|
+
// Mode 2: Random discovery
|
|
286
|
+
const excludePublicKey = publicKey || null;
|
|
287
|
+
const offer = await storage.getRandomOffer(tags, excludePublicKey);
|
|
425
288
|
|
|
426
289
|
if (!offer) {
|
|
427
290
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, 'No offers found matching tags');
|
|
@@ -429,7 +292,7 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
429
292
|
|
|
430
293
|
return {
|
|
431
294
|
offerId: offer.id,
|
|
432
|
-
|
|
295
|
+
publicKey: offer.publicKey,
|
|
433
296
|
tags: offer.tags,
|
|
434
297
|
sdp: offer.sdp,
|
|
435
298
|
createdAt: offer.createdAt,
|
|
@@ -440,20 +303,18 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
440
303
|
/**
|
|
441
304
|
* Publish offers with tags
|
|
442
305
|
*/
|
|
443
|
-
async publishOffer(params: PublishOfferParams,
|
|
306
|
+
async publishOffer(params: PublishOfferParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
444
307
|
const { tags, offers, ttl } = params;
|
|
445
308
|
|
|
446
|
-
if (!
|
|
447
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, '
|
|
309
|
+
if (!publicKey) {
|
|
310
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, 'Authentication required for offer publishing');
|
|
448
311
|
}
|
|
449
312
|
|
|
450
|
-
// Validate tags
|
|
451
313
|
const tagsValidation = validateTags(tags);
|
|
452
314
|
if (!tagsValidation.valid) {
|
|
453
315
|
throw new RpcError(ErrorCodes.INVALID_TAG, tagsValidation.error || 'Invalid tags');
|
|
454
316
|
}
|
|
455
317
|
|
|
456
|
-
// Validate offers
|
|
457
318
|
if (!offers || !Array.isArray(offers) || offers.length === 0) {
|
|
458
319
|
throw new RpcError(ErrorCodes.MISSING_PARAMS, 'Must provide at least one offer');
|
|
459
320
|
}
|
|
@@ -465,8 +326,7 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
465
326
|
);
|
|
466
327
|
}
|
|
467
328
|
|
|
468
|
-
|
|
469
|
-
const userOfferCount = await storage.getOfferCountByUsername(name);
|
|
329
|
+
const userOfferCount = await storage.getOfferCountByPublicKey(publicKey);
|
|
470
330
|
if (userOfferCount + offers.length > config.maxOffersPerUser) {
|
|
471
331
|
throw new RpcError(
|
|
472
332
|
ErrorCodes.TOO_MANY_OFFERS_PER_USER,
|
|
@@ -474,7 +334,6 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
474
334
|
);
|
|
475
335
|
}
|
|
476
336
|
|
|
477
|
-
// Check total offers limit
|
|
478
337
|
const totalOfferCount = await storage.getOfferCount();
|
|
479
338
|
if (totalOfferCount + offers.length > config.maxTotalOffers) {
|
|
480
339
|
throw new RpcError(
|
|
@@ -483,7 +342,6 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
483
342
|
);
|
|
484
343
|
}
|
|
485
344
|
|
|
486
|
-
// Validate each offer has valid SDP
|
|
487
345
|
offers.forEach((offer, index) => {
|
|
488
346
|
if (!offer || typeof offer !== 'object') {
|
|
489
347
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, `Invalid offer at index ${index}: must be an object`);
|
|
@@ -499,27 +357,21 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
499
357
|
}
|
|
500
358
|
});
|
|
501
359
|
|
|
502
|
-
// Validate TTL if provided
|
|
503
360
|
if (ttl !== undefined) {
|
|
504
361
|
if (typeof ttl !== 'number' || isNaN(ttl) || ttl < 0) {
|
|
505
362
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'TTL must be a non-negative number');
|
|
506
363
|
}
|
|
507
364
|
}
|
|
508
365
|
|
|
509
|
-
// Create offers with tags
|
|
510
366
|
const now = Date.now();
|
|
511
367
|
const offerTtl =
|
|
512
368
|
ttl !== undefined
|
|
513
|
-
? Math.min(
|
|
514
|
-
Math.max(ttl, config.offerMinTtl),
|
|
515
|
-
config.offerMaxTtl
|
|
516
|
-
)
|
|
369
|
+
? Math.min(Math.max(ttl, config.offerMinTtl), config.offerMaxTtl)
|
|
517
370
|
: config.offerDefaultTtl;
|
|
518
371
|
const expiresAt = now + offerTtl;
|
|
519
372
|
|
|
520
|
-
// Prepare offer requests with tags
|
|
521
373
|
const offerRequests = offers.map(offer => ({
|
|
522
|
-
|
|
374
|
+
publicKey,
|
|
523
375
|
tags,
|
|
524
376
|
sdp: offer.sdp,
|
|
525
377
|
expiresAt,
|
|
@@ -528,7 +380,7 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
528
380
|
const createdOffers = await storage.createOffers(offerRequests);
|
|
529
381
|
|
|
530
382
|
return {
|
|
531
|
-
|
|
383
|
+
publicKey,
|
|
532
384
|
tags,
|
|
533
385
|
offers: createdOffers.map(offer => ({
|
|
534
386
|
offerId: offer.id,
|
|
@@ -544,18 +396,18 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
544
396
|
/**
|
|
545
397
|
* Delete an offer by ID
|
|
546
398
|
*/
|
|
547
|
-
async deleteOffer(params: DeleteOfferParams,
|
|
399
|
+
async deleteOffer(params: DeleteOfferParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
548
400
|
const { offerId } = params;
|
|
549
401
|
|
|
550
|
-
if (!
|
|
551
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, '
|
|
402
|
+
if (!publicKey) {
|
|
403
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, 'Authentication required');
|
|
552
404
|
}
|
|
553
405
|
|
|
554
406
|
validateStringParam(offerId, 'offerId');
|
|
555
407
|
|
|
556
|
-
const deleted = await storage.deleteOffer(offerId,
|
|
408
|
+
const deleted = await storage.deleteOffer(offerId, publicKey);
|
|
557
409
|
if (!deleted) {
|
|
558
|
-
throw new RpcError(ErrorCodes.NOT_AUTHORIZED, 'Offer not found or not owned by this
|
|
410
|
+
throw new RpcError(ErrorCodes.NOT_AUTHORIZED, 'Offer not found or not owned by this identity');
|
|
559
411
|
}
|
|
560
412
|
|
|
561
413
|
return { success: true };
|
|
@@ -564,14 +416,13 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
564
416
|
/**
|
|
565
417
|
* Answer an offer
|
|
566
418
|
*/
|
|
567
|
-
async answerOffer(params: AnswerOfferParams,
|
|
568
|
-
const { offerId, sdp } = params;
|
|
419
|
+
async answerOffer(params: AnswerOfferParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
420
|
+
const { offerId, sdp, matchedTags } = params;
|
|
569
421
|
|
|
570
|
-
// Validate input parameters
|
|
571
422
|
validateStringParam(offerId, 'offerId');
|
|
572
423
|
|
|
573
|
-
if (!
|
|
574
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, '
|
|
424
|
+
if (!publicKey) {
|
|
425
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, 'Authentication required');
|
|
575
426
|
}
|
|
576
427
|
|
|
577
428
|
if (!sdp || typeof sdp !== 'string' || sdp.length === 0) {
|
|
@@ -582,16 +433,28 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
582
433
|
throw new RpcError(ErrorCodes.SDP_TOO_LARGE, `SDP too large (max ${config.maxSdpSize} bytes)`);
|
|
583
434
|
}
|
|
584
435
|
|
|
436
|
+
if (matchedTags !== undefined && !Array.isArray(matchedTags)) {
|
|
437
|
+
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'matchedTags must be an array');
|
|
438
|
+
}
|
|
439
|
+
|
|
585
440
|
const offer = await storage.getOfferById(offerId);
|
|
586
441
|
if (!offer) {
|
|
587
442
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, 'Offer not found');
|
|
588
443
|
}
|
|
589
444
|
|
|
590
|
-
if (offer.
|
|
445
|
+
if (offer.answererPublicKey) {
|
|
591
446
|
throw new RpcError(ErrorCodes.OFFER_ALREADY_ANSWERED, 'Offer already answered');
|
|
592
447
|
}
|
|
593
448
|
|
|
594
|
-
|
|
449
|
+
if (matchedTags && matchedTags.length > 0) {
|
|
450
|
+
const offerTagSet = new Set(offer.tags);
|
|
451
|
+
const invalidTags = matchedTags.filter(tag => !offerTagSet.has(tag));
|
|
452
|
+
if (invalidTags.length > 0) {
|
|
453
|
+
throw new RpcError(ErrorCodes.INVALID_PARAMS, `matchedTags contains tags not on offer: ${invalidTags.join(', ')}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await storage.answerOffer(offerId, publicKey, sdp, matchedTags);
|
|
595
458
|
|
|
596
459
|
return { success: true, offerId };
|
|
597
460
|
},
|
|
@@ -599,14 +462,13 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
599
462
|
/**
|
|
600
463
|
* Get answer for an offer
|
|
601
464
|
*/
|
|
602
|
-
async getOfferAnswer(params: GetOfferAnswerParams,
|
|
465
|
+
async getOfferAnswer(params: GetOfferAnswerParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
603
466
|
const { offerId } = params;
|
|
604
467
|
|
|
605
|
-
// Validate input parameters
|
|
606
468
|
validateStringParam(offerId, 'offerId');
|
|
607
469
|
|
|
608
|
-
if (!
|
|
609
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, '
|
|
470
|
+
if (!publicKey) {
|
|
471
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, 'Authentication required');
|
|
610
472
|
}
|
|
611
473
|
|
|
612
474
|
const offer = await storage.getOfferById(offerId);
|
|
@@ -614,18 +476,18 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
614
476
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, 'Offer not found');
|
|
615
477
|
}
|
|
616
478
|
|
|
617
|
-
if (offer.
|
|
479
|
+
if (offer.publicKey !== publicKey) {
|
|
618
480
|
throw new RpcError(ErrorCodes.NOT_AUTHORIZED, 'Not authorized to access this offer');
|
|
619
481
|
}
|
|
620
482
|
|
|
621
|
-
if (!offer.
|
|
483
|
+
if (!offer.answererPublicKey || !offer.answerSdp) {
|
|
622
484
|
throw new RpcError(ErrorCodes.OFFER_NOT_ANSWERED, 'Offer not yet answered');
|
|
623
485
|
}
|
|
624
486
|
|
|
625
487
|
return {
|
|
626
488
|
sdp: offer.answerSdp,
|
|
627
489
|
offerId: offer.id,
|
|
628
|
-
|
|
490
|
+
answererPublicKey: offer.answererPublicKey,
|
|
629
491
|
answeredAt: offer.answeredAt,
|
|
630
492
|
};
|
|
631
493
|
},
|
|
@@ -633,49 +495,38 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
633
495
|
/**
|
|
634
496
|
* Combined polling for answers and ICE candidates
|
|
635
497
|
*/
|
|
636
|
-
async poll(params: PollParams,
|
|
498
|
+
async poll(params: PollParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
637
499
|
const { since } = params;
|
|
638
500
|
|
|
639
|
-
if (!
|
|
640
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, '
|
|
501
|
+
if (!publicKey) {
|
|
502
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, 'Authentication required');
|
|
641
503
|
}
|
|
642
504
|
|
|
643
|
-
// Validate since parameter
|
|
644
505
|
if (since !== undefined && (typeof since !== 'number' || since < 0 || !Number.isFinite(since))) {
|
|
645
506
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'Invalid since parameter: must be a non-negative number');
|
|
646
507
|
}
|
|
647
508
|
const sinceTimestamp = since !== undefined ? since : 0;
|
|
648
509
|
|
|
649
|
-
|
|
650
|
-
const answeredOffers = await storage.getAnsweredOffers(name);
|
|
510
|
+
const answeredOffers = await storage.getAnsweredOffers(publicKey);
|
|
651
511
|
const filteredAnswers = answeredOffers.filter(
|
|
652
512
|
(offer) => offer.answeredAt && offer.answeredAt > sinceTimestamp
|
|
653
513
|
);
|
|
654
514
|
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
// Get all offers the user has answered (where user is answerer)
|
|
659
|
-
const answeredByUser = await storage.getOffersAnsweredBy(name);
|
|
515
|
+
const ownedOffers = await storage.getOffersByPublicKey(publicKey);
|
|
516
|
+
const answeredByUser = await storage.getOffersAnsweredBy(publicKey);
|
|
660
517
|
|
|
661
|
-
// Combine offer IDs from both sources for ICE candidate fetching
|
|
662
|
-
// The storage method handles filtering by role automatically
|
|
663
518
|
const allOfferIds = [
|
|
664
519
|
...ownedOffers.map(offer => offer.id),
|
|
665
520
|
...answeredByUser.map(offer => offer.id),
|
|
666
521
|
];
|
|
667
|
-
// Remove duplicates (shouldn't happen, but defensive)
|
|
668
522
|
const offerIds = [...new Set(allOfferIds)];
|
|
669
523
|
|
|
670
|
-
// Batch fetch ICE candidates for all offers using JOIN to avoid N+1 query problem
|
|
671
|
-
// Server filters by role - offerers get answerer candidates, answerers get offerer candidates
|
|
672
524
|
const iceCandidatesMap = await storage.getIceCandidatesForMultipleOffers(
|
|
673
525
|
offerIds,
|
|
674
|
-
|
|
526
|
+
publicKey,
|
|
675
527
|
sinceTimestamp
|
|
676
528
|
);
|
|
677
529
|
|
|
678
|
-
// Convert Map to Record for response
|
|
679
530
|
const iceCandidatesByOffer: Record<string, any[]> = {};
|
|
680
531
|
for (const [offerId, candidates] of iceCandidatesMap.entries()) {
|
|
681
532
|
iceCandidatesByOffer[offerId] = candidates;
|
|
@@ -684,7 +535,7 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
684
535
|
return {
|
|
685
536
|
answers: filteredAnswers.map((offer) => ({
|
|
686
537
|
offerId: offer.id,
|
|
687
|
-
|
|
538
|
+
answererPublicKey: offer.answererPublicKey,
|
|
688
539
|
sdp: offer.answerSdp,
|
|
689
540
|
answeredAt: offer.answeredAt,
|
|
690
541
|
})),
|
|
@@ -695,14 +546,13 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
695
546
|
/**
|
|
696
547
|
* Add ICE candidates
|
|
697
548
|
*/
|
|
698
|
-
async addIceCandidates(params: AddIceCandidatesParams,
|
|
549
|
+
async addIceCandidates(params: AddIceCandidatesParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
699
550
|
const { offerId, candidates } = params;
|
|
700
551
|
|
|
701
|
-
// Validate input parameters
|
|
702
552
|
validateStringParam(offerId, 'offerId');
|
|
703
553
|
|
|
704
|
-
if (!
|
|
705
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, '
|
|
554
|
+
if (!publicKey) {
|
|
555
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, 'Authentication required');
|
|
706
556
|
}
|
|
707
557
|
|
|
708
558
|
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
@@ -716,13 +566,11 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
716
566
|
);
|
|
717
567
|
}
|
|
718
568
|
|
|
719
|
-
// Validate each candidate is an object (don't enforce structure per CLAUDE.md)
|
|
720
569
|
candidates.forEach((candidate, index) => {
|
|
721
570
|
if (!candidate || typeof candidate !== 'object') {
|
|
722
571
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, `Invalid candidate at index ${index}: must be an object`);
|
|
723
572
|
}
|
|
724
573
|
|
|
725
|
-
// Check JSON depth to prevent stack overflow from deeply nested objects
|
|
726
574
|
const depth = getJsonDepth(candidate, config.maxCandidateDepth + 1);
|
|
727
575
|
if (depth > config.maxCandidateDepth) {
|
|
728
576
|
throw new RpcError(
|
|
@@ -731,7 +579,6 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
731
579
|
);
|
|
732
580
|
}
|
|
733
581
|
|
|
734
|
-
// Ensure candidate is serializable and check size (will be stored as JSON)
|
|
735
582
|
let candidateJson: string;
|
|
736
583
|
try {
|
|
737
584
|
candidateJson = JSON.stringify(candidate);
|
|
@@ -739,7 +586,6 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
739
586
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, `Candidate at index ${index} is not serializable`);
|
|
740
587
|
}
|
|
741
588
|
|
|
742
|
-
// Validate candidate size to prevent abuse
|
|
743
589
|
if (candidateJson.length > config.maxCandidateSize) {
|
|
744
590
|
throw new RpcError(
|
|
745
591
|
ErrorCodes.INVALID_PARAMS,
|
|
@@ -753,7 +599,6 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
753
599
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, 'Offer not found');
|
|
754
600
|
}
|
|
755
601
|
|
|
756
|
-
// Check ICE candidates limit per offer
|
|
757
602
|
const currentCandidateCount = await storage.getIceCandidateCount(offerId);
|
|
758
603
|
if (currentCandidateCount + candidates.length > config.maxIceCandidatesPerOffer) {
|
|
759
604
|
throw new RpcError(
|
|
@@ -762,10 +607,10 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
762
607
|
);
|
|
763
608
|
}
|
|
764
609
|
|
|
765
|
-
const role = offer.
|
|
610
|
+
const role = offer.publicKey === publicKey ? 'offerer' : 'answerer';
|
|
766
611
|
const count = await storage.addIceCandidates(
|
|
767
612
|
offerId,
|
|
768
|
-
|
|
613
|
+
publicKey,
|
|
769
614
|
role,
|
|
770
615
|
candidates
|
|
771
616
|
);
|
|
@@ -776,17 +621,15 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
776
621
|
/**
|
|
777
622
|
* Get ICE candidates
|
|
778
623
|
*/
|
|
779
|
-
async getIceCandidates(params: GetIceCandidatesParams,
|
|
624
|
+
async getIceCandidates(params: GetIceCandidatesParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
780
625
|
const { offerId, since } = params;
|
|
781
626
|
|
|
782
|
-
// Validate input parameters
|
|
783
627
|
validateStringParam(offerId, 'offerId');
|
|
784
628
|
|
|
785
|
-
if (!
|
|
786
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, '
|
|
629
|
+
if (!publicKey) {
|
|
630
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, 'Authentication required');
|
|
787
631
|
}
|
|
788
632
|
|
|
789
|
-
// Validate since parameter
|
|
790
633
|
if (since !== undefined && (typeof since !== 'number' || since < 0 || !Number.isFinite(since))) {
|
|
791
634
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, 'Invalid since parameter: must be a non-negative number');
|
|
792
635
|
}
|
|
@@ -797,10 +640,8 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
797
640
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, 'Offer not found');
|
|
798
641
|
}
|
|
799
642
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const isOfferer = offer.username === name;
|
|
803
|
-
const isAnswerer = offer.answererUsername === name;
|
|
643
|
+
const isOfferer = offer.publicKey === publicKey;
|
|
644
|
+
const isAnswerer = offer.answererPublicKey === publicKey;
|
|
804
645
|
|
|
805
646
|
if (!isOfferer && !isAnswerer) {
|
|
806
647
|
throw new RpcError(ErrorCodes.NOT_AUTHORIZED, 'Not authorized to access ICE candidates for this offer');
|
|
@@ -825,7 +666,7 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
825
666
|
};
|
|
826
667
|
|
|
827
668
|
// Methods that don't require authentication
|
|
828
|
-
const UNAUTHENTICATED_METHODS = new Set(['
|
|
669
|
+
const UNAUTHENTICATED_METHODS = new Set(['discover']);
|
|
829
670
|
|
|
830
671
|
/**
|
|
831
672
|
* Handle RPC batch request with header-based authentication
|
|
@@ -838,13 +679,11 @@ export async function handleRpc(
|
|
|
838
679
|
): Promise<RpcResponse[]> {
|
|
839
680
|
const responses: RpcResponse[] = [];
|
|
840
681
|
|
|
841
|
-
// Extract client IP for rate limiting
|
|
842
|
-
// Try multiple headers for proxy compatibility
|
|
843
682
|
const clientIp =
|
|
844
|
-
ctx.req.header('cf-connecting-ip') ||
|
|
845
|
-
ctx.req.header('x-real-ip') ||
|
|
683
|
+
ctx.req.header('cf-connecting-ip') ||
|
|
684
|
+
ctx.req.header('x-real-ip') ||
|
|
846
685
|
ctx.req.header('x-forwarded-for')?.split(',')[0].trim() ||
|
|
847
|
-
undefined;
|
|
686
|
+
undefined;
|
|
848
687
|
|
|
849
688
|
// General request rate limiting (per IP per second)
|
|
850
689
|
if (clientIp) {
|
|
@@ -856,7 +695,6 @@ export async function handleRpc(
|
|
|
856
695
|
);
|
|
857
696
|
|
|
858
697
|
if (!allowed) {
|
|
859
|
-
// Return error for all requests in the batch
|
|
860
698
|
return requests.map(() => ({
|
|
861
699
|
success: false,
|
|
862
700
|
error: `Rate limit exceeded. Maximum ${config.requestsPerIpPerSecond} requests per second per IP.`,
|
|
@@ -865,21 +703,16 @@ export async function handleRpc(
|
|
|
865
703
|
}
|
|
866
704
|
}
|
|
867
705
|
|
|
868
|
-
// Read auth headers (
|
|
869
|
-
const
|
|
706
|
+
// Read auth headers (X-PublicKey instead of X-Name)
|
|
707
|
+
const publicKey = ctx.req.header('X-PublicKey');
|
|
870
708
|
const timestampHeader = ctx.req.header('X-Timestamp');
|
|
871
709
|
const nonce = ctx.req.header('X-Nonce');
|
|
872
710
|
const signature = ctx.req.header('X-Signature');
|
|
873
711
|
|
|
874
|
-
// Parse timestamp if present
|
|
875
712
|
const timestamp = timestampHeader ? parseInt(timestampHeader, 10) : 0;
|
|
876
713
|
|
|
877
|
-
//
|
|
878
|
-
// This prevents DoS where first N requests complete before limit triggers
|
|
879
|
-
// Example attack prevented: 100 publishOffer × 100 offers = 10,000 operations
|
|
714
|
+
// Pre-calculate total operations
|
|
880
715
|
let totalOperations = 0;
|
|
881
|
-
|
|
882
|
-
// Count all operations across all requests first
|
|
883
716
|
for (const request of requests) {
|
|
884
717
|
const { method, params } = request;
|
|
885
718
|
if (method === 'publishOffer' && params?.offers && Array.isArray(params.offers)) {
|
|
@@ -887,13 +720,10 @@ export async function handleRpc(
|
|
|
887
720
|
} else if (method === 'addIceCandidates' && params?.candidates && Array.isArray(params.candidates)) {
|
|
888
721
|
totalOperations += params.candidates.length;
|
|
889
722
|
} else {
|
|
890
|
-
totalOperations += 1;
|
|
723
|
+
totalOperations += 1;
|
|
891
724
|
}
|
|
892
725
|
}
|
|
893
726
|
|
|
894
|
-
// Reject entire batch if total operations exceed limit
|
|
895
|
-
// This happens BEFORE processing any requests
|
|
896
|
-
// Return error for EACH request to maintain response array alignment
|
|
897
727
|
if (totalOperations > config.maxTotalOperations) {
|
|
898
728
|
return requests.map(() => ({
|
|
899
729
|
success: false,
|
|
@@ -907,7 +737,6 @@ export async function handleRpc(
|
|
|
907
737
|
try {
|
|
908
738
|
const { method, params } = request;
|
|
909
739
|
|
|
910
|
-
// Validate request
|
|
911
740
|
if (!method || typeof method !== 'string') {
|
|
912
741
|
responses.push({
|
|
913
742
|
success: false,
|
|
@@ -917,7 +746,6 @@ export async function handleRpc(
|
|
|
917
746
|
continue;
|
|
918
747
|
}
|
|
919
748
|
|
|
920
|
-
// Get handler
|
|
921
749
|
const handler = handlers[method];
|
|
922
750
|
if (!handler) {
|
|
923
751
|
responses.push({
|
|
@@ -928,14 +756,13 @@ export async function handleRpc(
|
|
|
928
756
|
continue;
|
|
929
757
|
}
|
|
930
758
|
|
|
931
|
-
// Validate auth headers only for methods that require authentication
|
|
932
759
|
const requiresAuth = !UNAUTHENTICATED_METHODS.has(method);
|
|
933
760
|
|
|
934
761
|
if (requiresAuth) {
|
|
935
|
-
if (!
|
|
762
|
+
if (!publicKey || typeof publicKey !== 'string') {
|
|
936
763
|
responses.push({
|
|
937
764
|
success: false,
|
|
938
|
-
error: 'Missing or invalid X-
|
|
765
|
+
error: 'Missing or invalid X-PublicKey header',
|
|
939
766
|
errorCode: ErrorCodes.AUTH_REQUIRED,
|
|
940
767
|
});
|
|
941
768
|
continue;
|
|
@@ -968,9 +795,9 @@ export async function handleRpc(
|
|
|
968
795
|
continue;
|
|
969
796
|
}
|
|
970
797
|
|
|
971
|
-
// Verify
|
|
798
|
+
// Verify Ed25519 signature
|
|
972
799
|
await verifyRequestSignature(
|
|
973
|
-
|
|
800
|
+
publicKey,
|
|
974
801
|
timestamp,
|
|
975
802
|
nonce,
|
|
976
803
|
signature,
|
|
@@ -980,10 +807,9 @@ export async function handleRpc(
|
|
|
980
807
|
config
|
|
981
808
|
);
|
|
982
809
|
|
|
983
|
-
// Execute handler with auth
|
|
984
810
|
const result = await handler(
|
|
985
811
|
params || {},
|
|
986
|
-
|
|
812
|
+
publicKey,
|
|
987
813
|
timestamp,
|
|
988
814
|
signature,
|
|
989
815
|
storage,
|
|
@@ -999,9 +825,9 @@ export async function handleRpc(
|
|
|
999
825
|
// Execute handler without strict auth requirement
|
|
1000
826
|
const result = await handler(
|
|
1001
827
|
params || {},
|
|
1002
|
-
|
|
1003
|
-
0,
|
|
1004
|
-
'',
|
|
828
|
+
publicKey || '',
|
|
829
|
+
0,
|
|
830
|
+
'',
|
|
1005
831
|
storage,
|
|
1006
832
|
config,
|
|
1007
833
|
{ ...request, clientIp }
|
|
@@ -1020,8 +846,6 @@ export async function handleRpc(
|
|
|
1020
846
|
errorCode: err.errorCode,
|
|
1021
847
|
});
|
|
1022
848
|
} else {
|
|
1023
|
-
// Generic error - don't leak internal details
|
|
1024
|
-
// Log the actual error for debugging
|
|
1025
849
|
console.error('Unexpected RPC error:', err);
|
|
1026
850
|
responses.push({
|
|
1027
851
|
success: false,
|