2026-06-29 17:51:54 -04:00
|
|
|
// Thin typed client over the tRPC backend (DESIGN.md §2). Slice 1 uses a fetch wrapper
|
|
|
|
|
// rather than @trpc/client to keep the Next app decoupled from the server's Node-native
|
|
|
|
|
// type graph; the server is a real tRPC server and full end-to-end client types can be
|
|
|
|
|
// layered later without changing this surface.
|
|
|
|
|
export interface Quote {
|
|
|
|
|
symbol: string;
|
|
|
|
|
price: number;
|
|
|
|
|
bid?: number | null;
|
|
|
|
|
ask?: number | null;
|
|
|
|
|
change?: number | null;
|
|
|
|
|
changePercent?: number | null;
|
|
|
|
|
iv?: number | null;
|
|
|
|
|
}
|
|
|
|
|
export interface PriceCandle {
|
|
|
|
|
ts: string;
|
|
|
|
|
o: number;
|
|
|
|
|
h: number;
|
|
|
|
|
l: number;
|
|
|
|
|
c: number;
|
|
|
|
|
v: number;
|
|
|
|
|
adjClose?: number | null;
|
|
|
|
|
}
|
|
|
|
|
export interface SymbolMeta {
|
|
|
|
|
symbol: string;
|
|
|
|
|
name?: string | null;
|
|
|
|
|
sector?: string | null;
|
|
|
|
|
industry?: string | null;
|
|
|
|
|
exchange?: string | null;
|
|
|
|
|
tickerKind: string;
|
|
|
|
|
peers?: string[] | null;
|
|
|
|
|
}
|
|
|
|
|
export interface Snapshot {
|
|
|
|
|
symbol: string;
|
|
|
|
|
quote: Quote | null;
|
|
|
|
|
candles: PriceCandle[] | null;
|
|
|
|
|
sector: SymbolMeta | null;
|
|
|
|
|
stale: { quote: boolean; candles: boolean; sector: boolean };
|
|
|
|
|
}
|
|
|
|
|
export interface AuthUser {
|
|
|
|
|
userId: string;
|
|
|
|
|
email?: string;
|
|
|
|
|
complexity?: string;
|
|
|
|
|
riskTolerance?: string;
|
|
|
|
|
convexityPosture?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function parse<T>(res: Response): Promise<T> {
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
if (json?.error) {
|
|
|
|
|
const msg = json.error.message ?? `tRPC error ${json.error.data?.code ?? ""}`;
|
|
|
|
|
throw new Error(typeof msg === "string" ? msg : "tRPC error");
|
|
|
|
|
}
|
|
|
|
|
return json.result.data as T;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function trpcQuery<T>(procedure: string, input?: unknown): Promise<T> {
|
|
|
|
|
const search = input ? `?input=${encodeURIComponent(JSON.stringify(input))}` : "";
|
|
|
|
|
const res = await fetch(`/api/trpc/${procedure}${search}`, {
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
});
|
|
|
|
|
return parse<T>(res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function trpcMutate<T>(procedure: string, input: unknown): Promise<T> {
|
|
|
|
|
const res = await fetch(`/api/trpc/${procedure}`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
return parse<T>(res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const api = {
|
|
|
|
|
market: { snapshot: (symbol: string) => trpcQuery<Snapshot>("market.snapshot", { symbol }) },
|
|
|
|
|
auth: {
|
|
|
|
|
signup: (email: string, password: string) => trpcMutate<{ userId: string }>("auth.signup", { email, password }),
|
2026-06-29 21:21:09 -04:00
|
|
|
login: (email: string, password: string, totp?: string) => trpcMutate<{ userId: string }>("auth.login", { email, password, ...(totp ? { totp } : {}) }),
|
2026-06-29 17:51:54 -04:00
|
|
|
logout: () => trpcMutate<{ ok: boolean }>("auth.logout", {}),
|
|
|
|
|
me: () => trpcQuery<AuthUser | null>("auth.me"),
|
2026-06-29 21:21:09 -04:00
|
|
|
enable2fa: () => trpcMutate<{ totpSecret: string; qrUrl: string; backupCodes: string[] }>("auth.enable2fa", {}),
|
|
|
|
|
confirm2fa: (totp: string) => trpcMutate<{ ok: boolean }>("auth.confirm2fa", { totp }),
|
|
|
|
|
oauthStart: (provider: "github" | "google", redirectUri: string) => trpcMutate<{ redirectUrl: string; state: string }>("auth.oauthStart", { provider, redirectUri }),
|
2026-06-29 17:51:54 -04:00
|
|
|
},
|
|
|
|
|
};
|