thumbgate 1.29.2 → 1.31.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/README.md +54 -16
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +143 -14
- package/adapters/opencode/opencode.json +1 -1
- package/bench/observability-eval-suite.json +2 -2
- package/bin/cli.js +154 -36
- package/config/evals/generation-quality-golden.json +95 -0
- package/config/evals/rag-answer-quality-golden.json +91 -0
- package/config/evals/retrieval-hybrid-ablation.json +66 -0
- package/config/evals/retrieval-ranking-golden.json +522 -0
- package/config/gates/default.json +217 -50
- package/config/mcp-allowlists.json +215 -185
- package/config/model-tiers.json +7 -2
- package/config/post-deploy-marketing-pages.json +26 -1
- package/glama.json +6 -0
- package/package.json +94 -11
- package/public/architecture.html +130 -0
- package/public/assets/diagrams/agent-integration.png +0 -0
- package/public/assets/diagrams/before-after.svg +22 -0
- package/public/assets/diagrams/decision.svg +36 -0
- package/public/assets/diagrams/feedback-pipeline.png +0 -0
- package/public/assets/diagrams/hero-thumbs.svg +68 -0
- package/public/assets/diagrams/loop.svg +40 -0
- package/public/assets/diagrams/plugin-topology.png +0 -0
- package/public/assets/diagrams/pre-action-gate-loop.svg +59 -0
- package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
- package/public/assets/diagrams/stack.svg +18 -0
- package/public/assets/diagrams/thumbgate-architecture.png +0 -0
- package/public/case-studies.html +151 -0
- package/public/compare.html +1 -0
- package/public/dashboard.html +126 -28
- package/public/eval-scorecard.html +195 -0
- package/public/eval-scorecard.json +18 -0
- package/public/evaluations.html +168 -0
- package/public/index.html +143 -13
- package/public/numbers.html +3 -2
- package/public/pricing.html +143 -30
- package/public/whitepaper.html +189 -0
- package/scripts/a-plus-evidence-scorecard.js +303 -0
- package/scripts/activation-quickstart.js +1 -0
- package/scripts/agent-outcome-monitor.js +71 -1
- package/scripts/async-eval-observability.js +36 -11
- package/scripts/audit-trail.js +37 -1
- package/scripts/auto-promote-gates.js +149 -34
- package/scripts/billing.js +3 -1
- package/scripts/claude-feedback-sync.js +3 -2
- package/scripts/cli-feedback.js +13 -7
- package/scripts/colbert-style-maxsim.js +236 -0
- package/scripts/cross-encoder-reranker.js +359 -126
- package/scripts/dashboard-chat.js +350 -17
- package/scripts/document-intake.js +283 -7
- package/scripts/eval-quality-suite.js +204 -0
- package/scripts/feedback-aggregate.js +5 -2
- package/scripts/feedback-loop.js +359 -189
- package/scripts/feedback-paths.js +32 -13
- package/scripts/feedback-quality.js +53 -0
- package/scripts/filesystem-search.js +17 -7
- package/scripts/gates-engine.js +98 -4
- package/scripts/generate-case-study-outreach.js +253 -0
- package/scripts/generate-eval-scorecard.js +276 -0
- package/scripts/growth-campaigns.js +183 -0
- package/scripts/harness-tool-names.js +70 -0
- package/scripts/hook-runtime.js +10 -3
- package/scripts/jsonl-watcher.js +1 -0
- package/scripts/lesson-db.js +16 -5
- package/scripts/lesson-embedding-index.js +67 -20
- package/scripts/lesson-embedding-maintenance.js +177 -0
- package/scripts/lesson-inference.js +23 -4
- package/scripts/lesson-reranker.js +55 -9
- package/scripts/lesson-retrieval.js +375 -32
- package/scripts/lesson-search.js +48 -11
- package/scripts/llm-client.js +304 -15
- package/scripts/mcp-config.js +26 -5
- package/scripts/mcp-oauth.js +37 -2
- package/scripts/model-eval.js +308 -0
- package/scripts/model-tier-router.js +593 -0
- package/scripts/parallel-workflow-orchestrator.js +86 -22
- package/scripts/pragmatic-hybrid-search.js +379 -0
- package/scripts/published-cli.js +11 -1
- package/scripts/rag-document-pipeline.js +461 -0
- package/scripts/rag-structured-output.js +441 -0
- package/scripts/ragas-style-metrics.js +351 -0
- package/scripts/refresh-proof-pack.js +261 -0
- package/scripts/request-envelope.js +178 -0
- package/scripts/rerank-pipeline.js +370 -0
- package/scripts/rerank-quality-eval.js +155 -0
- package/scripts/retrieval-hybrid-ablation.js +120 -0
- package/scripts/retrieval-quality-tier.js +118 -0
- package/scripts/risk-scorer.js +144 -15
- package/scripts/secret-scanner.js +395 -4
- package/scripts/self-distill-agent.js +7 -1
- package/scripts/self-healing-check.js +25 -0
- package/scripts/skill-packs.js +183 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/statusline-links.js +1 -1
- package/scripts/statusline-local-stats.js +1 -1
- package/scripts/statusline.sh +8 -1
- package/scripts/telemetry-analytics.js +13 -1
- package/scripts/thumbgate-bench.js +13 -0
- package/scripts/thumbgate-search.js +98 -6
- package/scripts/tier-budget-guard.js +186 -0
- package/scripts/tool-kpi-tracker.js +124 -0
- package/scripts/tool-registry.js +95 -1
- package/scripts/vector-store.js +108 -4
- package/scripts/verify-marketing-pages-deployed.js +85 -3
- package/server.json +44 -0
- package/smithery.yaml +17 -0
- package/src/api/server.js +424 -99
package/src/api/server.js
CHANGED
|
@@ -172,7 +172,8 @@ const {
|
|
|
172
172
|
readTaskOutcomes,
|
|
173
173
|
recordTaskOutcome,
|
|
174
174
|
} = require('../../scripts/task-outcomes');
|
|
175
|
-
const {
|
|
175
|
+
const { monitorProductionSignals } = require('../../scripts/agent-outcome-monitor');
|
|
176
|
+
const { computeToolKpis, recordToolCall } = require('../../scripts/tool-kpi-tracker');
|
|
176
177
|
const {
|
|
177
178
|
calculateEscalationMetrics,
|
|
178
179
|
decideEscalation,
|
|
@@ -209,7 +210,7 @@ const {
|
|
|
209
210
|
getSettingsStatus,
|
|
210
211
|
} = require('../../scripts/settings-hierarchy');
|
|
211
212
|
const {
|
|
212
|
-
|
|
213
|
+
searchThumbgateAsync,
|
|
213
214
|
} = require('../../scripts/thumbgate-search');
|
|
214
215
|
const {
|
|
215
216
|
appendTelemetryPing,
|
|
@@ -237,6 +238,7 @@ const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
|
|
|
237
238
|
const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
|
|
238
239
|
const mcpOauth = require('../../scripts/mcp-oauth');
|
|
239
240
|
const { packCheckoutReference } = require('../../scripts/checkout-attribution-reference');
|
|
241
|
+
const { normalizeCampaignId } = require('../../scripts/growth-campaigns');
|
|
240
242
|
// OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
|
|
241
243
|
// (Claude Connectors Directory requires OAuth for authenticated services).
|
|
242
244
|
const oauthStore = mcpOauth.createStore();
|
|
@@ -266,6 +268,11 @@ const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugi
|
|
|
266
268
|
const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
|
|
267
269
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
268
270
|
const NUMBERS_PAGE_PATH = path.resolve(__dirname, '../../public/numbers.html');
|
|
271
|
+
const ARCHITECTURE_PAGE_PATH = path.resolve(__dirname, '../../public/architecture.html');
|
|
272
|
+
const WHITEPAPER_PAGE_PATH = path.resolve(__dirname, '../../public/whitepaper.html');
|
|
273
|
+
const EVAL_SCORECARD_PAGE_PATH = path.resolve(__dirname, '../../public/eval-scorecard.html');
|
|
274
|
+
const EVALUATIONS_PAGE_PATH = path.resolve(__dirname, '../../public/evaluations.html');
|
|
275
|
+
const CASE_STUDIES_PAGE_PATH = path.resolve(__dirname, '../../public/case-studies.html');
|
|
269
276
|
const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
|
|
270
277
|
const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
|
|
271
278
|
const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
|
|
@@ -417,7 +424,7 @@ const BUILD_METADATA = resolveBuildMetadata();
|
|
|
417
424
|
const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
418
425
|
const IDLE_JOB_STATUSES = new Set(['queued', 'paused', 'resume_requested']);
|
|
419
426
|
const JOB_CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
|
|
420
|
-
const TRACKED_LINK_QUERY_KEYS = [
|
|
427
|
+
const TRACKED_LINK_QUERY_KEYS = new Set([
|
|
421
428
|
'utm_source',
|
|
422
429
|
'utm_medium',
|
|
423
430
|
'utm_campaign',
|
|
@@ -445,7 +452,7 @@ const TRACKED_LINK_QUERY_KEYS = [
|
|
|
445
452
|
'seat_count',
|
|
446
453
|
'landing_path',
|
|
447
454
|
'referrer_host',
|
|
448
|
-
];
|
|
455
|
+
]);
|
|
449
456
|
const TRACKED_LINK_TARGETS = Object.freeze({
|
|
450
457
|
gpt: {
|
|
451
458
|
href: 'https://chatgpt.com/g/g-69dcfd1cd5f881918ae31874631d6f08-thumbgate',
|
|
@@ -473,6 +480,25 @@ const TRACKED_LINK_TARGETS = Object.freeze({
|
|
|
473
480
|
},
|
|
474
481
|
allowCustomerEmail: true,
|
|
475
482
|
},
|
|
483
|
+
// Human skip from Pro interstitial: same-origin hop so crawlers never see buy.stripe.com
|
|
484
|
+
// in the interstitial HTML (bot-guard contract). Bots are deflected to /checkout/pro.
|
|
485
|
+
'pro-direct': {
|
|
486
|
+
fallbackHref: PRO_CHECKOUT_URL,
|
|
487
|
+
external: true,
|
|
488
|
+
botDeflectPath: '/checkout/pro',
|
|
489
|
+
ctaId: 'pro_checkout_direct_stripe',
|
|
490
|
+
ctaPlacement: 'checkout_interstitial',
|
|
491
|
+
eventType: 'pro_checkout_direct_stripe',
|
|
492
|
+
defaults: {
|
|
493
|
+
utm_source: 'website',
|
|
494
|
+
utm_medium: 'checkout_interstitial',
|
|
495
|
+
utm_campaign: 'pro_upgrade',
|
|
496
|
+
plan_id: 'pro',
|
|
497
|
+
billing_cycle: 'monthly',
|
|
498
|
+
},
|
|
499
|
+
allowCustomerEmail: true,
|
|
500
|
+
prefillStripeEmail: true,
|
|
501
|
+
},
|
|
476
502
|
// 2026-06-02: Teams/Aiventyx deprecated. Redirect legacy links to Pro.
|
|
477
503
|
teams: {
|
|
478
504
|
path: '/go/pro?utm_source=legacy_teams&utm_medium=redirect',
|
|
@@ -830,6 +856,67 @@ function getRequestFeedbackPaths(req, parsed) {
|
|
|
830
856
|
});
|
|
831
857
|
}
|
|
832
858
|
|
|
859
|
+
const DATA_IDENTITY_CACHE = new Map();
|
|
860
|
+
const DATA_IDENTITY_CACHE_LIMIT = 2048;
|
|
861
|
+
|
|
862
|
+
function hashDataIdentity(prefix, value) {
|
|
863
|
+
const normalized = String(value || '');
|
|
864
|
+
const cacheKey = `${prefix}\0${normalized}`;
|
|
865
|
+
const cached = DATA_IDENTITY_CACHE.get(cacheKey);
|
|
866
|
+
if (cached) return cached;
|
|
867
|
+
|
|
868
|
+
// Billing identifiers can be email-like and API keys must never be made
|
|
869
|
+
// guessable through a fast digest. A memory-hard KDF gives us a stable,
|
|
870
|
+
// path-safe pseudonym without persisting the source identifier.
|
|
871
|
+
const derived = `${prefix}_${crypto.scryptSync(
|
|
872
|
+
normalized,
|
|
873
|
+
`thumbgate-${prefix}-data-identity-v1`,
|
|
874
|
+
16,
|
|
875
|
+
{ N: 16384, r: 8, p: 1 }
|
|
876
|
+
).toString('hex').slice(0, 24)}`;
|
|
877
|
+
|
|
878
|
+
if (DATA_IDENTITY_CACHE.size >= DATA_IDENTITY_CACHE_LIMIT) {
|
|
879
|
+
DATA_IDENTITY_CACHE.clear();
|
|
880
|
+
}
|
|
881
|
+
DATA_IDENTITY_CACHE.set(cacheKey, derived);
|
|
882
|
+
return derived;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function buildHostedTenantIdentity(validation = {}) {
|
|
886
|
+
if (!validation || validation.valid !== true || !validation.customerId) return null;
|
|
887
|
+
const tenantSeed = validation.customerId;
|
|
888
|
+
const principalSeed = validation.installId || tenantSeed;
|
|
889
|
+
const tenantId = hashDataIdentity('tenant', tenantSeed);
|
|
890
|
+
return {
|
|
891
|
+
partition: tenantId,
|
|
892
|
+
accessContext: {
|
|
893
|
+
tenantId,
|
|
894
|
+
principalId: hashDataIdentity('principal', principalSeed),
|
|
895
|
+
isAdmin: false,
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function resolveRequestDataIdentity(req, expectedApiKey) {
|
|
901
|
+
const token = extractApiKey(req);
|
|
902
|
+
if (!token || (expectedApiKey && safeKeyEqual(token, expectedApiKey))) {
|
|
903
|
+
return { partition: null, accessContext: null };
|
|
904
|
+
}
|
|
905
|
+
const validation = validateApiKey(token);
|
|
906
|
+
const identity = buildHostedTenantIdentity(validation);
|
|
907
|
+
if (validation.valid === true && !identity) {
|
|
908
|
+
return { partition: null, accessContext: null, invalidHostedIdentity: true };
|
|
909
|
+
}
|
|
910
|
+
return identity || { partition: null, accessContext: null };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function partitionFeedbackPaths(paths, dataIdentity) {
|
|
914
|
+
if (!dataIdentity?.partition) return paths;
|
|
915
|
+
return getFeedbackPaths({
|
|
916
|
+
feedbackDir: path.join(paths.FEEDBACK_DIR, 'tenants', dataIdentity.partition),
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
833
920
|
function getSafeDataDir(req, parsed) {
|
|
834
921
|
const { FEEDBACK_LOG_PATH } = getRequestFeedbackPaths(req, parsed);
|
|
835
922
|
return path.resolve(path.dirname(FEEDBACK_LOG_PATH));
|
|
@@ -876,11 +963,13 @@ function updateLessonRecord(feedbackDir, lessonId, updater) {
|
|
|
876
963
|
}
|
|
877
964
|
|
|
878
965
|
function getPublicMcpTools() {
|
|
879
|
-
|
|
966
|
+
const { getExposedTools } = require('../../adapters/mcp/server-stdio');
|
|
967
|
+
return getExposedTools().map((tool) => ({
|
|
880
968
|
name: tool.name,
|
|
881
969
|
...(tool.title ? { title: tool.title } : {}),
|
|
882
970
|
description: tool.description,
|
|
883
971
|
inputSchema: tool.inputSchema,
|
|
972
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
884
973
|
// Serve the tool-registry annotations (readOnlyHint/destructiveHint). Required
|
|
885
974
|
// by the Claude Connectors Directory (missing annotations = the #1 rejection
|
|
886
975
|
// cause) and used by MCP clients for permission prompts. Was being dropped here.
|
|
@@ -889,13 +978,7 @@ function getPublicMcpTools() {
|
|
|
889
978
|
}
|
|
890
979
|
|
|
891
980
|
function getServerCardTools() {
|
|
892
|
-
return
|
|
893
|
-
name: tool.name,
|
|
894
|
-
...(tool.title ? { title: tool.title } : {}),
|
|
895
|
-
description: tool.description,
|
|
896
|
-
inputSchema: tool.inputSchema,
|
|
897
|
-
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
898
|
-
}));
|
|
981
|
+
return getPublicMcpTools();
|
|
899
982
|
}
|
|
900
983
|
|
|
901
984
|
function buildPublicUrl(hostedConfig, pathname) {
|
|
@@ -905,7 +988,7 @@ function buildPublicUrl(hostedConfig, pathname) {
|
|
|
905
988
|
const VERIFICATION_EVIDENCE_URL = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md';
|
|
906
989
|
|
|
907
990
|
function getToolDiscoveryIndex(hostedConfig) {
|
|
908
|
-
return
|
|
991
|
+
return getPublicMcpTools().map((tool) => ({
|
|
909
992
|
name: tool.name,
|
|
910
993
|
description: tool.description,
|
|
911
994
|
annotations: tool.annotations || {},
|
|
@@ -915,7 +998,7 @@ function getToolDiscoveryIndex(hostedConfig) {
|
|
|
915
998
|
|
|
916
999
|
function getContextFootprintReport(hostedConfig) {
|
|
917
1000
|
return buildContextFootprintReport({
|
|
918
|
-
tools:
|
|
1001
|
+
tools: getPublicMcpTools(),
|
|
919
1002
|
schemaUrlTemplate: buildPublicUrl(hostedConfig, '/.well-known/mcp/tools/{name}.json'),
|
|
920
1003
|
});
|
|
921
1004
|
}
|
|
@@ -1273,7 +1356,8 @@ function inferSearchQuery(referrer) {
|
|
|
1273
1356
|
|
|
1274
1357
|
function getAttributionValue(params, key, fallbackValue) {
|
|
1275
1358
|
const value = params.get(key);
|
|
1276
|
-
|
|
1359
|
+
const resolved = value?.trim() || fallbackValue;
|
|
1360
|
+
return key === 'utm_campaign' ? normalizeCampaignId(resolved) : resolved;
|
|
1277
1361
|
}
|
|
1278
1362
|
|
|
1279
1363
|
function parseUrlSearchParams(urlValue) {
|
|
@@ -1645,17 +1729,28 @@ function projectWorkflowIntakeQueueLead(lead = {}, options = {}) {
|
|
|
1645
1729
|
page: attribution.page || null,
|
|
1646
1730
|
landingPath: attribution.landingPath || null,
|
|
1647
1731
|
},
|
|
1648
|
-
nextOperatorStep: options
|
|
1649
|
-
? 'request_action_time_approval'
|
|
1650
|
-
: options.discoveryPacket?.status === 'approval_ready_not_authorized'
|
|
1651
|
-
? 'request_action_time_approval_for_discovery'
|
|
1652
|
-
: lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold',
|
|
1732
|
+
nextOperatorStep: resolveNextOperatorStep(lead, options),
|
|
1653
1733
|
qualificationCard: options.qualificationCard || buildIntakeQualificationCard(lead, options),
|
|
1654
1734
|
discoveryPacket: options.discoveryPacket || buildIntakeDiscoveryPacket(lead, options),
|
|
1655
1735
|
closePacket: options.closePacket || buildIntakeClosePacket(lead, options),
|
|
1656
1736
|
};
|
|
1657
1737
|
}
|
|
1658
1738
|
|
|
1739
|
+
/**
|
|
1740
|
+
* Which action the operator should take next on an intake lead. Extracted from a three-level
|
|
1741
|
+
* nested ternary: the precedence (close packet, then discovery packet, then lead status) is the
|
|
1742
|
+
* actual logic and is far easier to check when it reads top to bottom.
|
|
1743
|
+
*/
|
|
1744
|
+
function resolveNextOperatorStep(lead, options) {
|
|
1745
|
+
if (options.closePacket?.status === 'approval_ready_not_authorized') {
|
|
1746
|
+
return 'request_action_time_approval';
|
|
1747
|
+
}
|
|
1748
|
+
if (options.discoveryPacket?.status === 'approval_ready_not_authorized') {
|
|
1749
|
+
return 'request_action_time_approval_for_discovery';
|
|
1750
|
+
}
|
|
1751
|
+
return lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold';
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1659
1754
|
function buildWorkflowIntakeQueue(parsed, workflowSprintIntake, feedbackDir, options = {}) {
|
|
1660
1755
|
const { limit, statuses } = resolveWorkflowIntakeQueueOptions(parsed, workflowSprintIntake);
|
|
1661
1756
|
const leads = workflowSprintIntake.loadWorkflowSprintLeads(feedbackDir);
|
|
@@ -1734,7 +1829,7 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
|
|
|
1734
1829
|
}
|
|
1735
1830
|
|
|
1736
1831
|
function sendWorkflowIntakeQueueProblem(res, err) {
|
|
1737
|
-
const status = Number(err
|
|
1832
|
+
const status = Number(err?.statusCode) || 400;
|
|
1738
1833
|
sendProblem(res, {
|
|
1739
1834
|
type: status >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.INVALID_REQUEST,
|
|
1740
1835
|
title: status >= 500
|
|
@@ -2486,8 +2581,9 @@ ${hiddenInputs}
|
|
|
2486
2581
|
<p class="email-note">Required so the checkout has attributable buyer intent and your receipt can be recovered.</p>
|
|
2487
2582
|
<button type="submit" class="primary">Pay $19/mo with Stripe →</button>
|
|
2488
2583
|
</form>
|
|
2584
|
+
<a class="secondary" data-i="pro_checkout_direct_stripe" rel="nofollow" href="/go/pro-direct?utm_source=website&utm_medium=checkout_interstitial&utm_campaign=pro_upgrade&utm_content=skip_form&cta_id=pro_checkout_direct_stripe&cta_placement=checkout_interstitial">Skip form — open Stripe checkout directly →</a>
|
|
2489
2585
|
<a class="secondary" data-i="workflow_sprint_intake" href="/#workflow-sprint-intake">Not sure yet? Send the workflow first</a>
|
|
2490
|
-
<p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages.</p>
|
|
2586
|
+
<p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages. Email path keeps receipts attributable; skip uses a same-origin hop that bot-deflects crawlers.</p>
|
|
2491
2587
|
<div class="objection-box" aria-label="Checkout feedback">
|
|
2492
2588
|
<strong>Not buying today?</strong>
|
|
2493
2589
|
<p class="objection-note">Tap one reason so we know what to fix. This does not sign you up.</p>
|
|
@@ -2689,7 +2785,10 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
|
2689
2785
|
for (const key of TRACKED_LINK_QUERY_KEYS) {
|
|
2690
2786
|
const value = params.get(key);
|
|
2691
2787
|
if (value && value.trim()) {
|
|
2692
|
-
|
|
2788
|
+
const normalizedValue = key === 'utm_campaign'
|
|
2789
|
+
? normalizeCampaignId(value)
|
|
2790
|
+
: value.trim();
|
|
2791
|
+
destinationUrl.searchParams.set(key, normalizedValue);
|
|
2693
2792
|
}
|
|
2694
2793
|
}
|
|
2695
2794
|
if (target.allowCustomerEmail) {
|
|
@@ -2730,7 +2829,7 @@ function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinat
|
|
|
2730
2829
|
const source = pickFirstText(
|
|
2731
2830
|
params.get('source'),
|
|
2732
2831
|
params.get('utm_source'),
|
|
2733
|
-
target.defaults
|
|
2832
|
+
target.defaults?.utm_source,
|
|
2734
2833
|
inferSource(referrerHost)
|
|
2735
2834
|
);
|
|
2736
2835
|
|
|
@@ -2744,19 +2843,21 @@ function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinat
|
|
|
2744
2843
|
traceId: pickFirstText(params.get('trace_id')),
|
|
2745
2844
|
source,
|
|
2746
2845
|
utmSource: pickFirstText(params.get('utm_source'), source),
|
|
2747
|
-
utmMedium: pickFirstText(params.get('utm_medium'), target.defaults
|
|
2748
|
-
utmCampaign:
|
|
2846
|
+
utmMedium: pickFirstText(params.get('utm_medium'), target.defaults?.utm_medium, 'link_router'),
|
|
2847
|
+
utmCampaign: normalizeCampaignId(
|
|
2848
|
+
pickFirstText(params.get('utm_campaign'), target.defaults?.utm_campaign, 'first_party_redirect')
|
|
2849
|
+
),
|
|
2749
2850
|
utmContent: pickFirstText(params.get('utm_content')),
|
|
2750
2851
|
utmTerm: pickFirstText(params.get('utm_term')),
|
|
2751
2852
|
creator: pickFirstText(params.get('creator'), params.get('creator_handle')),
|
|
2752
2853
|
community: pickFirstText(params.get('community'), params.get('subreddit')),
|
|
2753
2854
|
postId: pickFirstText(params.get('post_id')),
|
|
2754
2855
|
commentId: pickFirstText(params.get('comment_id')),
|
|
2755
|
-
campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults
|
|
2856
|
+
campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults?.campaign_variant),
|
|
2756
2857
|
offerCode: pickFirstText(params.get('offer_code')),
|
|
2757
2858
|
ctaId: pickFirstText(params.get('cta_id'), target.ctaId),
|
|
2758
2859
|
ctaPlacement: pickFirstText(params.get('cta_placement'), target.ctaPlacement),
|
|
2759
|
-
planId: pickFirstText(params.get('plan_id'), target.defaults
|
|
2860
|
+
planId: pickFirstText(params.get('plan_id'), target.defaults?.plan_id),
|
|
2760
2861
|
landingPath: pickFirstText(params.get('landing_path'), `/go/${target.slug}`),
|
|
2761
2862
|
page: `/go/${target.slug}`,
|
|
2762
2863
|
referrer,
|
|
@@ -2805,6 +2906,37 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
|
|
|
2805
2906
|
|
|
2806
2907
|
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
2807
2908
|
const journeyState = resolveJourneyState(req, parsed);
|
|
2909
|
+
const botClassification = classifyRequester(req.headers);
|
|
2910
|
+
if (target.botDeflectPath && botClassification.isBot) {
|
|
2911
|
+
const deflectUrl = new URL(target.botDeflectPath, hostedConfig.appOrigin);
|
|
2912
|
+
appendTrackedLinkQueryParams(deflectUrl, parsed, target);
|
|
2913
|
+
if (!isHeadRequest) {
|
|
2914
|
+
appendBestEffortTelemetry(
|
|
2915
|
+
FEEDBACK_DIR,
|
|
2916
|
+
{
|
|
2917
|
+
eventType: 'checkout_bot_deflected',
|
|
2918
|
+
clientType: 'web',
|
|
2919
|
+
page: `/go/${target.slug}`,
|
|
2920
|
+
reason: botClassification.reason || 'bot',
|
|
2921
|
+
destinationSlug: target.slug,
|
|
2922
|
+
acquisitionId: journeyState.acquisitionId,
|
|
2923
|
+
visitorId: journeyState.visitorId,
|
|
2924
|
+
sessionId: journeyState.sessionId,
|
|
2925
|
+
},
|
|
2926
|
+
req.headers,
|
|
2927
|
+
`tracked_link_bot_deflect:${target.slug}`
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
res.writeHead(302, {
|
|
2931
|
+
...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
|
|
2932
|
+
'Cache-Control': 'no-store',
|
|
2933
|
+
'X-Robots-Tag': 'noindex,nofollow',
|
|
2934
|
+
'X-ThumbGate-Link-Slug': target.slug,
|
|
2935
|
+
Location: deflectUrl.toString(),
|
|
2936
|
+
});
|
|
2937
|
+
res.end();
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2808
2940
|
const destinationUrl = buildTrackedLinkDestination(target, hostedConfig, parsed);
|
|
2809
2941
|
const attribution = buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl);
|
|
2810
2942
|
if (target.external) {
|
|
@@ -3822,6 +3954,12 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3822
3954
|
{ path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
|
|
3823
3955
|
{ path: '/agents-cost-savings', changefreq: 'weekly', priority: '0.85' },
|
|
3824
3956
|
{ path: '/ai-malpractice-prevention', changefreq: 'weekly', priority: '0.9' },
|
|
3957
|
+
{ path: '/whitepaper', changefreq: 'weekly', priority: '0.9' },
|
|
3958
|
+
{ path: '/architecture', changefreq: 'weekly', priority: '0.85' },
|
|
3959
|
+
{ path: '/eval-scorecard', changefreq: 'weekly', priority: '0.9' },
|
|
3960
|
+
{ path: '/evaluations', changefreq: 'weekly', priority: '0.85' },
|
|
3961
|
+
{ path: '/case-studies', changefreq: 'weekly', priority: '0.9' },
|
|
3962
|
+
{ path: '/numbers', changefreq: 'weekly', priority: '0.8' },
|
|
3825
3963
|
{ path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
|
|
3826
3964
|
{ path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
|
|
3827
3965
|
{ path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
|
|
@@ -5198,9 +5336,10 @@ function createApiServer() {
|
|
|
5198
5336
|
sendJson(res, 403, { error: 'project selection is only available on localhost requests' });
|
|
5199
5337
|
return;
|
|
5200
5338
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5339
|
+
let requestFeedbackPaths = getRequestFeedbackPaths(req, parsed);
|
|
5340
|
+
let requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
|
|
5341
|
+
let requestSafeDataDir = getSafeDataDir(req, parsed);
|
|
5342
|
+
let requestDocumentAccessContext = null;
|
|
5204
5343
|
|
|
5205
5344
|
// PostHog reverse proxy -- bypasses ad blockers.
|
|
5206
5345
|
// Only allow known PostHog API paths to prevent SSRF (CodeQL js/request-forgery).
|
|
@@ -5273,6 +5412,14 @@ function createApiServer() {
|
|
|
5273
5412
|
? mcpOauth.tokenAudienceValid(oauthSession, resourceUrl)
|
|
5274
5413
|
: rawKeyValid;
|
|
5275
5414
|
if (!authed) {
|
|
5415
|
+
recordToolCall({
|
|
5416
|
+
toolName: msg.params?.name || 'unknown',
|
|
5417
|
+
serverName: 'mcp-http',
|
|
5418
|
+
latencyMs: 0,
|
|
5419
|
+
success: false,
|
|
5420
|
+
agentId: 'unauthenticated',
|
|
5421
|
+
metadata: { denied: true, deniedReason: 'authentication_required' },
|
|
5422
|
+
});
|
|
5276
5423
|
res.writeHead(401, {
|
|
5277
5424
|
'Content-Type': 'application/json',
|
|
5278
5425
|
// RFC 9728: point unauthenticated clients at the resource metadata.
|
|
@@ -5295,6 +5442,14 @@ function createApiServer() {
|
|
|
5295
5442
|
const tool = MCP_TOOLS.find((t) => t.name === name);
|
|
5296
5443
|
const readOnly = Boolean(tool && tool.annotations && tool.annotations.readOnlyHint === true);
|
|
5297
5444
|
if (!readOnly) {
|
|
5445
|
+
recordToolCall({
|
|
5446
|
+
toolName: name || 'unknown',
|
|
5447
|
+
serverName: 'mcp-http',
|
|
5448
|
+
latencyMs: 0,
|
|
5449
|
+
success: false,
|
|
5450
|
+
agentId: 'reviewer',
|
|
5451
|
+
metadata: { denied: true, deniedReason: 'reviewer_read_only' },
|
|
5452
|
+
});
|
|
5298
5453
|
sendJson(res, 200, {
|
|
5299
5454
|
jsonrpc: '2.0', id: msg.id,
|
|
5300
5455
|
error: { code: -32002, message: `Tool "${name}" requires write access; the reviewer credential is read-only.` },
|
|
@@ -5302,15 +5457,39 @@ function createApiServer() {
|
|
|
5302
5457
|
return;
|
|
5303
5458
|
}
|
|
5304
5459
|
}
|
|
5460
|
+
if (oauthSession) {
|
|
5461
|
+
const name = msg.params && msg.params.name;
|
|
5462
|
+
const tool = MCP_TOOLS.find((candidate) => candidate.name === name);
|
|
5463
|
+
const requiredScope = tool?.annotations?.readOnlyHint === true ? 'mcp:read' : 'mcp:write';
|
|
5464
|
+
if (!mcpOauth.scopeAllows(oauthSession, requiredScope)) {
|
|
5465
|
+
recordToolCall({
|
|
5466
|
+
toolName: name || 'unknown',
|
|
5467
|
+
serverName: 'mcp-http',
|
|
5468
|
+
latencyMs: 0,
|
|
5469
|
+
success: false,
|
|
5470
|
+
agentId: oauthSession.clientId || 'oauth-client',
|
|
5471
|
+
metadata: {
|
|
5472
|
+
denied: true,
|
|
5473
|
+
deniedReason: 'insufficient_scope',
|
|
5474
|
+
requiredScope,
|
|
5475
|
+
},
|
|
5476
|
+
});
|
|
5477
|
+
sendJson(res, 200, {
|
|
5478
|
+
jsonrpc: '2.0', id: msg.id,
|
|
5479
|
+
error: { code: -32003, message: `OAuth token is missing required scope "${requiredScope}".` },
|
|
5480
|
+
});
|
|
5481
|
+
return;
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5305
5484
|
(async () => {
|
|
5306
5485
|
try {
|
|
5307
5486
|
const { callTool } = require('../../adapters/mcp/server-stdio');
|
|
5308
|
-
const name = msg.params
|
|
5487
|
+
const name = msg.params?.name;
|
|
5309
5488
|
const args = (msg.params && msg.params.arguments) || {};
|
|
5310
5489
|
const result = await callTool(name, args);
|
|
5311
5490
|
sendJson(res, 200, {
|
|
5312
5491
|
jsonrpc: '2.0', id: msg.id,
|
|
5313
|
-
result
|
|
5492
|
+
result,
|
|
5314
5493
|
});
|
|
5315
5494
|
} catch (err) {
|
|
5316
5495
|
sendJson(res, 200, {
|
|
@@ -5593,7 +5772,7 @@ function createApiServer() {
|
|
|
5593
5772
|
let trackedParsed = parsed;
|
|
5594
5773
|
if (req.method === 'POST') {
|
|
5595
5774
|
const target = getTrackedLinkTarget(trackedLinkMatch[1]);
|
|
5596
|
-
if (!target
|
|
5775
|
+
if (!target?.requiresPost) {
|
|
5597
5776
|
sendJson(res, target ? 405 : 404, target
|
|
5598
5777
|
? { error: 'POST is not supported for this tracked link' }
|
|
5599
5778
|
: { error: 'Tracked link not found', allowed: Object.keys(TRACKED_LINK_TARGETS) }, {
|
|
@@ -5612,7 +5791,7 @@ function createApiServer() {
|
|
|
5612
5791
|
const form = await parseFormBody(req, 16 * 1024);
|
|
5613
5792
|
trackedParsed = new URL(parsed.toString());
|
|
5614
5793
|
for (const [key, value] of Object.entries(form)) {
|
|
5615
|
-
if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.
|
|
5794
|
+
if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.has(key)) {
|
|
5616
5795
|
trackedParsed.searchParams.set(key, String(value || '').trim());
|
|
5617
5796
|
}
|
|
5618
5797
|
}
|
|
@@ -5684,10 +5863,31 @@ function createApiServer() {
|
|
|
5684
5863
|
return;
|
|
5685
5864
|
}
|
|
5686
5865
|
|
|
5687
|
-
//
|
|
5688
|
-
|
|
5866
|
+
// GET/HEAD only render a confirmation. Capture requires an authenticated
|
|
5867
|
+
// POST so prefetchers and link checkers cannot create human feedback.
|
|
5868
|
+
if ((isGetLikeRequest || req.method === 'POST') && pathname === '/feedback/quick') {
|
|
5689
5869
|
const signal = parsed.searchParams.get('signal');
|
|
5690
5870
|
if (signal === 'up' || signal === 'down') {
|
|
5871
|
+
const emoji = signal === 'up' ? '👍' : '👎';
|
|
5872
|
+
const label = signal === 'up' ? 'Positive' : 'Negative';
|
|
5873
|
+
if (isHeadRequest) {
|
|
5874
|
+
sendHtml(res, 200, '', {}, { headOnly: true });
|
|
5875
|
+
return;
|
|
5876
|
+
}
|
|
5877
|
+
if (req.method === 'GET') {
|
|
5878
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ThumbGate feedback</title></head>
|
|
5879
|
+
<body style="background:#0a0a0a;color:#fff;font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">
|
|
5880
|
+
<form method="post" action="/feedback/quick?signal=${signal}" style="text-align:center;background:#141414;padding:40px;border-radius:16px">
|
|
5881
|
+
<div style="font-size:64px">${emoji}</div><h1>Record ${label.toLowerCase()} feedback?</h1>
|
|
5882
|
+
<p>This confirmation prevents previews and crawlers from recording feedback.</p>
|
|
5883
|
+
<button type="submit" style="padding:10px 20px;font-weight:700">Record ${emoji} feedback</button>
|
|
5884
|
+
</form></body></html>`);
|
|
5885
|
+
return;
|
|
5886
|
+
}
|
|
5887
|
+
if (!isAuthorized(req, expectedApiKey)) {
|
|
5888
|
+
sendJson(res, 401, { error: 'Authentication required to record human feedback' });
|
|
5889
|
+
return;
|
|
5890
|
+
}
|
|
5691
5891
|
const chatHistory = readRecentConversationWindow({
|
|
5692
5892
|
feedbackDir: requestSafeDataDir,
|
|
5693
5893
|
limit: 10,
|
|
@@ -5697,35 +5897,17 @@ function createApiServer() {
|
|
|
5697
5897
|
context: 'Quick capture from Claude Code statusline',
|
|
5698
5898
|
chatHistory,
|
|
5699
5899
|
tags: ['statusline', 'quick-capture'],
|
|
5900
|
+
reviewOrigin: 'human',
|
|
5700
5901
|
});
|
|
5701
|
-
const emoji = signal === 'up' ? '👍' : '👎';
|
|
5702
|
-
const color = signal === 'up' ? '#22c55e' : '#ef4444';
|
|
5703
|
-
const label = signal === 'up' ? 'Positive' : 'Negative';
|
|
5704
5902
|
const opposite = signal === 'up' ? 'down' : 'up';
|
|
5705
5903
|
const oppEmoji = signal === 'up' ? '👎' : '👍';
|
|
5706
5904
|
const feedbackId = result.feedbackEvent?.id || 'saved';
|
|
5707
5905
|
const promoted = result.accepted ? 'Promoted to memory' : 'Stored';
|
|
5708
5906
|
sendHtml(res, 200, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ThumbGate — ${label} feedback</title>
|
|
5709
5907
|
<style>
|
|
5710
|
-
*{box-sizing:border-box}
|
|
5711
5908
|
body{background:#0a0a0a;color:#fff;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
5712
5909
|
.card{text-align:center;background:#141414;border:1px solid #222;border-radius:16px;padding:48px 40px;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,.5)}
|
|
5713
|
-
.emoji{font-size:
|
|
5714
|
-
@keyframes pop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}
|
|
5715
|
-
.msg{font-size:22px;color:${color};font-weight:700;margin-bottom:4px}
|
|
5716
|
-
.sub{font-size:13px;color:#666;margin-top:6px;font-family:ui-monospace,monospace}
|
|
5717
|
-
.context-form{margin-top:20px;text-align:left}
|
|
5718
|
-
.context-form label{font-size:12px;color:#888;display:block;margin-bottom:6px}
|
|
5719
|
-
.context-form textarea{width:100%;background:#1a1a1a;border:1px solid #333;border-radius:8px;color:#ccc;padding:10px;font-size:13px;resize:vertical;min-height:60px;font-family:system-ui}
|
|
5720
|
-
.context-form textarea:focus{outline:none;border-color:${color}}
|
|
5721
|
-
.context-form button{margin-top:8px;background:${color};color:#000;border:none;border-radius:8px;padding:8px 20px;font-size:13px;font-weight:600;cursor:pointer}
|
|
5722
|
-
.context-form button:hover{opacity:.85}
|
|
5723
|
-
.actions{margin-top:24px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
|
5724
|
-
.actions a{color:#22d3ee;text-decoration:none;font-size:13px;padding:8px 16px;border:1px solid #333;border-radius:8px;transition:all .15s}
|
|
5725
|
-
.actions a:hover{background:#1a2a2e;border-color:#22d3ee}
|
|
5726
|
-
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#22c55e;color:#000;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:600;display:none;animation:slideUp .3s ease-out}
|
|
5727
|
-
@keyframes slideUp{from{opacity:0;transform:translateX(-50%) translateY(12px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
|
|
5728
|
-
.badge{display:inline-block;font-size:11px;padding:2px 8px;border-radius:4px;background:#1a1a1a;border:1px solid #333;color:#888;margin-top:8px}
|
|
5910
|
+
.emoji{font-size:64px}.sub,label{color:#888}textarea{width:100%;min-height:60px}.actions{margin-top:20px}.actions a{color:#22d3ee;margin:8px}
|
|
5729
5911
|
</style></head><body>
|
|
5730
5912
|
<div class="card">
|
|
5731
5913
|
<div class="emoji">${emoji}</div>
|
|
@@ -5738,7 +5920,7 @@ body{background:#0a0a0a;color:#fff;font-family:system-ui,-apple-system,sans-seri
|
|
|
5738
5920
|
</div>
|
|
5739
5921
|
<div class="actions">
|
|
5740
5922
|
<a href="/lessons/${feedbackId}" title="View the full lesson and edit it">📋 View Lesson</a>
|
|
5741
|
-
<a href="/feedback/quick?signal=${opposite}" title="Meant to click ${oppEmoji}?">
|
|
5923
|
+
<a href="/feedback/quick?signal=${opposite}" title="Meant to click ${oppEmoji}?">Record ${oppEmoji} instead</a>
|
|
5742
5924
|
<a href="/dashboard">Dashboard →</a>
|
|
5743
5925
|
</div>
|
|
5744
5926
|
</div>
|
|
@@ -5762,6 +5944,10 @@ async function addContext(){
|
|
|
5762
5944
|
}
|
|
5763
5945
|
|
|
5764
5946
|
if (req.method === 'POST' && pathname === '/feedback/quick/context') {
|
|
5947
|
+
if (!isAuthorized(req, expectedApiKey)) {
|
|
5948
|
+
sendJson(res, 401, { error: 'Authentication required to update human feedback' });
|
|
5949
|
+
return;
|
|
5950
|
+
}
|
|
5765
5951
|
const body = await parseJsonBody(req);
|
|
5766
5952
|
const signal = body.signal;
|
|
5767
5953
|
const context = typeof body.context === 'string' ? body.context.trim() : '';
|
|
@@ -6206,6 +6392,74 @@ async function addContext(){
|
|
|
6206
6392
|
return;
|
|
6207
6393
|
}
|
|
6208
6394
|
|
|
6395
|
+
if (isGetLikeRequest && (pathname === '/architecture' || pathname === '/architecture.html')) {
|
|
6396
|
+
try {
|
|
6397
|
+
servePublicMarketingPage({
|
|
6398
|
+
req,
|
|
6399
|
+
res,
|
|
6400
|
+
parsed,
|
|
6401
|
+
hostedConfig,
|
|
6402
|
+
isHeadRequest,
|
|
6403
|
+
renderHtml: () => fs.readFileSync(ARCHITECTURE_PAGE_PATH, 'utf-8'),
|
|
6404
|
+
extraTelemetry: { pageType: 'architecture' },
|
|
6405
|
+
});
|
|
6406
|
+
} catch {
|
|
6407
|
+
sendJson(res, 404, { error: 'Architecture page not found' });
|
|
6408
|
+
}
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
|
|
6412
|
+
if (isGetLikeRequest && (pathname === '/whitepaper' || pathname === '/whitepaper.html' || pathname === '/how-we-know' || pathname === '/how-we-know-it-works')) {
|
|
6413
|
+
try {
|
|
6414
|
+
servePublicMarketingPage({
|
|
6415
|
+
req,
|
|
6416
|
+
res,
|
|
6417
|
+
parsed,
|
|
6418
|
+
hostedConfig,
|
|
6419
|
+
isHeadRequest,
|
|
6420
|
+
renderHtml: () => fs.readFileSync(WHITEPAPER_PAGE_PATH, 'utf-8'),
|
|
6421
|
+
extraTelemetry: { pageType: 'whitepaper' },
|
|
6422
|
+
});
|
|
6423
|
+
} catch {
|
|
6424
|
+
sendJson(res, 404, { error: 'White paper page not found' });
|
|
6425
|
+
}
|
|
6426
|
+
return;
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
if (isGetLikeRequest && (pathname === '/eval-scorecard' || pathname === '/eval-scorecard.html' || pathname === '/scorecard' || pathname === '/bench')) {
|
|
6430
|
+
try {
|
|
6431
|
+
servePublicMarketingPage({
|
|
6432
|
+
req,
|
|
6433
|
+
res,
|
|
6434
|
+
parsed,
|
|
6435
|
+
hostedConfig,
|
|
6436
|
+
isHeadRequest,
|
|
6437
|
+
renderHtml: () => fs.readFileSync(EVAL_SCORECARD_PAGE_PATH, 'utf-8'),
|
|
6438
|
+
extraTelemetry: { pageType: 'eval_scorecard' },
|
|
6439
|
+
});
|
|
6440
|
+
} catch {
|
|
6441
|
+
sendJson(res, 404, { error: 'Eval scorecard page not found' });
|
|
6442
|
+
}
|
|
6443
|
+
return;
|
|
6444
|
+
}
|
|
6445
|
+
|
|
6446
|
+
if (isGetLikeRequest && (pathname === '/evaluations' || pathname === '/evaluations.html')) {
|
|
6447
|
+
try {
|
|
6448
|
+
servePublicMarketingPage({
|
|
6449
|
+
req,
|
|
6450
|
+
res,
|
|
6451
|
+
parsed,
|
|
6452
|
+
hostedConfig,
|
|
6453
|
+
isHeadRequest,
|
|
6454
|
+
renderHtml: () => fs.readFileSync(EVALUATIONS_PAGE_PATH, 'utf-8'),
|
|
6455
|
+
extraTelemetry: { pageType: 'evaluations' },
|
|
6456
|
+
});
|
|
6457
|
+
} catch {
|
|
6458
|
+
sendJson(res, 404, { error: 'Evaluations page not found' });
|
|
6459
|
+
}
|
|
6460
|
+
return;
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6209
6463
|
if (isGetLikeRequest && (pathname === '/federal' || pathname === '/federal.html' || pathname === '/government' || pathname === '/gov')) {
|
|
6210
6464
|
// Federal lead-gen page. Routed through servePublicMarketingPage so agency
|
|
6211
6465
|
// arrivals via SBIR / GSA / outbound channels capture UTM attribution and
|
|
@@ -6470,7 +6724,7 @@ async function addContext(){
|
|
|
6470
6724
|
if (
|
|
6471
6725
|
key === 'confirm'
|
|
6472
6726
|
|| key === 'customer_email'
|
|
6473
|
-
|| TRACKED_LINK_QUERY_KEYS.
|
|
6727
|
+
|| TRACKED_LINK_QUERY_KEYS.has(key)
|
|
6474
6728
|
) {
|
|
6475
6729
|
parsed.searchParams.set(key, String(value || '').trim());
|
|
6476
6730
|
}
|
|
@@ -6986,7 +7240,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6986
7240
|
// are configured (production) the key MUST match a configured admin /
|
|
6987
7241
|
// operator / reviewer key — otherwise OAuth would authenticate nobody.
|
|
6988
7242
|
// In insecure/dev mode (no keys configured) any non-empty key is accepted.
|
|
6989
|
-
|
|
7243
|
+
const keyRole = resolveKeyRole(form.get('api_key') || '');
|
|
7244
|
+
if (!keyRole) {
|
|
6990
7245
|
sendJson(res, 400, { error: 'access_denied', error_description: 'invalid ThumbGate API key' });
|
|
6991
7246
|
return;
|
|
6992
7247
|
}
|
|
@@ -6996,6 +7251,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6996
7251
|
codeChallenge: authorizationParams.codeChallenge,
|
|
6997
7252
|
codeChallengeMethod: authorizationParams.codeChallengeMethod,
|
|
6998
7253
|
scope: authorizationParams.scope,
|
|
7254
|
+
allowedScopes: keyRole === 'reviewer' ? ['mcp:read'] : mcpOauth.SUPPORTED_SCOPES,
|
|
6999
7255
|
resource: authorizationParams.resource || buildPublicUrl(hostedConfig, '/mcp'),
|
|
7000
7256
|
boundKey: form.get('api_key') || '',
|
|
7001
7257
|
state,
|
|
@@ -7039,7 +7295,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7039
7295
|
sendJson(res, 200, {
|
|
7040
7296
|
name: 'thumbgate',
|
|
7041
7297
|
version: pkg.version,
|
|
7042
|
-
count:
|
|
7298
|
+
count: getPublicMcpTools().length,
|
|
7043
7299
|
tools: getToolDiscoveryIndex(hostedConfig),
|
|
7044
7300
|
}, {}, {
|
|
7045
7301
|
headOnly: isHeadRequest,
|
|
@@ -7072,7 +7328,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7072
7328
|
});
|
|
7073
7329
|
return;
|
|
7074
7330
|
}
|
|
7075
|
-
const tool =
|
|
7331
|
+
const tool = getPublicMcpTools().find((candidate) => candidate.name === toolName);
|
|
7076
7332
|
if (!tool) {
|
|
7077
7333
|
sendJson(res, 404, {
|
|
7078
7334
|
error: 'tool_not_found',
|
|
@@ -7088,6 +7344,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7088
7344
|
description: tool.description,
|
|
7089
7345
|
annotations: tool.annotations || {},
|
|
7090
7346
|
inputSchema: tool.inputSchema,
|
|
7347
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
7091
7348
|
}, {}, {
|
|
7092
7349
|
headOnly: isHeadRequest,
|
|
7093
7350
|
});
|
|
@@ -8078,29 +8335,25 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8078
8335
|
return;
|
|
8079
8336
|
}
|
|
8080
8337
|
|
|
8081
|
-
// Public case studies — proof
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
<p><a href="https://thumbgate.ai">Home</a> · <a href="/pricing">Pricing</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a> · <a href="/support">Support</a></p>
|
|
8097
|
-
</footer>
|
|
8098
|
-
</body></html>`, {}, {
|
|
8099
|
-
headOnly: isHeadRequest,
|
|
8100
|
-
});
|
|
8338
|
+
// Public case studies — dogfood proof narratives (static HTML; no fabricated logos).
|
|
8339
|
+
if (isGetLikeRequest && (pathname === '/case-studies' || pathname === '/case-studies.html')) {
|
|
8340
|
+
try {
|
|
8341
|
+
servePublicMarketingPage({
|
|
8342
|
+
req,
|
|
8343
|
+
res,
|
|
8344
|
+
parsed,
|
|
8345
|
+
hostedConfig,
|
|
8346
|
+
isHeadRequest,
|
|
8347
|
+
renderHtml: () => fs.readFileSync(CASE_STUDIES_PAGE_PATH, 'utf-8'),
|
|
8348
|
+
extraTelemetry: { pageType: 'case_studies' },
|
|
8349
|
+
});
|
|
8350
|
+
} catch {
|
|
8351
|
+
sendJson(res, 404, { error: 'Case studies page not found' });
|
|
8352
|
+
}
|
|
8101
8353
|
return;
|
|
8102
8354
|
}
|
|
8103
8355
|
|
|
8356
|
+
|
|
8104
8357
|
// Public canonical pricing page. The audit flagged "pricing schizophrenia":
|
|
8105
8358
|
// sales/pricing.json said $49 / $299, COMMERCIAL_TRUTH.md said $19 / $149,
|
|
8106
8359
|
// and there was no buyer-facing surface to reconcile the two. This is now
|
|
@@ -8255,7 +8508,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8255
8508
|
});
|
|
8256
8509
|
return;
|
|
8257
8510
|
}
|
|
8258
|
-
if (result
|
|
8511
|
+
if (result?.retryable === true) {
|
|
8259
8512
|
sendProblem(res, {
|
|
8260
8513
|
type: PROBLEM_TYPES.INTERNAL,
|
|
8261
8514
|
title: 'Fulfillment temporarily unavailable',
|
|
@@ -8443,14 +8696,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8443
8696
|
}
|
|
8444
8697
|
|
|
8445
8698
|
const resolvedTraceId = result.traceId || requestedTraceId;
|
|
8446
|
-
|
|
8447
|
-
|
|
8699
|
+
let nextSteps = {};
|
|
8700
|
+
if (result.apiKey) {
|
|
8701
|
+
nextSteps = {
|
|
8448
8702
|
env: `THUMBGATE_API_KEY=${result.apiKey}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
|
|
8449
8703
|
curl: `curl -X POST ${hostedConfig.billingApiBaseUrl}/v1/feedback/capture \\\n+ -H 'Authorization: Bearer ${result.apiKey}' \\\n+ -H 'Content-Type: application/json' \\\n+ -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
|
|
8450
|
-
}
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8704
|
+
};
|
|
8705
|
+
} else if (result.offerKind === 'workflow_hardening_diagnostic') {
|
|
8706
|
+
nextSteps = { fulfillment: 'Payment confirmed. Diagnostic fulfillment is processing; an email will arrive when it completes.' };
|
|
8707
|
+
}
|
|
8454
8708
|
|
|
8455
8709
|
sendJson(res, 200, {
|
|
8456
8710
|
...result,
|
|
@@ -8504,6 +8758,25 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8504
8758
|
return;
|
|
8505
8759
|
}
|
|
8506
8760
|
|
|
8761
|
+
// Provisioned hosted keys are authenticated identities, not merely valid
|
|
8762
|
+
// booleans. Bind every private data path to a hash of the billing customer
|
|
8763
|
+
// before any endpoint reads or writes feedback, documents, traces, or jobs.
|
|
8764
|
+
// Static admin/local operation retains the existing single-tenant path.
|
|
8765
|
+
const requestDataIdentity = resolveRequestDataIdentity(req, expectedApiKey);
|
|
8766
|
+
if (requestDataIdentity.invalidHostedIdentity) {
|
|
8767
|
+
sendProblem(res, {
|
|
8768
|
+
type: PROBLEM_TYPES.UNAUTHORIZED,
|
|
8769
|
+
title: 'Unauthorized',
|
|
8770
|
+
status: 403,
|
|
8771
|
+
detail: 'The hosted API key is not bound to a customer identity.',
|
|
8772
|
+
});
|
|
8773
|
+
return;
|
|
8774
|
+
}
|
|
8775
|
+
requestFeedbackPaths = partitionFeedbackPaths(requestFeedbackPaths, requestDataIdentity);
|
|
8776
|
+
requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
|
|
8777
|
+
requestSafeDataDir = path.resolve(requestFeedbackDir);
|
|
8778
|
+
requestDocumentAccessContext = requestDataIdentity.accessContext;
|
|
8779
|
+
|
|
8507
8780
|
// Usage metering — record request for billing keys (not static THUMBGATE_API_KEY)
|
|
8508
8781
|
const _token = extractBearerToken(req);
|
|
8509
8782
|
if (_token && _token !== expectedApiKey) {
|
|
@@ -8514,7 +8787,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8514
8787
|
if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
|
|
8515
8788
|
const stats = shouldAggregateFeedback()
|
|
8516
8789
|
? computeAggregateFeedbackStats({ feedbackDir: requestFeedbackPaths.FEEDBACK_DIR })
|
|
8517
|
-
: analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH);
|
|
8790
|
+
: analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH, { humanOnly: true });
|
|
8518
8791
|
try {
|
|
8519
8792
|
const { getStatuslineMeta } = require('../../scripts/statusline-meta');
|
|
8520
8793
|
const meta = getStatuslineMeta({ env: process.env });
|
|
@@ -8524,6 +8797,33 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8524
8797
|
stats.tier = 'Pro';
|
|
8525
8798
|
}
|
|
8526
8799
|
|
|
8800
|
+
// Active gates for the dashboard hero cards. Connect only fetches
|
|
8801
|
+
// /v1/feedback/stats first; without these fields Active Gates stays
|
|
8802
|
+
// stuck on the HTML placeholder "—" until a later /v1/dashboard call
|
|
8803
|
+
// succeeds (and never updates if that call fails).
|
|
8804
|
+
try {
|
|
8805
|
+
const { computeGateStats } = require('../../scripts/dashboard');
|
|
8806
|
+
const gateStats = computeGateStats();
|
|
8807
|
+
stats.activeGateCount = Number(gateStats.totalGates) || 0;
|
|
8808
|
+
stats.gateStats = {
|
|
8809
|
+
totalGates: Number(gateStats.totalGates) || 0,
|
|
8810
|
+
manualCount: Number(gateStats.manualCount) || 0,
|
|
8811
|
+
autoCount: Number(gateStats.autoCount) || 0,
|
|
8812
|
+
blocked: Number(gateStats.blocked) || 0,
|
|
8813
|
+
warned: Number(gateStats.warned) || 0,
|
|
8814
|
+
};
|
|
8815
|
+
} catch (error) {
|
|
8816
|
+
debugApiFallback('gate stats unavailable for feedback/stats', error);
|
|
8817
|
+
stats.activeGateCount = 0;
|
|
8818
|
+
stats.gateStats = {
|
|
8819
|
+
totalGates: 0,
|
|
8820
|
+
manualCount: 0,
|
|
8821
|
+
autoCount: 0,
|
|
8822
|
+
blocked: 0,
|
|
8823
|
+
warned: 0,
|
|
8824
|
+
};
|
|
8825
|
+
}
|
|
8826
|
+
|
|
8527
8827
|
const projectChatSettings = readProjectChatSettings(req, parsed);
|
|
8528
8828
|
|
|
8529
8829
|
stats.geminiConfigured = Boolean(
|
|
@@ -9034,6 +9334,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9034
9334
|
workflowContract: body.workflowContract,
|
|
9035
9335
|
repoPath: body.repoPath,
|
|
9036
9336
|
localOnly: body.localOnly === true,
|
|
9337
|
+
ttlMs: body.ttlMs,
|
|
9037
9338
|
clear: body.clear === true,
|
|
9038
9339
|
});
|
|
9039
9340
|
sendJson(res, 200, { scope });
|
|
@@ -9116,7 +9417,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9116
9417
|
|
|
9117
9418
|
if (req.method === 'GET' && pathname === '/v1/task-outcomes/monitor') {
|
|
9118
9419
|
const outcomes = readTaskOutcomes({ feedbackDir: requestFeedbackDir });
|
|
9119
|
-
|
|
9420
|
+
const toolKpis = computeToolKpis({ feedbackDir: requestFeedbackDir });
|
|
9421
|
+
sendJson(res, 200, monitorProductionSignals(outcomes, toolKpis));
|
|
9120
9422
|
return;
|
|
9121
9423
|
}
|
|
9122
9424
|
|
|
@@ -9221,13 +9523,19 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9221
9523
|
const signal = parsed.searchParams.get('signal') || null;
|
|
9222
9524
|
let results;
|
|
9223
9525
|
try {
|
|
9224
|
-
|
|
9225
|
-
results = searchThumbgate({
|
|
9526
|
+
results = await searchThumbgateAsync({
|
|
9226
9527
|
query,
|
|
9227
9528
|
limit: Number.isFinite(limit) ? limit : 10,
|
|
9228
9529
|
source,
|
|
9229
9530
|
signal,
|
|
9230
|
-
feedbackDir:
|
|
9531
|
+
feedbackDir: requestFeedbackDir,
|
|
9532
|
+
metadataFilters: {
|
|
9533
|
+
tags: (parsed.searchParams.get('tags') || '').split(',').map((tag) => tag.trim()).filter(Boolean),
|
|
9534
|
+
sourceFormat: parsed.searchParams.get('sourceFormat') || undefined,
|
|
9535
|
+
sourceType: parsed.searchParams.get('sourceType') || undefined,
|
|
9536
|
+
},
|
|
9537
|
+
queryRewrite: parsed.searchParams.get('queryRewrite') !== 'false',
|
|
9538
|
+
accessContext: requestDocumentAccessContext,
|
|
9231
9539
|
});
|
|
9232
9540
|
} catch (err) {
|
|
9233
9541
|
throw createHttpError(400, err.message || 'Invalid ThumbGate search request');
|
|
@@ -9245,13 +9553,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9245
9553
|
const body = await parseJsonBody(req);
|
|
9246
9554
|
let results;
|
|
9247
9555
|
try {
|
|
9248
|
-
|
|
9249
|
-
results = searchThumbgate({
|
|
9556
|
+
results = await searchThumbgateAsync({
|
|
9250
9557
|
query: body.query || body.q || '',
|
|
9251
9558
|
limit: body.limit,
|
|
9252
9559
|
source: body.source,
|
|
9253
9560
|
signal: body.signal,
|
|
9254
|
-
feedbackDir:
|
|
9561
|
+
feedbackDir: requestFeedbackDir,
|
|
9562
|
+
metadataFilters: body.filters,
|
|
9563
|
+
queryRewrite: body.queryRewrite !== false,
|
|
9564
|
+
accessContext: requestDocumentAccessContext,
|
|
9255
9565
|
});
|
|
9256
9566
|
} catch (err) {
|
|
9257
9567
|
throw createHttpError(400, err.message || 'Invalid ThumbGate search request');
|
|
@@ -9269,6 +9579,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9269
9579
|
limit: Number.isFinite(limit) ? limit : 20,
|
|
9270
9580
|
query,
|
|
9271
9581
|
tag,
|
|
9582
|
+
accessContext: requestDocumentAccessContext,
|
|
9272
9583
|
});
|
|
9273
9584
|
sendJson(res, 200, results);
|
|
9274
9585
|
return;
|
|
@@ -9282,6 +9593,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9282
9593
|
}
|
|
9283
9594
|
const document = readImportedDocument(documentId, {
|
|
9284
9595
|
feedbackDir: requestFeedbackDir,
|
|
9596
|
+
accessContext: requestDocumentAccessContext,
|
|
9285
9597
|
});
|
|
9286
9598
|
if (!document) {
|
|
9287
9599
|
throw createHttpError(404, `Imported document not found: ${escapeHtml(documentId)}`);
|
|
@@ -9329,6 +9641,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9329
9641
|
guardrails: body.guardrails,
|
|
9330
9642
|
tags: extractTags(body.tags),
|
|
9331
9643
|
skill: body.skill,
|
|
9644
|
+
reviewOrigin: 'human',
|
|
9332
9645
|
});
|
|
9333
9646
|
const actionIntegration = inferActionIntegration(body, req.headers);
|
|
9334
9647
|
appendBestEffortTelemetry(requestFeedbackDir, {
|
|
@@ -9413,6 +9726,11 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9413
9726
|
tags: extractTags(body.tags),
|
|
9414
9727
|
proposeGates: body.proposeGates !== false,
|
|
9415
9728
|
feedbackDir: requestFeedbackDir,
|
|
9729
|
+
accessContext: requestDocumentAccessContext,
|
|
9730
|
+
visibility: body.visibility === 'private' ? 'private' : 'tenant',
|
|
9731
|
+
allowedPrincipals: Array.isArray(body.allowedPrincipals)
|
|
9732
|
+
? body.allowedPrincipals.map((value) => String(value || '').trim()).filter(Boolean)
|
|
9733
|
+
: [],
|
|
9416
9734
|
});
|
|
9417
9735
|
sendJson(res, 201, {
|
|
9418
9736
|
ok: true,
|
|
@@ -9463,14 +9781,19 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9463
9781
|
fs.writeFileSync(paths.outputPath, result.jsonl);
|
|
9464
9782
|
}
|
|
9465
9783
|
|
|
9466
|
-
|
|
9784
|
+
const response = {
|
|
9467
9785
|
pairs: result.pairs.length,
|
|
9786
|
+
pairCount: result.pairs.length,
|
|
9468
9787
|
errors: result.errors.length,
|
|
9469
9788
|
learnings: result.learnings.length,
|
|
9470
9789
|
unpairedErrors: result.unpairedErrors.length,
|
|
9471
9790
|
unpairedLearnings: result.unpairedLearnings.length,
|
|
9472
9791
|
outputPath: paths.outputPath,
|
|
9473
|
-
}
|
|
9792
|
+
};
|
|
9793
|
+
if (body.includePairs === true) {
|
|
9794
|
+
response.records = result.pairs;
|
|
9795
|
+
}
|
|
9796
|
+
sendJson(res, 200, response);
|
|
9474
9797
|
return;
|
|
9475
9798
|
}
|
|
9476
9799
|
|
|
@@ -10496,6 +10819,8 @@ module.exports = {
|
|
|
10496
10819
|
buildLossAnalyticsResponse,
|
|
10497
10820
|
buildIntakeAlertRateLimitKey,
|
|
10498
10821
|
sanitizeHtmlUnsafeJsonValue,
|
|
10822
|
+
buildHostedTenantIdentity,
|
|
10823
|
+
partitionFeedbackPaths,
|
|
10499
10824
|
},
|
|
10500
10825
|
};
|
|
10501
10826
|
|