@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 +145 -7
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/src/config.ts +29 -1
- package/src/rpc.ts +64 -6
- package/src/storage/d1.ts +26 -0
- package/src/storage/memory.ts +20 -0
- package/src/storage/mysql.ts +28 -0
- package/src/storage/postgres.ts +28 -0
- package/src/storage/sqlite.ts +22 -0
- package/src/storage/types.ts +28 -0
- package/wrangler.toml +1 -1
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 =
|
|
2386
|
+
rateLimit = 1;
|
|
2299
2387
|
} else {
|
|
2300
2388
|
rateLimitKey = `cred_gen:${request.clientIp}`;
|
|
2301
|
-
rateLimit =
|
|
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}
|
|
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 = "
|
|
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);
|