Source: RouterRunner/runnerV1/controller.js

// @ts-check
/**
 * Connector-facing Runner API (/api/runner/v1/*) — Topologie-Endpunkte.
 * Auth: X-Runner-Credential-Id + timestamp + signature (except register/exchange).
 *
 * Jobs/TLS/Preview-Records sind nach AP 1 (050-Migrationsplan) nicht mehr hier —
 * die Mitglieder-Runtime liegt beim Zusteller (ide-server).
 *
 * @import {ExpressResponse} from '../../types.js'
 */

import { sendOk, sendErrorFrom } from '../../utils/apiEnvelope.js';
import * as registration from '../registration/service.js';
import * as runnerService from '../runner/service.js';

/**
 * POST /api/runner/v1/register/exchange
 * @param {import('express').Request} req
 * @param {ExpressResponse} res
 */
export const exchange = async (req, res) => {
    try {
        const result = await registration.exchangeRegistration(req.body || {});
        sendOk(res, result.result);
    } catch (e) {
        sendErrorFrom(res, e);
    }
};

/**
 * Middleware: authenticate runner credential.
 */
export const requireRunnerAuth = async (req, res, next) => {
    try {
        req.runnerAuth = await registration.authenticateRunner(req);
        next();
    } catch (e) {
        sendErrorFrom(res, e);
    }
};

/**
 * POST /api/runner/v1/heartbeat — Topologie-Heartbeat in die Companion-Zeile.
 */
export const heartbeat = async (req, res) => {
    try {
        const { runnerUid, tenantUid } = req.runnerAuth;
        await runnerService.upsertRuntime(runnerUid, tenantUid, {
            capacity: req.body?.capacity,
            heartbeat: {
                version: req.body?.version,
                status: req.body?.status,
                at: new Date().toISOString(),
            },
        });
        // Statische Meldungen (tailnet/capabilities) nur in ObjectBase.Data,
        // wenn sie sich ändern; draining/revoked darf der Heartbeat nicht
        // überschreiben (Topologie-Zustand gewinnt).
        const meta = await runnerService.readRunnerMeta(runnerUid);
        if (meta && meta.status !== 'draining' && meta.status !== 'revoked') {
            await runnerService.patchRunnerData(runnerUid, runnerUid, {
                ...(req.body.tailnetIp ? { tailnetIp: req.body.tailnetIp } : {}),
                ...(req.body.tailnetNodeId ? { tailnetNodeId: req.body.tailnetNodeId } : {}),
                ...(req.body.capabilities ? { capabilities: req.body.capabilities } : {}),
                ...(req.body.architecture ? { architecture: req.body.architecture } : {}),
                ...(req.body.operatingSystem ? { operatingSystem: req.body.operatingSystem } : {}),
                status: 'online',
            });
        }
        sendOk(res, { ok: true });
    } catch (e) {
        sendErrorFrom(res, e);
    }
};