New cookie-gated "Users" tab on the operator dashboard: a sortable view
of every credit-ledger row (typed cloud/license/install) with computed
remaining/total balances, key filter, and a per-row "grant free credits"
action.
Endpoints (routes/admin.js):
- GET /admin/credits — snapshotAll() enriched with a type derived from
the credit-key prefix and a computed balance (computeRemaining against
live tier quotas), since the ledger stores consumed counters only.
- POST /admin/credits/grant {credit_key, amount} — adds free top-up via
addPurchasedCredits. Grants land in the never-expires purchased bucket
(spent after the tier allowance). Guards: positive integer, <=1,000,000,
and the row must already exist (a typo can't spawn a ghost row).
Admin-only; no /relay/* client contract change. Tests added in
server/test/admin-credits.test.js (mount the real router over HTTP).
Version bumped 0.2.124 -> 0.2.125.
Malformed JSON request bodies fell through to Express's default error
handler, which renders an HTML page including the local-filesystem stack
trace (info leak). Add a final error-handling middleware: JSON 400 for
entity.parse.failed, 413 for entity.too.large, generic 500 otherwise —
closing the stack-trace leak on every propagated error, not just JSON.
resolveIdentity and verifyOperatorKey compared the shared
relay_cloud_operator_key with ===/!==, which short-circuits on the first
differing byte — a timing oracle on a high-value key. Use a
timingSafeEqual-based constantTimeEqual, matching admin-auth.js.
saveMeeting/loadMeeting/deleteMeeting built path.join(meetingsDir, id +
'.json') straight from req.params.id, so an admin-authed :id like
'../../etc/passwd' could read/write/delete outside internal-meetings/.
Centralize a meetingPath() helper that strips anything outside
[A-Za-z0-9_-] (mirrors output-store.js) and throws on an empty result;
load/delete catch it as 404/no-op. Add a regression test.
multer 1.x is affected by CVE-2025-47944/47935/48997/7338 (malformed
multipart crashes the process / leaks memory). 2.x raises catchable
errors instead. Usage (diskStorage + .single("file")) is unchanged.
Commit the server lockfile so the Dockerfile's npm-ci path pins the fix.
extendUserTier called setUserTier, which unconditionally zeroed
monthly_consumed and re-anchored the cycle. A user who renewed mid-cycle
(or a webhook double-firing across a restart) got their full monthly
allotment back for free. The monthly cycle already rolls on its own
anniversary via ensureRenewalRollover, so renewal must not reset it. Add
resetCycle to setUserTier (default true, preserving operator-grant
behavior); extendUserTier passes false for an in-force subscription and
true only for a brand-new or lapsed one. Add regression tests.
downloadDirect fetched any caller-supplied media_url with redirect-follow
and no host/scheme validation; the route is reachable via a self-chosen
X-Recap-Install-Id, so a caller could probe the operator's LAN or cloud
metadata (169.254.169.254). Add safe-url.js: assertPublicHttpUrl rejects
non-http(s) schemes and hosts resolving to private/loopback/link-local/
reserved ranges, and safeFetch follows redirects manually, re-validating
each hop. Route downloadDirect through it (covers transcribe-url,
summarize-url, and admin-test-run).