Files
deflock/api/services/GithubClient.ts
T
Chris Bowles 34e2cbdc2d remove username as public input from /sponsors/github
Extends the previous commit's fix. The only caller (Donate.vue via
apiService.getSponsors()) never sends a username and always relied on the
server-side default, so the parameter had no legitimate use even after
being parameterized — it just let anyone use deflock's server as an
unauthenticated relay onto GitHub's GraphQL API under the server's own
token. Drop it entirely: the route no longer accepts a username
querystring param, and GithubClient.getSponsors() takes no argument,
hardcoding the maintainer's login via MAINTAINER_USERNAME. Keeps the
$username GraphQL-variable parameterization technique so reintroducing a
variable input later can't reintroduce the injection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 02:57:51 -04:00

50 lines
1.7 KiB
TypeScript

import { Type, Static } from '@sinclair/typebox';
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
const graphQLEndpoint = 'https://api.github.com/graphql';
const MAINTAINER_USERNAME = 'frillweeman';
export const SponsorSchema = Type.Object({
sponsor: Type.Object({
avatarUrl: Type.String(),
login: Type.String(),
name: Type.Union([Type.String(), Type.Null()]),
url: Type.String(),
}),
});
export type Sponsor = Static<typeof SponsorSchema>;
export const SponsorsResponseSchema = Type.Array(SponsorSchema);
// The maintainer's username is passed as a GraphQL variable rather than
// interpolated into the query string. It's a hardcoded constant today, but
// keeping this indirection means a future caller-supplied value can never
// break out of the query text the way the old string-interpolated version did.
export const buildSponsorsQuery = (): { query: string; variables: { username: string } } => ({
query: `query($username: String!) { user(login: $username) { sponsorshipsAsMaintainer(first: 100) { nodes { sponsor { login name avatarUrl url } } } } }`,
variables: { username: MAINTAINER_USERNAME },
});
export class GithubClient {
async getSponsors(): Promise<Sponsor[]> {
const { query, variables } = buildSponsorsQuery();
const body = JSON.stringify({ query, variables });
const response = await fetch(graphQLEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'User-Agent': 'Shotgun',
'Content-Type': 'application/json',
},
body,
});
if (!response.ok) {
throw new Error(`Failed to get sponsors: ${response.status}`);
}
const json = await response.json();
return json?.data?.user?.sponsorshipsAsMaintainer?.nodes || [];
}
}