From 07fe14010c4b357347c5e7a97d1b23bdf1ab30c1 Mon Sep 17 00:00:00 2001 From: local Date: Mon, 11 May 2026 21:34:31 -0500 Subject: [PATCH] v0.2.2 balance peek endpoint --- server/index.js | 2 ++ server/routes/balance.js | 52 ++++++++++++++++++++++++++++++++++++++ startos/versions/index.ts | 5 ++-- startos/versions/v0.2.2.ts | 13 ++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 server/routes/balance.js create mode 100644 startos/versions/v0.2.2.ts diff --git a/server/index.js b/server/index.js index 31ed85d..c09c1c7 100644 --- a/server/index.js +++ b/server/index.js @@ -21,6 +21,7 @@ import { import { transcribeRouter } from "./routes/transcribe.js"; import { analyzeRouter } from "./routes/analyze.js"; import { healthRouter } from "./routes/health.js"; +import { balanceRouter } from "./routes/balance.js"; import { adminRouter } from "./routes/admin.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -46,6 +47,7 @@ setupAdminAuthRoutes(app); // authenticates per-call via headers (X-Recap-Install-Id required, // Authorization optional). app.use("/relay", healthRouter()); +app.use("/relay", balanceRouter()); app.use("/relay", transcribeRouter()); app.use("/relay", analyzeRouter()); diff --git a/server/routes/balance.js b/server/routes/balance.js new file mode 100644 index 0000000..079e22e --- /dev/null +++ b/server/routes/balance.js @@ -0,0 +1,52 @@ +// 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". +// +// Same auth surface as the metered endpoints: +// X-Recap-Install-Id (required) +// Authorization (optional Bearer LIC1-... — absent = Core) +// +// 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. + +import express from "express"; +import { resolveLicense } from "../keysat-client.js"; +import { getOrCreateRow } 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) { + 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 body = await envelope({ + result: null, + installId, + tier, + creditCharged: 0, + }); + res.json(body); + }); + + return router; +} diff --git a/startos/versions/index.ts b/startos/versions/index.ts index 1157765..37e62aa 100644 --- a/startos/versions/index.ts +++ b/startos/versions/index.ts @@ -2,8 +2,9 @@ import { VersionGraph } from '@start9labs/start-sdk' import { v_0_1_0 } from './v0.1.0' import { v_0_2_0 } from './v0.2.0' import { v_0_2_1 } from './v0.2.1' +import { v_0_2_2 } from './v0.2.2' export const versionGraph = VersionGraph.of({ - current: v_0_2_1, - other: [v_0_2_0, v_0_1_0], + current: v_0_2_2, + other: [v_0_2_1, v_0_2_0, v_0_1_0], }) diff --git a/startos/versions/v0.2.2.ts b/startos/versions/v0.2.2.ts new file mode 100644 index 0000000..d621008 --- /dev/null +++ b/startos/versions/v0.2.2.ts @@ -0,0 +1,13 @@ +import { VersionInfo } from '@start9labs/start-sdk' + +export const v_0_2_2 = VersionInfo.of({ + version: '0.2.2:0', + releaseNotes: { + en_US: + 'New GET /relay/balance endpoint returns credit balance + tier without charging. Recap clients call this on boot so the "N credits remaining" banner is populated before the user runs any transcribe/analyze.', + }, + migrations: { + up: async ({ effects }) => {}, + down: async ({ effects }) => {}, + }, +})