@xtr-dev/rondevu-server 0.5.6 → 0.5.8

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/dist/index.js CHANGED
@@ -729,6 +729,21 @@ var init_memory = __esm({
729
729
  this.offersByTag.clear();
730
730
  this.offersByAnswerer.clear();
731
731
  }
732
+ // ===== Count Methods (for resource limits) =====
733
+ async getOfferCount() {
734
+ return this.offers.size;
735
+ }
736
+ async getOfferCountByUsername(username) {
737
+ const offerIds = this.offersByUsername.get(username);
738
+ return offerIds ? offerIds.size : 0;
739
+ }
740
+ async getCredentialCount() {
741
+ return this.credentials.size;
742
+ }
743
+ async getIceCandidateCount(offerId) {
744
+ const candidates = this.iceCandidates.get(offerId);
745
+ return candidates ? candidates.length : 0;
746
+ }
732
747
  // ===== Helper Methods =====
733
748
  removeOfferFromIndexes(offer) {
734
749
  const usernameOffers = this.offersByUsername.get(offer.username);
@@ -1241,6 +1256,23 @@ var init_sqlite = __esm({
1241
1256
  async close() {
1242
1257
  this.db.close();
1243
1258
  }
1259
+ // ===== Count Methods (for resource limits) =====
1260
+ async getOfferCount() {
1261
+ const result = this.db.prepare("SELECT COUNT(*) as count FROM offers").get();
1262
+ return result.count;
1263
+ }
1264
+ async getOfferCountByUsername(username) {
1265
+ const result = this.db.prepare("SELECT COUNT(*) as count FROM offers WHERE username = ?").get(username);
1266
+ return result.count;
1267
+ }
1268
+ async getCredentialCount() {
1269
+ const result = this.db.prepare("SELECT COUNT(*) as count FROM credentials").get();
1270
+ return result.count;
1271
+ }
1272
+ async getIceCandidateCount(offerId) {
1273
+ const result = this.db.prepare("SELECT COUNT(*) as count FROM ice_candidates WHERE offer_id = ?").get(offerId);
1274
+ return result.count;
1275
+ }
1244
1276
  // ===== Helper Methods =====
1245
1277
  /**
1246
1278
  * Helper method to convert database row to Offer object
@@ -1691,6 +1723,29 @@ var init_mysql = __esm({
1691
1723
  async close() {
1692
1724
  await this.pool.end();
1693
1725
  }
1726
+ // ===== Count Methods (for resource limits) =====
1727
+ async getOfferCount() {
1728
+ const [rows] = await this.pool.query("SELECT COUNT(*) as count FROM offers");
1729
+ return Number(rows[0].count);
1730
+ }
1731
+ async getOfferCountByUsername(username) {
1732
+ const [rows] = await this.pool.query(
1733
+ "SELECT COUNT(*) as count FROM offers WHERE username = ?",
1734
+ [username]
1735
+ );
1736
+ return Number(rows[0].count);
1737
+ }
1738
+ async getCredentialCount() {
1739
+ const [rows] = await this.pool.query("SELECT COUNT(*) as count FROM credentials");
1740
+ return Number(rows[0].count);
1741
+ }
1742
+ async getIceCandidateCount(offerId) {
1743
+ const [rows] = await this.pool.query(
1744
+ "SELECT COUNT(*) as count FROM ice_candidates WHERE offer_id = ?",
1745
+ [offerId]
1746
+ );
1747
+ return Number(rows[0].count);
1748
+ }
1694
1749
  // ===== Helper Methods =====
1695
1750
  rowToOffer(row) {
1696
1751
  return {
@@ -2154,6 +2209,29 @@ var init_postgres = __esm({
2154
2209
  async close() {
2155
2210
  await this.pool.end();
2156
2211
  }
2212
+ // ===== Count Methods (for resource limits) =====
2213
+ async getOfferCount() {
2214
+ const result = await this.pool.query("SELECT COUNT(*) as count FROM offers");
2215
+ return Number(result.rows[0].count);
2216
+ }
2217
+ async getOfferCountByUsername(username) {
2218
+ const result = await this.pool.query(
2219
+ "SELECT COUNT(*) as count FROM offers WHERE username = $1",
2220
+ [username]
2221
+ );
2222
+ return Number(result.rows[0].count);
2223
+ }
2224
+ async getCredentialCount() {
2225
+ const result = await this.pool.query("SELECT COUNT(*) as count FROM credentials");
2226
+ return Number(result.rows[0].count);
2227
+ }
2228
+ async getIceCandidateCount(offerId) {
2229
+ const result = await this.pool.query(
2230
+ "SELECT COUNT(*) as count FROM ice_candidates WHERE offer_id = $1",
2231
+ [offerId]
2232
+ );
2233
+ return Number(result.rows[0].count);
2234
+ }
2157
2235
  // ===== Helper Methods =====
2158
2236
  rowToOffer(row) {
2159
2237
  return {
@@ -2193,8 +2271,8 @@ var import_cors = require("hono/cors");
2193
2271
  // src/rpc.ts
2194
2272
  init_crypto();
2195
2273
  var MAX_PAGE_SIZE = 100;
2196
- var CREDENTIAL_RATE_LIMIT = 1;
2197
2274
  var CREDENTIAL_RATE_WINDOW = 1e3;
2275
+ var REQUEST_RATE_WINDOW = 1e3;
2198
2276
  function getJsonDepth(obj, maxDepth, currentDepth = 0) {
2199
2277
  if (obj === null || typeof obj !== "object") {
2200
2278
  return currentDepth;
@@ -2242,6 +2320,9 @@ var ErrorCodes = {
2242
2320
  SDP_TOO_LARGE: "SDP_TOO_LARGE",
2243
2321
  BATCH_TOO_LARGE: "BATCH_TOO_LARGE",
2244
2322
  RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
2323
+ TOO_MANY_OFFERS_PER_USER: "TOO_MANY_OFFERS_PER_USER",
2324
+ STORAGE_FULL: "STORAGE_FULL",
2325
+ TOO_MANY_ICE_CANDIDATES: "TOO_MANY_ICE_CANDIDATES",
2245
2326
  // Generic errors
2246
2327
  INTERNAL_ERROR: "INTERNAL_ERROR",
2247
2328
  UNKNOWN_METHOD: "UNKNOWN_METHOD"
@@ -2290,15 +2371,22 @@ var handlers = {
2290
2371
  * SECURITY: Rate limited per IP to prevent abuse (database-backed for multi-instance support)
2291
2372
  */
2292
2373
  async generateCredentials(params, name, timestamp, signature, storage, config, request) {
2374
+ const credentialCount = await storage.getCredentialCount();
2375
+ if (credentialCount >= config.maxTotalCredentials) {
2376
+ throw new RpcError(
2377
+ ErrorCodes.STORAGE_FULL,
2378
+ `Server credential limit reached (${config.maxTotalCredentials}). Try again later.`
2379
+ );
2380
+ }
2293
2381
  let rateLimitKey;
2294
2382
  let rateLimit;
2295
2383
  if (!request.clientIp) {
2296
2384
  console.warn("\u26A0\uFE0F WARNING: Unable to determine client IP for credential generation. Using global rate limit.");
2297
2385
  rateLimitKey = "cred_gen:global_unknown";
2298
- rateLimit = 2;
2386
+ rateLimit = 1;
2299
2387
  } else {
2300
2388
  rateLimitKey = `cred_gen:${request.clientIp}`;
2301
- rateLimit = CREDENTIAL_RATE_LIMIT;
2389
+ rateLimit = config.credentialsPerIpPerSecond;
2302
2390
  }
2303
2391
  const allowed = await storage.checkRateLimit(
2304
2392
  rateLimitKey,
@@ -2308,7 +2396,7 @@ var handlers = {
2308
2396
  if (!allowed) {
2309
2397
  throw new RpcError(
2310
2398
  ErrorCodes.RATE_LIMIT_EXCEEDED,
2311
- `Rate limit exceeded. Maximum ${rateLimit} credential per second${request.clientIp ? " per IP" : " (global limit for unidentified IPs)"}.`
2399
+ `Rate limit exceeded. Maximum ${rateLimit} credentials per second${request.clientIp ? " per IP" : " (global limit for unidentified IPs)"}.`
2312
2400
  );
2313
2401
  }
2314
2402
  if (params.name !== void 0) {
@@ -2427,6 +2515,20 @@ var handlers = {
2427
2515
  `Too many offers (max ${config.maxOffersPerRequest})`
2428
2516
  );
2429
2517
  }
2518
+ const userOfferCount = await storage.getOfferCountByUsername(name);
2519
+ if (userOfferCount + offers.length > config.maxOffersPerUser) {
2520
+ throw new RpcError(
2521
+ ErrorCodes.TOO_MANY_OFFERS_PER_USER,
2522
+ `User offer limit exceeded. You have ${userOfferCount} offers, limit is ${config.maxOffersPerUser}.`
2523
+ );
2524
+ }
2525
+ const totalOfferCount = await storage.getOfferCount();
2526
+ if (totalOfferCount + offers.length > config.maxTotalOffers) {
2527
+ throw new RpcError(
2528
+ ErrorCodes.STORAGE_FULL,
2529
+ `Server offer limit reached (${config.maxTotalOffers}). Try again later.`
2530
+ );
2531
+ }
2430
2532
  offers.forEach((offer, index) => {
2431
2533
  if (!offer || typeof offer !== "object") {
2432
2534
  throw new RpcError(ErrorCodes.INVALID_PARAMS, `Invalid offer at index ${index}: must be an object`);
@@ -2626,6 +2728,13 @@ var handlers = {
2626
2728
  if (!offer) {
2627
2729
  throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, "Offer not found");
2628
2730
  }
2731
+ const currentCandidateCount = await storage.getIceCandidateCount(offerId);
2732
+ if (currentCandidateCount + candidates.length > config.maxIceCandidatesPerOffer) {
2733
+ throw new RpcError(
2734
+ ErrorCodes.TOO_MANY_ICE_CANDIDATES,
2735
+ `ICE candidate limit exceeded for offer. Current: ${currentCandidateCount}, limit: ${config.maxIceCandidatesPerOffer}.`
2736
+ );
2737
+ }
2629
2738
  const role = offer.username === name ? "offerer" : "answerer";
2630
2739
  const count = await storage.addIceCandidates(
2631
2740
  offerId,
@@ -2678,6 +2787,21 @@ async function handleRpc(requests, ctx, storage, config) {
2678
2787
  const clientIp = ctx.req.header("cf-connecting-ip") || // Cloudflare
2679
2788
  ctx.req.header("x-real-ip") || // Nginx
2680
2789
  ctx.req.header("x-forwarded-for")?.split(",")[0].trim() || void 0;
2790
+ if (clientIp) {
2791
+ const rateLimitKey = `req:${clientIp}`;
2792
+ const allowed = await storage.checkRateLimit(
2793
+ rateLimitKey,
2794
+ config.requestsPerIpPerSecond,
2795
+ REQUEST_RATE_WINDOW
2796
+ );
2797
+ if (!allowed) {
2798
+ return requests.map(() => ({
2799
+ success: false,
2800
+ error: `Rate limit exceeded. Maximum ${config.requestsPerIpPerSecond} requests per second per IP.`,
2801
+ errorCode: ErrorCodes.RATE_LIMIT_EXCEEDED
2802
+ }));
2803
+ }
2804
+ }
2681
2805
  const name = ctx.req.header("X-Name");
2682
2806
  const timestampHeader = ctx.req.header("X-Timestamp");
2683
2807
  const nonce = ctx.req.header("X-Nonce");
@@ -2906,7 +3030,7 @@ function loadConfig() {
2906
3030
  console.error("\u26A0\uFE0F WARNING: Using insecure deterministic development key");
2907
3031
  console.error("\u26A0\uFE0F ONLY use NODE_ENV=development for local development");
2908
3032
  console.error("\u26A0\uFE0F Generate production key with: openssl rand -hex 32");
2909
- masterEncryptionKey = "a3f8b9c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2";
3033
+ masterEncryptionKey = "a3f8b9c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0";
2910
3034
  }
2911
3035
  if (masterEncryptionKey.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(masterEncryptionKey)) {
2912
3036
  throw new Error("MASTER_ENCRYPTION_KEY must be 64-character hex string (32 bytes). Generate with: openssl rand -hex 32");
@@ -2946,7 +3070,14 @@ function loadConfig() {
2946
3070
  // Min 1 second
2947
3071
  timestampMaxFuture: parsePositiveInt(process.env.TIMESTAMP_MAX_FUTURE, "60000", "TIMESTAMP_MAX_FUTURE", 1e3),
2948
3072
  // Min 1 second
2949
- masterEncryptionKey
3073
+ masterEncryptionKey,
3074
+ // Resource limits
3075
+ maxOffersPerUser: parsePositiveInt(process.env.MAX_OFFERS_PER_USER, "20", "MAX_OFFERS_PER_USER", 1),
3076
+ maxTotalOffers: parsePositiveInt(process.env.MAX_TOTAL_OFFERS, "10000", "MAX_TOTAL_OFFERS", 1),
3077
+ maxTotalCredentials: parsePositiveInt(process.env.MAX_TOTAL_CREDENTIALS, "50000", "MAX_TOTAL_CREDENTIALS", 1),
3078
+ maxIceCandidatesPerOffer: parsePositiveInt(process.env.MAX_ICE_CANDIDATES_PER_OFFER, "50", "MAX_ICE_CANDIDATES_PER_OFFER", 1),
3079
+ credentialsPerIpPerSecond: parsePositiveInt(process.env.CREDENTIALS_PER_IP_PER_SECOND, "1", "CREDENTIALS_PER_IP_PER_SECOND", 1),
3080
+ requestsPerIpPerSecond: parsePositiveInt(process.env.REQUESTS_PER_IP_PER_SECOND, "50", "REQUESTS_PER_IP_PER_SECOND", 1)
2950
3081
  };
2951
3082
  return config;
2952
3083
  }
@@ -2963,7 +3094,14 @@ var CONFIG_DEFAULTS = {
2963
3094
  maxCandidatesPerRequest: 100,
2964
3095
  maxTotalOperations: 1e3,
2965
3096
  timestampMaxAge: 6e4,
2966
- timestampMaxFuture: 6e4
3097
+ timestampMaxFuture: 6e4,
3098
+ // Resource limits
3099
+ maxOffersPerUser: 20,
3100
+ maxTotalOffers: 1e4,
3101
+ maxTotalCredentials: 5e4,
3102
+ maxIceCandidatesPerOffer: 50,
3103
+ credentialsPerIpPerSecond: 1,
3104
+ requestsPerIpPerSecond: 50
2967
3105
  };
2968
3106
  async function runCleanup(storage, now) {
2969
3107
  const offers = await storage.deleteExpiredOffers(now);