Skip to content

Commit 86765e5

Browse files
committed
Merge branch 'main' of https://github.com/Samuel1505/flowfi into four
# Conflicts: # backend/src/controllers/sse.controller.ts # docs/ARCHITECTURE.md # docs/DEVELOPMENT.md # frontend/src/components/dashboard/dashboard-view.tsx # frontend/src/components/ui/Skeleton.tsx # package-lock.json
2 parents 9412503 + 7b24e40 commit 86765e5

95 files changed

Lines changed: 8523 additions & 4067 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ jobs:
3131
run: npm run lint
3232
working-directory: frontend
3333

34+
- name: Install Rollup Native Binding
35+
run: npm install @rollup/rollup-linux-x64-gnu --no-save
36+
working-directory: frontend
37+
38+
- name: Run Frontend Tests
39+
run: npm test
40+
working-directory: frontend
41+
3442
- name: Build
3543
run: npm run build
3644
working-directory: frontend
@@ -116,3 +124,30 @@ jobs:
116124
- name: Run Contract Tests
117125
run: cargo test
118126
working-directory: contracts
127+
128+
- name: Install Stellar CLI
129+
run: |
130+
curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh -s -- --install-deps
131+
shell: bash
132+
133+
- name: Optimize WASM files
134+
run: |
135+
set -euo pipefail
136+
WASMS=$(find contracts/target -type f -name "*.wasm" -print)
137+
if [ -z "$WASMS" ]; then
138+
echo "No wasm files found"
139+
exit 1
140+
fi
141+
for w in $WASMS; do
142+
out="${w%%.wasm}.optimized.wasm"
143+
echo "Optimizing $w -> $out"
144+
stellar contract optimize --wasm "$w" --wasm-out "$out"
145+
done
146+
shell: bash
147+
148+
- name: Upload optimized WASM artifacts
149+
uses: actions/upload-artifact@v4
150+
with:
151+
name: optimized-wasm
152+
path: |
153+
contracts/target/**/**/*.optimized.wasm

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"kiroAgent.configureMCP": "Disabled"
3+
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ FlowFi consists of three main components that work together:
3737

3838
For a detailed explanation of how these components interact, where event indexing happens, and the overall system architecture, see the [Architecture Documentation](docs/ARCHITECTURE.md).
3939

40+
For full local setup and contributor onboarding, see the [Development Guide](docs/DEVELOPMENT.md).
41+
4042
## Getting Started
4143

4244
For full step-by-step instructions, see our [Development Guide](docs/DEVELOPMENT.md).
@@ -180,6 +182,8 @@ Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md)
180182
- Pull request process
181183
- Development scripts and CI workflows
182184

185+
Before your first change, run through the [Development Guide](docs/DEVELOPMENT.md) and review [Architecture Documentation](docs/ARCHITECTURE.md).
186+
183187
For architecture details, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
184188

185189
## Security

backend/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ DATABASE_URL="postgresql://user:password@localhost:5432/flowfi?schema=public"
55
PORT=3001
66
NODE_ENV=development
77
CORS_ALLOWED_ORIGINS="https://app.flowfi.xyz,https://flowfi.xyz"
8+
# Comma-separated list of allowed origins for CORS. In development, if unset,
9+
# defaults to http://localhost:3000
810

911
# Stellar Network (Testnet/Mainnet)
1012
STELLAR_NETWORK=testnet

backend/src/app.ts

Lines changed: 23 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ import { sandboxMiddleware } from './middleware/sandbox.middleware.js';
77
import { globalRateLimiter } from './middleware/rate-limiter.middleware.js';
88
import v1Routes from './routes/v1/index.js';
99

10+
import healthRoutes from './routes/health.routes.js';
11+
1012
const app = express();
1113
const isProduction = process.env.NODE_ENV === 'production';
12-
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS ?? '')
14+
const rawCors = process.env.CORS_ALLOWED_ORIGINS ?? '';
15+
const allowedOrigins = rawCors
1316
.split(',')
1417
.map((origin) => origin.trim())
1518
.filter(Boolean);
1619

20+
// Default in development to only localhost:3000 (frontend dev server)
21+
if (!process.env.CORS_ALLOWED_ORIGINS && !isProduction) {
22+
allowedOrigins.push('http://localhost:3000');
23+
}
24+
1725
// Apply global rate limiter first
1826
app.use(globalRateLimiter);
1927

@@ -35,11 +43,6 @@ app.use((req: Request, res: Response, next: NextFunction) => {
3543

3644
app.use(cors({
3745
origin(origin, callback) {
38-
if (!isProduction) {
39-
callback(null, true);
40-
return;
41-
}
42-
4346
// Allow non-browser clients (no Origin header)
4447
if (!origin) {
4548
callback(null, true);
@@ -51,10 +54,20 @@ app.use(cors({
5154
return;
5255
}
5356

57+
// Not allowed
5458
callback(new Error('CORS origin not allowed'));
5559
},
5660
credentials: true,
5761
}));
62+
63+
// Convert CORS errors into 403 responses so callers get a clear status code
64+
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
65+
if (err && err.message === 'CORS origin not allowed') {
66+
res.status(403).json({ error: 'CORS origin not allowed' });
67+
return;
68+
}
69+
next(err);
70+
});
5871
app.use(express.json());
5972

6073
// Sandbox mode detection (before versioning)
@@ -117,117 +130,25 @@ app.use('/events', (req: Request, res: Response, next) => {
117130
});
118131
});
119132

133+
// Health check routes
134+
app.use('/health', healthRoutes);
135+
120136
/**
121137
* @openapi
122138
* /:
123139
* get:
124140
* tags:
125141
* - Health
126-
* summary: Health check endpoint
142+
* summary: Simple health check
127143
* description: Returns a simple message to verify the API is running
128144
* responses:
129145
* 200:
130146
* description: API is running successfully
131-
* content:
132-
* text/plain:
133-
* schema:
134-
* type: string
135-
* example: FlowFi Backend is running
136147
*/
137148
app.get('/', (req: Request, res: Response) => {
138149
res.send('FlowFi Backend is running');
139150
});
140151

141-
/**
142-
* @openapi
143-
* /health:
144-
* get:
145-
* tags:
146-
* - Health
147-
* summary: Detailed health check
148-
* description: Returns detailed health information about the API
149-
* responses:
150-
* 200:
151-
* description: Health check details
152-
* content:
153-
* application/json:
154-
* schema:
155-
* type: object
156-
* properties:
157-
* status:
158-
* type: string
159-
* example: healthy
160-
* timestamp:
161-
* type: string
162-
* format: date-time
163-
* example: 2024-02-21T14:30:00.000Z
164-
* uptime:
165-
* type: number
166-
* description: Server uptime in seconds
167-
* example: 3600
168-
* version:
169-
* type: string
170-
* example: 1.0.0
171-
* apiVersions:
172-
* type: object
173-
* properties:
174-
* supported:
175-
* type: array
176-
* items:
177-
* type: string
178-
* example: ["v1"]
179-
* default:
180-
* type: string
181-
* example: "v1"
182-
*/
183-
app.get('/health', async (req: Request, res: Response) => {
184-
const { getSandboxConfig } = await import('./config/sandbox.js');
185-
const { prisma } = await import('./lib/prisma.js');
186-
const sandboxConfig = getSandboxConfig();
187-
188-
let dbStatus = 'healthy';
189-
try {
190-
await prisma.$queryRaw`SELECT 1`;
191-
} catch {
192-
dbStatus = 'unhealthy';
193-
}
194-
195-
let indexerStatus = 'unknown';
196-
let indexerLastLedger: number | null = null;
197-
try {
198-
const state = await prisma.indexerState.findUnique({ where: { id: 'singleton' } });
199-
if (state) {
200-
indexerLastLedger = state.lastLedger;
201-
indexerStatus = 'running';
202-
} else {
203-
indexerStatus = 'not_started';
204-
}
205-
} catch {
206-
indexerStatus = 'error';
207-
}
208-
209-
const status = dbStatus === 'healthy' ? 'healthy' : 'unhealthy';
210-
res.status(status === 'healthy' ? 200 : 503).json({
211-
status,
212-
db: dbStatus,
213-
indexer: { status: indexerStatus, lastLedger: indexerLastLedger },
214-
uptime: process.uptime(),
215-
timestamp: new Date().toISOString(),
216-
version: '1.0.0',
217-
apiVersions: {
218-
supported: ['v1'],
219-
default: 'v1',
220-
},
221-
services: {
222-
database: dbStatus,
223-
},
224-
sandbox: {
225-
enabled: sandboxConfig.enabled,
226-
available: sandboxConfig.enabled,
227-
},
228-
});
229-
});
230-
231152
import { errorHandler } from './middleware/error.middleware.js';
232153

233154
app.use(errorHandler);

backend/src/controllers/sse.controller.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,36 @@ const subscribeSchema = z.object({
99
all: z.boolean().optional().default(false),
1010
});
1111

12+
13+
function getClientIp(req: Request): string {
14+
const forwarded = req.headers['x-forwarded-for'];
15+
if (typeof forwarded === 'string' && forwarded.trim().length > 0) {
16+
return forwarded.split(',')[0]?.trim() || 'unknown';
17+
}
18+
19+
if (Array.isArray(forwarded) && forwarded.length > 0) {
20+
return forwarded[0] ?? 'unknown';
21+
}
22+
23+
return req.ip || req.socket.remoteAddress || 'unknown';
24+
}
1225
export const subscribe = async (req: Request, res: Response) => {
1326
if (sseService.isShuttingDown()) {
1427
return res.status(503).json({ message: 'Server is shutting down, please reconnect shortly.' });
1528
}
1629

1730
try {
31+
const sourceIp = getClientIp(req);
32+
const capacity = sseService.checkCapacity(sourceIp);
33+
if (!capacity.allowed) {
34+
if (capacity.retryAfterSeconds) {
35+
res.setHeader('Retry-After', String(capacity.retryAfterSeconds));
36+
}
37+
return res.status(capacity.status ?? 503).json({
38+
message: capacity.message ?? 'SSE connection rejected',
39+
});
40+
}
41+
1842
const { publicKey } = (req as AuthenticatedRequest).user;
1943
const { streams, all } = subscribeSchema.parse(req.query);
2044

@@ -23,17 +47,17 @@ export const subscribe = async (req: Request, res: Response) => {
2347
where: { OR: [{ sender: publicKey }, { recipient: publicKey }] },
2448
select: { streamId: true },
2549
});
26-
const ownedIds = new Set(ownedStreams.map((s) => String(s.streamId)));
50+
const ownedIds = new Set(ownedStreams.map((s: any) => String(s.streamId)));
2751

2852
let subscriptions: string[];
2953
if (all) {
3054
// "all" still scoped to the user's own streams
31-
subscriptions = [...ownedIds];
55+
subscriptions = [...ownedIds] as string[];
3256
} else if (streams.length > 0) {
3357
// Only allow subscribing to streams the user owns
3458
subscriptions = streams.filter((id) => ownedIds.has(id));
3559
} else {
36-
subscriptions = [...ownedIds];
60+
subscriptions = [...ownedIds] as string[];
3761
}
3862

3963
// Always add user-scoped subscription key
@@ -50,7 +74,7 @@ export const subscribe = async (req: Request, res: Response) => {
5074

5175
res.write(`data: ${JSON.stringify({ type: 'connected', clientId })}\n\n`);
5276

53-
sseService.addClient(clientId, res, subscriptions);
77+
sseService.addClient(clientId, res, subscriptions, sourceIp);
5478
} catch (error: any) {
5579
if (error.name === 'ZodError') {
5680
return res.status(400).json({

0 commit comments

Comments
 (0)