thumbgate 1.27.7 → 1.27.9
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/.well-known/llms.txt +1 -2
- package/README.md +0 -2
- package/bin/cli.js +259 -78
- package/package.json +12 -18
- 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 +10 -48
- package/public/learn/ac-dc-runtime-enforcement.html +277 -0
- package/public/learn/agent-harness-pattern.html +181 -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/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 +6 -50
- 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/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/vector-store.js +0 -44
- package/scripts/workspace-evolver.js +2 -62
- package/src/api/server.js +124 -335
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/scripts/hook-stop-anti-claim.js +0 -227
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,7 @@ 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' },
|
|
3529
3337
|
{ path: '/learn/pretix-stripe-connect-marketplaces', changefreq: 'weekly', priority: '0.9' },
|
|
3530
3338
|
{ path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
|
|
3531
3339
|
{ path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },
|
|
@@ -3535,30 +3343,30 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3535
3343
|
{ path: '/compare/anthropic-claude-for-legal', changefreq: 'weekly', priority: '0.9' },
|
|
3536
3344
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
3537
3345
|
];
|
|
3538
|
-
// Auto-include every hand-written
|
|
3539
|
-
// of sync with public/compare/*.html or public/guides/*.html.
|
|
3540
|
-
// answer engines
|
|
3541
|
-
//
|
|
3542
|
-
//
|
|
3346
|
+
// Auto-include every hand-written comparison and guide page so /sitemap.xml can
|
|
3347
|
+
// never drift out of sync with public/compare/*.html or public/guides/*.html.
|
|
3348
|
+
// Crawlers and AI answer engines (Google AI Overviews/AI Mode, ChatGPT,
|
|
3349
|
+
// Perplexity) only surface pages they can discover, so a page missing from the
|
|
3350
|
+
// sitemap is invisible on its buyer-intent query. De-duped against entries
|
|
3351
|
+
// already declared above (e.g. the seo-gsd specs), which keep their explicit
|
|
3352
|
+
// priorities.
|
|
3543
3353
|
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));
|
|
3354
|
+
const includePublicHtmlPages = (dirName, publicPrefix, priority) => {
|
|
3355
|
+
try {
|
|
3356
|
+
const files = fs.readdirSync(path.join(PUBLIC_DIR, dirName)).sort((a, b) => a.localeCompare(b));
|
|
3551
3357
|
for (const file of files) {
|
|
3552
3358
|
if (!file.endsWith('.html')) continue;
|
|
3553
|
-
const
|
|
3554
|
-
if (declaredPaths.has(
|
|
3555
|
-
declaredPaths.add(
|
|
3556
|
-
entries.push({ path:
|
|
3359
|
+
const pagePath = `${publicPrefix}/${file.replace(/\.html$/, '')}`;
|
|
3360
|
+
if (declaredPaths.has(pagePath)) continue;
|
|
3361
|
+
declaredPaths.add(pagePath);
|
|
3362
|
+
entries.push({ path: pagePath, changefreq: 'weekly', priority });
|
|
3557
3363
|
}
|
|
3364
|
+
} catch {
|
|
3365
|
+
// Directory absent in a stripped bundle — fall back to the static entries.
|
|
3558
3366
|
}
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
|
-
|
|
3367
|
+
};
|
|
3368
|
+
includePublicHtmlPages('compare', '/compare', '0.85');
|
|
3369
|
+
includePublicHtmlPages('guides', '/guides', '0.82');
|
|
3562
3370
|
return [
|
|
3563
3371
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
3564
3372
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
@@ -3684,7 +3492,7 @@ function servePublicMarketingPage({
|
|
|
3684
3492
|
}, req.headers, 'seo_landing_view');
|
|
3685
3493
|
}
|
|
3686
3494
|
|
|
3687
|
-
const requestHost =
|
|
3495
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
3688
3496
|
const html = renderHtml(hostedConfig, {
|
|
3689
3497
|
serverVisitorId: journeyState.visitorId,
|
|
3690
3498
|
serverSessionId: journeyState.sessionId,
|
|
@@ -4063,15 +3871,34 @@ function renderCheckoutCancelledPage(runtimeConfig) {
|
|
|
4063
3871
|
const workflowSprintCheckoutUrl = runtimeConfig.workflowSprintCheckoutUrl
|
|
4064
3872
|
? escapeHtmlAttribute(runtimeConfig.workflowSprintCheckoutUrl)
|
|
4065
3873
|
: '';
|
|
3874
|
+
const paypalDiagnosticCheckoutUrl = runtimeConfig.paypalDiagnosticCheckoutUrl
|
|
3875
|
+
? escapeHtmlAttribute(runtimeConfig.paypalDiagnosticCheckoutUrl)
|
|
3876
|
+
: '';
|
|
3877
|
+
const paypalWorkflowSprintCheckoutUrl = runtimeConfig.paypalWorkflowSprintCheckoutUrl
|
|
3878
|
+
? escapeHtmlAttribute(runtimeConfig.paypalWorkflowSprintCheckoutUrl)
|
|
3879
|
+
: '';
|
|
3880
|
+
const morSnapshotCheckoutUrl = runtimeConfig.morSnapshotCheckoutUrl
|
|
3881
|
+
? escapeHtmlAttribute(runtimeConfig.morSnapshotCheckoutUrl)
|
|
3882
|
+
: '';
|
|
4066
3883
|
const sprintDiagnosticPriceDollars = runtimeConfig.sprintDiagnosticPriceDollars || 499;
|
|
4067
3884
|
const workflowSprintPriceDollars = runtimeConfig.workflowSprintPriceDollars || 1500;
|
|
3885
|
+
const snapshotPriceDollars = runtimeConfig.snapshotPriceDollars || 97;
|
|
4068
3886
|
const workflowSprintIntakeUrl = `${escapeHtmlAttribute(runtimeConfig.appOrigin)}/#workflow-sprint-intake`;
|
|
4069
3887
|
const recoveryOfferLinks = [
|
|
4070
3888
|
diagnosticCheckoutUrl
|
|
4071
|
-
? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="
|
|
3889
|
+
? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic_stripe" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic by Stripe</a>`
|
|
3890
|
+
: '',
|
|
3891
|
+
paypalDiagnosticCheckoutUrl
|
|
3892
|
+
? `<a href="${paypalDiagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic_paypal" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic by PayPal</a>`
|
|
4072
3893
|
: '',
|
|
4073
3894
|
workflowSprintCheckoutUrl
|
|
4074
|
-
? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="
|
|
3895
|
+
? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="workflow_sprint_stripe" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint by Stripe</a>`
|
|
3896
|
+
: '',
|
|
3897
|
+
paypalWorkflowSprintCheckoutUrl
|
|
3898
|
+
? `<a href="${paypalWorkflowSprintCheckoutUrl}" data-recovery-offer="workflow_sprint_paypal" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint by PayPal</a>`
|
|
3899
|
+
: '',
|
|
3900
|
+
morSnapshotCheckoutUrl
|
|
3901
|
+
? `<a href="${morSnapshotCheckoutUrl}" data-recovery-offer="snapshot_mor" data-offer-price="${snapshotPriceDollars}">Buy $${snapshotPriceDollars} snapshot</a>`
|
|
4075
3902
|
: '',
|
|
4076
3903
|
`<a href="${workflowTeardownCheckoutUrl}" data-recovery-offer="workflow_teardown" data-offer-price="99">Pay $99 teardown</a>`,
|
|
4077
3904
|
`<a href="${quickReadCheckoutUrl}" data-recovery-offer="quick_read" data-offer-price="19">Pay $19 quick read</a>`,
|
|
@@ -4769,42 +4596,6 @@ function normalizeJobIdFromPath(pathname, suffix = '') {
|
|
|
4769
4596
|
return match ? decodeURIComponent(match[1]) : null;
|
|
4770
4597
|
}
|
|
4771
4598
|
|
|
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
4599
|
function normalizeDocumentIdFromPath(pathname) {
|
|
4809
4600
|
const match = pathname.match(/^\/v1\/documents\/([^/]+)$/);
|
|
4810
4601
|
return match ? decodeURIComponent(match[1]) : null;
|
|
@@ -5872,12 +5663,13 @@ async function addContext(){
|
|
|
5872
5663
|
|
|
5873
5664
|
if (isGetLikeRequest && pathname.startsWith('/learn/')) {
|
|
5874
5665
|
try {
|
|
5875
|
-
const
|
|
5876
|
-
|
|
5877
|
-
|
|
5666
|
+
const slug = normalizePublicPageSlug(pathname.replace('/learn/', ''));
|
|
5667
|
+
const articlePath = path.join(LEARN_DIR, `${slug}.html`);
|
|
5668
|
+
if (!articlePath.startsWith(LEARN_DIR)) {
|
|
5669
|
+
sendJson(res, 403, { error: 'Forbidden' });
|
|
5878
5670
|
return;
|
|
5879
5671
|
}
|
|
5880
|
-
const requestHost =
|
|
5672
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5881
5673
|
const html = normalizePublicMarketingHtml(fs.readFileSync(articlePath, 'utf-8'), hostedConfig, requestHost);
|
|
5882
5674
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5883
5675
|
} catch {
|
|
@@ -5888,9 +5680,10 @@ async function addContext(){
|
|
|
5888
5680
|
|
|
5889
5681
|
if (isGetLikeRequest && pathname.startsWith('/guides/')) {
|
|
5890
5682
|
try {
|
|
5891
|
-
const
|
|
5892
|
-
|
|
5893
|
-
|
|
5683
|
+
const slug = normalizePublicPageSlug(pathname.replace('/guides/', ''));
|
|
5684
|
+
const guidePath = path.join(GUIDES_DIR, `${slug}.html`);
|
|
5685
|
+
if (!guidePath.startsWith(GUIDES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
5686
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5894
5687
|
const html = normalizePublicMarketingHtml(fs.readFileSync(guidePath, 'utf-8'), hostedConfig, requestHost);
|
|
5895
5688
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5896
5689
|
} catch { sendJson(res, 404, { error: 'Guide not found' }); }
|
|
@@ -5899,9 +5692,10 @@ async function addContext(){
|
|
|
5899
5692
|
|
|
5900
5693
|
if (isGetLikeRequest && pathname.startsWith('/compare/') && pathname !== '/compare') {
|
|
5901
5694
|
try {
|
|
5902
|
-
const
|
|
5903
|
-
|
|
5904
|
-
|
|
5695
|
+
const slug = normalizePublicPageSlug(pathname.replace('/compare/', ''));
|
|
5696
|
+
const comparePath = path.join(COMPARE_DIR, `${slug}.html`);
|
|
5697
|
+
if (!comparePath.startsWith(COMPARE_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
5698
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5905
5699
|
const html = normalizePublicMarketingHtml(fs.readFileSync(comparePath, 'utf-8'), hostedConfig, requestHost);
|
|
5906
5700
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5907
5701
|
} catch { sendJson(res, 404, { error: 'Comparison not found' }); }
|
|
@@ -5910,9 +5704,10 @@ async function addContext(){
|
|
|
5910
5704
|
|
|
5911
5705
|
if (isGetLikeRequest && pathname.startsWith('/use-cases/')) {
|
|
5912
5706
|
try {
|
|
5913
|
-
const
|
|
5914
|
-
|
|
5915
|
-
|
|
5707
|
+
const slug = normalizePublicPageSlug(pathname.replace('/use-cases/', ''));
|
|
5708
|
+
const useCasePath = path.join(USE_CASES_DIR, `${slug}.html`);
|
|
5709
|
+
if (!useCasePath.startsWith(USE_CASES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
5710
|
+
const requestHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
5916
5711
|
const html = normalizePublicMarketingHtml(fs.readFileSync(useCasePath, 'utf-8'), hostedConfig, requestHost);
|
|
5917
5712
|
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5918
5713
|
} catch { sendJson(res, 404, { error: 'Use case not found' }); }
|
|
@@ -6415,7 +6210,7 @@ async function addContext(){
|
|
|
6415
6210
|
// Public-facing broker lead-flow audit landing page. Wedge for the
|
|
6416
6211
|
// real-estate broker outreach. Static HTML served from src/api/static.
|
|
6417
6212
|
try {
|
|
6418
|
-
const host =
|
|
6213
|
+
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
|
6419
6214
|
const html = normalizePublicMarketingHtml(fillTemplate(fs.readFileSync(
|
|
6420
6215
|
path.resolve(__dirname, '../../assets/static/broker-audit.html'),
|
|
6421
6216
|
'utf8'
|
|
@@ -6473,9 +6268,9 @@ async function addContext(){
|
|
|
6473
6268
|
// Authorization endpoint: GET renders consent, POST issues the code.
|
|
6474
6269
|
if (pathname === '/oauth/authorize') {
|
|
6475
6270
|
if (isGetLikeRequest) {
|
|
6476
|
-
const
|
|
6477
|
-
|
|
6478
|
-
);
|
|
6271
|
+
const q = parsed.searchParams;
|
|
6272
|
+
const fields = ['client_id', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'scope', 'state', 'resource'];
|
|
6273
|
+
const hidden = fields.map((f) => `<input type="hidden" name="${f}" value="${escapeHtmlAttribute(q.get(f) || '')}">`).join('\n');
|
|
6479
6274
|
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Authorize ThumbGate</title>
|
|
6480
6275
|
<style>body{font:15px system-ui;margin:0;background:#0b0b0c;color:#eee;display:flex;min-height:100vh;align-items:center;justify-content:center}
|
|
6481
6276
|
.card{background:#161618;border:1px solid #2a2a2e;border-radius:12px;padding:28px;max-width:420px}
|
|
@@ -6484,7 +6279,7 @@ button{width:100%;padding:11px;border-radius:8px;border:0;background:#10b981;col
|
|
|
6484
6279
|
a{color:#8b9}</style></head><body><form class="card" method="post" action="/oauth/authorize">
|
|
6485
6280
|
<h2>Authorize Claude → ThumbGate</h2>
|
|
6486
6281
|
<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
|
-
|
|
6282
|
+
${hidden}
|
|
6488
6283
|
<input type="password" name="api_key" placeholder="ThumbGate API key" autocomplete="off" required>
|
|
6489
6284
|
<button type="submit" name="approve" value="yes">Approve</button>
|
|
6490
6285
|
</form></body></html>`;
|
|
@@ -6496,9 +6291,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6496
6291
|
req.on('data', (c) => { body += c; if (body.length > 16384) req.destroy(); });
|
|
6497
6292
|
req.on('end', () => {
|
|
6498
6293
|
const form = new URLSearchParams(body);
|
|
6499
|
-
const
|
|
6500
|
-
const
|
|
6501
|
-
const state = authorizationParams.state;
|
|
6294
|
+
const redirectUri = form.get('redirect_uri') || '';
|
|
6295
|
+
const state = form.get('state') || '';
|
|
6502
6296
|
// Validate the presented ThumbGate key before issuing a code. When keys
|
|
6503
6297
|
// are configured (production) the key MUST match a configured admin /
|
|
6504
6298
|
// operator / reviewer key — otherwise OAuth would authenticate nobody.
|
|
@@ -6508,12 +6302,12 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6508
6302
|
return;
|
|
6509
6303
|
}
|
|
6510
6304
|
const issued = mcpOauth.createAuthorizationCode(oauthStore, {
|
|
6511
|
-
clientId:
|
|
6305
|
+
clientId: form.get('client_id') || '',
|
|
6512
6306
|
redirectUri,
|
|
6513
|
-
codeChallenge:
|
|
6514
|
-
codeChallengeMethod:
|
|
6515
|
-
scope:
|
|
6516
|
-
resource:
|
|
6307
|
+
codeChallenge: form.get('code_challenge') || '',
|
|
6308
|
+
codeChallengeMethod: form.get('code_challenge_method') || '',
|
|
6309
|
+
scope: form.get('scope') || undefined,
|
|
6310
|
+
resource: form.get('resource') || buildPublicUrl(hostedConfig, '/mcp'),
|
|
6517
6311
|
boundKey: form.get('api_key') || '',
|
|
6518
6312
|
state,
|
|
6519
6313
|
});
|
|
@@ -8606,14 +8400,11 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8606
8400
|
{
|
|
8607
8401
|
const documentId = normalizeDocumentIdFromPath(pathname);
|
|
8608
8402
|
if (req.method === 'GET' && documentId) {
|
|
8609
|
-
if (!/^[a-zA-Z0-9-_]+$/.test(documentId)) {
|
|
8610
|
-
throw createHttpError(400, 'Invalid document ID format');
|
|
8611
|
-
}
|
|
8612
8403
|
const document = readImportedDocument(documentId, {
|
|
8613
8404
|
feedbackDir: requestFeedbackDir,
|
|
8614
8405
|
});
|
|
8615
8406
|
if (!document) {
|
|
8616
|
-
throw createHttpError(404, `Imported document not found: ${
|
|
8407
|
+
throw createHttpError(404, `Imported document not found: ${documentId}`);
|
|
8617
8408
|
}
|
|
8618
8409
|
sendJson(res, 200, { document });
|
|
8619
8410
|
return;
|
|
@@ -9709,8 +9500,6 @@ module.exports = {
|
|
|
9709
9500
|
buildEnterpriseChatAnswer,
|
|
9710
9501
|
answerEnterpriseDataChat,
|
|
9711
9502
|
answerEnterpriseDialogflowChat,
|
|
9712
|
-
buildLossAnalyticsResponse,
|
|
9713
|
-
sanitizeHtmlUnsafeJsonValue,
|
|
9714
9503
|
},
|
|
9715
9504
|
};
|
|
9716
9505
|
|