Files

87 lines
3.0 KiB
JavaScript

// Standard response envelope. Every /relay/* response (success and
// error both) goes through this so Recap clients can keep their
// credit-balance display accurate regardless of what happened.
//
// Shape: { result, credits_remaining, tier, credit_charged }
import { getOrCreateRow, computeRemaining } from "../credits.js";
import { getTierQuotas } from "../config.js";
// Build the envelope around a result object.
export async function envelope({
result = null,
installId,
// License is optional but recommended — without it, balance lookups
// route to the install-keyed row even for paid users, which would
// briefly underreport their balance after a commitCredit landed on
// their license-keyed row. Routes pass it through from resolveLicense.
license = null,
// Explicit ledger key override (cloud `user:<id>` path). Takes
// precedence over (installId, license) when present.
creditKey = null,
tier,
creditCharged = 0,
}) {
const quota = await getTierQuotas();
const row = await getOrCreateRow({ installId, license, creditKey });
// tier_snapshot on the row was just updated by commitCredit; if no
// credit was committed (free reuse via job_id) it still reflects
// the last-known tier for this install, which is fine.
const balance = computeRemaining(row, quota);
return {
result,
// `total` = tier allotment + purchased top-up. Recap renders this
// as the headline number on its credits pill. `remaining` alone
// wouldn't reflect purchased credits at all — so a buyer who
// just bought 5 credits and had 0 tier credits left would still
// see "0 relay credits" until their tier renewed.
credits_remaining: balance.total, // null = unlimited (Max)
// Breakdown for clients that want to display it.
tier_remaining: balance.remaining,
purchased_balance: balance.purchased,
tier,
credit_charged: creditCharged,
};
}
// Same shape but for error responses. The error reason goes in `error`
// alongside `result: null`. Clients should still update their balance
// display from `credits_remaining` so failed calls (which were
// refunded) reflect the unchanged balance.
export async function errorEnvelope({
error,
installId,
license = null,
creditKey = null,
tier = "core",
statusHint = 500,
}) {
let creditsRemaining = null;
let tierRemaining = null;
let purchased = 0;
try {
const quota = await getTierQuotas();
const row = await getOrCreateRow({
installId: creditKey ? null : installId || "unknown",
license,
creditKey,
});
const balance = computeRemaining(row, quota);
creditsRemaining = balance.total;
tierRemaining = balance.remaining;
purchased = balance.purchased;
} catch {}
return {
statusHint,
body: {
result: null,
error: typeof error === "string" ? error : error?.message || "unknown_error",
credits_remaining: creditsRemaining,
tier_remaining: tierRemaining,
purchased_balance: purchased,
tier,
credit_charged: 0,
},
};
}