thumbgate 1.34.2 → 1.35.0

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.
package/server.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "source": "github",
9
9
  "url": "https://github.com/IgorGanapolsky/ThumbGate"
10
10
  },
11
- "version": "1.34.2",
11
+ "version": "1.35.0",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "thumbgate",
17
- "version": "1.34.2",
17
+ "version": "1.35.0",
18
18
  "runtimeHint": "npx",
19
19
  "runtimeArguments": [
20
20
  {
package/src/api/server.js CHANGED
@@ -195,6 +195,10 @@ const {
195
195
  writeDashboardReviewState,
196
196
  collectAllFeedbackEntries,
197
197
  } = require('../../scripts/dashboard');
198
+ const {
199
+ isDashboardDataLimitError,
200
+ formatDashboardLimitDetail,
201
+ } = require('../../scripts/dashboard-limits');
198
202
  const {
199
203
  collectAggregateLogEntries,
200
204
  computeAggregateFeedbackStats,
@@ -883,7 +887,7 @@ function hashDataIdentity(prefix, value) {
883
887
  }
884
888
 
885
889
  function buildHostedTenantIdentity(validation = {}) {
886
- if (!validation || validation.valid !== true || !validation.customerId) return null;
890
+ if (validation?.valid !== true || !validation.customerId) return null;
887
891
  const tenantSeed = validation.customerId;
888
892
  const principalSeed = validation.installId || tenantSeed;
889
893
  const tenantId = hashDataIdentity('tenant', tenantSeed);
@@ -1820,11 +1824,20 @@ function buildWorkflowIntakeQueue(parsed, workflowSprintIntake, feedbackDir, opt
1820
1824
  }
1821
1825
 
1822
1826
  function sendInvalidAnalyticsWindowProblem(res, title, err) {
1827
+ if (isDashboardDataLimitError(err)) {
1828
+ sendProblem(res, {
1829
+ type: PROBLEM_TYPES.INTERNAL,
1830
+ title: 'Dashboard data too large',
1831
+ status: 503,
1832
+ detail: formatDashboardLimitDetail(err, { phase: 'assembly' }),
1833
+ });
1834
+ return;
1835
+ }
1823
1836
  sendProblem(res, {
1824
1837
  type: PROBLEM_TYPES.INVALID_REQUEST,
1825
1838
  title,
1826
1839
  status: 400,
1827
- detail: err?.message ? err.message : 'Invalid analytics window request.',
1840
+ detail: err && err.message ? err.message : 'Invalid analytics window request.',
1828
1841
  });
1829
1842
  }
1830
1843
 
@@ -2775,32 +2788,37 @@ function getTrackedLinkTarget(slug) {
2775
2788
  : null;
2776
2789
  }
2777
2790
 
2778
- function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
2779
- const params = parsed.searchParams;
2791
+ function applyTrackedLinkDefaults(destinationUrl, target) {
2780
2792
  for (const [key, value] of Object.entries(target.defaults || {})) {
2781
2793
  if (!destinationUrl.searchParams.has(key)) {
2782
2794
  appendQueryParam(destinationUrl, key, value);
2783
2795
  }
2784
2796
  }
2797
+ }
2798
+
2799
+ function applyTrackedLinkQueryKeys(destinationUrl, params) {
2785
2800
  for (const key of TRACKED_LINK_QUERY_KEYS) {
2786
2801
  const value = params.get(key);
2787
- if (value && value.trim()) {
2788
- const normalizedValue = key === 'utm_campaign'
2789
- ? normalizeCampaignId(value)
2790
- : value.trim();
2791
- destinationUrl.searchParams.set(key, normalizedValue);
2792
- }
2793
- }
2794
- if (target.allowCustomerEmail) {
2795
- const customerEmail = normalizeCheckoutCustomerEmail(params.get('customer_email'));
2796
- if (customerEmail) {
2797
- appendQueryParam(
2798
- destinationUrl,
2799
- target.prefillStripeEmail ? 'prefilled_email' : 'customer_email',
2800
- customerEmail
2801
- );
2802
- }
2802
+ if (!value || !value.trim()) continue;
2803
+ const normalizedValue = key === 'utm_campaign'
2804
+ ? normalizeCampaignId(value)
2805
+ : value.trim();
2806
+ destinationUrl.searchParams.set(key, normalizedValue);
2803
2807
  }
2808
+ }
2809
+
2810
+ function applyTrackedLinkCustomerEmail(destinationUrl, params, target) {
2811
+ if (!target.allowCustomerEmail) return;
2812
+ const customerEmail = normalizeCheckoutCustomerEmail(params.get('customer_email'));
2813
+ if (!customerEmail) return;
2814
+ appendQueryParam(
2815
+ destinationUrl,
2816
+ target.prefillStripeEmail ? 'prefilled_email' : 'customer_email',
2817
+ customerEmail,
2818
+ );
2819
+ }
2820
+
2821
+ function applyTrackedLinkCtaDefaults(destinationUrl, target) {
2804
2822
  if (!destinationUrl.searchParams.has('cta_id')) {
2805
2823
  appendQueryParam(destinationUrl, 'cta_id', target.ctaId);
2806
2824
  }
@@ -2812,6 +2830,14 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
2812
2830
  }
2813
2831
  }
2814
2832
 
2833
+ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
2834
+ const params = parsed.searchParams;
2835
+ applyTrackedLinkDefaults(destinationUrl, target);
2836
+ applyTrackedLinkQueryKeys(destinationUrl, params);
2837
+ applyTrackedLinkCustomerEmail(destinationUrl, params, target);
2838
+ applyTrackedLinkCtaDefaults(destinationUrl, target);
2839
+ }
2840
+
2815
2841
  function buildTrackedLinkDestination(target, hostedConfig, parsed) {
2816
2842
  const configuredHref = target.configUrlKey ? hostedConfig[target.configUrlKey] : null;
2817
2843
  const href = target.href || configuredHref || target.fallbackHref;
@@ -3006,7 +3032,21 @@ function resolveCheckoutOfferSummary(metadata = {}) {
3006
3032
 
3007
3033
  function sendJson(res, statusCode, payload, extraHeaders = {}, options = {}) {
3008
3034
  const { headOnly = false } = options;
3009
- const body = JSON.stringify(payload);
3035
+ let body;
3036
+ try {
3037
+ body = JSON.stringify(payload);
3038
+ } catch (err) {
3039
+ if (isDashboardDataLimitError(err)) {
3040
+ sendProblem(res, {
3041
+ type: PROBLEM_TYPES.INTERNAL,
3042
+ title: 'Dashboard response too large',
3043
+ status: 503,
3044
+ detail: formatDashboardLimitDetail(err, { phase: 'stringify' }),
3045
+ });
3046
+ return;
3047
+ }
3048
+ throw err;
3049
+ }
3010
3050
  res.writeHead(statusCode, {
3011
3051
  'Content-Type': 'application/json; charset=utf-8',
3012
3052
  'X-Content-Type-Options': 'nosniff',
@@ -5169,6 +5209,23 @@ function isBillingSummaryAuthorized(req, expectedAdminKey, expectedOperatorKey)
5169
5209
  return false;
5170
5210
  }
5171
5211
 
5212
+ /** Read-only paths the operator key may access (CLI dashboard / CFO / north-star). */
5213
+ const OPERATOR_READONLY_GET_PATHS = new Set([
5214
+ '/v1/billing/summary',
5215
+ '/v1/dashboard',
5216
+ '/v1/dashboard/render-spec',
5217
+ '/v1/dashboard/ai-inventory',
5218
+ '/v1/dashboard/review-state',
5219
+ '/v1/intake/workflow-sprint/queue',
5220
+ '/v1/task-outcomes/monitor',
5221
+ ]);
5222
+
5223
+ function isOperatorReadonlyGet(req, expectedOperatorKey, pathname) {
5224
+ if (!expectedOperatorKey || req.method !== 'GET') return false;
5225
+ if (!OPERATOR_READONLY_GET_PATHS.has(pathname)) return false;
5226
+ return safeKeyEqual(extractApiKey(req), expectedOperatorKey);
5227
+ }
5228
+
5172
5229
  function extractTags(input) {
5173
5230
  if (Array.isArray(input)) return input;
5174
5231
  if (typeof input === 'string') {
@@ -8736,19 +8793,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8736
8793
  return;
8737
8794
  }
8738
8795
 
8739
- // Operator key is allowed to bypass the general admin gate for dedicated
8740
- // read-only operational endpoints.
8741
- const _reqToken = extractApiKey(req);
8742
- const isOperatorReadRequest = Boolean(expectedOperatorKey)
8743
- && safeKeyEqual(_reqToken, expectedOperatorKey)
8744
- && req.method === 'GET'
8745
- && [
8746
- '/v1/billing/summary',
8747
- '/v1/intake/workflow-sprint/queue',
8748
- '/v1/task-outcomes/monitor',
8749
- ].includes(pathname);
8750
-
8751
- if (!isOperatorReadRequest && !isAuthorized(req, expectedApiKey)) {
8796
+ // Operator key: read-only operational endpoints only (see OPERATOR_READONLY_GET_PATHS).
8797
+ if (!isOperatorReadonlyGet(req, expectedOperatorKey, pathname) && !isAuthorized(req, expectedApiKey)) {
8752
8798
  sendProblem(res, {
8753
8799
  type: PROBLEM_TYPES.UNAUTHORIZED,
8754
8800
  title: 'Unauthorized',