initial commit

This commit is contained in:
Matthias
2026-06-15 11:33:23 +02:00
commit fc0a6fb975
155 changed files with 24526 additions and 0 deletions

196
convex/comdirect/client.ts Normal file
View File

@@ -0,0 +1,196 @@
const BASE_URL = "https://api.comdirect.de";
export function createRequestId(): string {
return Math.floor(Math.random() * 1_000_000_000)
.toString()
.padStart(9, "0");
}
export function buildRequestInfo(sessionUuid: string, requestId?: string): string {
return JSON.stringify({
clientRequestId: {
sessionId: sessionUuid,
requestId: requestId ?? createRequestId(),
},
});
}
export async function oauthToken(
body: Record<string, string>,
clientId: string,
clientSecret: string,
): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
const params = new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
...body,
});
const response = await fetch(`${BASE_URL}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});
if (!response.ok) {
throw new Error(`OAuth fehlgeschlagen: ${response.status}`);
}
return await response.json();
}
export async function getSessions(
accessToken: string,
sessionUuid: string,
): Promise<Array<{ identifier: string; sessionTanActive: boolean; activated2FA: boolean }>> {
const response = await fetch(`${BASE_URL}/api/session/clients/user/v1/sessions`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"Content-Type": "application/json",
"x-http-request-info": buildRequestInfo(sessionUuid),
},
});
if (!response.ok) throw new Error(`Sessions-Abruf fehlgeschlagen: ${response.status}`);
return await response.json();
}
export async function validateSession(
accessToken: string,
sessionUuid: string,
identifier: string,
): Promise<{ challengeId: string; challengeType: string; challenge?: string }> {
const response = await fetch(
`${BASE_URL}/api/session/clients/user/v1/sessions/${identifier}/validate`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"Content-Type": "application/json",
"x-http-request-info": buildRequestInfo(sessionUuid),
},
body: JSON.stringify({
identifier,
sessionTanActive: true,
activated2FA: true,
}),
},
);
if (response.status !== 201) {
throw new Error(`Session-Validierung fehlgeschlagen: ${response.status}`);
}
const authInfo = response.headers.get("x-once-authentication-info");
if (!authInfo) throw new Error("Keine TAN-Challenge erhalten");
const parsed = JSON.parse(authInfo) as { id: string; typ: string; challenge?: string };
return {
challengeId: parsed.id,
challengeType: parsed.typ,
challenge: parsed.challenge,
};
}
export async function activateSession(
accessToken: string,
sessionUuid: string,
identifier: string,
challengeId: string,
tan?: string,
): Promise<void> {
const headers: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"Content-Type": "application/json",
"x-http-request-info": buildRequestInfo(sessionUuid),
"x-once-authentication-info": JSON.stringify({ id: challengeId }),
};
const body: Record<string, unknown> = {
identifier,
sessionTanActive: true,
activated2FA: true,
};
if (tan) {
body.tan = tan;
}
const response = await fetch(
`${BASE_URL}/api/session/clients/user/v1/sessions/${identifier}`,
{
method: "PATCH",
headers,
body: JSON.stringify(body),
},
);
if (!response.ok) {
throw new Error(`Session-Aktivierung fehlgeschlagen: ${response.status}`);
}
}
export async function getSecondaryToken(
accessToken: string,
clientId: string,
clientSecret: string,
): Promise<{ access_token: string; refresh_token: string }> {
return await oauthToken(
{ grant_type: "cd_secondary", token: accessToken },
clientId,
clientSecret,
);
}
export async function refreshAccessToken(
refreshToken: string,
clientId: string,
clientSecret: string,
): Promise<{ access_token: string; refresh_token: string }> {
return await oauthToken(
{ grant_type: "refresh_token", refresh_token: refreshToken },
clientId,
clientSecret,
);
}
export async function getAccountBalances(
accessToken: string,
sessionUuid: string,
): Promise<{ values: Array<Record<string, unknown>> }> {
const response = await fetch(`${BASE_URL}/api/banking/clients/user/v2/accounts/balances`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"Content-Type": "application/json",
"x-http-request-info": buildRequestInfo(sessionUuid),
},
});
if (!response.ok) throw new Error(`Salden-Abruf fehlgeschlagen: ${response.status}`);
return await response.json();
}
export async function getTransactions(
accessToken: string,
sessionUuid: string,
accountId: string,
params: {
transactionState: "BOOKED" | "NOTBOOKED";
pagingFirst: number;
minBookingDate: string;
maxBookingDate: string;
},
): Promise<{ paging: { index: number; matches: number }; values: Array<Record<string, unknown>> }> {
const query = new URLSearchParams({
transactionState: params.transactionState,
"paging-first": String(params.pagingFirst),
"paging-count": "50",
"min-bookingDate": params.minBookingDate,
"max-bookingDate": params.maxBookingDate,
});
const response = await fetch(
`${BASE_URL}/api/banking/v1/accounts/${accountId}/transactions?${query.toString()}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"Content-Type": "application/json",
"x-http-request-info": buildRequestInfo(sessionUuid),
},
},
);
if (!response.ok) throw new Error(`Umsatz-Abruf fehlgeschlagen: ${response.status}`);
return await response.json();
}