All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 26s
Build and Push Docker Images / build-frontend (push) Successful in 16s
Build and Push Docker Images / build-website (push) Successful in 16s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
Build and Push Docker Images / build-central-services-admin (push) Successful in 41s
Build and Push Docker Images / build-docs (push) Successful in 16s
151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
import { createHash, createHmac, randomUUID } from "node:crypto"
|
|
|
|
import { secrets } from "../utils/secrets"
|
|
|
|
type PushServerDevicePlatform = "web" | "ios" | "android"
|
|
|
|
export type RegisterPushServerDeviceInput = {
|
|
localDeviceId: string
|
|
platform: PushServerDevicePlatform
|
|
providerToken?: string
|
|
subscription?: Record<string, unknown>
|
|
meta?: Record<string, unknown>
|
|
}
|
|
|
|
export type RegisterPushServerDeviceResult = {
|
|
centralDeviceId: string
|
|
status: string
|
|
}
|
|
|
|
export type SendPushServerMessageInput = {
|
|
idempotencyKey: string
|
|
devices: string[]
|
|
priority?: "normal" | "high"
|
|
ttlSeconds?: number
|
|
collapseKey?: string
|
|
notification?: {
|
|
title?: string
|
|
body?: string
|
|
}
|
|
data?: Record<string, unknown>
|
|
}
|
|
|
|
export type CentralServicesHeartbeatInput = {
|
|
fedeoVersion?: string
|
|
baseUrl?: string
|
|
capabilities?: string[]
|
|
}
|
|
|
|
function configured() {
|
|
return Boolean(secrets.PUSH_SERVER_URL && secrets.PUSH_SERVER_INSTANCE_ID && secrets.PUSH_SERVER_SECRET)
|
|
}
|
|
|
|
function normalizeBaseUrl() {
|
|
return String(secrets.PUSH_SERVER_URL || "").replace(/\/+$/, "")
|
|
}
|
|
|
|
function bodyHash(body: string) {
|
|
return createHash("sha256").update(body).digest("hex")
|
|
}
|
|
|
|
function signature(method: string, path: string, timestamp: string, body: string) {
|
|
const canonical = [
|
|
method.toUpperCase(),
|
|
path,
|
|
timestamp,
|
|
bodyHash(body),
|
|
secrets.PUSH_SERVER_INSTANCE_ID,
|
|
].join("\n")
|
|
|
|
return createHmac("sha256", secrets.PUSH_SERVER_SECRET).update(canonical).digest("hex")
|
|
}
|
|
|
|
async function requestPushServer<T>(method: "GET" | "POST" | "DELETE", path: string, payload?: unknown): Promise<T> {
|
|
if (!configured()) {
|
|
throw new Error("Zentraler Push-Server ist nicht konfiguriert")
|
|
}
|
|
|
|
const body = payload === undefined ? "" : JSON.stringify(payload)
|
|
const timestamp = new Date().toISOString()
|
|
const signedPath = path.split("?")[0]
|
|
const response = await fetch(`${normalizeBaseUrl()}${path}`, {
|
|
method,
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
"X-Fedeo-Instance-Id": secrets.PUSH_SERVER_INSTANCE_ID,
|
|
"X-Fedeo-Timestamp": timestamp,
|
|
"X-Fedeo-Signature": signature(method, signedPath, timestamp, body),
|
|
"X-Fedeo-Request-Id": randomUUID(),
|
|
},
|
|
body: body || undefined,
|
|
})
|
|
|
|
const text = await response.text()
|
|
const data = text ? JSON.parse(text) : null
|
|
|
|
if (!response.ok) {
|
|
const message = data?.message || data?.error || `Push-Server Anfrage fehlgeschlagen (${response.status})`
|
|
throw new Error(message)
|
|
}
|
|
|
|
return data as T
|
|
}
|
|
|
|
export const pushServerClient = {
|
|
configured,
|
|
|
|
registerDevice(input: RegisterPushServerDeviceInput) {
|
|
return requestPushServer<RegisterPushServerDeviceResult>("POST", "/v1/devices", input)
|
|
},
|
|
|
|
sendPush(input: SendPushServerMessageInput) {
|
|
return requestPushServer<{ accepted: number; rejected: number; deliveryJobId: string }>("POST", "/v1/push", {
|
|
priority: "normal",
|
|
ttlSeconds: 3600,
|
|
...input,
|
|
})
|
|
},
|
|
}
|
|
|
|
export const centralServicesClient = {
|
|
configured,
|
|
|
|
heartbeat(input: CentralServicesHeartbeatInput) {
|
|
return requestPushServer<{
|
|
status: string
|
|
instanceId: string
|
|
payloadMode: string
|
|
capabilities: string[]
|
|
}>("POST", "/v1/instances/heartbeat", input)
|
|
},
|
|
|
|
aiChatCompletions(payload: Record<string, unknown>) {
|
|
return requestPushServer<any>("POST", "/v1/services/ai/chat-completions", payload)
|
|
},
|
|
|
|
bankingInstitutions(country = "DE") {
|
|
return requestPushServer<any[]>("GET", `/v1/services/banking/institutions?country=${encodeURIComponent(country)}`)
|
|
},
|
|
|
|
createBankingRequisition(input: { institutionId: string; redirect: string; userLanguage?: string }) {
|
|
return requestPushServer<any>("POST", "/v1/services/banking/requisitions", input)
|
|
},
|
|
|
|
getBankingRequisition(id: string) {
|
|
return requestPushServer<any>("GET", `/v1/services/banking/requisitions/${encodeURIComponent(id)}`)
|
|
},
|
|
|
|
getBankingAccount(id: string) {
|
|
return requestPushServer<any>("GET", `/v1/services/banking/accounts/${encodeURIComponent(id)}`)
|
|
},
|
|
|
|
getBankingBalances(id: string) {
|
|
return requestPushServer<any>("GET", `/v1/services/banking/accounts/${encodeURIComponent(id)}/balances`)
|
|
},
|
|
|
|
getBankingTransactions(id: string) {
|
|
return requestPushServer<any>("GET", `/v1/services/banking/accounts/${encodeURIComponent(id)}/transactions`)
|
|
},
|
|
}
|