/**
* Addon Visibility Proxy — leitet visibilityCheck-Pfade an den Bot-Host weiter
*
* Die Host-App ruft `apiSelect(a.visibilityCheck.replace('{UID}', UID))` gegen
* den Members-Host auf (siehe members-app src/show/group/Group.jsx). Der
* visibilityCheck ist ein relativer Pfad wie `/project-bot/groups/{UID}/visibility`.
*
* Dieses Modul stellt einen generischen Proxy bereit: Für jede Anfrage wird aus
* den registrierten Addons (Runtime-Config-Cache) der passende Bot-Host
* ermittelt — abgeleitet aus der `url` des Addons (`https://{botHost}/addons/...`).
* Die Anfrage wird unverändert (Headers, Query, Body) an den Bot weitergeleitet.
*
* Der Proxy ist bewusst generisch gehalten, damit jeder Bot mit `visibilityCheck`
* funktioniert, ohne dass der Members-Host Bot-spezifische Routen kennt.
*
* @module RouterAddons/visibilityProxy
*/
import { Router } from 'express'
import { configs } from '../utils/compileTemplates.js'
import { errorLogger } from '../utils/requestLogger.js'
const router = Router()
/**
* Sammelt aus dem Config-Cache alle Addons mit `visibilityCheck` und leitet
* daraus das Präfix → Bot-Basis-URL Mapping ab.
*
* @returns {Map<string, string>} Map: Pfad-Präfix (z.B. `/project-bot`) → Basis-URL (z.B. `https://project-bot.dev.commtool.org`)
*/
const buildPrefixMap = () => {
const prefixMap = new Map()
for (const app of Object.keys(configs)) {
const appConfigs = configs[app]
if (!appConfigs) continue
for (const orga of Object.keys(appConfigs)) {
const addons = appConfigs[orga]?.addons
if (!Array.isArray(addons)) continue
for (const addon of addons) {
if (!addon?.visibilityCheck || !addon?.url) continue
const prefix = visibilityPrefix(addon.visibilityCheck)
if (!prefix) continue
try {
const base = new URL(addon.url).origin
// Erstes Mapping gewinnt — bei mehreren Addons mit gleichem
// Präfix verhält sich der Proxy deterministisch.
if (!prefixMap.has(prefix)) {
prefixMap.set(prefix, base)
}
} catch {
// Ungültige url ignorieren
}
}
}
}
return prefixMap
}
/**
* Extrahiert das statische Präfix aus einem visibilityCheck-Pfad.
* `/project-bot/groups/{UID}/visibility` → `/project-bot`
*
* @param {string} visibilityCheck
* @returns {string|null} Präfix oder `null` wenn keines ableitbar
*/
const visibilityPrefix = (visibilityCheck) => {
const match = visibilityCheck.match(/^(\/[^/{}]+)/)
return match ? match[1] : null
}
/**
* Proxy-Middleware: leitet Anfragen, deren Pfad mit einem visibilityCheck-Präfix
* beginnt, an den zugehörigen Bot-Host weiter. Für alle anderen Pfade ruft sie
* `next()` auf, damit bestehende Routen nicht blockiert werden.
*
* @type {import('express').RequestHandler}
*/
const proxyHandler = async (req, res, next) => {
const prefixMap = buildPrefixMap()
let targetBase = null
for (const [prefix, base] of prefixMap) {
if (req.path === prefix || req.path.startsWith(prefix + '/')) {
targetBase = base
break
}
}
if (!targetBase) {
return next()
}
// 1:1-Weiterleitung: Der vollständige Originalpfad (inkl. Präfix) bleibt
// erhalten, weil der Bot die visibilityCheck-Route unter demselben Pfad
// bereitstellt wie im visibilityCheck-Wert definiert.
const targetUrl = new URL(req.originalUrl, targetBase)
try {
const headers = new Headers()
for (const [key, value] of Object.entries(req.headers)) {
if (key === 'host' || key === 'connection' || key === 'content-length') continue
if (typeof value === 'string') {
headers.set(key, value)
} else if (Array.isArray(value)) {
for (const v of value) headers.append(key, v)
}
}
const init = { method: req.method, headers, redirect: 'follow' }
if (!['GET', 'HEAD'].includes(req.method)) {
init.body = JSON.stringify(req.body ?? {})
if (!headers.has('content-type')) {
headers.set('content-type', 'application/json')
}
}
const upstream = await fetch(targetUrl.toString(), init)
const upstreamHeaders = new Headers()
for (const [key, value] of upstream.headers.entries()) {
if (['content-encoding', 'transfer-encoding', 'connection'].includes(key)) continue
upstreamHeaders.set(key, value)
}
const contentType = upstream.headers.get('content-type') || ''
if (contentType.includes('application/json')) {
const body = await upstream.json()
res.status(upstream.status).json(body)
} else {
const body = await upstream.arrayBuffer()
res.status(upstream.status).set(Object.fromEntries(upstreamHeaders)).send(Buffer.from(body))
}
} catch (err) {
errorLogger(err)
res.status(502).json({ success: false, message: `visibilityCheck-Proxyfehler: ${err.message}` })
}
}
// Alle Methoden abdecken — visibilityCheck ist GET, aber der Proxy ist generisch
router.all('*', proxyHandler)
export default router