thumbgate 1.26.2 → 1.26.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,178 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const os = require('node:os');
6
- const { getBillingSummaryLive } = require('./billing');
7
- const { resolveAnalyticsWindow } = require('./analytics-window');
8
- const { resolveHostedBillingConfig } = require('./hosted-config');
9
-
10
- // Configure fetch proxy when running behind a corporate/sandbox proxy
11
- (function configureProxy() {
12
- const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy
13
- || process.env.HTTP_PROXY || process.env.http_proxy;
14
- if (!proxyUrl) return;
15
- try {
16
- const { ProxyAgent, setGlobalDispatcher } = require('undici');
17
- setGlobalDispatcher(new ProxyAgent(proxyUrl));
18
- } catch {
19
- // undici not available — fetch will use default dispatcher
20
- }
21
- }());
22
-
23
- const OPERATOR_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
24
-
25
- function normalizeText(value) {
26
- if (value === undefined || value === null) return null;
27
- const text = String(value).trim();
28
- return text || null;
29
- }
30
-
31
- function loadOperatorConfig(configPath = OPERATOR_CONFIG_PATH) {
32
- try {
33
- const raw = fs.readFileSync(configPath, 'utf8');
34
- const parsed = JSON.parse(raw);
35
- return {
36
- operatorKey: normalizeText(parsed.operatorKey),
37
- baseUrl: normalizeText(parsed.baseUrl),
38
- };
39
- } catch {
40
- return { operatorKey: null, baseUrl: null };
41
- }
42
- }
43
-
44
- function shouldPreferHostedSummary() {
45
- return String(process.env.THUMBGATE_METRICS_SOURCE || '').trim().toLowerCase() !== 'local';
46
- }
47
-
48
- function resolveHostedSummaryConfig() {
49
- const runtimeConfig = resolveHostedBillingConfig();
50
- const operatorConfig = loadOperatorConfig();
51
- // Priority: env THUMBGATE_OPERATOR_KEY > local config file > env THUMBGATE_API_KEY
52
- const apiKey = normalizeText(process.env.THUMBGATE_OPERATOR_KEY)
53
- || operatorConfig.operatorKey
54
- || normalizeText(process.env.THUMBGATE_API_KEY);
55
- const apiBaseUrl = normalizeText(process.env.THUMBGATE_BILLING_API_BASE_URL)
56
- || operatorConfig.baseUrl
57
- || runtimeConfig.billingApiBaseUrl;
58
- return {
59
- apiBaseUrl,
60
- apiKey,
61
- };
62
- }
63
-
64
- async function fetchHostedBillingSummary(options = {}, config = resolveHostedSummaryConfig()) {
65
- const analyticsWindow = resolveAnalyticsWindow(options);
66
- if (!shouldPreferHostedSummary()) {
67
- const err = new Error('Hosted operational summary is disabled.');
68
- err.code = 'hosted_summary_disabled';
69
- throw err;
70
- }
71
- if (!config.apiBaseUrl || !config.apiKey) {
72
- const err = new Error('Hosted operational summary is not configured.');
73
- err.code = 'hosted_summary_unconfigured';
74
- throw err;
75
- }
76
-
77
- const requestUrl = new URL('/v1/billing/summary', config.apiBaseUrl);
78
- requestUrl.searchParams.set('window', analyticsWindow.window);
79
- requestUrl.searchParams.set('timezone', analyticsWindow.timeZone);
80
- if (options.now !== undefined && options.now !== null && options.now !== '') {
81
- requestUrl.searchParams.set('now', analyticsWindow.now);
82
- }
83
-
84
- const response = await fetch(requestUrl, {
85
- method: 'GET',
86
- headers: {
87
- authorization: `Bearer ${config.apiKey}`,
88
- accept: 'application/json',
89
- },
90
- });
91
-
92
- if (!response.ok) {
93
- const detail = await response.text().catch(() => '');
94
- const err = new Error(`Hosted operational summary request failed (${response.status}): ${detail || 'unknown error'}`);
95
- err.code = 'hosted_summary_http_error';
96
- err.status = response.status;
97
- throw err;
98
- }
99
-
100
- return response.json();
101
- }
102
-
103
- async function getOperationalBillingSummary(options = {}) {
104
- const analyticsWindow = resolveAnalyticsWindow(options);
105
- try {
106
- const summary = await fetchHostedBillingSummary(analyticsWindow);
107
- return {
108
- source: 'hosted',
109
- summary,
110
- fallbackReason: null,
111
- hostedStatus: 200,
112
- summaryWindow: analyticsWindow.window,
113
- };
114
- } catch (err) {
115
- const reason = err && err.message ? err.message : 'hosted_summary_unavailable';
116
- const status = err && typeof err.status === 'number' ? err.status : null;
117
- const code = err && err.code ? err.code : null;
118
-
119
- // Hosted deliberately disabled or never configured — local fallback is
120
- // intentional, not a degraded state. Tag as plain 'local'.
121
- if (code === 'hosted_summary_disabled' || code === 'hosted_summary_unconfigured') {
122
- return {
123
- source: 'local',
124
- summary: await getBillingSummaryLive(analyticsWindow),
125
- fallbackReason: reason,
126
- hostedStatus: null,
127
- summaryWindow: analyticsWindow.window,
128
- };
129
- }
130
-
131
- // Auth failure is the most dangerous case: if hosted Stripe data says
132
- // we have paid customers and local ledgers are empty, silently returning
133
- // "$0.00" is a lie that hides actual revenue. Refuse to guess — surface
134
- // an actionable error so the operator fixes the key before any
135
- // downstream report renders wrong numbers.
136
- if (status === 401 || status === 403) {
137
- const authErr = new Error(
138
- `Hosted billing summary rejected credentials (HTTP ${status}). ` +
139
- `The operator key on this machine does not match the one on the ` +
140
- `hosted deployment. Fix: set THUMBGATE_OPERATOR_KEY in this shell, ` +
141
- `or update the operatorKey field in ~/.config/thumbgate/operator.json, ` +
142
- `to match Railway's THUMBGATE_OPERATOR_KEY. ` +
143
- `Running this command without hosted auth would report local-only ` +
144
- `data as ground truth, which may not reflect actual Stripe revenue. ` +
145
- `Original response: ${reason}`
146
- );
147
- authErr.code = 'hosted_summary_unauthorized';
148
- authErr.status = status;
149
- throw authErr;
150
- }
151
-
152
- // Non-auth failure (network, 5xx, config) — local fallback is still
153
- // useful for dev workflows, but tag the source so downstream renderers
154
- // and agents do not mistake it for verified hosted truth.
155
- //
156
- // Log only the status code (trusted) — the full reason contains upstream
157
- // response text and is only returned structurally via fallbackReason.
158
- console.warn(
159
- `[operational-summary] Hosted billing unreachable (status=${status ?? 'network'}); ` +
160
- `falling back to LOCAL-UNVERIFIED state. Numbers below may not reflect actual Stripe revenue.`
161
- );
162
- return {
163
- source: 'local-unverified',
164
- summary: await getBillingSummaryLive(analyticsWindow),
165
- fallbackReason: reason,
166
- hostedStatus: status,
167
- summaryWindow: analyticsWindow.window,
168
- };
169
- }
170
- }
171
-
172
- module.exports = {
173
- fetchHostedBillingSummary,
174
- getOperationalBillingSummary,
175
- resolveHostedSummaryConfig,
176
- shouldPreferHostedSummary,
177
- loadOperatorConfig,
178
- };