From 385eda3cb12d9bf9a8f804e9123e86ddac6fdf43 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Wed, 1 Jul 2026 21:31:11 -0500 Subject: [PATCH] feat(#146): owner-only CLI to mint billing:read tokens (scripts/mint-billing-token.js) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The billing:read scope + dual-path gate were built but there was no way to MINT a token (and it must NOT go in the workspace-scoped, self-service API-Tokens UI). Adds a server-side, owner-only CLI — no new UI, no network endpoint. Owner-only BY CONSTRUCTION: it's a host-side script, so filesystem/shell access = the platform owner. - server/lib/billing-token.js (testable): mintBillingToken/revokeBillingToken/ listBillingTokens. Reuses the EXACT existing token path — same secret (st_ + 32 bytes base64url), same SHA-256 hashing (hashToken), same api_tokens columns — no second format. Resolves the platform OWNER (oldest platform_admin/superadmin; #14 collapsed superadmin -> platform_admin so that's the top tier) and binds to their workspace. api_tokens.user_id + workspace_id are BOTH NOT NULL (no platform-level token exists); the workspace binding is VESTIGIAL for billing (billing:read is off-ladder -> can't reach any workspace router; billing is platform-global), documented in-file rather than loosening NOT NULL pre-release. - scripts/mint-billing-token.js: thin CLI wrapper. --name mints and prints the secret ONCE (+ id, + "run as owner on host" warning), --list, --revoke (soft revoke, mirrors the dashboard DELETE). Tests (4, test/billing-token-mint.test.js): minted row is scope EXACTLY billing:read with a matching SHA-256 hash and no read/write/full/agency scope; the token reads GET /api/billing/usage (200) but is refused on /api/devices (403) and /api/admin (401) — scope isolation; revocation -> 401; mint requires a name; revoke refuses a non-billing id. CLI smoked live (mint/list/revoke). Suite 310/310. SPEC-vs-REALITY (again): spec said bcrypt + JSON `scopes`; this codebase uses SHA-256 + a single `scope` TEXT column. Built to the real system. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/billing.md | 7 +++ scripts/mint-billing-token.js | 74 +++++++++++++++++++++++ server/lib/billing-token.js | 83 +++++++++++++++++++++++++ server/test/billing-token-mint.test.js | 84 ++++++++++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100755 scripts/mint-billing-token.js create mode 100644 server/lib/billing-token.js create mode 100644 server/test/billing-token-mint.test.js diff --git a/docs/billing.md b/docs/billing.md index 8971c82..0772cf1 100644 --- a/docs/billing.md +++ b/docs/billing.md @@ -57,6 +57,13 @@ aggregate; it must not touch the hot status path). Reads the rollup only. Return `{ month, days_in_month, days_elapsed, provisioned_screens, billable_screens, billable_screens_final?, is_final, tier, rate_usd, cost_usd, daily:[{day, active_screen_days}] }`. +**Minting a `billing:read` token — owner only:** billing tokens are minted server-side by +the platform owner via `node scripts/mint-billing-token.js --name "