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/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +37 -2
- package/config/gate-templates.json +72 -0
- package/config/gates/default.json +13 -0
- package/config/merge-quality-checks.json +4 -1
- package/config/model-candidates.json +75 -0
- package/package.json +27 -11
- package/public/index.html +2 -2
- package/public/numbers.html +2 -2
- package/scripts/auto-promote-gates.js +178 -27
- package/scripts/cli-progress.js +111 -0
- package/scripts/dashboard-limits.js +27 -0
- package/scripts/dashboard.js +57 -6
- package/scripts/gate-stats.js +2 -2
- package/scripts/gates-engine.js +58 -4
- package/server.json +2 -2
- package/src/api/server.js +80 -34
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.
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
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
|
-
|
|
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
|
|
8740
|
-
|
|
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',
|