Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions backend/logs/audit_fallback.log
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,24 @@
2026-04-26T12:10:52.741Z | {"merchant_id":"merchant-uuid-004","action":"login","event_type":"login_attempt"} | error: DB connection lost
2026-04-26T12:11:43.372Z | {"merchant_id":"merchant-uuid-004","action":"login","event_type":"login_attempt"} | error: DB connection lost
2026-04-26T13:07:19.913Z | {"merchant_id":"merchant-uuid-004","action":"login","event_type":"login_attempt"} | error: DB connection lost
2026-06-28T08:01:32.927Z | {"merchant_id":"merchant-uuid-001","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:01:32.993Z | {"merchant_id":"merchant-uuid-002","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:01:32.995Z | {"merchant_id":null,"action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:01:33.003Z | {"merchant_id":"merchant-uuid-003","action":"login","status":"success","ip_address":null,"user_agent":null,"event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:01:33.005Z | {"merchant_id":"merchant-uuid-005","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:01:33.007Z | {"merchant_id":"merchant-uuid-004","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:01:33.312Z | {"merchant_id":"merchant-uuid-006","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:02:14.968Z | {"merchant_id":"merchant-uuid-001","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:02:14.983Z | {"merchant_id":"merchant-uuid-002","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:02:14.985Z | {"merchant_id":null,"action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:02:14.988Z | {"merchant_id":"merchant-uuid-003","action":"login","status":"success","ip_address":null,"user_agent":null,"event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:02:14.990Z | {"merchant_id":"merchant-uuid-005","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:02:14.991Z | {"merchant_id":"merchant-uuid-004","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:02:15.295Z | {"merchant_id":"merchant-uuid-006","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:03:05.927Z | {"merchant_id":"merchant-uuid-001","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:03:05.977Z | {"merchant_id":"merchant-uuid-002","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:03:05.979Z | {"merchant_id":null,"action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:03:05.980Z | {"merchant_id":"merchant-uuid-003","action":"login","status":"success","ip_address":null,"user_agent":null,"event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:03:05.982Z | {"merchant_id":"merchant-uuid-005","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:03:05.984Z | {"merchant_id":"merchant-uuid-004","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
2026-06-28T08:03:06.289Z | {"merchant_id":"merchant-uuid-006","action":"login","event_type":"login_attempt"} | error: optimizedWrite is not defined
45 changes: 1 addition & 44 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"nock": "^14.0.11",
"nodemon": "^3.1.14",
"supertest": "^7.0.0",
"vitest": "^2.0.0"
"vitest": "^2.1.9"
},
"dependencies": {
"@sentry/node": "^10.46.0",
Expand Down
120 changes: 115 additions & 5 deletions backend/src/lib/trustline-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import rateLimit, { ipKeyGenerator } from "express-rate-limit";
export const TRUSTLINE_RATE_LIMIT_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
export const TRUSTLINE_RATE_LIMIT_MAX = 20; // 20 operations per window
export const TRUSTLINE_VERIFICATION_RATE_LIMIT_MAX = 50; // 50 verifications per window
export const TRUSTLINE_BURST_WINDOW_MS = 10 * 1000; // 10 seconds burst window
export const TRUSTLINE_BURST_MAX = 5; // 5 requests per burst window
const TRUSTLINE_STATS_TIMEFRAMES = new Set([
"1 hour",
"24 hours",
Expand All @@ -45,6 +47,12 @@ const OPERATION_TIMEOUT_MS = 15 * 1000; // default per-operation timeout
const DLQ_MAX_SIZE = 100; // maximum dead-letter queue entries
const ERROR_RECOVERY_METRICS_WINDOW_MS = 60 * 1000; // 1 minute window for recovery metrics

/**
* Per-key rate limit violation tracking.
* Provides metrics for security monitoring and alerting on abusive actors.
*/
const rateLimitViolations = new Map();

/**
* Per-context circuit breaker states.
* Key: context string (or 'default'). Value: CircuitBreakerState.
Expand Down Expand Up @@ -299,10 +307,14 @@ export class TrustlineSignatureVerifier {
* - Per-merchant trustline operation limits
* - Per-IP verification limits
* - Adaptive rate limiting based on account type
* - Burst protection (secondary tight window)
* - Internal service bypass via trusted header
* - Violation metrics for security monitoring
*/
export class TrustlineRateLimiter {
/**
* Generate rate limit key for trustline operations
* Generate rate limit key for trustline operations.
* Priority: merchant ID > hashed API key > IP address.
*/
static getTrustlineOperationKey(req) {
const merchantId = req?.merchant?.id;
Expand All @@ -325,7 +337,8 @@ export class TrustlineRateLimiter {
}

/**
* Generate rate limit key for trustline verifications
* Generate rate limit key for trustline verifications.
* Priority: merchant ID > IP address.
*/
static getTrustlineVerificationKey(req) {
const merchantId = req?.merchant?.id;
Expand All @@ -338,7 +351,88 @@ export class TrustlineRateLimiter {
}

/**
* Create rate limiter for trustline operations
* Check whether the request comes from a trusted internal service.
* Internal services present a shared secret via x-internal-service-token.
* The server-side secret is configured via the INTERNAL_SERVICE_TOKEN env var.
*/
static isInternalService(req) {
const internalToken = req?.headers?.["x-internal-service-token"];
const configuredToken = process.env.INTERNAL_SERVICE_TOKEN;
return Boolean(
configuredToken && internalToken && internalToken === configuredToken,
);
}

/**
* Record a rate limit violation for a given key.
* Call this from rate limiter handlers to build abuse metrics.
*/
static recordViolation(key) {
const current = rateLimitViolations.get(key) || {
count: 0,
lastSeen: null,
};
rateLimitViolations.set(key, {
count: current.count + 1,
lastSeen: new Date().toISOString(),
});
}

/**
* Return a snapshot of all recorded rate limit violations.
* Useful for security dashboards and alerting pipelines.
*/
static getRateLimitViolationMetrics() {
const snapshot = {};
for (const [key, data] of rateLimitViolations.entries()) {
snapshot[key] = { ...data };
}
return snapshot;
}

/**
* Clear all recorded violation metrics.
* Used in tests and administrative reset endpoints.
*/
static resetViolationMetrics() {
rateLimitViolations.clear();
}

/**
* Create burst protection rate limiter.
* Enforces a tight secondary window (default 5 req / 10 s) to stop
* rapid-fire bursts that stay under the primary per-window quota.
*/
static createTrustlineBurstRateLimit({
store,
rateLimitFactory = rateLimit,
} = {}) {
return rateLimitFactory({
windowMs: TRUSTLINE_BURST_WINDOW_MS,
max: TRUSTLINE_BURST_MAX,
message: {
error: "Burst limit exceeded. Slow down your trustline requests.",
code: "TRUSTLINE_BURST_LIMIT",
retryAfter: Math.ceil(TRUSTLINE_BURST_WINDOW_MS / 1000),
},
standardHeaders: true,
legacyHeaders: false,
validate: { ip: false },
keyGenerator: this.getTrustlineOperationKey,
handler: (req, res, _next, options) => {
const key = this.getTrustlineOperationKey(req);
this.recordViolation(key);
res.status(options.statusCode).json(options.message);
},
store,
passOnStoreError: true,
skip: (req) => this.isInternalService(req),
});
}

/**
* Create rate limiter for trustline operations.
* Skips enterprise/premium merchants and trusted internal services.
*/
static createTrustlineOperationRateLimit({
store,
Expand All @@ -350,25 +444,33 @@ export class TrustlineRateLimiter {
message: {
error:
"Too many trustline operations. Please wait before creating more trustlines.",
code: "TRUSTLINE_RATE_LIMIT",
retryAfter: Math.ceil(TRUSTLINE_RATE_LIMIT_WINDOW_MS / 1000),
},
standardHeaders: true,
legacyHeaders: false,
validate: { ip: false },
keyGenerator: this.getTrustlineOperationKey,
handler: (req, res, _next, options) => {
const key = this.getTrustlineOperationKey(req);
this.recordViolation(key);
res.status(options.statusCode).json(options.message);
},
requestWasSuccessful: (_req, res) => res.statusCode < 400,
store,
passOnStoreError: true,
// Skip rate limiting for high-tier merchants
skip: (req) => {
if (this.isInternalService(req)) return true;
const merchantTier = req?.merchant?.metadata?.tier;
return merchantTier === "enterprise" || merchantTier === "premium";
},
});
}

/**
* Create rate limiter for trustline verifications
* Create rate limiter for trustline verifications.
* Higher quota than operations since verification is read-only.
* Skips trusted internal services.
*/
static createTrustlineVerificationRateLimit({
store,
Expand All @@ -379,15 +481,22 @@ export class TrustlineRateLimiter {
max: TRUSTLINE_VERIFICATION_RATE_LIMIT_MAX,
message: {
error: "Too many trustline verification requests. Please slow down.",
code: "TRUSTLINE_VERIFY_RATE_LIMIT",
retryAfter: Math.ceil(TRUSTLINE_RATE_LIMIT_WINDOW_MS / 1000),
},
standardHeaders: true,
legacyHeaders: false,
validate: { ip: false },
keyGenerator: this.getTrustlineVerificationKey,
handler: (req, res, _next, options) => {
const key = this.getTrustlineVerificationKey(req);
this.recordViolation(key);
res.status(options.statusCode).json(options.message);
},
requestWasSuccessful: (_req, res) => res.statusCode < 400,
store,
passOnStoreError: true,
skip: (req) => this.isInternalService(req),
});
}
}
Expand Down Expand Up @@ -1405,5 +1514,6 @@ export const createTrustlineRateLimits = (redisClient) => {
verifications: TrustlineRateLimiter.createTrustlineVerificationRateLimit({
store,
}),
burst: TrustlineRateLimiter.createTrustlineBurstRateLimit({ store }),
};
};
Loading
Loading