thumbgate 1.27.11 → 1.27.13
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/llms.txt +2 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +2 -4
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bin/cli.js +78 -259
- package/config/builtin-lessons.json +23 -0
- package/config/gate-templates.json +0 -228
- package/config/gates/claim-verification.json +0 -18
- package/package.json +35 -25
- package/public/assets/brand/thumbgate-logo-transparent.svg +22 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +19 -0
- package/public/assets/brand/thumbgate-mark.svg +11 -5
- package/public/blog.html +0 -30
- package/public/brand/thumbgate-mark.svg +9 -5
- package/public/chatgpt-app.html +2 -2
- package/public/compare.html +2 -1
- package/public/dashboard.html +1 -1
- package/public/federal.html +1 -1
- package/public/index.html +95 -216
- package/public/learn.html +59 -35
- package/public/lessons.html +1 -1
- package/public/numbers.html +2 -2
- package/public/pro.html +7 -7
- package/scripts/aws-blocks-guardrails.js +228 -0
- package/scripts/cli-schema.js +22 -10
- package/scripts/dashboard-chat.js +2 -1
- package/scripts/document-intake.js +1 -49
- package/scripts/durability/step.js +3 -3
- package/scripts/gate-stats.js +5 -11
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-stop-anti-claim.js +116 -184
- package/scripts/hosted-config.js +0 -12
- package/scripts/llm-client.js +187 -5
- package/scripts/plausible-domain-config.js +3 -1
- package/scripts/seo-gsd.js +240 -1
- package/scripts/tool-registry.js +2 -2
- package/scripts/vector-store.js +44 -0
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +340 -131
- package/public/assets/brand/thumbgate-mark-inline.svg +0 -15
- package/public/compare/adopt-ai.html +0 -219
- package/public/compare/agentix-labs.html +0 -197
- package/public/compare/ai-experience-orchestration.html +0 -216
- package/public/compare/anthropic-claude-for-legal.html +0 -260
- package/public/compare/anthropic-containment.html +0 -280
- package/public/compare/arcade.html +0 -175
- package/public/compare/arcjet.html +0 -239
- package/public/compare/bumblebee.html +0 -307
- package/public/compare/claude-code-hooks.html +0 -294
- package/public/compare/databricks-unity-ai-gateway.html +0 -215
- package/public/compare/fallow.html +0 -351
- package/public/compare/heidi.html +0 -233
- package/public/compare/mem0.html +0 -342
- package/public/compare/oak-and-sparrow-gatekeeper.html +0 -289
- package/public/compare/rein.html +0 -236
- package/public/compare/sigmashake.html +0 -256
- package/public/compare/speclock.html +0 -342
- package/public/guides/agent-harness-optimization.html +0 -342
- package/public/guides/agentic-web-governance.html +0 -406
- package/public/guides/ai-agent-governance-sprint.html +0 -415
- package/public/guides/ai-agent-pre-action-approval-gates.html +0 -401
- package/public/guides/ai-agent-workflow-migration-checklist.html +0 -392
- package/public/guides/ai-deployment-readiness.html +0 -415
- package/public/guides/ai-mode-ads-agent-governance.html +0 -401
- package/public/guides/ai-search-topical-presence.html +0 -342
- package/public/guides/autoresearch-agent-safety.html +0 -342
- package/public/guides/background-agent-governance.html +0 -358
- package/public/guides/best-tools-stop-ai-agents-breaking-production.html +0 -363
- package/public/guides/browser-automation-safety.html +0 -342
- package/public/guides/chatgpt-ads-trust.html +0 -353
- package/public/guides/claude-code-feedback.html +0 -339
- package/public/guides/claude-code-prevent-repeated-mistakes.html +0 -161
- package/public/guides/claude-code-skills-guardrails.html +0 -343
- package/public/guides/claude-desktop.html +0 -356
- package/public/guides/code-knowledge-graph-guardrails.html +0 -365
- package/public/guides/codex-cli-guardrails.html +0 -339
- package/public/guides/cursor-agent-guardrails.html +0 -339
- package/public/guides/cursor-prevent-repeated-mistakes.html +0 -161
- package/public/guides/database-agent-safety.html +0 -406
- package/public/guides/deepseek-v4-runtime-guardrails.html +0 -346
- package/public/guides/developer-machine-supply-chain-guardrails.html +0 -358
- package/public/guides/gcp-mcp-guardrails.html +0 -147
- package/public/guides/gemini-cli-feedback-memory.html +0 -339
- package/public/guides/gpt-5-5-model-evaluation.html +0 -358
- package/public/guides/internal-ai-engineering-stack-guardrails.html +0 -348
- package/public/guides/long-running-agent-context-management.html +0 -346
- package/public/guides/mcp-tool-governance.html +0 -401
- package/public/guides/multica-thumbgate-setup.html +0 -134
- package/public/guides/native-messaging-host-security.html +0 -342
- package/public/guides/policy-engine-pre-action-gates.html +0 -346
- package/public/guides/pre-action-checks.html +0 -342
- package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +0 -342
- package/public/guides/prompt-tricks-to-workflow-rules.html +0 -365
- package/public/guides/proxy-pointer-rag-guardrails.html +0 -352
- package/public/guides/rag-precision-tuning-guardrails.html +0 -352
- package/public/guides/reasoning-compression-guardrails.html +0 -346
- package/public/guides/relational-knowledge-ai-recommendations.html +0 -342
- package/public/guides/roo-code-alternative-cline.html +0 -339
- package/public/guides/semantic-programmatic-seo-guardrails.html +0 -352
- package/public/guides/seo-agent-skills-guardrails.html +0 -344
- package/public/guides/stop-repeated-ai-agent-mistakes.html +0 -342
- package/public/learn/ac-dc-runtime-enforcement.html +0 -277
- package/public/learn/agent-harness-pattern.html +0 -181
- package/public/learn/agent-identity-connector-governance.html +0 -146
- package/public/learn/agent-swarms-shared-gates.html +0 -173
- package/public/learn/agentic-enterprise-context-brain.html +0 -117
- package/public/learn/agentic-os-team-governance.html +0 -146
- package/public/learn/ai-agent-governance.html +0 -158
- package/public/learn/ai-agent-persistent-memory.html +0 -211
- package/public/learn/anthropomorphic-claim-gates.html +0 -180
- package/public/learn/background-agent-control-layer.html +0 -184
- package/public/learn/claude-code-goal-with-rubrics.html +0 -205
- package/public/learn/codex-role-plugins-need-governance.html +0 -125
- package/public/learn/cost-aware-agent-gate-routing.html +0 -173
- package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +0 -157
- package/public/learn/deterministic-agent-workflows.html +0 -185
- package/public/learn/feedback-loop-vs-decision-layer.html +0 -283
- package/public/learn/from-prototype-to-production.html +0 -223
- package/public/learn/learn.css +0 -51
- package/public/learn/mcp-pre-action-checks-explained.html +0 -172
- package/public/learn/pretix-stripe-connect-marketplaces.html +0 -161
- package/public/learn/regulated-agent-execution-boundary.html +0 -196
- package/public/learn/spec-driven-development.html +0 -168
- package/public/learn/stop-ai-agent-force-push.html +0 -134
- package/public/learn/vibe-coding-safety-net.html +0 -142
- package/scripts/reddit-browser-notification-watch.js +0 -230
package/src/api/server.js
CHANGED
|
@@ -3,6 +3,7 @@ const http = require('http');
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
6
7
|
const { EventEmitter } = require('node:events');
|
|
7
8
|
const pkg = require('../../package.json');
|
|
8
9
|
const {
|
|
@@ -215,6 +216,8 @@ const mcpOauth = require('../../scripts/mcp-oauth');
|
|
|
215
216
|
// OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
|
|
216
217
|
// (Claude Connectors Directory requires OAuth for authenticated services).
|
|
217
218
|
const oauthStore = mcpOauth.createStore();
|
|
219
|
+
const pendingOauthAuthorizeRequests = new Map();
|
|
220
|
+
const OAUTH_AUTHORIZE_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
218
221
|
const resendMailer = require('../../scripts/mailer/resend-mailer');
|
|
219
222
|
const {
|
|
220
223
|
buildContextFootprintReport,
|
|
@@ -244,6 +247,10 @@ const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
|
|
|
244
247
|
const USE_CASES_DIR = path.resolve(__dirname, '../../public/use-cases');
|
|
245
248
|
const PUBLIC_DIR = path.resolve(__dirname, '../../public');
|
|
246
249
|
const PUBLIC_ASSETS_DIR = path.resolve(__dirname, '../../public/assets');
|
|
250
|
+
const LEARN_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(LEARN_DIR);
|
|
251
|
+
const GUIDE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(GUIDES_DIR);
|
|
252
|
+
const COMPARE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(COMPARE_DIR);
|
|
253
|
+
const USE_CASE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(USE_CASES_DIR);
|
|
247
254
|
const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
|
|
248
255
|
const STATIC_MIME_BY_EXT = Object.freeze({
|
|
249
256
|
'.png': 'image/png',
|
|
@@ -460,6 +467,36 @@ const TRACKED_LINK_TARGETS = Object.freeze({
|
|
|
460
467
|
},
|
|
461
468
|
allowCustomerEmail: true,
|
|
462
469
|
},
|
|
470
|
+
diagnostic: {
|
|
471
|
+
configUrlKey: 'sprintDiagnosticCheckoutUrl',
|
|
472
|
+
fallbackHref: SPRINT_DIAGNOSTIC_CHECKOUT_URL,
|
|
473
|
+
external: true,
|
|
474
|
+
ctaId: 'go_diagnostic',
|
|
475
|
+
ctaPlacement: 'link_router',
|
|
476
|
+
eventType: 'cta_click',
|
|
477
|
+
defaults: {
|
|
478
|
+
utm_source: 'website',
|
|
479
|
+
utm_medium: 'link_router',
|
|
480
|
+
utm_campaign: 'sprint_diagnostic',
|
|
481
|
+
plan_id: 'sprint_diagnostic',
|
|
482
|
+
},
|
|
483
|
+
allowCustomerEmail: true,
|
|
484
|
+
},
|
|
485
|
+
sprint: {
|
|
486
|
+
configUrlKey: 'workflowSprintCheckoutUrl',
|
|
487
|
+
fallbackHref: WORKFLOW_SPRINT_CHECKOUT_URL,
|
|
488
|
+
external: true,
|
|
489
|
+
ctaId: 'go_sprint',
|
|
490
|
+
ctaPlacement: 'link_router',
|
|
491
|
+
eventType: 'cta_click',
|
|
492
|
+
defaults: {
|
|
493
|
+
utm_source: 'website',
|
|
494
|
+
utm_medium: 'link_router',
|
|
495
|
+
utm_campaign: 'workflow_sprint',
|
|
496
|
+
plan_id: 'workflow_sprint',
|
|
497
|
+
},
|
|
498
|
+
allowCustomerEmail: true,
|
|
499
|
+
},
|
|
463
500
|
trial: {
|
|
464
501
|
path: '/guide',
|
|
465
502
|
ctaId: 'go_trial',
|
|
@@ -1680,6 +1717,8 @@ const FEEDBACK_LIST_LABELS = Object.freeze({
|
|
|
1680
1717
|
positive: 'Recent wins',
|
|
1681
1718
|
});
|
|
1682
1719
|
|
|
1720
|
+
const FEEDBACK_OMITTED_TAGS = Object.freeze(['audit-trail', 'auto-capture']);
|
|
1721
|
+
|
|
1683
1722
|
function formatChatTimestamp(isoString) {
|
|
1684
1723
|
if (!isoString) return 'unknown time';
|
|
1685
1724
|
try {
|
|
@@ -1698,9 +1737,33 @@ function formatChatTimestamp(isoString) {
|
|
|
1698
1737
|
}
|
|
1699
1738
|
}
|
|
1700
1739
|
|
|
1740
|
+
function buildFeedbackEntries(windowed, signal) {
|
|
1741
|
+
return (signal ? windowed.filter((r) => r.signal === signal) : windowed)
|
|
1742
|
+
.filter((r) => r.context && !isPlaceholder(r.context))
|
|
1743
|
+
.slice(0, 5);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function formatFeedbackEntry(entry) {
|
|
1747
|
+
const tsFormatted = formatChatTimestamp(entry.timestamp);
|
|
1748
|
+
const signalLabel = entry.signal ? ` [${entry.signal}]` : '';
|
|
1749
|
+
const tagsList = Array.isArray(entry.tags)
|
|
1750
|
+
? entry.tags.filter((tag) => !FEEDBACK_OMITTED_TAGS.includes(tag))
|
|
1751
|
+
: [];
|
|
1752
|
+
const tagsStr = tagsList.length ? ` (${tagsList.join(', ')})` : '';
|
|
1753
|
+
return ` • ${tsFormatted}${signalLabel}${tagsStr} — ${entry.context}`;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function appendFeedbackListLines(lines, { entries, signal, intent }) {
|
|
1757
|
+
if (!entries.length) {
|
|
1758
|
+
lines.push(`No ${signal || 'feedback'} entries found ${intent.windowLabel}.`);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
lines.push(`${FEEDBACK_LIST_LABELS[signal] || 'Recent feedback'} (${intent.windowLabel}):`);
|
|
1762
|
+
lines.push(...entries.map(formatFeedbackEntry));
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1701
1765
|
function buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeline }) {
|
|
1702
1766
|
const signal = detectFeedbackSignalFromPrompt(ctx.prompt);
|
|
1703
|
-
|
|
1704
1767
|
// One read of the time-windowed log, then in-memory counts + (signal-filtered,
|
|
1705
1768
|
// placeholder-stripped) list. Counts include ALL entries (so "Feedback today: 5"
|
|
1706
1769
|
// matches the dashboard tile); the list drops vague entries like literal "thumbs
|
|
@@ -1708,30 +1771,16 @@ function buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeli
|
|
|
1708
1771
|
const windowed = readRecentFeedbackEntries(feedbackDir, null, intent.windowMs, 10000, { includeSignal: true });
|
|
1709
1772
|
const windowPos = windowed.filter((r) => r.signal === 'positive').length;
|
|
1710
1773
|
const windowNeg = windowed.filter((r) => r.signal === 'negative').length;
|
|
1711
|
-
const entries = (
|
|
1712
|
-
.filter((r) => r.context && !isPlaceholder(r.context))
|
|
1713
|
-
.slice(0, 5);
|
|
1774
|
+
const entries = buildFeedbackEntries(windowed, signal);
|
|
1714
1775
|
|
|
1715
|
-
const lines = [
|
|
1716
|
-
|
|
1776
|
+
const lines = [
|
|
1777
|
+
intent.windowMs
|
|
1717
1778
|
? `Feedback ${intent.windowLabel}: ${windowed.length} (${windowPos} positive, ${windowNeg} negative).`
|
|
1718
|
-
: `Feedback total: ${compactNumber(approval.total)} (${compactNumber(approval.positive)} positive, ${compactNumber(approval.negative)} negative)
|
|
1779
|
+
: `Feedback total: ${compactNumber(approval.total)} (${compactNumber(approval.positive)} positive, ${compactNumber(approval.negative)} negative).`,
|
|
1780
|
+
];
|
|
1719
1781
|
|
|
1720
1782
|
if (intent.wantsList) {
|
|
1721
|
-
|
|
1722
|
-
lines.push(`${FEEDBACK_LIST_LABELS[signal] || 'Recent feedback'} (${intent.windowLabel}):`);
|
|
1723
|
-
for (const e of entries) {
|
|
1724
|
-
const tsFormatted = formatChatTimestamp(e.timestamp);
|
|
1725
|
-
const signalLabel = e.signal ? ` [${e.signal}]` : '';
|
|
1726
|
-
const tagsList = Array.isArray(e.tags)
|
|
1727
|
-
? e.tags.filter(t => !['audit-trail', 'auto-capture'].includes(t))
|
|
1728
|
-
: [];
|
|
1729
|
-
const tagsStr = tagsList.length ? ` (${tagsList.join(', ')})` : '';
|
|
1730
|
-
lines.push(` • ${tsFormatted}${signalLabel}${tagsStr} — ${e.context}`);
|
|
1731
|
-
}
|
|
1732
|
-
} else {
|
|
1733
|
-
lines.push(`No ${signal || 'feedback'} entries found ${intent.windowLabel}.`);
|
|
1734
|
-
}
|
|
1783
|
+
appendFeedbackListLines(lines, { entries, signal, intent });
|
|
1735
1784
|
} else {
|
|
1736
1785
|
lines.push(`Lesson pipeline: ${compactNumber(lessonPipeline.lessons || lessonPipeline.generated || 0)} lessons visible in the current dashboard snapshot.`);
|
|
1737
1786
|
}
|
|
@@ -1939,7 +1988,7 @@ async function answerEnterpriseDataChat({ prompt, feedbackDir, parsed }) {
|
|
|
1939
1988
|
const answerEnterpriseDialogflowChat = answerEnterpriseDataChat;
|
|
1940
1989
|
|
|
1941
1990
|
function buildLossAnalyticsResponse(data, summaryOptions) {
|
|
1942
|
-
return {
|
|
1991
|
+
return sanitizeHtmlUnsafeJsonValue({
|
|
1943
1992
|
window: data.analytics.window || summaryOptions,
|
|
1944
1993
|
lossAnalysis: data.analytics.lossAnalysis || null,
|
|
1945
1994
|
buyerLoss: data.analytics.buyerLoss || null,
|
|
@@ -1951,7 +2000,7 @@ function buildLossAnalyticsResponse(data, summaryOptions) {
|
|
|
1951
2000
|
ctas: data.analytics.telemetry && data.analytics.telemetry.ctas,
|
|
1952
2001
|
visitors: data.analytics.telemetry && data.analytics.telemetry.visitors,
|
|
1953
2002
|
},
|
|
1954
|
-
};
|
|
2003
|
+
});
|
|
1955
2004
|
}
|
|
1956
2005
|
|
|
1957
2006
|
function createJourneyId(prefix) {
|
|
@@ -2054,17 +2103,111 @@ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
|
|
|
2054
2103
|
});
|
|
2055
2104
|
}
|
|
2056
2105
|
|
|
2106
|
+
const CHECKOUT_HIDDEN_ATTRIBUTION_KEYS = Object.freeze([
|
|
2107
|
+
'trace_id',
|
|
2108
|
+
'acquisition_id',
|
|
2109
|
+
'visitor_id',
|
|
2110
|
+
'session_id',
|
|
2111
|
+
'visitor_session_id',
|
|
2112
|
+
'install_id',
|
|
2113
|
+
'utm_source',
|
|
2114
|
+
'utm_medium',
|
|
2115
|
+
'utm_campaign',
|
|
2116
|
+
'utm_content',
|
|
2117
|
+
'utm_term',
|
|
2118
|
+
'creator',
|
|
2119
|
+
'community',
|
|
2120
|
+
'post_id',
|
|
2121
|
+
'comment_id',
|
|
2122
|
+
'campaign_variant',
|
|
2123
|
+
'offer_code',
|
|
2124
|
+
'cta_id',
|
|
2125
|
+
'cta_placement',
|
|
2126
|
+
'plan_id',
|
|
2127
|
+
'billing_cycle',
|
|
2128
|
+
'seat_count',
|
|
2129
|
+
'landing_path',
|
|
2130
|
+
'referrer_host',
|
|
2131
|
+
]);
|
|
2132
|
+
|
|
2133
|
+
function normalizeHiddenAttributionValue(value) {
|
|
2134
|
+
const normalized = normalizeNullableText(value);
|
|
2135
|
+
if (!normalized) return '';
|
|
2136
|
+
return normalized.slice(0, 512);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function buildCheckoutHiddenAttributionInputs(parsed = null) {
|
|
2140
|
+
if (!parsed?.searchParams) return '';
|
|
2141
|
+
|
|
2142
|
+
const inputs = [];
|
|
2143
|
+
for (const key of CHECKOUT_HIDDEN_ATTRIBUTION_KEYS) {
|
|
2144
|
+
const value = normalizeHiddenAttributionValue(parsed.searchParams.get(key));
|
|
2145
|
+
if (value) {
|
|
2146
|
+
inputs.push(`<input type="hidden" name="${key}" value="${escapeHtmlAttribute(value)}">`);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return inputs.join('');
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
function prunePendingOauthAuthorizeRequests(now = Date.now()) {
|
|
2153
|
+
for (const [token, entry] of pendingOauthAuthorizeRequests.entries()) {
|
|
2154
|
+
if (!entry || entry.expiresAt <= now) {
|
|
2155
|
+
pendingOauthAuthorizeRequests.delete(token);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
function createPendingOauthAuthorizeRequest(params, now = Date.now()) {
|
|
2161
|
+
prunePendingOauthAuthorizeRequests(now);
|
|
2162
|
+
const token = crypto.randomBytes(32).toString('base64url');
|
|
2163
|
+
pendingOauthAuthorizeRequests.set(token, {
|
|
2164
|
+
params,
|
|
2165
|
+
expiresAt: now + OAUTH_AUTHORIZE_REQUEST_TTL_MS,
|
|
2166
|
+
});
|
|
2167
|
+
return token;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
function consumePendingOauthAuthorizeRequest(token, now = Date.now()) {
|
|
2171
|
+
if (!token) return null;
|
|
2172
|
+
prunePendingOauthAuthorizeRequests(now);
|
|
2173
|
+
const entry = pendingOauthAuthorizeRequests.get(token);
|
|
2174
|
+
if (!entry) return null;
|
|
2175
|
+
pendingOauthAuthorizeRequests.delete(token);
|
|
2176
|
+
return entry.params;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function getOauthAuthorizeParamsFromQuery(searchParams) {
|
|
2180
|
+
return {
|
|
2181
|
+
clientId: searchParams.get('client_id') || '',
|
|
2182
|
+
redirectUri: searchParams.get('redirect_uri') || '',
|
|
2183
|
+
codeChallenge: searchParams.get('code_challenge') || '',
|
|
2184
|
+
codeChallengeMethod: searchParams.get('code_challenge_method') || '',
|
|
2185
|
+
scope: searchParams.get('scope') || undefined,
|
|
2186
|
+
state: searchParams.get('state') || '',
|
|
2187
|
+
resource: searchParams.get('resource') || '',
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function getOauthAuthorizeParamsFromForm(form, hostedConfig) {
|
|
2192
|
+
const pending = consumePendingOauthAuthorizeRequest(form.get('auth_request_token') || '');
|
|
2193
|
+
if (pending) return pending;
|
|
2194
|
+
return {
|
|
2195
|
+
clientId: form.get('client_id') || '',
|
|
2196
|
+
redirectUri: form.get('redirect_uri') || '',
|
|
2197
|
+
codeChallenge: form.get('code_challenge') || '',
|
|
2198
|
+
codeChallengeMethod: form.get('code_challenge_method') || '',
|
|
2199
|
+
scope: form.get('scope') || undefined,
|
|
2200
|
+
state: form.get('state') || '',
|
|
2201
|
+
resource: form.get('resource') || buildPublicUrl(hostedConfig, '/mcp'),
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2057
2205
|
function renderCheckoutIntentPage(prefilledEmail = '', parsed = null, options = {}) {
|
|
2058
2206
|
const plausibleDomain = escapeHtmlAttribute(resolvePlausibleDataDomain({ host: 'thumbgate.ai' }));
|
|
2059
2207
|
const includeHiddenAttribution = options.includeHiddenAttribution === true;
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
if (key !== 'confirm' && key !== 'customer_email') {
|
|
2064
|
-
hiddenInputs += `<input type="hidden" name="${escapeHtmlAttribute(key)}" value="${escapeHtmlAttribute(value)}">`;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2208
|
+
const hiddenInputs = includeHiddenAttribution
|
|
2209
|
+
? buildCheckoutHiddenAttributionInputs(parsed)
|
|
2210
|
+
: '';
|
|
2068
2211
|
return `<!doctype html>
|
|
2069
2212
|
<html lang="en">
|
|
2070
2213
|
<head>
|
|
@@ -2077,7 +2220,7 @@ function renderCheckoutIntentPage(prefilledEmail = '', parsed = null, options =
|
|
|
2077
2220
|
body{background:#0a0a0a;color:#eee;font-family:system-ui,-apple-system,sans-serif;line-height:1.5}
|
|
2078
2221
|
main{max-width:520px;margin:8vh auto;padding:0 20px}
|
|
2079
2222
|
.brand{display:flex;align-items:center;gap:10px;margin-bottom:24px;font-size:14px;color:#94a3b8}
|
|
2080
|
-
.brand-mark{width:
|
|
2223
|
+
.brand-mark{width:36px;height:36px;display:block;flex:0 0 auto;border-radius:9px}
|
|
2081
2224
|
h1{font-size:24px;margin:0 0 8px;color:#fff}.price{font-size:32px;font-weight:700;color:#22d3ee;margin:8px 0 4px}.price small{font-size:14px;color:#94a3b8;font-weight:400}
|
|
2082
2225
|
p{color:#cbd5e1;margin:8px 0}form{margin:0}
|
|
2083
2226
|
input[type=email]{width:100%;box-sizing:border-box;padding:14px 16px;border:1px solid #374151;border-radius:8px;background:#111827;color:#fff;font-size:15px;margin:16px 0 0;outline:none}
|
|
@@ -2096,7 +2239,7 @@ a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:
|
|
|
2096
2239
|
</head>
|
|
2097
2240
|
<body>
|
|
2098
2241
|
<main>
|
|
2099
|
-
<div class="brand"><
|
|
2242
|
+
<div class="brand"><img src="/thumbgate-icon.png?v=20260623-stripe-match" alt="" class="brand-mark" width="36" height="36"><span>ThumbGate</span></div>
|
|
2100
2243
|
<h1>Start ThumbGate Pro</h1>
|
|
2101
2244
|
<div class="price">$19<small>/mo</small></div>
|
|
2102
2245
|
<p>The npm package runs your gates locally. <strong>Pro</strong> is what keeps them working across every machine, every agent runtime, and every breaking-change week.</p>
|
|
@@ -2262,17 +2405,6 @@ function normalizeCheckoutCustomerEmail(value) {
|
|
|
2262
2405
|
return email;
|
|
2263
2406
|
}
|
|
2264
2407
|
|
|
2265
|
-
function renderCheckoutIntentGate(parsed, responseHeaders = {}) {
|
|
2266
|
-
let hiddenInputs = '';
|
|
2267
|
-
for (const [key, value] of parsed.searchParams.entries()) {
|
|
2268
|
-
if (key !== 'confirm' && key !== 'customer_email') hiddenInputs += `<input type=hidden name=${escapeHtmlAttribute(key)} value=${escapeHtmlAttribute(value)}>`;
|
|
2269
|
-
}
|
|
2270
|
-
return {
|
|
2271
|
-
html: `<!doctype html><h1>Email for Stripe receipt</h1><form action=/checkout/pro>${hiddenInputs}<input type=hidden name=confirm value=1><input name=customer_email type=email required><button>Continue</button></form>`,
|
|
2272
|
-
headers: responseHeaders,
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
2408
|
function normalizeTrackedLinkSlug(value) {
|
|
2277
2409
|
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
2278
2410
|
}
|
|
@@ -2283,6 +2415,27 @@ function normalizePublicPageSlug(value) {
|
|
|
2283
2415
|
.replace(/[^a-z0-9-]/g, '');
|
|
2284
2416
|
}
|
|
2285
2417
|
|
|
2418
|
+
function buildPublicHtmlFileMap(directory) {
|
|
2419
|
+
const entries = new Map();
|
|
2420
|
+
try {
|
|
2421
|
+
for (const fileName of fs.readdirSync(directory)) {
|
|
2422
|
+
if (!/^[a-z0-9-]+\.html$/i.test(fileName)) continue;
|
|
2423
|
+
const slug = normalizePublicPageSlug(fileName);
|
|
2424
|
+
if (!slug) continue;
|
|
2425
|
+
entries.set(slug, path.join(directory, fileName));
|
|
2426
|
+
}
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
2429
|
+
}
|
|
2430
|
+
return entries;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
function resolvePublicHtmlFile(publicPageMap, rawSlug) {
|
|
2434
|
+
const slug = normalizePublicPageSlug(rawSlug);
|
|
2435
|
+
if (!slug) return null;
|
|
2436
|
+
return publicPageMap.get(slug) || null;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2286
2439
|
function getTrackedLinkTarget(slug) {
|
|
2287
2440
|
const normalizedSlug = normalizeTrackedLinkSlug(slug);
|
|
2288
2441
|
return TRACKED_LINK_TARGETS[normalizedSlug]
|
|
@@ -2318,8 +2471,10 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
|
2318
2471
|
}
|
|
2319
2472
|
|
|
2320
2473
|
function buildTrackedLinkDestination(target, hostedConfig, parsed) {
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2474
|
+
const configuredHref = target.configUrlKey ? hostedConfig[target.configUrlKey] : null;
|
|
2475
|
+
const href = target.href || configuredHref || target.fallbackHref;
|
|
2476
|
+
const destinationUrl = href
|
|
2477
|
+
? new URL(href)
|
|
2323
2478
|
: new URL(target.path || '/', hostedConfig.appOrigin);
|
|
2324
2479
|
appendTrackedLinkQueryParams(destinationUrl, parsed, target);
|
|
2325
2480
|
return destinationUrl;
|
|
@@ -2446,6 +2601,7 @@ function sendJson(res, statusCode, payload, extraHeaders = {}, options = {}) {
|
|
|
2446
2601
|
const body = JSON.stringify(payload);
|
|
2447
2602
|
res.writeHead(statusCode, {
|
|
2448
2603
|
'Content-Type': 'application/json; charset=utf-8',
|
|
2604
|
+
'X-Content-Type-Options': 'nosniff',
|
|
2449
2605
|
'Content-Length': Buffer.byteLength(body),
|
|
2450
2606
|
...extraHeaders,
|
|
2451
2607
|
});
|
|
@@ -2539,8 +2695,9 @@ function chatgptActionEventType(integration, suffix) {
|
|
|
2539
2695
|
}
|
|
2540
2696
|
|
|
2541
2697
|
function getPublicOrigin(req) {
|
|
2542
|
-
const
|
|
2543
|
-
const
|
|
2698
|
+
const rawProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase();
|
|
2699
|
+
const proto = rawProto === 'https' ? 'https' : 'http';
|
|
2700
|
+
const host = getSafePublicRequestHost(req) || 'localhost';
|
|
2544
2701
|
return `${proto}://${host}`;
|
|
2545
2702
|
}
|
|
2546
2703
|
|
|
@@ -2559,6 +2716,47 @@ function getRequestHostHeader(req) {
|
|
|
2559
2716
|
return forwardedHost || req.headers.host || '';
|
|
2560
2717
|
}
|
|
2561
2718
|
|
|
2719
|
+
function normalizePublicRequestHost(value) {
|
|
2720
|
+
const rawHost = String(value || '').split(',')[0].trim().toLowerCase();
|
|
2721
|
+
if (!rawHost || rawHost.length > 253) return '';
|
|
2722
|
+
|
|
2723
|
+
const hostWithoutPort = rawHost.startsWith('[')
|
|
2724
|
+
? rawHost.slice(1).split(']')[0]
|
|
2725
|
+
: rawHost.split(':')[0];
|
|
2726
|
+
const port = rawHost.startsWith('[')
|
|
2727
|
+
? rawHost.split(']:')[1] || ''
|
|
2728
|
+
: rawHost.split(':')[1] || '';
|
|
2729
|
+
|
|
2730
|
+
if (!isAllowedPublicHostName(hostWithoutPort)) {
|
|
2731
|
+
return '';
|
|
2732
|
+
}
|
|
2733
|
+
if (port && !/^\d{1,5}$/.test(port)) return '';
|
|
2734
|
+
if (port && Number(port) > 65535) return '';
|
|
2735
|
+
return port ? `${hostWithoutPort}:${port}` : hostWithoutPort;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
function isAllowedPublicHostName(hostname) {
|
|
2739
|
+
return hostname === 'localhost'
|
|
2740
|
+
|| hostname === '::1'
|
|
2741
|
+
|| isIpv4Host(hostname)
|
|
2742
|
+
|| isDnsHostName(hostname);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function isIpv4Host(hostname) {
|
|
2746
|
+
const parts = hostname.split('.');
|
|
2747
|
+
return parts.length === 4 && parts.every((part) => /^\d{1,3}$/.test(part));
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
function isDnsHostName(hostname) {
|
|
2751
|
+
return hostname
|
|
2752
|
+
.split('.')
|
|
2753
|
+
.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
function getSafePublicRequestHost(req) {
|
|
2757
|
+
return normalizePublicRequestHost(getRequestHostHeader(req));
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2562
2760
|
function isLoopbackHost(hostValue) {
|
|
2563
2761
|
const rawHost = String(hostValue || '').split(',')[0].trim();
|
|
2564
2762
|
if (!rawHost) {
|
|
@@ -2613,7 +2811,7 @@ function normalizePublicMarketingHtml(html, runtimeConfig, requestHost) {
|
|
|
2613
2811
|
let output = String(html);
|
|
2614
2812
|
output = output.replaceAll(DEFAULT_PUBLIC_APP_ORIGIN, appOrigin);
|
|
2615
2813
|
try {
|
|
2616
|
-
const host = requestHost || new URL(appOrigin).host;
|
|
2814
|
+
const host = normalizePublicRequestHost(requestHost) || new URL(appOrigin).host;
|
|
2617
2815
|
const plausibleDomain = resolvePlausibleDataDomain({ host });
|
|
2618
2816
|
output = output.replaceAll(
|
|
2619
2817
|
'data-domain="thumbgate-production.up.railway.app"',
|
|
@@ -2654,13 +2852,8 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
2654
2852
|
'__PRO_PRICE_LABEL__': runtimeConfig.proPriceLabel,
|
|
2655
2853
|
'__SPRINT_DIAGNOSTIC_CHECKOUT_URL__': runtimeConfig.sprintDiagnosticCheckoutUrl || SPRINT_DIAGNOSTIC_CHECKOUT_URL,
|
|
2656
2854
|
'__WORKFLOW_SPRINT_CHECKOUT_URL__': runtimeConfig.workflowSprintCheckoutUrl || WORKFLOW_SPRINT_CHECKOUT_URL,
|
|
2657
|
-
'__PAYPAL_DIAGNOSTIC_CHECKOUT_URL__': runtimeConfig.paypalDiagnosticCheckoutUrl || '',
|
|
2658
|
-
'__PAYPAL_WORKFLOW_SPRINT_CHECKOUT_URL__': runtimeConfig.paypalWorkflowSprintCheckoutUrl || '',
|
|
2659
|
-
'__MOR_SNAPSHOT_CHECKOUT_URL__': runtimeConfig.morSnapshotCheckoutUrl || '',
|
|
2660
|
-
'__MOR_PROVIDER__': runtimeConfig.morProvider || 'Lemon Squeezy or Paddle',
|
|
2661
2855
|
'__SPRINT_DIAGNOSTIC_PRICE_DOLLARS__': runtimeConfig.sprintDiagnosticPriceDollars || 499,
|
|
2662
2856
|
'__WORKFLOW_SPRINT_PRICE_DOLLARS__': runtimeConfig.workflowSprintPriceDollars || 1500,
|
|
2663
|
-
'__SNAPSHOT_PRICE_DOLLARS__': runtimeConfig.snapshotPriceDollars || 97,
|
|
2664
2857
|
'__GA_MEASUREMENT_ID__': runtimeConfig.gaMeasurementId || '',
|
|
2665
2858
|
'__GA_BOOTSTRAP__': gaBootstrap,
|
|
2666
2859
|
'__GOOGLE_SITE_VERIFICATION_META__': googleSiteVerificationMeta,
|
|
@@ -3140,7 +3333,7 @@ nav .container { display: flex; justify-content: space-between; align-items: cen
|
|
|
3140
3333
|
</head>
|
|
3141
3334
|
<body>
|
|
3142
3335
|
<nav><div class="container">
|
|
3143
|
-
<a href="/dashboard" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate</span></a>
|
|
3336
|
+
<a href="/dashboard" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline-v3.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate</span></a>
|
|
3144
3337
|
<div class="nav-links">
|
|
3145
3338
|
<a href="/dashboard">Dashboard</a>
|
|
3146
3339
|
<a href="/lessons">Lessons</a>
|
|
@@ -3333,9 +3526,6 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3333
3526
|
{ path: '/learn/codex-role-plugins-need-governance', changefreq: 'weekly', priority: '0.85' },
|
|
3334
3527
|
{ path: '/learn/agentic-os-team-governance', changefreq: 'weekly', priority: '0.85' },
|
|
3335
3528
|
{ path: '/learn/cost-aware-agent-gate-routing', changefreq: 'weekly', priority: '0.85' },
|
|
3336
|
-
{ path: '/learn/databricks-unity-ai-gateway-runtime-governance', changefreq: 'weekly', priority: '0.85' },
|
|
3337
|
-
{ path: '/learn/anthropomorphic-claim-gates', changefreq: 'weekly', priority: '0.85' },
|
|
3338
|
-
{ path: '/learn/agent-identity-connector-governance', changefreq: 'weekly', priority: '0.9' },
|
|
3339
3529
|
{ path: '/learn/pretix-stripe-connect-marketplaces', changefreq: 'weekly', priority: '0.9' },
|
|
3340
3530
|
{ path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
|
|
3341
3531
|
{ path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },
|
|
@@ -3345,30 +3535,30 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3345
3535
|
{ path: '/compare/anthropic-claude-for-legal', changefreq: 'weekly', priority: '0.9' },
|
|
3346
3536
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
3347
3537
|
];
|
|
3348
|
-
// Auto-include every hand-written
|
|
3349
|
-
//
|
|
3350
|
-
//
|
|
3351
|
-
//
|
|
3352
|
-
//
|
|
3353
|
-
// already declared above (e.g. the seo-gsd specs), which keep their explicit
|
|
3354
|
-
// priorities.
|
|
3538
|
+
// Auto-include every hand-written SEO page so /sitemap.xml can never drift out
|
|
3539
|
+
// of sync with public/compare/*.html or public/guides/*.html. Crawlers and AI
|
|
3540
|
+
// answer engines only surface pages they can discover, so a buyer-intent page
|
|
3541
|
+
// missing from the sitemap is invisible on its query. De-duped against entries
|
|
3542
|
+
// already declared above (e.g. seo-gsd specs), which keep explicit priorities.
|
|
3355
3543
|
const declaredPaths = new Set(entries.map((entry) => entry.path));
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3544
|
+
try {
|
|
3545
|
+
const seoDirectories = [
|
|
3546
|
+
{ dir: 'compare', route: '/compare', priority: '0.85' },
|
|
3547
|
+
{ dir: 'guides', route: '/guides', priority: '0.85' },
|
|
3548
|
+
];
|
|
3549
|
+
for (const catalog of seoDirectories) {
|
|
3550
|
+
const files = fs.readdirSync(path.join(PUBLIC_DIR, catalog.dir)).sort((a, b) => a.localeCompare(b));
|
|
3359
3551
|
for (const file of files) {
|
|
3360
3552
|
if (!file.endsWith('.html')) continue;
|
|
3361
|
-
const
|
|
3362
|
-
if (declaredPaths.has(
|
|
3363
|
-
declaredPaths.add(
|
|
3364
|
-
entries.push({ path:
|
|
3553
|
+
const publicPath = `${catalog.route}/${file.replace(/\.html$/, '')}`;
|
|
3554
|
+
if (declaredPaths.has(publicPath)) continue;
|
|
3555
|
+
declaredPaths.add(publicPath);
|
|
3556
|
+
entries.push({ path: publicPath, changefreq: 'weekly', priority: catalog.priority });
|
|
3365
3557
|
}
|
|
3366
|
-
} catch {
|
|
3367
|
-
// Directory absent in a stripped bundle — fall back to the static entries.
|
|
3368
3558
|
}
|
|
3369
|
-
}
|
|
3370
|
-
|
|
3371
|
-
|
|
3559
|
+
} catch {
|
|
3560
|
+
// SEO directories absent in a stripped bundle — fall back to static entries.
|
|
3561
|
+
}
|
|
3372
3562
|
return [
|
|
3373
3563
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
3374
3564
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
@@ -3494,7 +3684,7 @@ function servePublicMarketingPage({
|
|
|
3494
3684
|
}, req.headers, 'seo_landing_view');
|
|
3495
3685
|
}
|
|
3496
3686
|
|
|
3497
|
-
const requestHost =
|
|
3687
|
+
const requestHost = getSafePublicRequestHost(req);
|
|
3498
3688
|
const html = renderHtml(hostedConfig, {
|
|
3499
3689
|
serverVisitorId: journeyState.visitorId,
|
|
3500
3690
|
serverSessionId: journeyState.sessionId,
|
|
@@ -3645,7 +3835,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
3645
3835
|
</head>
|
|
3646
3836
|
<body>
|
|
3647
3837
|
<main>
|
|
3648
|
-
<a href="/" class="brand-header"><img src="/assets/brand/thumbgate-mark-inline.svg" alt="ThumbGate" class="logo-mark" width="32" height="32"><span class="logo-text">ThumbGate</span></a>
|
|
3838
|
+
<a href="/" class="brand-header"><img src="/assets/brand/thumbgate-mark-inline-v3.svg" alt="ThumbGate" class="logo-mark" width="32" height="32"><span class="logo-text">ThumbGate</span></a>
|
|
3649
3839
|
<span class="eyebrow">ThumbGate Pro</span>
|
|
3650
3840
|
<h1>Your local Pro dashboard is ready.</h1>
|
|
3651
3841
|
<p class="lead">This page verifies your Stripe session, provisions the key if needed, and gives you the exact command to save your license and launch your personal local dashboard.</p>
|
|
@@ -3873,34 +4063,15 @@ function renderCheckoutCancelledPage(runtimeConfig) {
|
|
|
3873
4063
|
const workflowSprintCheckoutUrl = runtimeConfig.workflowSprintCheckoutUrl
|
|
3874
4064
|
? escapeHtmlAttribute(runtimeConfig.workflowSprintCheckoutUrl)
|
|
3875
4065
|
: '';
|
|
3876
|
-
const paypalDiagnosticCheckoutUrl = runtimeConfig.paypalDiagnosticCheckoutUrl
|
|
3877
|
-
? escapeHtmlAttribute(runtimeConfig.paypalDiagnosticCheckoutUrl)
|
|
3878
|
-
: '';
|
|
3879
|
-
const paypalWorkflowSprintCheckoutUrl = runtimeConfig.paypalWorkflowSprintCheckoutUrl
|
|
3880
|
-
? escapeHtmlAttribute(runtimeConfig.paypalWorkflowSprintCheckoutUrl)
|
|
3881
|
-
: '';
|
|
3882
|
-
const morSnapshotCheckoutUrl = runtimeConfig.morSnapshotCheckoutUrl
|
|
3883
|
-
? escapeHtmlAttribute(runtimeConfig.morSnapshotCheckoutUrl)
|
|
3884
|
-
: '';
|
|
3885
4066
|
const sprintDiagnosticPriceDollars = runtimeConfig.sprintDiagnosticPriceDollars || 499;
|
|
3886
4067
|
const workflowSprintPriceDollars = runtimeConfig.workflowSprintPriceDollars || 1500;
|
|
3887
|
-
const snapshotPriceDollars = runtimeConfig.snapshotPriceDollars || 97;
|
|
3888
4068
|
const workflowSprintIntakeUrl = `${escapeHtmlAttribute(runtimeConfig.appOrigin)}/#workflow-sprint-intake`;
|
|
3889
4069
|
const recoveryOfferLinks = [
|
|
3890
4070
|
diagnosticCheckoutUrl
|
|
3891
|
-
? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="
|
|
3892
|
-
: '',
|
|
3893
|
-
paypalDiagnosticCheckoutUrl
|
|
3894
|
-
? `<a href="${paypalDiagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic_paypal" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic by PayPal</a>`
|
|
4071
|
+
? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
|
|
3895
4072
|
: '',
|
|
3896
4073
|
workflowSprintCheckoutUrl
|
|
3897
|
-
? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="
|
|
3898
|
-
: '',
|
|
3899
|
-
paypalWorkflowSprintCheckoutUrl
|
|
3900
|
-
? `<a href="${paypalWorkflowSprintCheckoutUrl}" data-recovery-offer="workflow_sprint_paypal" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint by PayPal</a>`
|
|
3901
|
-
: '',
|
|
3902
|
-
morSnapshotCheckoutUrl
|
|
3903
|
-
? `<a href="${morSnapshotCheckoutUrl}" data-recovery-offer="snapshot_mor" data-offer-price="${snapshotPriceDollars}">Buy $${snapshotPriceDollars} snapshot</a>`
|
|
4074
|
+
? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="workflow_sprint" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint</a>`
|
|
3904
4075
|
: '',
|
|
3905
4076
|
`<a href="${workflowTeardownCheckoutUrl}" data-recovery-offer="workflow_teardown" data-offer-price="99">Pay $99 teardown</a>`,
|
|
3906
4077
|
`<a href="${quickReadCheckoutUrl}" data-recovery-offer="quick_read" data-offer-price="19">Pay $19 quick read</a>`,
|
|
@@ -4598,6 +4769,42 @@ function normalizeJobIdFromPath(pathname, suffix = '') {
|
|
|
4598
4769
|
return match ? decodeURIComponent(match[1]) : null;
|
|
4599
4770
|
}
|
|
4600
4771
|
|
|
4772
|
+
function escapeHtml(value) {
|
|
4773
|
+
return String(value)
|
|
4774
|
+
.replaceAll('&', '&')
|
|
4775
|
+
.replaceAll('<', '<')
|
|
4776
|
+
.replaceAll('>', '>')
|
|
4777
|
+
.replaceAll('"', '"')
|
|
4778
|
+
.replaceAll("'", ''');
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
function escapeHtmlUnsafeJsonString(value) {
|
|
4782
|
+
return String(value)
|
|
4783
|
+
.replaceAll('<', String.raw`\u003c`)
|
|
4784
|
+
.replaceAll('>', String.raw`\u003e`)
|
|
4785
|
+
.replaceAll('&', String.raw`\u0026`)
|
|
4786
|
+
.replaceAll('\u2028', String.raw`\u2028`)
|
|
4787
|
+
.replaceAll('\u2029', String.raw`\u2029`);
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4790
|
+
function sanitizeHtmlUnsafeJsonValue(value) {
|
|
4791
|
+
if (typeof value === 'string') {
|
|
4792
|
+
return escapeHtmlUnsafeJsonString(value);
|
|
4793
|
+
}
|
|
4794
|
+
if (Array.isArray(value)) {
|
|
4795
|
+
return value.map((entry) => sanitizeHtmlUnsafeJsonValue(entry));
|
|
4796
|
+
}
|
|
4797
|
+
if (!value || typeof value !== 'object') {
|
|
4798
|
+
return value;
|
|
4799
|
+
}
|
|
4800
|
+
return Object.fromEntries(
|
|
4801
|
+
Object.entries(value).map(([key, entry]) => [
|
|
4802
|
+
escapeHtmlUnsafeJsonString(key),
|
|
4803
|
+
sanitizeHtmlUnsafeJsonValue(entry),
|
|
4804
|
+
])
|
|
4805
|
+
);
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4601
4808
|
function normalizeDocumentIdFromPath(pathname) {
|
|
4602
4809
|
const match = pathname.match(/^\/v1\/documents\/([^/]+)$/);
|
|
4603
4810
|
return match ? decodeURIComponent(match[1]) : null;
|
|
@@ -5523,7 +5730,7 @@ async function addContext(){
|
|
|
5523
5730
|
if (isGetLikeRequest && (pathname === '/numbers' || pathname === '/numbers.html')) {
|
|
5524
5731
|
// Route through servePublicMarketingPage so landing_page_view telemetry
|
|
5525
5732
|
// + funnel-events.jsonl `discovery/landing_view` get captured with UTM
|
|
5526
|
-
// attribution — critical for
|
|
5733
|
+
// attribution — critical for DirectSocial social CTAs that target /numbers.
|
|
5527
5734
|
try {
|
|
5528
5735
|
servePublicMarketingPage({
|
|
5529
5736
|
req,
|
|
@@ -5665,13 +5872,12 @@ async function addContext(){
|
|
|
5665
5872
|
|
|
5666
5873
|
if (isGetLikeRequest && pathname.startsWith('/learn/')) {
|
|
5667
5874
|
try {
|
|
5668
|
-
const
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
sendJson(res, 403, { error: 'Forbidden' });
|
|
5875
|
+
const articlePath = resolvePublicHtmlFile(LEARN_PAGE_PATHS_BY_SLUG, pathname.replace('/learn/', ''));
|
|
5876
|
+
if (!articlePath) {
|
|
5877
|
+
sendJson(res, 404, { error: 'Article not found' });
|
|
5672
5878
|
return;
|
|
5673
5879
|
}
|
|
5674
|
-
const requestHost =
|
|
5880
|
+
const requestHost = getSafePublicRequestHost(req);
|
|
5675
5881
|
const html = normalizePublicMarketingHtml(fs.readFileSync(articlePath, 'utf-8'), hostedConfig, requestHost);
|
|
5676
5882
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5677
5883
|
} catch {
|
|
@@ -5682,10 +5888,9 @@ async function addContext(){
|
|
|
5682
5888
|
|
|
5683
5889
|
if (isGetLikeRequest && pathname.startsWith('/guides/')) {
|
|
5684
5890
|
try {
|
|
5685
|
-
const
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5891
|
+
const guidePath = resolvePublicHtmlFile(GUIDE_PAGE_PATHS_BY_SLUG, pathname.replace('/guides/', ''));
|
|
5892
|
+
if (!guidePath) { sendJson(res, 404, { error: 'Guide not found' }); return; }
|
|
5893
|
+
const requestHost = getSafePublicRequestHost(req);
|
|
5689
5894
|
const html = normalizePublicMarketingHtml(fs.readFileSync(guidePath, 'utf-8'), hostedConfig, requestHost);
|
|
5690
5895
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5691
5896
|
} catch { sendJson(res, 404, { error: 'Guide not found' }); }
|
|
@@ -5694,10 +5899,9 @@ async function addContext(){
|
|
|
5694
5899
|
|
|
5695
5900
|
if (isGetLikeRequest && pathname.startsWith('/compare/') && pathname !== '/compare') {
|
|
5696
5901
|
try {
|
|
5697
|
-
const
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5902
|
+
const comparePath = resolvePublicHtmlFile(COMPARE_PAGE_PATHS_BY_SLUG, pathname.replace('/compare/', ''));
|
|
5903
|
+
if (!comparePath) { sendJson(res, 404, { error: 'Comparison not found' }); return; }
|
|
5904
|
+
const requestHost = getSafePublicRequestHost(req);
|
|
5701
5905
|
const html = normalizePublicMarketingHtml(fs.readFileSync(comparePath, 'utf-8'), hostedConfig, requestHost);
|
|
5702
5906
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5703
5907
|
} catch { sendJson(res, 404, { error: 'Comparison not found' }); }
|
|
@@ -5706,10 +5910,9 @@ async function addContext(){
|
|
|
5706
5910
|
|
|
5707
5911
|
if (isGetLikeRequest && pathname.startsWith('/use-cases/')) {
|
|
5708
5912
|
try {
|
|
5709
|
-
const
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5913
|
+
const useCasePath = resolvePublicHtmlFile(USE_CASE_PAGE_PATHS_BY_SLUG, pathname.replace('/use-cases/', ''));
|
|
5914
|
+
if (!useCasePath) { sendJson(res, 404, { error: 'Use case not found' }); return; }
|
|
5915
|
+
const requestHost = getSafePublicRequestHost(req);
|
|
5713
5916
|
const html = normalizePublicMarketingHtml(fs.readFileSync(useCasePath, 'utf-8'), hostedConfig, requestHost);
|
|
5714
5917
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5715
5918
|
} catch { sendJson(res, 404, { error: 'Use case not found' }); }
|
|
@@ -6212,7 +6415,7 @@ async function addContext(){
|
|
|
6212
6415
|
// Public-facing broker lead-flow audit landing page. Wedge for the
|
|
6213
6416
|
// real-estate broker outreach. Static HTML served from src/api/static.
|
|
6214
6417
|
try {
|
|
6215
|
-
const host =
|
|
6418
|
+
const host = getSafePublicRequestHost(req);
|
|
6216
6419
|
const html = normalizePublicMarketingHtml(fillTemplate(fs.readFileSync(
|
|
6217
6420
|
path.resolve(__dirname, '../../assets/static/broker-audit.html'),
|
|
6218
6421
|
'utf8'
|
|
@@ -6270,9 +6473,9 @@ async function addContext(){
|
|
|
6270
6473
|
// Authorization endpoint: GET renders consent, POST issues the code.
|
|
6271
6474
|
if (pathname === '/oauth/authorize') {
|
|
6272
6475
|
if (isGetLikeRequest) {
|
|
6273
|
-
const
|
|
6274
|
-
|
|
6275
|
-
|
|
6476
|
+
const authRequestToken = createPendingOauthAuthorizeRequest(
|
|
6477
|
+
getOauthAuthorizeParamsFromQuery(parsed.searchParams)
|
|
6478
|
+
);
|
|
6276
6479
|
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Authorize ThumbGate</title>
|
|
6277
6480
|
<style>body{font:15px system-ui;margin:0;background:#0b0b0c;color:#eee;display:flex;min-height:100vh;align-items:center;justify-content:center}
|
|
6278
6481
|
.card{background:#161618;border:1px solid #2a2a2e;border-radius:12px;padding:28px;max-width:420px}
|
|
@@ -6281,7 +6484,7 @@ button{width:100%;padding:11px;border-radius:8px;border:0;background:#10b981;col
|
|
|
6281
6484
|
a{color:#8b9}</style></head><body><form class="card" method="post" action="/oauth/authorize">
|
|
6282
6485
|
<h2>Authorize Claude → ThumbGate</h2>
|
|
6283
6486
|
<p>Paste your ThumbGate API key to let this connector act as you. Get one with <code>npx thumbgate init</code> or from your <a href="/dashboard">dashboard</a>.</p>
|
|
6284
|
-
${
|
|
6487
|
+
<input type="hidden" name="auth_request_token" value="${escapeHtmlAttribute(authRequestToken)}">
|
|
6285
6488
|
<input type="password" name="api_key" placeholder="ThumbGate API key" autocomplete="off" required>
|
|
6286
6489
|
<button type="submit" name="approve" value="yes">Approve</button>
|
|
6287
6490
|
</form></body></html>`;
|
|
@@ -6293,8 +6496,9 @@ ${hidden}
|
|
|
6293
6496
|
req.on('data', (c) => { body += c; if (body.length > 16384) req.destroy(); });
|
|
6294
6497
|
req.on('end', () => {
|
|
6295
6498
|
const form = new URLSearchParams(body);
|
|
6296
|
-
const
|
|
6297
|
-
const
|
|
6499
|
+
const authorizationParams = getOauthAuthorizeParamsFromForm(form, hostedConfig);
|
|
6500
|
+
const redirectUri = authorizationParams.redirectUri;
|
|
6501
|
+
const state = authorizationParams.state;
|
|
6298
6502
|
// Validate the presented ThumbGate key before issuing a code. When keys
|
|
6299
6503
|
// are configured (production) the key MUST match a configured admin /
|
|
6300
6504
|
// operator / reviewer key — otherwise OAuth would authenticate nobody.
|
|
@@ -6304,12 +6508,12 @@ ${hidden}
|
|
|
6304
6508
|
return;
|
|
6305
6509
|
}
|
|
6306
6510
|
const issued = mcpOauth.createAuthorizationCode(oauthStore, {
|
|
6307
|
-
clientId:
|
|
6511
|
+
clientId: authorizationParams.clientId,
|
|
6308
6512
|
redirectUri,
|
|
6309
|
-
codeChallenge:
|
|
6310
|
-
codeChallengeMethod:
|
|
6311
|
-
scope:
|
|
6312
|
-
resource:
|
|
6513
|
+
codeChallenge: authorizationParams.codeChallenge,
|
|
6514
|
+
codeChallengeMethod: authorizationParams.codeChallengeMethod,
|
|
6515
|
+
scope: authorizationParams.scope,
|
|
6516
|
+
resource: authorizationParams.resource || buildPublicUrl(hostedConfig, '/mcp'),
|
|
6313
6517
|
boundKey: form.get('api_key') || '',
|
|
6314
6518
|
state,
|
|
6315
6519
|
});
|
|
@@ -8402,11 +8606,14 @@ ${hidden}
|
|
|
8402
8606
|
{
|
|
8403
8607
|
const documentId = normalizeDocumentIdFromPath(pathname);
|
|
8404
8608
|
if (req.method === 'GET' && documentId) {
|
|
8609
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(documentId)) {
|
|
8610
|
+
throw createHttpError(400, 'Invalid document ID format');
|
|
8611
|
+
}
|
|
8405
8612
|
const document = readImportedDocument(documentId, {
|
|
8406
8613
|
feedbackDir: requestFeedbackDir,
|
|
8407
8614
|
});
|
|
8408
8615
|
if (!document) {
|
|
8409
|
-
throw createHttpError(404, `Imported document not found: ${documentId}`);
|
|
8616
|
+
throw createHttpError(404, `Imported document not found: ${escapeHtml(documentId)}`);
|
|
8410
8617
|
}
|
|
8411
8618
|
sendJson(res, 200, { document });
|
|
8412
8619
|
return;
|
|
@@ -9502,6 +9709,8 @@ module.exports = {
|
|
|
9502
9709
|
buildEnterpriseChatAnswer,
|
|
9503
9710
|
answerEnterpriseDataChat,
|
|
9504
9711
|
answerEnterpriseDialogflowChat,
|
|
9712
|
+
buildLossAnalyticsResponse,
|
|
9713
|
+
sanitizeHtmlUnsafeJsonValue,
|
|
9505
9714
|
},
|
|
9506
9715
|
};
|
|
9507
9716
|
|