// 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, tier, creditCharged = 0, }) { const quota = await getTierQuotas(); const row = await getOrCreateRow(installId); // 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, credits_remaining: balance.remaining, // null = unlimited (Max) 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, tier = "core", statusHint = 500, }) { let creditsRemaining = null; try { const quota = await getTierQuotas(); const row = await getOrCreateRow(installId || "unknown"); const balance = computeRemaining(row, quota); creditsRemaining = balance.remaining; } catch {} return { statusHint, body: { result: null, error: typeof error === "string" ? error : error?.message || "unknown_error", credits_remaining: creditsRemaining, tier, credit_charged: 0, }, }; }