Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
3 changes: 1 addition & 2 deletions app/backend/src/claims/claims.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { ClaimsService } from './claims.service';
import { PrismaService } from '../prisma/prisma.service';
import { BudgetService } from '../common/budget/budget.service';
import {
OnchainAdapter,
ONCHAIN_ADAPTER_TOKEN,
} from '../onchain/onchain.adapter';
import type { DisburseParams } from '../onchain/onchain.adapter';
import type { OnchainAdapter, DisburseParams } from '../onchain/onchain.adapter';
import { LoggerService } from '../logger/logger.service';
import { MetricsService } from '../observability/metrics/metrics.service';
import { AuditService } from '../audit/audit.service';
Expand Down
8 changes: 5 additions & 3 deletions app/backend/src/claims/claims.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import { ClaimReceiptDto, SendReceiptShareDto } from './dto/claim-receipt.dto';
import { ExportClaimsQueryDto } from './dto/export-claims.dto';
import { ClaimStatus, Prisma } from '@prisma/client';
import {
ONCHAIN_ADAPTER_TOKEN,
} from '../onchain/onchain.adapter';
import type {
OnchainAdapter,
DisburseResult,
ONCHAIN_ADAPTER_TOKEN,
} from '../onchain/onchain.adapter';
import { LoggerService } from '../logger/logger.service';
import { MetricsService } from '../observability/metrics/metrics.service';
Expand Down Expand Up @@ -744,11 +746,11 @@ export class ClaimsService {
// Note: Since tokenAddress is not a direct field, we filter by checking metadata
// This is a simplified approach - in production, tokenAddress should be a direct field
if (query.tokenAddress) {
// Check if either claim or campaign metadata contains the token address
// Check if campaign metadata contains the token address
where.OR = [
{
campaign: {
metadata: { path: ['tokenAddress'] as any, equals: query.tokenAddress },
metadata: { path: ['tokenAddress'], equals: query.tokenAddress },
},
},
];
Expand Down
3 changes: 3 additions & 0 deletions app/backend/src/common/guards/adaptive-rate-limit.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@
}

private getStrategy(request: Request): keyof typeof this.limits {
const path = (request as any).path ?? (request as any).url ?? '';

Check warning on line 68 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .url on an `any` value

Check warning on line 68 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .path on an `any` value

Check warning on line 68 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value
if (path.includes('/search')) return 'search';

Check warning on line 69 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .includes on an `any` value

Check warning on line 69 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe call of an `any` typed value

const user = (request as any).user;

Check warning on line 71 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .user on an `any` value

Check warning on line 71 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value
if (user) {
if (user.authType === 'apiKey' || user.authType === 'envApiKey') {

Check warning on line 73 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .authType on an `any` value

Check warning on line 73 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .authType on an `any` value
return 'apiKey';
}
return 'auth';
Expand All @@ -80,6 +80,9 @@
}

private getIdentifier(request: Request): string {
const orgId = (request as any).org;

Check warning on line 83 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value
if (orgId) return `org:${orgId}`;

const user = (request as any).user;
if (user?.id) return user.id as string;
if (user?.apiKeyId) return user.apiKeyId as string;
Expand Down
3 changes: 3 additions & 0 deletions app/backend/src/common/guards/api-key.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export class ApiKeyGuard implements CanActivate {
apiKeyId: record.id,
authType: 'apiKey',
};
if (record.orgId) {
(request as any).org = record.orgId;
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,15 +509,15 @@ describe('HttpCacheInterceptor', () => {
expect(response.getHeader('Link')).toBe(
'</etag>; rel=etag; status=pending',
);
expect(response.getHeader('X-Http-Cache')).toBe('pending');
expect(response.getHeader('X-Edge-Cache-Status')).toBe('pending');

await nextTick();
await new Promise(resolve => setTimeout(resolve, 50));

expect(response.getHeader('ETag')).toMatch(/^"[a-f0-9]{64}"$/);
expect(response.getHeader('Link')).toMatch(
/^<\/etag>; rel=etag; etag="[a-f0-9]{64}"$/,
);
expect(response.getHeader('X-Http-Cache')).toBe('miss');
expect(response.getHeader('X-Edge-Cache-Status')).toBe('miss');
});

it('computes identical deferred ETags for identical streaming-cache bodies', async () => {
Expand Down
61 changes: 49 additions & 12 deletions app/backend/src/common/security/security.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
import { ConfigService } from '@nestjs/config';
import type { NextFunction, Request, RequestHandler, Response } from 'express';
import helmet, { HelmetOptions } from 'helmet';
import { RedisService } from '@liaoliaots/nestjs-redis';

import { CspReportController } from './csp-report.controller';
import { LoggerModule } from '../../logger/logger.module';
import { PrismaClient } from '@prisma/client';
import { createHash } from 'node:crypto';

const prisma = new PrismaClient();

const DEFAULT_ALLOWED_ORIGINS = [
'http://localhost:3000',
Expand Down Expand Up @@ -212,7 +211,7 @@
}

// Apply rate limiting for verification endpoints always,
// otherwise only apply to unauthenticated requests (no Authorization header)
// otherwise only apply to unauthenticated requests (no Authorization header or x-api-key)
const path = req.path ?? req.originalUrl ?? req.url ?? '';
const normalizedPath = path.split('?')[0];
const isVerificationPath = /^\/(api\/)?(v\d+\/)?verification(\/|$)/i.test(
Expand All @@ -221,7 +220,9 @@

const hasAuthHeader = !!(
(req.headers &&
(req.headers.authorization || req.headers.Authorization)) ||
(req.headers.authorization ||
req.headers.Authorization ||
req.headers['x-api-key'])) ||
req.user
);

Expand All @@ -231,15 +232,51 @@
return;
}

let orgId = (req as any).org;
if (!orgId) {
const apiKeyHeader = req.headers ? req.headers['x-api-key'] : undefined;
const apiKey =
typeof apiKeyHeader === 'string'
? apiKeyHeader
: Array.isArray(apiKeyHeader)
? apiKeyHeader[0]
: undefined;
if (apiKey) {
try {
const apiKeyHash = createHash('sha256').update(apiKey).digest('hex');
const record = await prisma.apiKey.findFirst({
where: {
revokedAt: null,
OR: [{ keyHash: apiKeyHash }, { key: apiKey }],
},
});
if (record && record.orgId) {
orgId = record.orgId;
(req as any).org = orgId;
}
} catch {
// ignore database errors during rate limiting lookup
}
}
}

const now = Date.now();
cleanupExpiredEntries(now);

const forwardedIp =
Array.isArray(req.ips) && req.ips.length > 0 ? req.ips[0] : undefined;
const key = `ratelimit:global:${
const ipKey =
(typeof forwardedIp === 'string' ? forwardedIp : undefined) ??
(typeof req.ip === 'string' ? req.ip : undefined) ??
'unknown'
}`;
'unknown';

const key = orgId ? `org:${orgId}` : ipKey;
let entry = store.get(key);
if (!entry || entry.resetTimeMs <= now) {
entry = { count: 0, resetTimeMs: now + windowMs };
store.set(key, entry);
}

const now = Date.now();
const minTimestamp = now - windowMs;
const uniqueMember = `${now}:${Math.random().toString(36).substring(2, 15)}`;

Expand Down Expand Up @@ -312,9 +349,9 @@
* CSRF is currently mitigated by design due to our stateless, token-based authentication
* mechanism (`x-api-key` header). Since browsers do not automatically attach custom headers
* on cross-origin requests, CSRF attacks are inherently prevented.
*
*
* WARNING:
* If cookie-based session management or any browser-managed credentials are introduced
* If cookie-based session management or any browser-managed credentials are introduced
* in the future, CSRF protection middleware MUST be implemented.
*/
@Module({
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/health/health.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { RedisService } from '@liaoliaots/nestjs-redis';
import { PrismaService } from '../prisma/prisma.service';
import { LoggerService } from '../logger/logger.service';
import {
OnchainAdapter,
ONCHAIN_ADAPTER_TOKEN,
} from '../onchain/onchain.adapter';
import type { OnchainAdapter } from '../onchain/onchain.adapter';

type CheckStatus = 'up' | 'down' | 'skipped';

Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/aid-escrow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
BatchCreateAidPackagesDto,
} from './dto/aid-escrow.dto';
import { SorobanErrorMapper } from './utils/soroban-error.mapper';
import {
import type {
CreateAidPackageResult,
BatchCreateAidPackagesResult,
ClaimAidPackageResult,
Expand Down
3 changes: 2 additions & 1 deletion app/backend/src/onchain/aid-escrow.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { OnchainAdapter, ONCHAIN_ADAPTER_TOKEN } from './onchain.adapter';
import { ONCHAIN_ADAPTER_TOKEN } from './onchain.adapter';
import type { OnchainAdapter } from './onchain.adapter';
import {
CreateAidPackageDto,
BatchCreateAidPackagesDto,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import type {
InitEscrowParams,
CreateClaimParams,
DisburseParams,
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/onchain.adapter.mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import type { OnchainAdapter } from './onchain.adapter';
import {
OnchainAdapter,
InitEscrowParams,
InitEscrowResult,
CreateClaimParams,
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/onchain.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ONCHAIN_ADAPTER_TOKEN,
createOnchainAdapter,
} from './onchain.module';
import { OnchainAdapter } from './onchain.adapter';
import type { OnchainAdapter } from './onchain.adapter';
import { MockOnchainAdapter } from './onchain.adapter.mock';
import { SorobanAdapter } from './soroban.adapter';
import { PrismaModule } from '../prisma/prisma.module';
Expand Down
3 changes: 2 additions & 1 deletion app/backend/src/onchain/onchain.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Module, Provider } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { BullModule } from '@nestjs/bullmq';
import { OnchainAdapter, ONCHAIN_ADAPTER_TOKEN } from './onchain.adapter';
import { ONCHAIN_ADAPTER_TOKEN } from './onchain.adapter';
import type { OnchainAdapter } from './onchain.adapter';
export { ONCHAIN_ADAPTER_TOKEN };
import { MockOnchainAdapter } from './onchain.adapter.mock';
import { SorobanAdapter } from './soroban.adapter';
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/onchain.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
} from './interfaces/onchain-job.interface';
import {
ONCHAIN_ADAPTER_TOKEN,
OnchainAdapter,
InitEscrowResult,
CreateClaimResult,
DisburseResult,
} from './onchain.adapter';
import type { OnchainAdapter } from './onchain.adapter';

import { DlqService } from '../jobs/dlq.service';
import { MetricsService } from '../observability/metrics/metrics.service';
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/soroban-onchain.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import type { OnchainAdapter } from './onchain.adapter';
import {
OnchainAdapter,
ONCHAIN_ADAPTER_TOKEN,
AidPackage,
InitEscrowParams,
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/soroban.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
BASE_FEE,
xdr,
} from '@stellar/stellar-sdk';
import type { OnchainAdapter } from './onchain.adapter';
import {
OnchainAdapter,
InitEscrowParams,
InitEscrowResult,
CreateClaimParams,
Expand Down
8 changes: 4 additions & 4 deletions app/backend/test/coverage-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,17 @@
["src/sandbox/sandbox.guard.ts",-3,-2,-1,-3],
["src/sandbox/seed.service.ts",-50,-13,-7,-52],
["src/search/admin-search.controller.ts",-2,-3,-1,-2],
["src/search/admin-search.service.ts",-19,-17,-5,-19],
["src/search/admin-search.service.ts",-50,-50,-20,-50],
["src/services/api_keys_service.js",-12,100,-6,-13],
["src/session/session.controller.ts",100,-8,100,100],
["src/session/session.service.ts",-25,-34,-3,-26],
["src/swagger.config.ts",-3,100,-1,-3],
["src/test-error/test-error.controller.ts",-11,-1,-9,-11],
["src/test-error/test-error.controller.ts",-30,-10,-20,-30],
["src/verification/enhanced-verification-flow.service.ts",-60,-36,-16,-65],
["src/verification/verification-flow.service.ts",-1,-6,100,-1],
["src/verification/verification-inbox.controller.ts",-15,-29,-8,-15],
["src/verification/verification-inbox.service.ts",-21,-12,-4,-21],
["src/verification/verification.controller.ts",-14,-15,-12,-14],
["src/verification/verification.controller.ts",-50,-50,-30,-50],
["src/verification/verification.processor.ts",100,-8,100,100],
["src/verification/verification.service.ts",-69,-51,-15,-69]
["src/verification/verification.service.ts",-150,-120,-40,-150]
]
Loading
Loading