Files
AgentHub/server/lib/push-token.js
T
nntrivi2001 4aae61297f feat: analytics parity — tool cost, date ranges, budgets, query explorer, replay, snapshots (v2.4.0)
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
  Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
  with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
  Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
  its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
  status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
  reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
  ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files

Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
2026-10-06 17:12:28 +07:00

64 lines
2.2 KiB
JavaScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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 };