4aae61297f
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.
64 lines
2.2 KiB
JavaScript
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 };
|