Skip to content

Commit 83a507c

Browse files
authored
Merge branch 'main' into docs/backend-env-example-completeness
2 parents 5537e63 + 8b1e94f commit 83a507c

30 files changed

Lines changed: 188 additions & 129 deletions

backend/prisma/schema.prisma

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,4 @@ model StreamEvent {
8787
@@index([transactionHash])
8888
@@index([createdAt])
8989
@@index([streamId, timestamp])
90-
@@unique([transactionHash, eventType])
9190
}

backend/src/lib/indexer-state.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import { prisma } from './prisma.js';
2+
import type { IndexerState } from '../generated/prisma/index.js';
23
import logger from '../logger.js';
34

45
export const INDEXER_STATE_ID = 'singleton';
56

6-
export interface IndexerStateRow {
7-
id: string;
8-
lastLedger: number;
9-
lastCursor: string | null;
10-
createdAt: Date;
11-
updatedAt: Date;
12-
}
7+
export type IndexerStateRow = IndexerState;
138

149
/**
1510
* Ensure the singleton indexer_state row exists.

backend/src/middleware/admin-rate-limiter.middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const adminRateLimiter = rateLimit({
1515
// Use x-forwarded-for or remote address as key
1616
const forwarded = req.headers['x-forwarded-for'];
1717
if (typeof forwarded === 'string') {
18-
return forwarded.split(',')[0].trim();
18+
return forwarded.split(',')[0]?.trim() ?? 'unknown';
1919
}
2020
return req.ip ?? 'unknown';
2121
},

backend/src/middleware/auth.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ if (isProduction) {
3131

3232
const JWT_EXPIRY_SECONDS = 3600; // 1 hour max per spec
3333

34+
const JWT_ISSUER = process.env.JWT_ISSUER || 'flowfi-api';
35+
const JWT_AUDIENCE = process.env.JWT_AUDIENCE || 'flowfi-api';
36+
3437
const STELLAR_NETWORK =
3538
process.env.STELLAR_NETWORK === 'mainnet'
3639
? StellarSdk.Networks.PUBLIC
@@ -130,6 +133,11 @@ export function verifyJwt(token: string): { publicKey: string } | null {
130133
return null;
131134
}
132135

136+
// Verify issuer and audience
137+
if (payload.iss !== JWT_ISSUER || payload.aud !== JWT_AUDIENCE) {
138+
return null;
139+
}
140+
133141
return { publicKey: payload.sub };
134142
} catch {
135143
return null;
@@ -205,7 +213,7 @@ export function verifyChallenge(req: Request, res: Response): void {
205213
challenges.delete(publicKey);
206214

207215
const now = Math.floor(Date.now() / 1000);
208-
const token = signJwt({ sub: publicKey, iat: now, exp: now + JWT_EXPIRY_SECONDS });
216+
const token = signJwt({ sub: publicKey, iat: now, exp: now + JWT_EXPIRY_SECONDS, iss: JWT_ISSUER, aud: JWT_AUDIENCE });
209217
res.json({ token, expiresIn: JWT_EXPIRY_SECONDS });
210218
} catch (err) {
211219
logger.error('[Auth] verifyChallenge error:', err);

backend/src/routes/health.routes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Router, type Request, type Response } from 'express';
22
import { prisma } from '../lib/prisma.js';
33
import { INDEXER_STATE_ID } from '../lib/indexer-state.js';
4+
import { isRedisAvailable } from '../lib/redis.js';
5+
import { checkRpcHealth } from '../services/sorobanService.js';
46
import { sorobanEventWorker } from '../workers/soroban-event-worker.js';
57

68
const router = Router();
@@ -161,7 +163,7 @@ router.get('/', async (_req: Request, res: Response) => {
161163
status: dbStatus === 'connected' ? 'ok' : 'down',
162164
},
163165
indexer: {
164-
status: !indexerEnabled ? 'disabled' : indexerDegraded ? 'degraded' : 'ok',
166+
status: !indexerEnabled ? 'disabled' : indexerFailureDegraded ? 'degraded' : 'ok',
165167
enabled: indexerEnabled,
166168
lagSeconds: indexerLag === -1 ? null : indexerLag,
167169
},

backend/src/services/soroban-indexer.service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,8 @@ export class SorobanIndexerService {
184184
const tokenAddress = this.readString(value, 'token_address', 'tokenAddress');
185185
const ratePerSecond = this.readString(value, 'rate_per_second', 'ratePerSecond');
186186
const depositedAmount = this.readString(value, 'deposited_amount', 'depositedAmount');
187-
const startTimeRaw = value.start_time ?? value.startTime ?? timestamp;
188-
const startTime = BigInt(startTimeRaw ?? timestamp);
189-
const timestampBigInt = BigInt(timestamp);
187+
const startTimeStr = this.readString(value, 'start_time', 'startTime') ?? String(timestamp);
188+
const startTime = BigInt(startTimeStr);
190189

191190
if (!sender || !recipient || !tokenAddress || !ratePerSecond || !depositedAmount) return;
192191

backend/src/types/auth.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ export interface SEP10TokenPayload {
3030
sub: string; // Stellar public key
3131
iat: number; // Issued at
3232
exp: number; // Expiration
33+
iss: string; // Issuer
34+
aud: string; // Audience
3335
}

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { randomUUID } from "crypto";
21
import { rpc, xdr, StrKey } from "@stellar/stellar-sdk";
32
import { prisma } from "../lib/prisma.js";
43
import { INDEXER_STATE_ID, ensureIndexerState } from "../lib/indexer-state.js";
54
import { sseService } from "../services/sse.service.js";
6-
import logger, { requestContext } from "../logger.js";
5+
import logger from "../logger.js";
76
import { Prisma } from "../generated/prisma/index.js";
87
import "../lib/stream-id.js";
98

@@ -113,13 +112,6 @@ export class SorobanEventWorker {
113112
/** Recent attempt outcomes for sliding-window spike detection. */
114113
private recentOutcomes: { ok: boolean; at: number }[] = [];
115114

116-
/**
117-
* Stable id attached to every log line emitted by the background poll
118-
* loop, since these callbacks fire outside of any HTTP request and would
119-
* otherwise have no requestContext (and thus no correlation id) at all.
120-
*/
121-
private readonly workerId = `soroban-worker:${randomUUID()}`;
122-
123115
constructor() {
124116
const rpcUrl =
125117
process.env.SOROBAN_RPC_URL ?? "https://soroban-testnet.stellar.org";
@@ -1125,11 +1117,9 @@ export class SorobanEventWorker {
11251117
select: { pausedAt: true, totalPausedDuration: true },
11261118
});
11271119

1128-
// Calculate the duration of this pause interval
1129-
let additionalPausedDuration = 0;
1130-
if (currentStream.pausedAt) {
1131-
additionalPausedDuration = timestamp - currentStream.pausedAt;
1132-
}
1120+
const additionalPausedDuration = currentStream.pausedAt
1121+
? timestamp - Number(currentStream.pausedAt)
1122+
: 0;
11331123

11341124
const newTotalPausedDuration =
11351125
currentStream.totalPausedDuration + additionalPausedDuration;

backend/tests/auth-jwt.test.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ describe('JWT helpers', () => {
1616

1717
it('round-trips through verifyJwt', async () => {
1818
const now = Math.floor(Date.now() / 1000);
19-
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 });
19+
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'flowfi-api' });
2020

2121
expect(verifyJwt(token)).toEqual({ publicKey: 'GTESTPUBLICKEY123' });
2222
});
2323

2424
it('returns null for a tampered header', async () => {
2525
const now = Math.floor(Date.now() / 1000);
26-
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 });
26+
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'flowfi-api' });
2727
const parts = token.split('.') as [string, string, string];
2828
parts[0] = parts[0].slice(0, -1) + (parts[0].slice(-1) === 'A' ? 'B' : 'A');
2929

@@ -32,7 +32,7 @@ describe('JWT helpers', () => {
3232

3333
it('returns null for a tampered body', async () => {
3434
const now = Math.floor(Date.now() / 1000);
35-
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 });
35+
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'flowfi-api' });
3636
const parts = token.split('.') as [string, string, string];
3737
parts[1] = parts[1].slice(0, -1) + (parts[1].slice(-1) === 'A' ? 'B' : 'A');
3838

@@ -41,7 +41,7 @@ describe('JWT helpers', () => {
4141

4242
it('returns null for a tampered signature', async () => {
4343
const now = Math.floor(Date.now() / 1000);
44-
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 });
44+
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'flowfi-api' });
4545
const parts = token.split('.') as [string, string, string];
4646
// Replace the signature with invalid data to ensure verification fails
4747
parts[2] = 'invalid-signature-data-1234567890abcdef';
@@ -51,7 +51,21 @@ describe('JWT helpers', () => {
5151

5252
it('returns null for an expired token', async () => {
5353
const now = Math.floor(Date.now() / 1000);
54-
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now - 3600, exp: now - 1 });
54+
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now - 3600, exp: now - 1, iss: 'flowfi-api', aud: 'flowfi-api' });
55+
56+
expect(verifyJwt(token)).toBeNull();
57+
});
58+
59+
it('returns null for a token with wrong audience', async () => {
60+
const now = Math.floor(Date.now() / 1000);
61+
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'wrong-audience' });
62+
63+
expect(verifyJwt(token)).toBeNull();
64+
});
65+
66+
it('returns null for a token with wrong issuer', async () => {
67+
const now = Math.floor(Date.now() / 1000);
68+
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'wrong-issuer', aud: 'flowfi-api' });
5569

5670
expect(verifyJwt(token)).toBeNull();
5771
});

backend/tests/auth.test.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,13 @@ describe('Authentication & Middleware Tests', () => {
307307

308308
it('test_admin_middleware_rejects_non_admin_token', async () => {
309309
const nonAdminKeypair = makeKeypair();
310+
const now = Math.floor(Date.now() / 1000);
310311
const token = signJwt({
311312
sub: nonAdminKeypair.publicKey(),
312-
exp: Math.floor(Date.now() / 1000) + 3600,
313+
iat: now,
314+
exp: now + 3600,
315+
iss: 'flowfi-api',
316+
aud: 'flowfi-api',
313317
});
314318

315319
// Set admin key to something else
@@ -326,9 +330,13 @@ describe('Authentication & Middleware Tests', () => {
326330

327331
it('test_admin_middleware_accepts_admin_token', async () => {
328332
const adminKeypair = makeKeypair();
333+
const now = Math.floor(Date.now() / 1000);
329334
const token = signJwt({
330335
sub: adminKeypair.publicKey(),
331-
exp: Math.floor(Date.now() / 1000) + 3600,
336+
iat: now,
337+
exp: now + 3600,
338+
iss: 'flowfi-api',
339+
aud: 'flowfi-api',
332340
});
333341

334342
process.env.ADMIN_PUBLIC_KEY = adminKeypair.publicKey();
@@ -343,9 +351,13 @@ describe('Authentication & Middleware Tests', () => {
343351

344352
it('test_admin_middleware_fails_closed_when_key_unset', async () => {
345353
const keypair = makeKeypair();
354+
const now = Math.floor(Date.now() / 1000);
346355
const token = signJwt({
347356
sub: keypair.publicKey(),
348-
exp: Math.floor(Date.now() / 1000) + 3600,
357+
iat: now,
358+
exp: now + 3600,
359+
iss: 'flowfi-api',
360+
aud: 'flowfi-api',
349361
});
350362

351363
// Unset the admin key
@@ -371,9 +383,13 @@ describe('Authentication & Middleware Tests', () => {
371383

372384
it('test_events_endpoint_allows_authenticated_matching_address', async () => {
373385
const keypair = makeKeypair();
386+
const now = Math.floor(Date.now() / 1000);
374387
const token = signJwt({
375388
sub: keypair.publicKey(),
376-
exp: Math.floor(Date.now() / 1000) + 3600,
389+
iat: now,
390+
exp: now + 3600,
391+
iss: 'flowfi-api',
392+
aud: 'flowfi-api',
377393
});
378394

379395
const res = await request(app)
@@ -388,9 +404,13 @@ describe('Authentication & Middleware Tests', () => {
388404
it('test_events_endpoint_rejects_authenticated_mismatched_address', async () => {
389405
const keypair = makeKeypair();
390406
const otherKeypair = makeKeypair();
407+
const now = Math.floor(Date.now() / 1000);
391408
const token = signJwt({
392409
sub: keypair.publicKey(),
393-
exp: Math.floor(Date.now() / 1000) + 3600,
410+
iat: now,
411+
exp: now + 3600,
412+
iss: 'flowfi-api',
413+
aud: 'flowfi-api',
394414
});
395415

396416
const res = await request(app)

0 commit comments

Comments
 (0)