Add self-serve billing: tiers, credits, BTCPay and Zaprite
This commit is contained in:
+43
-26
@@ -1,47 +1,64 @@
|
||||
// GET /relay/balance — peek at the current install's credit balance +
|
||||
// tier WITHOUT charging anything. Recap clients call this to populate
|
||||
// the "N credits remaining · Tier: X" banner before the user runs any
|
||||
// transcribe/analyze, so the display is accurate on first load instead
|
||||
// of saying "balance unknown — no relay calls yet".
|
||||
// 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.
|
||||
//
|
||||
// Same auth surface as the metered endpoints:
|
||||
// X-Recap-Install-Id (required)
|
||||
// Authorization (optional Bearer LIC1-... — absent = Core)
|
||||
// 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 shape with result=null and
|
||||
// credit_charged=0. The license-resolution path is identical to
|
||||
// /relay/transcribe and /relay/analyze, so the cached online check
|
||||
// against keysat happens here too — but no row mutation, no job-id
|
||||
// reservation, no upstream backend call.
|
||||
// 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 { resolveLicense } from "../keysat-client.js";
|
||||
import { getOrCreateRow } from "../credits.js";
|
||||
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) => {
|
||||
const installId = req.header("X-Recap-Install-Id");
|
||||
const auth = req.header("Authorization");
|
||||
if (!installId) {
|
||||
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 license = await resolveLicense(auth);
|
||||
const tier = license.tier;
|
||||
// Touch the row so tier_snapshot reflects the most recently seen
|
||||
// license tier — same as the metered endpoints do — but commit
|
||||
// nothing.
|
||||
const row = await getOrCreateRow(installId);
|
||||
row.tier_snapshot = tier;
|
||||
|
||||
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,
|
||||
installId,
|
||||
creditKey: identity.creditKey,
|
||||
installId: identity.installId,
|
||||
license: identity.license,
|
||||
tier,
|
||||
creditCharged: 0,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user