Files
recap-relay/server/routes/balance.js
T

70 lines
2.4 KiB
JavaScript

// GET /relay/balance — peek at the caller's credit balance + tier WITHOUT
// charging anything. Recap clients call this to populate the
// "N credits remaining · Tier: X" banner.
//
// Two auth surfaces (see identity.js):
// - cloud: X-Recap-User-Id + X-Recap-Operator-Key → user:<id> pool,
// tier is the relay's stored (operator-set) value.
// - license: X-Recap-Install-Id (+ optional Bearer license) → legacy
// license/install pool, tier from the license.
//
// Returns the standard envelope (result=null, credit_charged=0). No row
// mutation beyond keeping tier_snapshot synced on the license path.
import express from "express";
import { resolveIdentity, identityTier } from "../identity.js";
import { getOrCreateRow, applyTierPromotion } from "../credits.js";
import { envelope, errorEnvelope } from "./envelope.js";
export function balanceRouter() {
const router = express.Router();
router.get("/balance", async (req, res) => {
let identity;
try {
identity = await resolveIdentity(req);
} catch (err) {
const e = await errorEnvelope({
error: err?.message || "auth_error",
statusHint: err?.status || 401,
});
return res.status(e.statusHint || 401).json(e.body);
}
if (identity.kind === "license" && !identity.installId) {
const e = await errorEnvelope({
error: "missing X-Recap-Install-Id header",
statusHint: 400,
});
return res.status(400).json(e.body);
}
const row = await getOrCreateRow({
creditKey: identity.creditKey,
installId: identity.installId,
license: identity.license,
});
// License path: fire the Core→paid promotion bookkeeping (this is
// typically the FIRST relay call after a license activation) and keep
// tier_snapshot synced to the license. Cloud path: the tier is the
// relay's stored, operator-set value — leave it untouched.
if (identity.kind === "license") {
const tier = identity.license.tier;
const promoted = await applyTierPromotion(row, tier);
if (!promoted) row.tier_snapshot = tier;
}
const tier = identityTier(identity, row);
const body = await envelope({
result: null,
creditKey: identity.creditKey,
installId: identity.installId,
license: identity.license,
tier,
creditCharged: 0,
});
res.json(body);
});
return router;
}