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