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.
Files changed (117) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/llms.txt +1 -2
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +4 -2
  5. package/adapters/claude/.mcp.json +2 -2
  6. package/adapters/mcp/server-stdio.js +1 -1
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bin/cli.js +259 -78
  9. package/config/gate-templates.json +228 -0
  10. package/config/gates/claim-verification.json +18 -0
  11. package/package.json +14 -21
  12. package/public/blog.html +30 -0
  13. package/public/compare/adopt-ai.html +219 -0
  14. package/public/compare/agentix-labs.html +197 -0
  15. package/public/compare/ai-experience-orchestration.html +216 -0
  16. package/public/compare/anthropic-claude-for-legal.html +260 -0
  17. package/public/compare/anthropic-containment.html +280 -0
  18. package/public/compare/arcade.html +175 -0
  19. package/public/compare/arcjet.html +239 -0
  20. package/public/compare/bumblebee.html +307 -0
  21. package/public/compare/claude-code-hooks.html +294 -0
  22. package/public/compare/databricks-unity-ai-gateway.html +215 -0
  23. package/public/compare/fallow.html +351 -0
  24. package/public/compare/heidi.html +233 -0
  25. package/public/compare/mem0.html +342 -0
  26. package/public/compare/oak-and-sparrow-gatekeeper.html +289 -0
  27. package/public/compare/rein.html +236 -0
  28. package/public/compare/sigmashake.html +256 -0
  29. package/public/compare/speclock.html +342 -0
  30. package/public/compare.html +2 -0
  31. package/public/guides/agent-harness-optimization.html +342 -0
  32. package/public/guides/agentic-web-governance.html +406 -0
  33. package/public/guides/ai-agent-governance-sprint.html +415 -0
  34. package/public/guides/ai-agent-pre-action-approval-gates.html +401 -0
  35. package/public/guides/ai-agent-workflow-migration-checklist.html +392 -0
  36. package/public/guides/ai-deployment-readiness.html +415 -0
  37. package/public/guides/ai-mode-ads-agent-governance.html +401 -0
  38. package/public/guides/ai-search-topical-presence.html +342 -0
  39. package/public/guides/autoresearch-agent-safety.html +342 -0
  40. package/public/guides/background-agent-governance.html +358 -0
  41. package/public/guides/best-tools-stop-ai-agents-breaking-production.html +363 -0
  42. package/public/guides/browser-automation-safety.html +342 -0
  43. package/public/guides/chatgpt-ads-trust.html +353 -0
  44. package/public/guides/claude-code-feedback.html +339 -0
  45. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  46. package/public/guides/claude-code-skills-guardrails.html +343 -0
  47. package/public/guides/claude-desktop.html +356 -0
  48. package/public/guides/code-knowledge-graph-guardrails.html +365 -0
  49. package/public/guides/codex-cli-guardrails.html +339 -0
  50. package/public/guides/cursor-agent-guardrails.html +339 -0
  51. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  52. package/public/guides/database-agent-safety.html +406 -0
  53. package/public/guides/deepseek-v4-runtime-guardrails.html +346 -0
  54. package/public/guides/developer-machine-supply-chain-guardrails.html +358 -0
  55. package/public/guides/gcp-mcp-guardrails.html +147 -0
  56. package/public/guides/gemini-cli-feedback-memory.html +339 -0
  57. package/public/guides/gpt-5-5-model-evaluation.html +358 -0
  58. package/public/guides/internal-ai-engineering-stack-guardrails.html +348 -0
  59. package/public/guides/long-running-agent-context-management.html +346 -0
  60. package/public/guides/mcp-tool-governance.html +401 -0
  61. package/public/guides/multica-thumbgate-setup.html +134 -0
  62. package/public/guides/native-messaging-host-security.html +342 -0
  63. package/public/guides/policy-engine-pre-action-gates.html +346 -0
  64. package/public/guides/pre-action-checks.html +342 -0
  65. package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +342 -0
  66. package/public/guides/prompt-tricks-to-workflow-rules.html +365 -0
  67. package/public/guides/proxy-pointer-rag-guardrails.html +352 -0
  68. package/public/guides/rag-precision-tuning-guardrails.html +352 -0
  69. package/public/guides/reasoning-compression-guardrails.html +346 -0
  70. package/public/guides/relational-knowledge-ai-recommendations.html +342 -0
  71. package/public/guides/roo-code-alternative-cline.html +339 -0
  72. package/public/guides/semantic-programmatic-seo-guardrails.html +352 -0
  73. package/public/guides/seo-agent-skills-guardrails.html +344 -0
  74. package/public/guides/stop-repeated-ai-agent-mistakes.html +342 -0
  75. package/public/index.html +192 -50
  76. package/public/learn/ac-dc-runtime-enforcement.html +277 -0
  77. package/public/learn/agent-harness-pattern.html +181 -0
  78. package/public/learn/agent-identity-connector-governance.html +146 -0
  79. package/public/learn/agent-swarms-shared-gates.html +173 -0
  80. package/public/learn/agentic-enterprise-context-brain.html +117 -0
  81. package/public/learn/agentic-os-team-governance.html +146 -0
  82. package/public/learn/ai-agent-governance.html +158 -0
  83. package/public/learn/ai-agent-persistent-memory.html +211 -0
  84. package/public/learn/anthropomorphic-claim-gates.html +180 -0
  85. package/public/learn/background-agent-control-layer.html +184 -0
  86. package/public/learn/claude-code-goal-with-rubrics.html +205 -0
  87. package/public/learn/codex-role-plugins-need-governance.html +125 -0
  88. package/public/learn/cost-aware-agent-gate-routing.html +173 -0
  89. package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +157 -0
  90. package/public/learn/deterministic-agent-workflows.html +185 -0
  91. package/public/learn/feedback-loop-vs-decision-layer.html +283 -0
  92. package/public/learn/from-prototype-to-production.html +223 -0
  93. package/public/learn/learn.css +51 -0
  94. package/public/learn/mcp-pre-action-checks-explained.html +172 -0
  95. package/public/learn/pretix-stripe-connect-marketplaces.html +161 -0
  96. package/public/learn/regulated-agent-execution-boundary.html +196 -0
  97. package/public/learn/spec-driven-development.html +168 -0
  98. package/public/learn/stop-ai-agent-force-push.html +134 -0
  99. package/public/learn/vibe-coding-safety-net.html +142 -0
  100. package/public/learn.html +34 -50
  101. package/public/numbers.html +2 -2
  102. package/public/pro.html +6 -6
  103. package/scripts/cli-schema.js +10 -22
  104. package/scripts/dashboard-chat.js +1 -2
  105. package/scripts/document-intake.js +49 -1
  106. package/scripts/gemini-embedding-policy.js +1 -2
  107. package/scripts/hook-stop-anti-claim.js +103 -42
  108. package/scripts/hosted-config.js +12 -0
  109. package/scripts/plausible-domain-config.js +1 -3
  110. package/scripts/reddit-browser-notification-watch.js +230 -0
  111. package/scripts/seo-gsd.js +0 -239
  112. package/scripts/tool-registry.js +2 -2
  113. package/scripts/vector-store.js +0 -44
  114. package/scripts/workspace-evolver.js +2 -62
  115. package/src/api/server.js +126 -335
  116. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  117. 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 = buildFeedbackEntries(windowed, signal);
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
- intent.windowMs
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
- appendFeedbackListLines(lines, { entries, signal, intent });
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 sanitizeHtmlUnsafeJsonValue({
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
- const hiddenInputs = includeHiddenAttribution
2209
- ? buildCheckoutHiddenAttributionInputs(parsed)
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 configuredHref = target.configUrlKey ? hostedConfig[target.configUrlKey] : null;
2475
- const href = target.href || configuredHref || target.fallbackHref;
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 rawProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase();
2699
- const proto = rawProto === 'https' ? 'https' : 'http';
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 = normalizePublicRequestHost(requestHost) || new URL(appOrigin).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 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.
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
- 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));
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 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 });
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
- } catch {
3560
- // SEO directories absent in a stripped bundle — fall back to static entries.
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 = getSafePublicRequestHost(req);
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="sprint_diagnostic" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
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="workflow_sprint" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint</a>`
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('&', '&amp;')
4775
- .replaceAll('<', '&lt;')
4776
- .replaceAll('>', '&gt;')
4777
- .replaceAll('"', '&quot;')
4778
- .replaceAll("'", '&#39;');
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 articlePath = resolvePublicHtmlFile(LEARN_PAGE_PATHS_BY_SLUG, pathname.replace('/learn/', ''));
5876
- if (!articlePath) {
5877
- sendJson(res, 404, { error: 'Article not found' });
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 = getSafePublicRequestHost(req);
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 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);
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 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);
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 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);
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 = getSafePublicRequestHost(req);
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 authRequestToken = createPendingOauthAuthorizeRequest(
6477
- getOauthAuthorizeParamsFromQuery(parsed.searchParams)
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
- <input type="hidden" name="auth_request_token" value="${escapeHtmlAttribute(authRequestToken)}">
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 authorizationParams = getOauthAuthorizeParamsFromForm(form, hostedConfig);
6500
- const redirectUri = authorizationParams.redirectUri;
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: authorizationParams.clientId,
6307
+ clientId: form.get('client_id') || '',
6512
6308
  redirectUri,
6513
- codeChallenge: authorizationParams.codeChallenge,
6514
- codeChallengeMethod: authorizationParams.codeChallengeMethod,
6515
- scope: authorizationParams.scope,
6516
- resource: authorizationParams.resource || buildPublicUrl(hostedConfig, '/mcp'),
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: ${escapeHtml(documentId)}`);
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