Files
FEDEO/backend/src/modules/push-server.client.ts
flfeders d18ba16f8a
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
feat: central services heartbeat senden
2026-08-05 13:49:22 +02:00

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`)
},
}