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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
"jest": "^26.0.0"
},
"jest": {
"moduleNameMapper": {
"^axios$": "axios/dist/node/axios.cjs"
},
"transform": {
"\\.(ts|tsx)$": "ts-jest",
"\\.css$": "<rootDir>/node_modules/razzle/config/jest/cssTransform.js",
Expand Down
46 changes: 45 additions & 1 deletion src/server/handlers/private-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { signupClientIp } from "./private-api";
import { parseSupportSettingsPayload, signupClientIp } from "./private-api";

// Minimal express.Request stand-in: signupClientIp only reads `headers`.
const reqWith = (headers: Record<string, string | string[]>): any => ({ headers });
Expand Down Expand Up @@ -28,3 +28,47 @@ describe("signupClientIp", () => {
);
});
});

describe("parseSupportSettingsPayload", () => {
it("accepts integers within 0..100", () => {
expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: 10 })).toEqual({
beneficiary_percent: 5,
curation_percent: 10
});
});

it("accepts the 0 and 100 boundaries", () => {
expect(parseSupportSettingsPayload({ beneficiary_percent: 0, curation_percent: 100 })).toEqual({
beneficiary_percent: 0,
curation_percent: 100
});
});

it("rejects values out of range", () => {
expect(parseSupportSettingsPayload({ beneficiary_percent: 101, curation_percent: 10 })).toBeNull();
expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: -1 })).toBeNull();
});

it("rejects floats", () => {
expect(parseSupportSettingsPayload({ beneficiary_percent: 5.5, curation_percent: 10 })).toBeNull();
expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: 0.1 })).toBeNull();
});

it("rejects strings", () => {
expect(parseSupportSettingsPayload({ beneficiary_percent: "5", curation_percent: 10 })).toBeNull();
expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: "10" })).toBeNull();
});

it("rejects booleans", () => {
expect(parseSupportSettingsPayload({ beneficiary_percent: true, curation_percent: 10 })).toBeNull();
expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: false })).toBeNull();
});

it("rejects missing fields and bodies", () => {
expect(parseSupportSettingsPayload({})).toBeNull();
expect(parseSupportSettingsPayload({ beneficiary_percent: 5 })).toBeNull();
expect(parseSupportSettingsPayload({ curation_percent: 5 })).toBeNull();
expect(parseSupportSettingsPayload(undefined)).toBeNull();
expect(parseSupportSettingsPayload(null)).toBeNull();
});
});
53 changes: 53 additions & 0 deletions src/server/handlers/private-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,59 @@ export const bookmarksDelete = async (req: express.Request, res: express.Respons
pipe(apiRequest(`bookmarks/${username}/${id}`, "DELETE"), res);
}

/**
* Parse and validate the support-settings payload. Both fields must be integers
* within 0..100 (booleans, strings and floats are rejected). Returns the parsed
* pair, or null when the payload is invalid. Exported for unit tests.
*/
export const parseSupportSettingsPayload = (
body: unknown
): { beneficiary_percent: number; curation_percent: number } | null => {
const { beneficiary_percent, curation_percent } = (body || {}) as {
beneficiary_percent?: unknown;
curation_percent?: unknown;
};
const isPercent = (v: unknown): v is number =>
typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 100;
if (!isPercent(beneficiary_percent) || !isPercent(curation_percent)) {
return null;
}
return { beneficiary_percent, curation_percent };
};

// Support settings are per-user opt-ins, so the username is taken from the
// authenticated token (validateCode), never from the request body.
export const supportSettings = async (req: express.Request, res: express.Response) => {
const username = await requireAuthedUsername(req, res);
if (!username) {
return;
}
pipe(apiRequest(`support-settings/${username}`, "GET"), res);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Support Settings Query Dropped

When a client calls /private-api/support-settings with query parameters, this handler drops them before proxying to the backend. The neighboring read handlers forward req.query, so any backend-supported support-settings options would be silently ignored and callers would receive the default response instead.

Suggested change
pipe(apiRequest(`support-settings/${username}`, "GET"), res);
pipe(apiRequest(`support-settings/${username}`, "GET", {}, {}, req.query), res);

Fix in Claude Code

};

export const supportSettingsUpdate = async (req: express.Request, res: express.Response) => {
const username = await requireAuthedUsername(req, res);
if (!username) {
return;
}
const parsed = parseSupportSettingsPayload(req.body);
if (!parsed) {
res.status(400).send(
"beneficiary_percent and curation_percent must be integers between 0 and 100"
);
return;
}
const { beneficiary_percent, curation_percent } = parsed;
pipe(
apiRequest(`support-settings/${username}`, "PUT", {}, {
username,
beneficiary_percent,
curation_percent
}),
res
);
};

export const schedules = async (req: express.Request, res: express.Response) => {
const username = await validateCode(req);
if (!username) {
Expand Down
2 changes: 2 additions & 0 deletions src/server/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ server
.post("^/private-api/bookmarks$", privateApi.bookmarks)
.post("^/private-api/bookmarks-add$", privateApi.bookmarksAdd)
.post("^/private-api/bookmarks-delete$", privateApi.bookmarksDelete)
.post("^/private-api/support-settings$", privateApi.supportSettings)
.post("^/private-api/support-settings-update$", privateApi.supportSettingsUpdate)
.post("^/private-api/schedules$", privateApi.schedules)
.post("^/private-api/schedules-add$", privateApi.schedulesAdd)
.post("^/private-api/schedules-delete$", privateApi.schedulesDelete)
Expand Down