thumbgate 1.27.8 → 1.27.10
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 +1 -2
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +4 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +259 -78
- package/config/gate-templates.json +228 -0
- package/config/gates/claim-verification.json +18 -0
- package/package.json +14 -21
- package/public/blog.html +30 -0
- package/public/compare/adopt-ai.html +219 -0
- package/public/compare/agentix-labs.html +197 -0
- package/public/compare/ai-experience-orchestration.html +216 -0
- package/public/compare/anthropic-claude-for-legal.html +260 -0
- package/public/compare/anthropic-containment.html +280 -0
- package/public/compare/arcade.html +175 -0
- package/public/compare/arcjet.html +239 -0
- package/public/compare/bumblebee.html +307 -0
- package/public/compare/claude-code-hooks.html +294 -0
- package/public/compare/databricks-unity-ai-gateway.html +215 -0
- package/public/compare/fallow.html +351 -0
- package/public/compare/heidi.html +233 -0
- package/public/compare/mem0.html +342 -0
- package/public/compare/oak-and-sparrow-gatekeeper.html +289 -0
- package/public/compare/rein.html +236 -0
- package/public/compare/sigmashake.html +256 -0
- package/public/compare/speclock.html +342 -0
- package/public/compare.html +2 -0
- package/public/guides/agent-harness-optimization.html +342 -0
- package/public/guides/agentic-web-governance.html +406 -0
- package/public/guides/ai-agent-governance-sprint.html +415 -0
- package/public/guides/ai-agent-pre-action-approval-gates.html +401 -0
- package/public/guides/ai-agent-workflow-migration-checklist.html +392 -0
- package/public/guides/ai-deployment-readiness.html +415 -0
- package/public/guides/ai-mode-ads-agent-governance.html +401 -0
- package/public/guides/ai-search-topical-presence.html +342 -0
- package/public/guides/autoresearch-agent-safety.html +342 -0
- package/public/guides/background-agent-governance.html +358 -0
- package/public/guides/best-tools-stop-ai-agents-breaking-production.html +363 -0
- package/public/guides/browser-automation-safety.html +342 -0
- package/public/guides/chatgpt-ads-trust.html +353 -0
- package/public/guides/claude-code-feedback.html +339 -0
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/claude-code-skills-guardrails.html +343 -0
- package/public/guides/claude-desktop.html +356 -0
- package/public/guides/code-knowledge-graph-guardrails.html +365 -0
- package/public/guides/codex-cli-guardrails.html +339 -0
- package/public/guides/cursor-agent-guardrails.html +339 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/database-agent-safety.html +406 -0
- package/public/guides/deepseek-v4-runtime-guardrails.html +346 -0
- package/public/guides/developer-machine-supply-chain-guardrails.html +358 -0
- package/public/guides/gcp-mcp-guardrails.html +147 -0
- package/public/guides/gemini-cli-feedback-memory.html +339 -0
- package/public/guides/gpt-5-5-model-evaluation.html +358 -0
- package/public/guides/internal-ai-engineering-stack-guardrails.html +348 -0
- package/public/guides/long-running-agent-context-management.html +346 -0
- package/public/guides/mcp-tool-governance.html +401 -0
- package/public/guides/multica-thumbgate-setup.html +134 -0
- package/public/guides/native-messaging-host-security.html +342 -0
- package/public/guides/policy-engine-pre-action-gates.html +346 -0
- package/public/guides/pre-action-checks.html +342 -0
- package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +342 -0
- package/public/guides/prompt-tricks-to-workflow-rules.html +365 -0
- package/public/guides/proxy-pointer-rag-guardrails.html +352 -0
- package/public/guides/rag-precision-tuning-guardrails.html +352 -0
- package/public/guides/reasoning-compression-guardrails.html +346 -0
- package/public/guides/relational-knowledge-ai-recommendations.html +342 -0
- package/public/guides/roo-code-alternative-cline.html +339 -0
- package/public/guides/semantic-programmatic-seo-guardrails.html +352 -0
- package/public/guides/seo-agent-skills-guardrails.html +344 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +342 -0
- package/public/index.html +192 -50
- package/public/learn/ac-dc-runtime-enforcement.html +277 -0
- package/public/learn/agent-harness-pattern.html +181 -0
- package/public/learn/agent-identity-connector-governance.html +146 -0
- package/public/learn/agent-swarms-shared-gates.html +173 -0
- package/public/learn/agentic-enterprise-context-brain.html +117 -0
- package/public/learn/agentic-os-team-governance.html +146 -0
- package/public/learn/ai-agent-governance.html +158 -0
- package/public/learn/ai-agent-persistent-memory.html +211 -0
- package/public/learn/anthropomorphic-claim-gates.html +180 -0
- package/public/learn/background-agent-control-layer.html +184 -0
- package/public/learn/claude-code-goal-with-rubrics.html +205 -0
- package/public/learn/codex-role-plugins-need-governance.html +125 -0
- package/public/learn/cost-aware-agent-gate-routing.html +173 -0
- package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +157 -0
- package/public/learn/deterministic-agent-workflows.html +185 -0
- package/public/learn/feedback-loop-vs-decision-layer.html +283 -0
- package/public/learn/from-prototype-to-production.html +223 -0
- package/public/learn/learn.css +51 -0
- package/public/learn/mcp-pre-action-checks-explained.html +172 -0
- package/public/learn/pretix-stripe-connect-marketplaces.html +161 -0
- package/public/learn/regulated-agent-execution-boundary.html +196 -0
- package/public/learn/spec-driven-development.html +168 -0
- package/public/learn/stop-ai-agent-force-push.html +134 -0
- package/public/learn/vibe-coding-safety-net.html +142 -0
- package/public/learn.html +34 -50
- package/public/numbers.html +2 -2
- package/public/pro.html +6 -6
- package/scripts/cli-schema.js +10 -22
- package/scripts/dashboard-chat.js +1 -2
- package/scripts/document-intake.js +49 -1
- package/scripts/gemini-embedding-policy.js +1 -2
- package/scripts/hook-stop-anti-claim.js +103 -42
- package/scripts/hosted-config.js +12 -0
- package/scripts/plausible-domain-config.js +1 -3
- package/scripts/reddit-browser-notification-watch.js +230 -0
- package/scripts/seo-gsd.js +0 -239
- package/scripts/tool-registry.js +2 -2
- package/scripts/vector-store.js +0 -44
- package/scripts/workspace-evolver.js +2 -62
- package/src/api/server.js +126 -335
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
package/src/api/server.js
CHANGED
|
@@ -3,7 +3,6 @@ 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');
|
|
7
6
|
const { EventEmitter } = require('node:events');
|
|
8
7
|
const pkg = require('../../package.json');
|
|
9
8
|
const {
|
|
@@ -216,8 +215,6 @@ const mcpOauth = require('../../scripts/mcp-oauth');
|
|
|
216
215
|
// OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
|
|
217
216
|
// (Claude Connectors Directory requires OAuth for authenticated services).
|
|
218
217
|
const oauthStore = mcpOauth.createStore();
|
|
219
|
-
const pendingOauthAuthorizeRequests = new Map();
|
|
220
|
-
const OAUTH_AUTHORIZE_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
221
218
|
const resendMailer = require('../../scripts/mailer/resend-mailer');
|
|
222
219
|
const {
|
|
223
220
|
buildContextFootprintReport,
|
|
@@ -247,10 +244,6 @@ const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
|
|
|
247
244
|
const USE_CASES_DIR = path.resolve(__dirname, '../../public/use-cases');
|
|
248
245
|
const PUBLIC_DIR = path.resolve(__dirname, '../../public');
|
|
249
246
|
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);
|
|
254
247
|
const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
|
|
255
248
|
const STATIC_MIME_BY_EXT = Object.freeze({
|
|
256
249
|
'.png': 'image/png',
|
|
@@ -467,36 +460,6 @@ const TRACKED_LINK_TARGETS = Object.freeze({
|
|
|
467
460
|
},
|
|
468
461
|
allowCustomerEmail: true,
|
|
469
462
|
},
|
|
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
|
-
},
|
|
500
463
|
trial: {
|
|
501
464
|
path: '/guide',
|
|
502
465
|
ctaId: 'go_trial',
|
|
@@ -1717,8 +1680,6 @@ const FEEDBACK_LIST_LABELS = Object.freeze({
|
|
|
1717
1680
|
positive: 'Recent wins',
|
|
1718
1681
|
});
|
|
1719
1682
|
|
|
1720
|
-
const FEEDBACK_OMITTED_TAGS = Object.freeze(['audit-trail', 'auto-capture']);
|
|
1721
|
-
|
|
1722
1683
|
function formatChatTimestamp(isoString) {
|
|
1723
1684
|
if (!isoString) return 'unknown time';
|
|
1724
1685
|
try {
|
|
@@ -1737,33 +1698,9 @@ function formatChatTimestamp(isoString) {
|
|
|
1737
1698
|
}
|
|
1738
1699
|
}
|
|
1739
1700
|
|
|
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
|
-
|
|
1765
1701
|
function buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeline }) {
|
|
1766
1702
|
const signal = detectFeedbackSignalFromPrompt(ctx.prompt);
|
|
1703
|
+
|
|
1767
1704
|
// One read of the time-windowed log, then in-memory counts + (signal-filtered,
|
|
1768
1705
|
// placeholder-stripped) list. Counts include ALL entries (so "Feedback today: 5"
|
|
1769
1706
|
// matches the dashboard tile); the list drops vague entries like literal "thumbs
|
|
@@ -1771,16 +1708,30 @@ function buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeli
|
|
|
1771
1708
|
const windowed = readRecentFeedbackEntries(feedbackDir, null, intent.windowMs, 10000, { includeSignal: true });
|
|
1772
1709
|
const windowPos = windowed.filter((r) => r.signal === 'positive').length;
|
|
1773
1710
|
const windowNeg = windowed.filter((r) => r.signal === 'negative').length;
|
|
1774
|
-
const entries =
|
|
1711
|
+
const entries = (signal ? windowed.filter((r) => r.signal === signal) : windowed)
|
|
1712
|
+
.filter((r) => r.context && !isPlaceholder(r.context))
|
|
1713
|
+
.slice(0, 5);
|
|
1775
1714
|
|
|
1776
|
-
const lines = [
|
|
1777
|
-
|
|
1715
|
+
const lines = [];
|
|
1716
|
+
lines.push(intent.windowMs
|
|
1778
1717
|
? `Feedback ${intent.windowLabel}: ${windowed.length} (${windowPos} positive, ${windowNeg} negative).`
|
|
1779
|
-
: `Feedback total: ${compactNumber(approval.total)} (${compactNumber(approval.positive)} positive, ${compactNumber(approval.negative)} negative)
|
|
1780
|
-
];
|
|
1718
|
+
: `Feedback total: ${compactNumber(approval.total)} (${compactNumber(approval.positive)} positive, ${compactNumber(approval.negative)} negative).`);
|
|
1781
1719
|
|
|
1782
1720
|
if (intent.wantsList) {
|
|
1783
|
-
|
|
1721
|
+
if (entries.length) {
|
|
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
|
+
}
|
|
1784
1735
|
} else {
|
|
1785
1736
|
lines.push(`Lesson pipeline: ${compactNumber(lessonPipeline.lessons || lessonPipeline.generated || 0)} lessons visible in the current dashboard snapshot.`);
|
|
1786
1737
|
}
|
|
@@ -1988,7 +1939,7 @@ async function answerEnterpriseDataChat({ prompt, feedbackDir, parsed }) {
|
|
|
1988
1939
|
const answerEnterpriseDialogflowChat = answerEnterpriseDataChat;
|
|
1989
1940
|
|
|
1990
1941
|
function buildLossAnalyticsResponse(data, summaryOptions) {
|
|
1991
|
-
return
|
|
1942
|
+
return {
|
|
1992
1943
|
window: data.analytics.window || summaryOptions,
|
|
1993
1944
|
lossAnalysis: data.analytics.lossAnalysis || null,
|
|
1994
1945
|
buyerLoss: data.analytics.buyerLoss || null,
|
|
@@ -2000,7 +1951,7 @@ function buildLossAnalyticsResponse(data, summaryOptions) {
|
|
|
2000
1951
|
ctas: data.analytics.telemetry && data.analytics.telemetry.ctas,
|
|
2001
1952
|
visitors: data.analytics.telemetry && data.analytics.telemetry.visitors,
|
|
2002
1953
|
},
|
|
2003
|
-
}
|
|
1954
|
+
};
|
|
2004
1955
|
}
|
|
2005
1956
|
|
|
2006
1957
|
function createJourneyId(prefix) {
|
|
@@ -2103,111 +2054,17 @@ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
|
|
|
2103
2054
|
});
|
|
2104
2055
|
}
|
|
2105
2056
|
|
|
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
|
-
|
|
2205
2057
|
function renderCheckoutIntentPage(prefilledEmail = '', parsed = null, options = {}) {
|
|
2206
2058
|
const plausibleDomain = escapeHtmlAttribute(resolvePlausibleDataDomain({ host: 'thumbgate.ai' }));
|
|
2207
2059
|
const includeHiddenAttribution = options.includeHiddenAttribution === true;
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2060
|
+
let hiddenInputs = '';
|
|
2061
|
+
if (includeHiddenAttribution && parsed?.searchParams) {
|
|
2062
|
+
for (const [key, value] of parsed.searchParams.entries()) {
|
|
2063
|
+
if (key !== 'confirm' && key !== 'customer_email') {
|
|
2064
|
+
hiddenInputs += `<input type="hidden" name="${escapeHtmlAttribute(key)}" value="${escapeHtmlAttribute(value)}">`;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2211
2068
|
return `<!doctype html>
|
|
2212
2069
|
<html lang="en">
|
|
2213
2070
|
<head>
|
|
@@ -2405,6 +2262,17 @@ function normalizeCheckoutCustomerEmail(value) {
|
|
|
2405
2262
|
return email;
|
|
2406
2263
|
}
|
|
2407
2264
|
|
|
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
|
+
|
|
2408
2276
|
function normalizeTrackedLinkSlug(value) {
|
|
2409
2277
|
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
2410
2278
|
}
|
|
@@ -2415,27 +2283,6 @@ function normalizePublicPageSlug(value) {
|
|
|
2415
2283
|
.replace(/[^a-z0-9-]/g, '');
|
|
2416
2284
|
}
|
|
2417
2285
|
|
|
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
|
-
|
|
2439
2286
|
function getTrackedLinkTarget(slug) {
|
|
2440
2287
|
const normalizedSlug = normalizeTrackedLinkSlug(slug);
|
|
2441
2288
|
return TRACKED_LINK_TARGETS[normalizedSlug]
|
|
@@ -2471,10 +2318,8 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
|
2471
2318
|
}
|
|
2472
2319
|
|
|
2473
2320
|
function buildTrackedLinkDestination(target, hostedConfig, parsed) {
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
const destinationUrl = href
|
|
2477
|
-
? new URL(href)
|
|
2321
|
+
const destinationUrl = target.href
|
|
2322
|
+
? new URL(target.href)
|
|
2478
2323
|
: new URL(target.path || '/', hostedConfig.appOrigin);
|
|
2479
2324
|
appendTrackedLinkQueryParams(destinationUrl, parsed, target);
|
|
2480
2325
|
return destinationUrl;
|
|
@@ -2601,7 +2446,6 @@ function sendJson(res, statusCode, payload, extraHeaders = {}, options = {}) {
|
|
|
2601
2446
|
const body = JSON.stringify(payload);
|
|
2602
2447
|
res.writeHead(statusCode, {
|
|
2603
2448
|
'Content-Type': 'application/json; charset=utf-8',
|
|
2604
|
-
'X-Content-Type-Options': 'nosniff',
|
|
2605
2449
|
'Content-Length': Buffer.byteLength(body),
|
|
2606
2450
|
...extraHeaders,
|
|
2607
2451
|
});
|
|
@@ -2695,9 +2539,8 @@ function chatgptActionEventType(integration, suffix) {
|
|
|
2695
2539
|
}
|
|
2696
2540
|
|
|
2697
2541
|
function getPublicOrigin(req) {
|
|
2698
|
-
const
|
|
2699
|
-
const
|
|
2700
|
-
const host = getSafePublicRequestHost(req) || 'localhost';
|
|
2542
|
+
const proto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim() || 'http';
|
|
2543
|
+
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim() || 'localhost';
|
|
2701
2544
|
return `${proto}://${host}`;
|
|
2702
2545
|
}
|
|
2703
2546
|
|
|
@@ -2716,47 +2559,6 @@ function getRequestHostHeader(req) {
|
|
|
2716
2559
|
return forwardedHost || req.headers.host || '';
|
|
2717
2560
|
}
|
|
2718
2561
|
|
|
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
|
-
|
|
2760
2562
|
function isLoopbackHost(hostValue) {
|
|
2761
2563
|
const rawHost = String(hostValue || '').split(',')[0].trim();
|
|
2762
2564
|
if (!rawHost) {
|
|
@@ -2811,7 +2613,7 @@ function normalizePublicMarketingHtml(html, runtimeConfig, requestHost) {
|
|
|
2811
2613
|
let output = String(html);
|
|
2812
2614
|
output = output.replaceAll(DEFAULT_PUBLIC_APP_ORIGIN, appOrigin);
|
|
2813
2615
|
try {
|
|
2814
|
-
const host =
|
|
2616
|
+
const host = requestHost || new URL(appOrigin).host;
|
|
2815
2617
|
const plausibleDomain = resolvePlausibleDataDomain({ host });
|
|
2816
2618
|
output = output.replaceAll(
|
|
2817
2619
|
'data-domain="thumbgate-production.up.railway.app"',
|
|
@@ -2852,8 +2654,13 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
2852
2654
|
'__PRO_PRICE_LABEL__': runtimeConfig.proPriceLabel,
|
|
2853
2655
|
'__SPRINT_DIAGNOSTIC_CHECKOUT_URL__': runtimeConfig.sprintDiagnosticCheckoutUrl || SPRINT_DIAGNOSTIC_CHECKOUT_URL,
|
|
2854
2656
|
'__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',
|
|
2855
2661
|
'__SPRINT_DIAGNOSTIC_PRICE_DOLLARS__': runtimeConfig.sprintDiagnosticPriceDollars || 499,
|
|
2856
2662
|
'__WORKFLOW_SPRINT_PRICE_DOLLARS__': runtimeConfig.workflowSprintPriceDollars || 1500,
|
|
2663
|
+
'__SNAPSHOT_PRICE_DOLLARS__': runtimeConfig.snapshotPriceDollars || 97,
|
|
2857
2664
|
'__GA_MEASUREMENT_ID__': runtimeConfig.gaMeasurementId || '',
|
|
2858
2665
|
'__GA_BOOTSTRAP__': gaBootstrap,
|
|
2859
2666
|
'__GOOGLE_SITE_VERIFICATION_META__': googleSiteVerificationMeta,
|
|
@@ -3526,6 +3333,9 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3526
3333
|
{ path: '/learn/codex-role-plugins-need-governance', changefreq: 'weekly', priority: '0.85' },
|
|
3527
3334
|
{ path: '/learn/agentic-os-team-governance', changefreq: 'weekly', priority: '0.85' },
|
|
3528
3335
|
{ 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' },
|
|
3529
3339
|
{ path: '/learn/pretix-stripe-connect-marketplaces', changefreq: 'weekly', priority: '0.9' },
|
|
3530
3340
|
{ path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
|
|
3531
3341
|
{ path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },
|
|
@@ -3535,30 +3345,30 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3535
3345
|
{ path: '/compare/anthropic-claude-for-legal', changefreq: 'weekly', priority: '0.9' },
|
|
3536
3346
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
3537
3347
|
];
|
|
3538
|
-
// Auto-include every hand-written
|
|
3539
|
-
// of sync with public/compare/*.html or public/guides/*.html.
|
|
3540
|
-
// answer engines
|
|
3541
|
-
//
|
|
3542
|
-
//
|
|
3348
|
+
// Auto-include every hand-written comparison and guide page so /sitemap.xml can
|
|
3349
|
+
// never drift out of sync with public/compare/*.html or public/guides/*.html.
|
|
3350
|
+
// Crawlers and AI answer engines (Google AI Overviews/AI Mode, ChatGPT,
|
|
3351
|
+
// Perplexity) only surface pages they can discover, so a page missing from the
|
|
3352
|
+
// sitemap is invisible on its buyer-intent query. De-duped against entries
|
|
3353
|
+
// already declared above (e.g. the seo-gsd specs), which keep their explicit
|
|
3354
|
+
// priorities.
|
|
3543
3355
|
const declaredPaths = new Set(entries.map((entry) => entry.path));
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
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));
|
|
3356
|
+
const includePublicHtmlPages = (dirName, publicPrefix, priority) => {
|
|
3357
|
+
try {
|
|
3358
|
+
const files = fs.readdirSync(path.join(PUBLIC_DIR, dirName)).sort((a, b) => a.localeCompare(b));
|
|
3551
3359
|
for (const file of files) {
|
|
3552
3360
|
if (!file.endsWith('.html')) continue;
|
|
3553
|
-
const
|
|
3554
|
-
if (declaredPaths.has(
|
|
3555
|
-
declaredPaths.add(
|
|
3556
|
-
entries.push({ path:
|
|
3361
|
+
const pagePath = `${publicPrefix}/${file.replace(/\.html$/, '')}`;
|
|
3362
|
+
if (declaredPaths.has(pagePath)) continue;
|
|
3363
|
+
declaredPaths.add(pagePath);
|
|
3364
|
+
entries.push({ path: pagePath, changefreq: 'weekly', priority });
|
|
3557
3365
|
}
|
|
3366
|
+
} catch {
|
|
3367
|
+
// Directory absent in a stripped bundle — fall back to the static entries.
|
|
3558
3368
|
}
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
|
-
|
|
3369
|
+
};
|
|
3370
|
+
includePublicHtmlPages('compare', '/compare', '0.85');
|
|
3371
|
+
includePublicHtmlPages('guides', '/guides', '0.82');
|
|
3562
3372
|
return [
|
|
3563
3373
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
3564
3374
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
@@ -3684,7 +3494,7 @@ function servePublicMarketingPage({
|
|
|
3684
3494
|
}, req.headers, 'seo_landing_view');
|
|
3685
3495
|
}
|
|
3686
3496
|
|
|
3687
|
-
const requestHost =
|
|
3497
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
3688
3498
|
const html = renderHtml(hostedConfig, {
|
|
3689
3499
|
serverVisitorId: journeyState.visitorId,
|
|
3690
3500
|
serverSessionId: journeyState.sessionId,
|
|
@@ -4063,15 +3873,34 @@ function renderCheckoutCancelledPage(runtimeConfig) {
|
|
|
4063
3873
|
const workflowSprintCheckoutUrl = runtimeConfig.workflowSprintCheckoutUrl
|
|
4064
3874
|
? escapeHtmlAttribute(runtimeConfig.workflowSprintCheckoutUrl)
|
|
4065
3875
|
: '';
|
|
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
|
+
: '';
|
|
4066
3885
|
const sprintDiagnosticPriceDollars = runtimeConfig.sprintDiagnosticPriceDollars || 499;
|
|
4067
3886
|
const workflowSprintPriceDollars = runtimeConfig.workflowSprintPriceDollars || 1500;
|
|
3887
|
+
const snapshotPriceDollars = runtimeConfig.snapshotPriceDollars || 97;
|
|
4068
3888
|
const workflowSprintIntakeUrl = `${escapeHtmlAttribute(runtimeConfig.appOrigin)}/#workflow-sprint-intake`;
|
|
4069
3889
|
const recoveryOfferLinks = [
|
|
4070
3890
|
diagnosticCheckoutUrl
|
|
4071
|
-
? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="
|
|
3891
|
+
? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic_stripe" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic by Stripe</a>`
|
|
3892
|
+
: '',
|
|
3893
|
+
paypalDiagnosticCheckoutUrl
|
|
3894
|
+
? `<a href="${paypalDiagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic_paypal" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic by PayPal</a>`
|
|
4072
3895
|
: '',
|
|
4073
3896
|
workflowSprintCheckoutUrl
|
|
4074
|
-
? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="
|
|
3897
|
+
? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="workflow_sprint_stripe" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint by Stripe</a>`
|
|
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>`
|
|
4075
3904
|
: '',
|
|
4076
3905
|
`<a href="${workflowTeardownCheckoutUrl}" data-recovery-offer="workflow_teardown" data-offer-price="99">Pay $99 teardown</a>`,
|
|
4077
3906
|
`<a href="${quickReadCheckoutUrl}" data-recovery-offer="quick_read" data-offer-price="19">Pay $19 quick read</a>`,
|
|
@@ -4769,42 +4598,6 @@ function normalizeJobIdFromPath(pathname, suffix = '') {
|
|
|
4769
4598
|
return match ? decodeURIComponent(match[1]) : null;
|
|
4770
4599
|
}
|
|
4771
4600
|
|
|
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
|
-
|
|
4808
4601
|
function normalizeDocumentIdFromPath(pathname) {
|
|
4809
4602
|
const match = pathname.match(/^\/v1\/documents\/([^/]+)$/);
|
|
4810
4603
|
return match ? decodeURIComponent(match[1]) : null;
|
|
@@ -5872,12 +5665,13 @@ async function addContext(){
|
|
|
5872
5665
|
|
|
5873
5666
|
if (isGetLikeRequest && pathname.startsWith('/learn/')) {
|
|
5874
5667
|
try {
|
|
5875
|
-
const
|
|
5876
|
-
|
|
5877
|
-
|
|
5668
|
+
const slug = normalizePublicPageSlug(pathname.replace('/learn/', ''));
|
|
5669
|
+
const articlePath = path.join(LEARN_DIR, `${slug}.html`);
|
|
5670
|
+
if (!articlePath.startsWith(LEARN_DIR)) {
|
|
5671
|
+
sendJson(res, 403, { error: 'Forbidden' });
|
|
5878
5672
|
return;
|
|
5879
5673
|
}
|
|
5880
|
-
const requestHost =
|
|
5674
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5881
5675
|
const html = normalizePublicMarketingHtml(fs.readFileSync(articlePath, 'utf-8'), hostedConfig, requestHost);
|
|
5882
5676
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5883
5677
|
} catch {
|
|
@@ -5888,9 +5682,10 @@ async function addContext(){
|
|
|
5888
5682
|
|
|
5889
5683
|
if (isGetLikeRequest && pathname.startsWith('/guides/')) {
|
|
5890
5684
|
try {
|
|
5891
|
-
const
|
|
5892
|
-
|
|
5893
|
-
|
|
5685
|
+
const slug = normalizePublicPageSlug(pathname.replace('/guides/', ''));
|
|
5686
|
+
const guidePath = path.join(GUIDES_DIR, `${slug}.html`);
|
|
5687
|
+
if (!guidePath.startsWith(GUIDES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
5688
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5894
5689
|
const html = normalizePublicMarketingHtml(fs.readFileSync(guidePath, 'utf-8'), hostedConfig, requestHost);
|
|
5895
5690
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5896
5691
|
} catch { sendJson(res, 404, { error: 'Guide not found' }); }
|
|
@@ -5899,9 +5694,10 @@ async function addContext(){
|
|
|
5899
5694
|
|
|
5900
5695
|
if (isGetLikeRequest && pathname.startsWith('/compare/') && pathname !== '/compare') {
|
|
5901
5696
|
try {
|
|
5902
|
-
const
|
|
5903
|
-
|
|
5904
|
-
|
|
5697
|
+
const slug = normalizePublicPageSlug(pathname.replace('/compare/', ''));
|
|
5698
|
+
const comparePath = path.join(COMPARE_DIR, `${slug}.html`);
|
|
5699
|
+
if (!comparePath.startsWith(COMPARE_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
5700
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5905
5701
|
const html = normalizePublicMarketingHtml(fs.readFileSync(comparePath, 'utf-8'), hostedConfig, requestHost);
|
|
5906
5702
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5907
5703
|
} catch { sendJson(res, 404, { error: 'Comparison not found' }); }
|
|
@@ -5910,9 +5706,10 @@ async function addContext(){
|
|
|
5910
5706
|
|
|
5911
5707
|
if (isGetLikeRequest && pathname.startsWith('/use-cases/')) {
|
|
5912
5708
|
try {
|
|
5913
|
-
const
|
|
5914
|
-
|
|
5915
|
-
|
|
5709
|
+
const slug = normalizePublicPageSlug(pathname.replace('/use-cases/', ''));
|
|
5710
|
+
const useCasePath = path.join(USE_CASES_DIR, `${slug}.html`);
|
|
5711
|
+
if (!useCasePath.startsWith(USE_CASES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
5712
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5916
5713
|
const html = normalizePublicMarketingHtml(fs.readFileSync(useCasePath, 'utf-8'), hostedConfig, requestHost);
|
|
5917
5714
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5918
5715
|
} catch { sendJson(res, 404, { error: 'Use case not found' }); }
|
|
@@ -6415,7 +6212,7 @@ async function addContext(){
|
|
|
6415
6212
|
// Public-facing broker lead-flow audit landing page. Wedge for the
|
|
6416
6213
|
// real-estate broker outreach. Static HTML served from src/api/static.
|
|
6417
6214
|
try {
|
|
6418
|
-
const host =
|
|
6215
|
+
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
6419
6216
|
const html = normalizePublicMarketingHtml(fillTemplate(fs.readFileSync(
|
|
6420
6217
|
path.resolve(__dirname, '../../assets/static/broker-audit.html'),
|
|
6421
6218
|
'utf8'
|
|
@@ -6473,9 +6270,9 @@ async function addContext(){
|
|
|
6473
6270
|
// Authorization endpoint: GET renders consent, POST issues the code.
|
|
6474
6271
|
if (pathname === '/oauth/authorize') {
|
|
6475
6272
|
if (isGetLikeRequest) {
|
|
6476
|
-
const
|
|
6477
|
-
|
|
6478
|
-
);
|
|
6273
|
+
const q = parsed.searchParams;
|
|
6274
|
+
const fields = ['client_id', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'scope', 'state', 'resource'];
|
|
6275
|
+
const hidden = fields.map((f) => `<input type="hidden" name="${f}" value="${escapeHtmlAttribute(q.get(f) || '')}">`).join('\n');
|
|
6479
6276
|
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Authorize ThumbGate</title>
|
|
6480
6277
|
<style>body{font:15px system-ui;margin:0;background:#0b0b0c;color:#eee;display:flex;min-height:100vh;align-items:center;justify-content:center}
|
|
6481
6278
|
.card{background:#161618;border:1px solid #2a2a2e;border-radius:12px;padding:28px;max-width:420px}
|
|
@@ -6484,7 +6281,7 @@ button{width:100%;padding:11px;border-radius:8px;border:0;background:#10b981;col
|
|
|
6484
6281
|
a{color:#8b9}</style></head><body><form class="card" method="post" action="/oauth/authorize">
|
|
6485
6282
|
<h2>Authorize Claude → ThumbGate</h2>
|
|
6486
6283
|
<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>
|
|
6487
|
-
|
|
6284
|
+
${hidden}
|
|
6488
6285
|
<input type="password" name="api_key" placeholder="ThumbGate API key" autocomplete="off" required>
|
|
6489
6286
|
<button type="submit" name="approve" value="yes">Approve</button>
|
|
6490
6287
|
</form></body></html>`;
|
|
@@ -6496,9 +6293,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6496
6293
|
req.on('data', (c) => { body += c; if (body.length > 16384) req.destroy(); });
|
|
6497
6294
|
req.on('end', () => {
|
|
6498
6295
|
const form = new URLSearchParams(body);
|
|
6499
|
-
const
|
|
6500
|
-
const
|
|
6501
|
-
const state = authorizationParams.state;
|
|
6296
|
+
const redirectUri = form.get('redirect_uri') || '';
|
|
6297
|
+
const state = form.get('state') || '';
|
|
6502
6298
|
// Validate the presented ThumbGate key before issuing a code. When keys
|
|
6503
6299
|
// are configured (production) the key MUST match a configured admin /
|
|
6504
6300
|
// operator / reviewer key — otherwise OAuth would authenticate nobody.
|
|
@@ -6508,12 +6304,12 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6508
6304
|
return;
|
|
6509
6305
|
}
|
|
6510
6306
|
const issued = mcpOauth.createAuthorizationCode(oauthStore, {
|
|
6511
|
-
clientId:
|
|
6307
|
+
clientId: form.get('client_id') || '',
|
|
6512
6308
|
redirectUri,
|
|
6513
|
-
codeChallenge:
|
|
6514
|
-
codeChallengeMethod:
|
|
6515
|
-
scope:
|
|
6516
|
-
resource:
|
|
6309
|
+
codeChallenge: form.get('code_challenge') || '',
|
|
6310
|
+
codeChallengeMethod: form.get('code_challenge_method') || '',
|
|
6311
|
+
scope: form.get('scope') || undefined,
|
|
6312
|
+
resource: form.get('resource') || buildPublicUrl(hostedConfig, '/mcp'),
|
|
6517
6313
|
boundKey: form.get('api_key') || '',
|
|
6518
6314
|
state,
|
|
6519
6315
|
});
|
|
@@ -8606,14 +8402,11 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8606
8402
|
{
|
|
8607
8403
|
const documentId = normalizeDocumentIdFromPath(pathname);
|
|
8608
8404
|
if (req.method === 'GET' && documentId) {
|
|
8609
|
-
if (!/^[a-zA-Z0-9-_]+$/.test(documentId)) {
|
|
8610
|
-
throw createHttpError(400, 'Invalid document ID format');
|
|
8611
|
-
}
|
|
8612
8405
|
const document = readImportedDocument(documentId, {
|
|
8613
8406
|
feedbackDir: requestFeedbackDir,
|
|
8614
8407
|
});
|
|
8615
8408
|
if (!document) {
|
|
8616
|
-
throw createHttpError(404, `Imported document not found: ${
|
|
8409
|
+
throw createHttpError(404, `Imported document not found: ${documentId}`);
|
|
8617
8410
|
}
|
|
8618
8411
|
sendJson(res, 200, { document });
|
|
8619
8412
|
return;
|
|
@@ -9709,8 +9502,6 @@ module.exports = {
|
|
|
9709
9502
|
buildEnterpriseChatAnswer,
|
|
9710
9503
|
answerEnterpriseDataChat,
|
|
9711
9504
|
answerEnterpriseDialogflowChat,
|
|
9712
|
-
buildLossAnalyticsResponse,
|
|
9713
|
-
sanitizeHtmlUnsafeJsonValue,
|
|
9714
9505
|
},
|
|
9715
9506
|
};
|
|
9716
9507
|
|