Skip to content

Commit 1a7aa38

Browse files
authored
Merge branch 'main' into test/815-stream-post-negative-cases
2 parents 3fbf6ee + 6e5cafd commit 1a7aa38

31 files changed

Lines changed: 520 additions & 417 deletions

backend/docs/DEPRECATION_POLICY.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,10 @@ X-API-Migration-Path: /v1/streams
148148
- `/streams``/v1/streams`
149149
- `/events``/v1/events`
150150

151-
**Status:** Deprecated (as of 2024-02-21)
151+
**Status:** Removed (deprecated 2024-02-21, sunset 2024-12-31, handlers deleted 2026-06-30)
152152

153-
**Sunset Date:** 2024-12-31
153+
These routes no longer exist in the codebase. Clients still calling the unversioned
154+
paths will receive a 404. Update all callers to use the `/v1/` prefix.
154155

155156
**Migration:**
156157
```javascript

backend/src/app.ts

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@ app.use(requestIdMiddleware);
3232
app.disable('x-powered-by');
3333

3434
// Helmet-equivalent core headers without external dependency.
35+
// Strict CSP applied globally; the /api-docs route overrides it below for Swagger UI.
3536
app.use((req: Request, res: Response, next: NextFunction) => {
3637
res.setHeader('X-Content-Type-Options', 'nosniff');
3738
res.setHeader('X-Frame-Options', 'DENY');
3839
res.setHeader('Referrer-Policy', 'no-referrer');
3940
res.setHeader('X-DNS-Prefetch-Control', 'off');
4041
res.setHeader('X-Download-Options', 'noopen');
4142
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
42-
if (isProduction) {
43+
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'");
44+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
45+
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
46+
if (process.env.NODE_ENV === 'production') {
4347
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
4448
}
4549
next();
@@ -78,7 +82,11 @@ app.use(express.json());
7882
app.use(sandboxMiddleware);
7983

8084
// Swagger UI setup
81-
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
85+
// Override CSP for /api-docs only: Swagger UI requires inline scripts/styles.
86+
app.use('/api-docs', (req: Request, res: Response, next: NextFunction) => {
87+
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'");
88+
next();
89+
}, swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
8290
customCss: '.swagger-ui .topbar { display: none }',
8391
customSiteTitle: 'FlowFi API Documentation',
8492
}));
@@ -105,35 +113,6 @@ app.use((req: Request, res: Response, next: NextFunction) => {
105113
return next(); // Not versioned, continue to deprecated handlers
106114
});
107115

108-
// Legacy routes (deprecated - redirect to v1)
109-
// These will be removed in a future version
110-
// Only match unversioned requests
111-
app.use('/streams', (req: Request, res: Response, next) => {
112-
res.status(410).json({
113-
error: 'Deprecated endpoint',
114-
message: 'This endpoint has been deprecated. Please use /v1/streams instead.',
115-
deprecated: true,
116-
migration: {
117-
old: '/streams',
118-
new: '/v1/streams',
119-
},
120-
sunsetDate: '2024-12-31',
121-
});
122-
});
123-
124-
app.use('/events', (req: Request, res: Response, next) => {
125-
res.status(410).json({
126-
error: 'Deprecated endpoint',
127-
message: 'This endpoint has been deprecated. Please use /v1/events instead.',
128-
deprecated: true,
129-
migration: {
130-
old: '/events',
131-
new: '/v1/events',
132-
},
133-
sunsetDate: '2024-12-31',
134-
});
135-
});
136-
137116
// Health check routes
138117
app.use('/health', healthRoutes);
139118

backend/src/controllers/sse.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export const subscribe = async (req: Request, res: Response) => {
4444
const { publicKey } = (req as AuthenticatedRequest).user;
4545
const { streams, users, all } = subscribeSchema.parse(req.query);
4646

47+
// Consistent with GET /v1/events/ (which requires requireAuth and is scoped to user's address),
48+
// SSE subscriptions are also restricted to streams owned by the authenticated user.
4749
// Scope: only streams where the authenticated user is sender or recipient
4850
const ownedStreams = await prisma.stream.findMany({
4951
where: { OR: [{ sender: publicKey }, { recipient: publicKey }] },

backend/src/middleware/error.middleware.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export const errorHandler = (
1414
) => {
1515
logger.error('Unhandled error:', err);
1616

17+
if (res.headersSent) {
18+
return next(err);
19+
}
20+
1721
// Handle Zod Validation Errors
1822
if (err instanceof ZodError) {
1923
return res.status(400).json({

backend/src/routes/v1/events.routes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Request, Response, NextFunction } from 'express';
33
import { subscribe } from '../../controllers/sse.controller.js';
44
import { sseService } from '../../services/sse.service.js';
55
import { requireAuth } from '../../middleware/auth.js';
6+
import type { AuthenticatedRequest } from '../../types/auth.types.js';
67
import { prisma } from '../../lib/prisma.js';
78
import logger from '../../logger.js';
89

@@ -65,14 +66,21 @@ export const DEFAULT_EVENTS_PAGE_SIZE = 50;
6566
* 200:
6667
* description: Paginated event list
6768
*/
68-
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
69+
router.get('/', requireAuth, async (req: Request, res: Response, next: NextFunction) => {
6970
try {
71+
const { publicKey } = (req as AuthenticatedRequest).user;
7072
const address = typeof req.query.address === 'string' ? req.query.address.trim() : '';
7173
if (!address) {
7274
res.status(400).json({ error: 'address query parameter is required' });
7375
return;
7476
}
7577

78+
// Aligned with SSE security: history queries require authentication and are scoped to the caller.
79+
if (address !== publicKey) {
80+
res.status(403).json({ error: 'Forbidden', message: 'You can only view your own event history' });
81+
return;
82+
}
83+
7684
const rawType = typeof req.query.type === 'string' ? req.query.type : '';
7785
const requested = rawType
7886
.split(',')

backend/src/workers/soroban-event-worker.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { rpc, xdr, StrKey } from '@stellar/stellar-sdk';
2-
import { prisma } from '../lib/prisma.js';
2+
import { prisma, Prisma } from '../lib/prisma.js';
33
import { INDEXER_STATE_ID } from '../lib/indexer-state.js';
44
import { sseService } from '../services/sse.service.js';
55
import logger from '../logger.js';
@@ -146,7 +146,7 @@ export class SorobanEventWorker {
146146
this.pollTimer = setTimeout(() => this.poll(), this.pollIntervalMs);
147147
}
148148

149-
private async ensureSystemStream(tx: any): Promise<void> {
149+
private async ensureSystemStream(tx: Prisma.TransactionClient): Promise<void> {
150150
const systemUser = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF';
151151
await tx.user.upsert({
152152
where: { publicKey: systemUser },
@@ -351,7 +351,7 @@ export class SorobanEventWorker {
351351
const newFeeRateBps = decodeU32(body['new_fee_rate_bps']);
352352
const timestamp = Math.floor(Date.now() / 1000);
353353

354-
await prisma.$transaction(async (tx: any) => {
354+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
355355
await this.ensureSystemStream(tx);
356356

357357
await tx.streamEvent.upsert({
@@ -399,7 +399,7 @@ export class SorobanEventWorker {
399399
const newAdmin = decodeAddress(body['new_admin']);
400400
const timestamp = Math.floor(Date.now() / 1000);
401401

402-
await prisma.$transaction(async (tx: any) => {
402+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
403403
await this.ensureSystemStream(tx);
404404

405405
await tx.streamEvent.upsert({
@@ -462,7 +462,7 @@ export class SorobanEventWorker {
462462
? null
463463
: startTime + Number(BigInt(depositedAmount) / ratePerSecondBigInt);
464464

465-
await prisma.$transaction(async (tx: any) => {
465+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
466466
await tx.user.upsert({
467467
where: { publicKey: sender },
468468
create: { publicKey: sender },
@@ -551,7 +551,7 @@ export class SorobanEventWorker {
551551
const newDepositedAmount = decodeI128(body['new_deposited_amount']);
552552
const timestamp = Math.floor(Date.now() / 1000);
553553

554-
await prisma.$transaction(async (tx: any) => {
554+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
555555
const stream = await tx.stream.findUniqueOrThrow({
556556
where: { streamId },
557557
select: { ratePerSecond: true, startTime: true, totalPausedDuration: true }
@@ -622,7 +622,7 @@ export class SorobanEventWorker {
622622
const amount = decodeI128(body['amount']);
623623
const timestamp = Number(decodeU64(body['timestamp']));
624624

625-
await prisma.$transaction(async (tx: any) => {
625+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
626626
const stream = await tx.stream.findUniqueOrThrow({
627627
where: { streamId },
628628
select: { withdrawnAmount: true },
@@ -688,7 +688,7 @@ export class SorobanEventWorker {
688688
const refundedAmount = decodeI128(body['refunded_amount']);
689689
const timestamp = Math.floor(Date.now() / 1000);
690690

691-
await prisma.$transaction(async (tx: any) => {
691+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
692692
await tx.stream.update({
693693
where: { streamId },
694694
data: {
@@ -746,7 +746,7 @@ export class SorobanEventWorker {
746746
const totalWithdrawn = decodeI128(body['total_withdrawn']);
747747
const timestamp = Math.floor(Date.now() / 1000);
748748

749-
await prisma.$transaction(async (tx: any) => {
749+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
750750
await tx.stream.update({
751751
where: { streamId },
752752
data: {
@@ -853,7 +853,7 @@ export class SorobanEventWorker {
853853
const pausedAt = Number(decodeU64(body['paused_at']));
854854
const timestamp = Math.floor(Date.now() / 1000);
855855

856-
await prisma.$transaction(async (tx: any) => {
856+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
857857
await tx.stream.update({
858858
where: { streamId },
859859
data: {
@@ -910,7 +910,7 @@ export class SorobanEventWorker {
910910
const newEndTime = Number(decodeU64(body['new_end_time']));
911911
const timestamp = Math.floor(Date.now() / 1000);
912912

913-
await prisma.$transaction(async (tx: any) => {
913+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
914914
// Get current stream to calculate paused duration
915915
const currentStream = await tx.stream.findUniqueOrThrow({
916916
where: { streamId },

0 commit comments

Comments
 (0)