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
37 changes: 31 additions & 6 deletions backend/package-lock.json

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

5 changes: 3 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.2",
"p-limit": "^4.0.0",

"swagger-ui-express": "^5.0.1",
"ws": "^8.20.0",
"zod": "^4.3.6"
Expand All @@ -34,10 +35,10 @@
"@types/supertest": "^6.0.2",
"@types/swagger-ui-express": "^4.1.8",
"@types/ws": "^8.5.10",
"@vitest/coverage-v8": "^1.0.0",
"supertest": "^7.0.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.2.2",
"vitest": "^1.0.0",
"@vitest/coverage-v8": "^1.0.0"
"vitest": "^1.0.0"
}
}
14 changes: 7 additions & 7 deletions backend/src/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('Authentication Logic & Middleware', () => {
expect(response.status).toBe(401);
expect(response.body).toMatchObject({
error: "Missing or invalid authorization header.",
code: "UNAUTHORIZED",
code: "unauthorized",
});
});

Expand All @@ -55,7 +55,7 @@ describe('Authentication Logic & Middleware', () => {
.set('Authorization', 'Basic wrongformat');

expect(response.status).toBe(401);
expect(response.body.code).toBe('UNAUTHORIZED');
expect(response.body.code).toBe('unauthorized');
});

it('should reject requests with an invalid token (401)', async () => {
Expand All @@ -65,15 +65,15 @@ describe('Authentication Logic & Middleware', () => {

expect(response.status).toBe(401);
expect(response.body).toMatchObject({
error: "Invalid or expired authorization token.",
code: "UNAUTHORIZED",
error: "Invalid authorization token.",
code: "invalid_token",
});
});

it('should reject requests with an expired token (401)', async () => {
const expiredToken = jwt.sign(
{ accountId: testAccountId },
TEST_SECRET,
getJwtSecret(),
{ expiresIn: '-1h' }
);

Expand All @@ -83,8 +83,8 @@ describe('Authentication Logic & Middleware', () => {

expect(response.status).toBe(401);
expect(response.body).toMatchObject({
error: "Invalid or expired authorization token.",
code: "UNAUTHORIZED",
error: "Authorization token has expired.",
code: "token_expired",
});
});

Expand Down
23 changes: 14 additions & 9 deletions backend/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const streamStoreMocks = vi.hoisted(() => ({
getStream: vi.fn(),
initSoroban: vi.fn(),
listStreams: vi.fn(),
listStreamsBySender: vi.fn(),
syncStreams: vi.fn(),
updateStreamStartAt: vi.fn(),
}));
Expand Down Expand Up @@ -53,11 +54,11 @@ type TestProgress = {
percentComplete: number;
};

const SENDER_A = "GA6W6AAAAAAAAAAW6AAAAAAAAAAW6AAAAAAAAAAW6AAAAAAAAAAW6AAA";
const SENDER_B = "GA6W6BBBBBBBBBBW6BBBBBBBBBBW6BBBBBBBBBBW6BBBBBBBBBBW6BBB";
const SENDER_C = "GA6W6CCCCCCCCCCW6CCCCCCCCCCW6CCCCCCCCCCW6CCCCCCCCCCW6CCC";
const RECIPIENT_1 = "GA6W61111111111W61111111111W61111111111W61111111111W6111";
const RECIPIENT_2 = "GA6W62222222222W62222222222W62222222222W62222222222W6222";
const SENDER_A = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
const SENDER_B = "GBLHBYX72TJQH5EVPUN4ATAREH6TWYXQAH37MHNCVQG2NKLHFDSMFS3D";
const SENDER_C = "GANNU4KAOYHV6FSY7Z44QWUEUCRBH56Y5BOP6NP6OKU3AUL3B54V34HU";
const RECIPIENT_1 = "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
const RECIPIENT_2 = "GBRPYHIL2CI3WHZDTOOQFC6EB4CGQOFSNQB3BHPOMONNHJGKYJPJJYFF";

const streams: TestStream[] = [
{
Expand Down Expand Up @@ -212,6 +213,9 @@ beforeEach(() => {
streamStoreMocks.listStreams.mockReturnValue(streams);
streamStoreMocks.calculateProgress.mockImplementation((stream: TestStream) => progressById[stream.id]);

streamStoreMocks.listStreamsBySender.mockReset();
streamStoreMocks.listStreamsBySender.mockImplementation((sender: string) => streams.filter(s => s.sender === sender));

eventHistoryMocks.getGlobalEvents.mockReset();
eventHistoryMocks.countAllEvents.mockReset();
eventHistoryMocks.getStreamHistory.mockReset();
Expand Down Expand Up @@ -370,7 +374,7 @@ describe("GET /api/senders/:accountId/streams", () => {
});

it("filters by search term", () => {
const { status, body } = invokeSenderStreamsRoute(SENDER_A, { q: "GA6W6AAAAAAAAAAW6AAAAAAAAAAW6AAAAAAAAAAW6AAAAAAAAAAW6AAA" });
const { status, body } = invokeSenderStreamsRoute(SENDER_A, { q: SENDER_A });

expect(status).toBe(200);
expect(body.total).toBe(2);
Expand All @@ -380,7 +384,7 @@ describe("GET /api/senders/:accountId/streams", () => {
const { status, body } = invokeSenderStreamsRoute("invalid_account");

expect(status).toBe(400);
expect(body.error).toContain("Must be a valid Stellar account ID");
expect(body.error.toLowerCase()).toContain("must be a valid stellar account id");
expect(body.statusCode).toBe(400);
expect(body.requestId).toBe("test-request-id");
expect(body.code).toBe("VALIDATION_ERROR");
Expand Down Expand Up @@ -473,6 +477,7 @@ describe("GET /api/events", () => {
expect.any(Number),
expect.any(Number),
"created",
undefined,
);
});

Expand All @@ -488,7 +493,7 @@ describe("GET /api/events", () => {
expect(body.total).toBe(4);
expect(body.data).toHaveLength(2);
// offset should be (2-1)*2 = 2
expect(eventHistoryMocks.getGlobalEvents).toHaveBeenCalledWith(2, 2, undefined);
expect(eventHistoryMocks.getGlobalEvents).toHaveBeenCalledWith(2, 2, undefined, undefined);
});

it("uses default limit of 20 when only page is provided", () => {
Expand All @@ -509,7 +514,7 @@ describe("GET /api/events", () => {
expect(status).toBe(200);
expect(body.page).toBe(1);
expect(body.limit).toBe(2);
expect(eventHistoryMocks.getGlobalEvents).toHaveBeenCalledWith(2, 0, undefined);
expect(eventHistoryMocks.getGlobalEvents).toHaveBeenCalledWith(2, 0, undefined, undefined);
});

it("returns 400 for an invalid eventType", () => {
Expand Down
8 changes: 4 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,7 @@ app.post(
"/api/streams/:id/pause",
mutationLimiter,
authMiddleware,
(req: Request, res: Response) => {
async (req: Request, res: Response) => {
const parsedId = parseStreamId(req.params.id);
if (!parsedId.ok) {
sendValidationError(req, res, parsedId.issues);
Expand All @@ -832,7 +832,7 @@ app.post(
}

try {
const updated = pauseStream(parsedId.value);
const updated = await pauseStream(parsedId.value);
res.json({ data: { ...updated, progress: calculateProgress(updated) } });
} catch (error: any) {
const normalizedError = normalizeUnknownApiError(
Expand All @@ -857,7 +857,7 @@ app.post(
"/api/streams/:id/resume",
mutationLimiter,
authMiddleware,
(req: Request, res: Response) => {
async (req: Request, res: Response) => {
const parsedId = parseStreamId(req.params.id);
if (!parsedId.ok) {
sendValidationError(req, res, parsedId.issues);
Expand All @@ -879,7 +879,7 @@ app.post(
}

try {
const updated = resumeStream(parsedId.value);
const updated = await resumeStream(parsedId.value);
res.json({ data: { ...updated, progress: calculateProgress(updated) } });
} catch (error: any) {
const normalizedError = normalizeUnknownApiError(
Expand Down
2 changes: 1 addition & 1 deletion backend/src/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ describe("Backend Integration Tests", () => {
// Clean database before each test
const db = getDb();
db.exec("DELETE FROM stream_events");
db.exec("DELETE FROM streams");
db.exec("DELETE FROM webhook_deliveries");
db.exec("DELETE FROM streams");
});

afterAll(() => {
Expand Down
10 changes: 8 additions & 2 deletions backend/src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,15 @@ export async function verifyChallengeAndIssueToken(
return token;
} catch (error: any) {
if (error.message?.includes("TimeBounds")) {
throw new Error("Challenge has expired. Please request a new one.");
const err = new Error("Challenge has expired. Please request a new one.");
(err as any).statusCode = 401;
(err as any).code = "UNAUTHORIZED";
throw err;
}
throw new Error(`Challenge verification failed: ${error.message}`);
const err = new Error(`Challenge verification failed: ${error.message}`);
(err as any).statusCode = 401;
(err as any).code = "UNAUTHORIZED";
throw err;
Comment on lines +266 to +274
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use lowercase auth error codes for challenge failures.

verifyChallengeAndIssueToken now emits "UNAUTHORIZED" while the rest of auth paths use lowercase codes. This creates an inconsistent API contract (Line 170, Line 175).

Proposed fix
-      (err as any).code = "UNAUTHORIZED";
+      (err as any).code = "unauthorized";
...
-    (err as any).code = "UNAUTHORIZED";
+    (err as any).code = "unauthorized";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const err = new Error("Challenge has expired. Please request a new one.");
(err as any).statusCode = 401;
(err as any).code = "UNAUTHORIZED";
throw err;
}
throw new Error(`Challenge verification failed: ${error.message}`);
const err = new Error(`Challenge verification failed: ${error.message}`);
(err as any).statusCode = 401;
(err as any).code = "UNAUTHORIZED";
throw err;
const err = new Error("Challenge has expired. Please request a new one.");
(err as any).statusCode = 401;
(err as any).code = "unauthorized";
throw err;
}
const err = new Error(`Challenge verification failed: ${error.message}`);
(err as any).statusCode = 401;
(err as any).code = "unauthorized";
throw err;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/auth.ts` around lines 168 - 176, In
verifyChallengeAndIssueToken, replace the uppercase error code "UNAUTHORIZED"
with the lowercase "unauthorized" for both thrown errors (the expired-challenge
error and the challenge verification-failed error) so the auth API uses
consistent lowercase error codes; update the (err as any).code assignments in
those two throw paths to "unauthorized".

}
}

Expand Down
Loading