/** * @file push-token.js * @description The bearer token that opens `POST /api/hooks/ingest-batch` — the * push path for a machine the dashboard cannot reach over SSH (routes/hooks.js). * * The server makes the token (32 random bytes) and shows it ONCE, in the answer * to the request that created it; what persists is only its SHA-256, as * `AGENTHUB_PUSH_TOKEN_SHA256` in AgentHub's settings file (settings-env.js), so * no route, file or log can hand the token back. No hash = push ingest off, * which is the default. A check hashes the presented token and compares the two * digests in constant time. * @author Nguyễn Ngọc Trí Vĩ */ const crypto = require("node:crypto"); const { applySetting, envOwned } = require("./settings-env"); const KEY = "AGENTHUB_PUSH_TOKEN_SHA256"; const HEX64 = /^[0-9a-f]{64}$/; const sha256 = (s) => crypto.createHash("sha256").update(s).digest(); /** The stored digest, or null while push ingest is off. */ function storedDigest() { const v = String(process.env[KEY] || "").trim(); return HEX64.test(v) ? Buffer.from(v, "hex") : null; } /** `{enabled, env_owned}` — never the token, which is not stored. */ function pushTokenStatus() { return { enabled: storedDigest() !== null, env_owned: envOwned.has(KEY) }; } /** Whether `given` is the current token. Constant time; false while off. */ function checkPushToken(given) { const want = storedDigest(); if (!want || typeof given !== "string" || given.length === 0 || given.length > 512) return false; return crypto.timingSafeEqual(sha256(given), want); } function refuseEnvOwned() { if (envOwned.has(KEY)) { throw Object.assign(new Error(`${KEY} is set by the environment; change it there`), { code: "EENVOWNED", }); } } /** Make a new token (the old one stops working) and return it — the only time it is seen. */ function rotatePushToken() { refuseEnvOwned(); const token = `ahp_${crypto.randomBytes(32).toString("base64url")}`; applySetting(KEY, sha256(token).toString("hex")); return token; } /** Turn push ingest off. */ function revokePushToken() { refuseEnvOwned(); applySetting(KEY, ""); } module.exports = { KEY, pushTokenStatus, checkPushToken, rotatePushToken, revokePushToken };