screentinker/server/routes/billing.js
ScreenTinker 677b17028e feat(#146): billing:read scoped token — dual-path auth for the Usage Report (Option C)
Least-privilege way to read GET /api/billing/usage without requiring platform admin.
Additive + isolated: reuses the existing api_tokens scope system (the off-ladder 'agency'
scope is the precedent) and does NOT touch the shared role/permission checks other
endpoints rely on.

- New off-ladder scope 'billing:read' (routes/tokens.js SCOPES). Like 'agency' it is NOT
  on the read<write<full ladder, so tokenScopeGate rejects a billing token on every
  PUBLIC_ROUTER and JWT-only routers reject any st_ token -> the scope grants billing-read
  and NOTHING else.
- DUAL-PATH gate requireBillingRead (middleware/apiToken.js), written as an EXPLICIT OR:
  authorize if (billing:read token) OR (platform-admin session). Admins keep read access
  but are NOT required to; the token path doesn't lock out admins or vice versa. Billing
  route now mounted with bearerAuth (token OR JWT front door) + requireBillingRead (was
  requireAuth + requirePlatformAdmin).
- MINTING is platform-admin only (stricter than read/write/full/agency, which any
  workspace member may mint) since a billing:read token grants GLOBAL billing-read. Note:
  no finer "owner" tier exists here (#14 collapsed superadmin->platform_admin), so
  PLATFORM_ROLES is the top level required.

Tests (5, test/billing-authz.test.js): dual-path positive (token AND admin session both
200) + negative (user 403 / anon 401); scope isolation (billing token 403 on /api/devices,
401 on /api/admin; read token 200 on devices but 403 on billing); minting owner-only
(user + ordinary-admin 403, platform-admin 201); revocation -> 401. Existing token
firewall/partition suite (api.test.js) + billing-endpoint tests unchanged & green. Reused
the exact SHA-256 token-verification path (no bcrypt/new mechanism). Suite 306/306.

NOTE: spec described bcrypt + JSON `scopes` + an analytics:read precedent; this codebase
actually uses SHA-256 + a single `scope` TEXT column + 'agency' as the off-ladder
precedent. Implemented faithfully to the real system.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:16:21 -05:00

26 lines
1.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
// #146 BILLING — admin-gated Usage Report (ByteTinkerBold agreement §4.1 system-of-record
// / §4.2 verification). DELIBERATELY a standalone route, separate from routes/status.js:
// /api/status is the hot, constantly-polled path; this is a heavier admin-only aggregate
// and must not touch it. Reads the daily rollup ONLY (cheap — no raw-log scans).
const express = require('express');
const router = express.Router();
const { requireBillingRead } = require('../middleware/apiToken');
const billing = require('../lib/billing');
// GET /api/billing/usage?month=YYYY-MM (default: current month)
// #146 Option C — DUAL PATH: authorized by a 'billing:read' scoped API token OR a
// platform-admin session (requireBillingRead, explicit OR). Admins keep read access but a
// least-privilege token is the intended consumer (tooling / invoice-time pulls / §4.2).
router.get('/usage', requireBillingRead, (req, res) => {
try {
res.json(billing.buildUsageReport(req.query.month));
} catch (e) {
res.status(400).json({ error: e.message });
}
});
module.exports = router;