thumbgate 1.23.0 → 1.23.2

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 (40) hide show
  1. package/.claude-plugin/marketplace.json +5 -5
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.well-known/llms.txt +26 -11
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +69 -34
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/mcp/server-stdio.js +1 -1
  8. package/adapters/opencode/opencode.json +1 -1
  9. package/bin/cli.js +57 -16
  10. package/bin/postinstall.js +11 -22
  11. package/config/gate-templates.json +72 -0
  12. package/config/github-about.json +1 -1
  13. package/config/post-deploy-marketing-pages.json +10 -0
  14. package/package.json +6 -6
  15. package/public/agent-manager.html +3 -3
  16. package/public/agents-cost-savings.html +3 -3
  17. package/public/ai-malpractice-prevention.html +726 -149
  18. package/public/blog.html +3 -3
  19. package/public/codex-enterprise.html +3 -3
  20. package/public/codex-plugin.html +4 -4
  21. package/public/compare.html +6 -6
  22. package/public/dashboard.html +211 -126
  23. package/public/guide.html +5 -5
  24. package/public/index.html +187 -47
  25. package/public/learn.html +24 -10
  26. package/public/lessons.html +2 -2
  27. package/public/numbers.html +6 -6
  28. package/public/pricing.html +6 -5
  29. package/public/pro.html +23 -0
  30. package/scripts/billing.js +17 -0
  31. package/scripts/commercial-offer.js +75 -0
  32. package/scripts/dashboard.js +53 -1
  33. package/scripts/gates-engine.js +3 -3
  34. package/scripts/plausible-server-events.js +2 -1
  35. package/scripts/rate-limiter.js +16 -12
  36. package/scripts/seo-gsd.js +167 -1
  37. package/scripts/telemetry-analytics.js +310 -0
  38. package/scripts/visitor-journey.js +172 -0
  39. package/src/api/server.js +65 -29
  40. package/adapters/chatgpt/openapi.yaml +0 -1705
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const STAGE_ORDER = [
4
+ 'landing',
5
+ 'pricing',
6
+ 'checkout_viewed',
7
+ 'email_submitted',
8
+ 'stripe_redirect',
9
+ 'purchase',
10
+ ];
11
+
12
+ function parseTimestamp(row) {
13
+ const raw = row && (row.timestamp || row.receivedAt || row.ts);
14
+ const ms = raw ? Date.parse(raw) : NaN;
15
+ return Number.isFinite(ms) ? ms : null;
16
+ }
17
+
18
+ function pickText(...values) {
19
+ for (const value of values) {
20
+ const text = String(value || '').trim();
21
+ if (text) return text;
22
+ }
23
+ return '';
24
+ }
25
+
26
+ function extractSessionId(row) {
27
+ return pickText(
28
+ row.sessionId,
29
+ row.visitorSessionId,
30
+ row.visitor_session_id,
31
+ row.attribution && row.attribution.sessionId,
32
+ row.metadata && row.metadata.sessionId,
33
+ row.stripeSessionId,
34
+ row.traceId,
35
+ row.acquisitionId,
36
+ );
37
+ }
38
+
39
+ function extractVisitorId(row) {
40
+ return pickText(
41
+ row.visitorId,
42
+ row.visitor_id,
43
+ row.installId,
44
+ row.attribution && row.attribution.visitorId,
45
+ row.metadata && row.metadata.visitorId,
46
+ );
47
+ }
48
+
49
+ function extractPath(row) {
50
+ return pickText(
51
+ row.path,
52
+ row.page,
53
+ row.landingPath,
54
+ row.landing_path,
55
+ row.attribution && row.attribution.landingPath,
56
+ );
57
+ }
58
+
59
+ function classifyStage(row) {
60
+ const event = String(row.eventType || row.event || '').toLowerCase();
61
+ const stage = String(row.stage || '').toLowerCase();
62
+ const path = extractPath(row).toLowerCase();
63
+ const cta = String(row.ctaId || row.cta_id || '').toLowerCase();
64
+
65
+ if (/purchase|paid|checkout\.session\.completed/.test(event) || stage === 'purchase') return 'purchase';
66
+ if (/stripe_redirect|stripe redirect|redirect started/.test(event) || stage === 'stripe_redirect') return 'stripe_redirect';
67
+ if (/email_submitted|email submitted|checkout_interstitial_cta_clicked/.test(event)) return 'email_submitted';
68
+ if (/checkout/.test(path) || /checkout.*view|checkout pro viewed/.test(event)) return 'checkout_viewed';
69
+ if (/pricing/.test(path) || /pricing/.test(cta) || /pricing_cta/.test(event)) return 'pricing';
70
+ if (/landing|page_view|landing_page_view|discovery/.test(event) || stage === 'discovery') return 'landing';
71
+ return null;
72
+ }
73
+
74
+ function stageRank(stage) {
75
+ const index = STAGE_ORDER.indexOf(stage);
76
+ return index === -1 ? -1 : index;
77
+ }
78
+
79
+ function nextMissingStage(maxStage) {
80
+ const index = stageRank(maxStage);
81
+ if (index < 0) return 'unknown';
82
+ if (maxStage === 'purchase') return 'converted';
83
+ return STAGE_ORDER[index + 1] || 'unknown';
84
+ }
85
+
86
+ function buildVisitorJourneySummary({ telemetryRows = [], funnelRows = [], limit = 100 } = {}) {
87
+ const sessions = new Map();
88
+ const stageCounts = Object.fromEntries(STAGE_ORDER.map((stage) => [stage, 0]));
89
+
90
+ function ingest(row, source) {
91
+ const ts = parseTimestamp(row);
92
+ if (ts == null) return;
93
+ const sessionId = extractSessionId(row) || `anonymous:${extractVisitorId(row) || ts}`;
94
+ const visitorId = extractVisitorId(row) || null;
95
+ const stage = classifyStage(row);
96
+ const path = extractPath(row);
97
+ const existing = sessions.get(sessionId) || {
98
+ sessionId,
99
+ visitorId,
100
+ firstSeen: new Date(ts).toISOString(),
101
+ lastSeen: new Date(ts).toISOString(),
102
+ maxStage: null,
103
+ dropoffStage: 'unknown',
104
+ events: [],
105
+ paths: [],
106
+ ctas: [],
107
+ sources: [],
108
+ visitorType: row.visitorType || null,
109
+ utm: {},
110
+ referrerHost: pickText(row.referrerHost, row.referrer_host, row.referrer),
111
+ };
112
+
113
+ existing.visitorId = existing.visitorId || visitorId;
114
+ existing.firstSeen = new Date(Math.min(Date.parse(existing.firstSeen), ts)).toISOString();
115
+ existing.lastSeen = new Date(Math.max(Date.parse(existing.lastSeen), ts)).toISOString();
116
+ existing.sources.push(source);
117
+ if (stage) {
118
+ stageCounts[stage] += 1;
119
+ if (stageRank(stage) > stageRank(existing.maxStage)) existing.maxStage = stage;
120
+ }
121
+ if (path && !existing.paths.includes(path)) existing.paths.push(path);
122
+ const cta = pickText(row.ctaId, row.cta_id);
123
+ if (cta && !existing.ctas.includes(cta)) existing.ctas.push(cta);
124
+ for (const key of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) {
125
+ const value = pickText(row[key], row.attribution && row.attribution[key]);
126
+ if (value && !existing.utm[key]) existing.utm[key] = value;
127
+ }
128
+ existing.events.push({
129
+ timestamp: new Date(ts).toISOString(),
130
+ source,
131
+ eventType: pickText(row.eventType, row.event, row.stage, 'unknown'),
132
+ stage,
133
+ path: path || null,
134
+ ctaId: cta || null,
135
+ });
136
+ sessions.set(sessionId, existing);
137
+ }
138
+
139
+ for (const row of telemetryRows) ingest(row, 'telemetry');
140
+ for (const row of funnelRows) ingest(row, 'funnel');
141
+
142
+ const journeys = [...sessions.values()].map((session) => {
143
+ session.sources = [...new Set(session.sources)];
144
+ session.events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
145
+ session.maxStage = session.maxStage || 'unknown';
146
+ session.dropoffStage = nextMissingStage(session.maxStage);
147
+ session.eventCount = session.events.length;
148
+ return session;
149
+ }).sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen));
150
+
151
+ const dropoffCounts = {};
152
+ for (const journey of journeys) {
153
+ dropoffCounts[journey.dropoffStage] = (dropoffCounts[journey.dropoffStage] || 0) + 1;
154
+ }
155
+
156
+ return {
157
+ generatedAt: new Date().toISOString(),
158
+ sessionCount: journeys.length,
159
+ stageCounts,
160
+ dropoffCounts,
161
+ journeys: journeys.slice(0, Math.max(1, Math.min(Number(limit) || 100, 500))),
162
+ truncated: journeys.length > Math.max(1, Math.min(Number(limit) || 100, 500)),
163
+ };
164
+ }
165
+
166
+ module.exports = {
167
+ STAGE_ORDER,
168
+ buildVisitorJourneySummary,
169
+ classifyStage,
170
+ extractSessionId,
171
+ extractVisitorId,
172
+ };
package/src/api/server.js CHANGED
@@ -1532,15 +1532,8 @@ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
1532
1532
  });
1533
1533
  }
1534
1534
 
1535
- function renderCheckoutIntentPage({
1536
- workflowIntakeHref,
1537
- confirmHiddenParams,
1538
- }) {
1539
- const safeWorkflowIntakeHref = escapeHtmlAttribute(workflowIntakeHref);
1540
- const hiddenFields = (confirmHiddenParams || [])
1541
- .map(([k, v]) => `<input type="hidden" name="${escapeHtmlAttribute(k)}" value="${escapeHtmlAttribute(v)}">`)
1542
- .join('');
1543
- return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm — ThumbGate Pro</title><style>body{background:#0a0a0a;color:#eee;font-family:system-ui,-apple-system,sans-serif;line-height:1.5}main{max-width:520px;margin:8vh auto;padding:0 20px}.brand{display:flex;align-items:center;gap:10px;margin-bottom:24px;font-size:14px;color:#94a3b8}.brand-mark{width:24px;height:24px;background:#22d3ee;border-radius:6px;display:inline-block}h1{font-size:24px;margin:0 0 8px;color:#fff}.price{font-size:32px;font-weight:700;color:#22d3ee;margin:8px 0 4px}.price small{font-size:14px;color:#94a3b8;font-weight:400}p{color:#cbd5e1;margin:8px 0}form{margin:0}input[type=email]{width:100%;box-sizing:border-box;padding:14px 16px;border:1px solid #374151;border-radius:8px;background:#111827;color:#fff;font-size:15px;margin:16px 0 0;outline:none}input[type=email]:focus{border-color:#22d3ee}input[type=email]::placeholder{color:#64748b}button.primary{background:#22d3ee;color:#000;padding:16px;text-align:center;border-radius:8px;font-weight:700;font-size:16px;margin:10px 0;border:none;cursor:pointer;width:100%}a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:#cbd5e1;padding:12px;text-align:center;border-radius:8px;margin:8px 0 0;font-size:14px}.trust{margin:24px 0;padding:16px;border:1px solid #1f2937;border-radius:8px;background:#0f172a}.trust-item{font-size:13px;color:#cbd5e1;padding:4px 0;display:flex;gap:8px}.trust-item::before{content:"✓";color:#22d3ee;font-weight:700}.choice-note{font-size:13px;color:#94a3b8;margin-top:14px}.back{text-align:center;color:#64748b;font-size:12px;margin-top:24px}.back a{color:#64748b;display:inline}.email-note{font-size:12px;color:#64748b;margin:4px 0 0}</style><main><div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div><h1>Start ThumbGate Pro</h1><div class="price">$19<small>/mo</small></div><p>The npm package runs your gates locally. <strong>Pro</strong> is what keeps them working across every machine, every agent runtime, and every breaking-change week.</p><form action="/checkout/pro" method="GET" data-i="pro_checkout_confirmed">${hiddenFields}<input type="hidden" name="confirm" value="1"><input type="email" name="customer_email" placeholder="you@company.com" required autocomplete="email"><p class="email-note">Pre-fills your Stripe receipt. We only email if you ask.</p><button type="submit" class="primary">Pay $19/mo with Stripe →</button></form><a class="secondary" data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Not sure yet? Send the workflow first</a><p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages.</p><div class="trust"><div class="trust-item">Lessons synced across all your machines — no local SQLite to babysit</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode — version drift is our problem, not yours</div><div class="trust-item">Hosted dashboard: gate stats, DPO export, org-wide rule library</div><div class="trust-item">24×7 ops on the rule engine — SonarCloud regressions fixed in &lt;24h</div></div><p class="back"><a href="/">← Back to thumbgate.ai</a></p></main><script>document.querySelector('form').addEventListener('submit',e=>{if(navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:'pro_checkout_confirmed',ctaPlacement:'checkout_interstitial',customerEmail:document.querySelector('input[name=customer_email]').value})],{type:'application/json'}))})</script></html>`;
1535
+ function renderCheckoutIntentPage() {
1536
+ return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm — ThumbGate Pro</title><script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.tagged-events.js"></script><script>window.plausible=window.plausible||function(){(window.plausible.q=window.plausible.q||[]).push(arguments)};</script><style>body{background:#0a0a0a;color:#eee;font-family:system-ui,-apple-system,sans-serif;line-height:1.5}main{max-width:520px;margin:8vh auto;padding:0 20px}.brand{display:flex;align-items:center;gap:10px;margin-bottom:24px;font-size:14px;color:#94a3b8}.brand-mark{width:24px;height:24px;background:#22d3ee;border-radius:6px;display:inline-block}h1{font-size:24px;margin:0 0 8px;color:#fff}.price{font-size:32px;font-weight:700;color:#22d3ee;margin:8px 0 4px}.price small{font-size:14px;color:#94a3b8;font-weight:400}p{color:#cbd5e1;margin:8px 0}form{margin:0}input[type=email]{width:100%;box-sizing:border-box;padding:14px 16px;border:1px solid #374151;border-radius:8px;background:#111827;color:#fff;font-size:15px;margin:16px 0 0;outline:none}input[type=email]:focus{border-color:#22d3ee}input[type=email]::placeholder{color:#64748b}button.primary{background:#22d3ee;color:#000;padding:16px;text-align:center;border-radius:8px;font-weight:700;font-size:16px;margin:10px 0;border:none;cursor:pointer;width:100%}a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:#cbd5e1;padding:12px;text-align:center;border-radius:8px;margin:8px 0 0;font-size:14px}.trust{margin:24px 0;padding:16px;border:1px solid #1f2937;border-radius:8px;background:#0f172a}.trust-item{font-size:13px;color:#cbd5e1;padding:4px 0;display:flex;gap:8px}.trust-item::before{content:"✓";color:#22d3ee;font-weight:700}.choice-note{font-size:13px;color:#94a3b8;margin-top:14px}.back{text-align:center;color:#64748b;font-size:12px;margin-top:24px}.back a{color:#64748b;display:inline}.email-note{font-size:12px;color:#64748b;margin:4px 0 0}</style><main><div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div><h1>Start ThumbGate Pro</h1><div class="price">$19<small>/mo</small></div><p>The npm package runs your gates locally. <strong>Pro</strong> is what keeps them working across every machine, every agent runtime, and every breaking-change week.</p><form action="/checkout/pro" method="GET" data-i="pro_checkout_confirmed"><input type="hidden" name="confirm" value="1"><input type="email" name="customer_email" placeholder="you@company.com" required autocomplete="email"><p class="email-note">Pre-fills your Stripe receipt. We only email if you ask.</p><button type="submit" class="primary">Pay $19/mo with Stripe →</button></form><a class="secondary" data-i="workflow_sprint_intake" href="/#workflow-sprint-intake">Not sure yet? Send the workflow first</a><p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages.</p><div class="trust"><div class="trust-item">Lessons synced across all your machines — no local SQLite to babysit</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode — version drift is our problem, not yours</div><div class="trust-item">Hosted dashboard: gate stats, DPO export, org-wide rule library</div><div class="trust-item">24×7 ops on the rule engine — SonarCloud regressions fixed in &lt;24h</div></div><p class="back"><a href="/">← Back to thumbgate.ai</a></p></main><script>document.querySelector('form').addEventListener('submit',e=>{if(navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:'pro_checkout_confirmed',ctaPlacement:'checkout_interstitial',customerEmail:document.querySelector('input[name=customer_email]').value})],{type:'application/json'}));try{window.plausible&&window.plausible('Checkout Pro Email Submitted',{props:{page:'/checkout/pro',source:'interstitial'}})}catch(_){}})</script></html>`;
1544
1537
  }
1545
1538
 
1546
1539
  function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
@@ -2522,6 +2515,16 @@ function renderRobotsTxt(runtimeConfig) {
2522
2515
  'Disallow: /v1/billing/',
2523
2516
  '',
2524
2517
  '# AI crawler access — allow all major LLM crawlers',
2518
+ 'User-agent: OAI-SearchBot',
2519
+ 'Allow: /',
2520
+ 'Disallow: /checkout/',
2521
+ 'Disallow: /v1/billing/',
2522
+ '',
2523
+ 'User-agent: ChatGPT-User',
2524
+ 'Allow: /',
2525
+ 'Disallow: /checkout/',
2526
+ 'Disallow: /v1/billing/',
2527
+ '',
2525
2528
  'User-agent: GPTBot',
2526
2529
  'Allow: /',
2527
2530
  'Disallow: /checkout/',
@@ -2530,9 +2533,18 @@ function renderRobotsTxt(runtimeConfig) {
2530
2533
  'User-agent: ClaudeBot',
2531
2534
  'Allow: /',
2532
2535
  '',
2536
+ 'User-agent: Claude-SearchBot',
2537
+ 'Allow: /',
2538
+ '',
2539
+ 'User-agent: Claude-User',
2540
+ 'Allow: /',
2541
+ '',
2533
2542
  'User-agent: PerplexityBot',
2534
2543
  'Allow: /',
2535
2544
  '',
2545
+ 'User-agent: Perplexity-User',
2546
+ 'Allow: /',
2547
+ '',
2536
2548
  'User-agent: Googlebot',
2537
2549
  'Allow: /',
2538
2550
  '',
@@ -2547,6 +2559,7 @@ function renderRobotsTxt(runtimeConfig) {
2547
2559
  '',
2548
2560
  '# LLM context document — clean declarative content for AI retrieval',
2549
2561
  `# ${runtimeConfig.appOrigin}/llm-context.md`,
2562
+ `# ${runtimeConfig.appOrigin}/llms.txt`,
2550
2563
  '',
2551
2564
  `Sitemap: ${runtimeConfig.appOrigin}/sitemap.xml`,
2552
2565
  ].join('\n');
@@ -2562,6 +2575,15 @@ function renderSitemapXml(runtimeConfig) {
2562
2575
  { path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
2563
2576
  { path: '/agents-cost-savings', changefreq: 'weekly', priority: '0.85' },
2564
2577
  { path: '/ai-malpractice-prevention', changefreq: 'weekly', priority: '0.9' },
2578
+ { path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
2579
+ { path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
2580
+ { path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
2581
+ { path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
2582
+ { path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },
2583
+ { path: '/compare/anthropic-containment', changefreq: 'weekly', priority: '0.85' },
2584
+ { path: '/compare/oak-and-sparrow-gatekeeper', changefreq: 'weekly', priority: '0.85' },
2585
+ { path: '/compare/arcjet', changefreq: 'weekly', priority: '0.85' },
2586
+ { path: '/compare/anthropic-claude-for-legal', changefreq: 'weekly', priority: '0.9' },
2565
2587
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
2566
2588
  ];
2567
2589
  return [
@@ -2833,7 +2855,8 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2833
2855
  </style>
2834
2856
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
2835
2857
  <link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
2836
- <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
2858
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.tagged-events.js"></script>
2859
+ <script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments); };</script>
2837
2860
  </head>
2838
2861
  <body>
2839
2862
  <main>
@@ -3011,6 +3034,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
3011
3034
  }
3012
3035
 
3013
3036
  sendTelemetryOnce('checkout_paid_confirmed');
3037
+ try { window.plausible && window.plausible('Checkout Pro Success Page Confirmed', { props: { sessionId: sessionId || '', traceId: traceId || '', source: 'success_page' } }); } catch (_) {}
3014
3038
  statusEl.textContent = 'ThumbGate Pro activated.';
3015
3039
  const resolvedTraceId = body.traceId || traceId || '';
3016
3040
  const emailStatus = body.trialEmail || {};
@@ -4118,7 +4142,7 @@ function createApiServer() {
4118
4142
  return;
4119
4143
  }
4120
4144
 
4121
- if (isGetLikeRequest && pathname === '/.well-known/llms.txt') {
4145
+ if (isGetLikeRequest && (pathname === '/.well-known/llms.txt' || pathname === '/llms.txt')) {
4122
4146
  const llmsTxtPath = path.join(__dirname, '..', '..', '.well-known', 'llms.txt');
4123
4147
  try {
4124
4148
  const content = fs.readFileSync(llmsTxtPath, 'utf8');
@@ -4849,26 +4873,12 @@ async function addContext(){
4849
4873
  ctaId: analyticsMetadata.ctaId,
4850
4874
  ctaPlacement: analyticsMetadata.ctaPlacement,
4851
4875
  planId: analyticsMetadata.planId,
4876
+ billingCycle: analyticsMetadata.billingCycle,
4877
+ landingPath: analyticsMetadata.landingPath,
4852
4878
  isBot: botClassification.isBot ? 'true' : 'false',
4853
4879
  reason: botClassification.reason,
4854
4880
  }, req.headers, eventType);
4855
- const workflowIntakeHref = buildCheckoutIntentHref(`${hostedConfig.appOrigin}/#workflow-sprint-intake`, analyticsMetadata, {
4856
- utmMedium: 'checkout_interstitial_recovery',
4857
- utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_sprint',
4858
- ctaId: 'checkout_interstitial_workflow_sprint_intake',
4859
- ctaPlacement: 'checkout_interstitial',
4860
- planId: 'team',
4861
- });
4862
- const confirmHiddenParams = [];
4863
- for (const [key, value] of parsed.searchParams.entries()) {
4864
- if (key !== 'confirm' && key !== 'customer_email') {
4865
- confirmHiddenParams.push([key, value]);
4866
- }
4867
- }
4868
- const html = renderCheckoutIntentPage({
4869
- workflowIntakeHref,
4870
- confirmHiddenParams,
4871
- });
4881
+ const html = renderCheckoutIntentPage();
4872
4882
  sendHtml(res, 200, html, responseHeaders);
4873
4883
  return;
4874
4884
  }
@@ -5597,6 +5607,7 @@ async function addContext(){
5597
5607
  source,
5598
5608
  telemetry: { rows: [], truncated: false, totalAfterSince: 0 },
5599
5609
  funnel: { rows: [], truncated: false, totalAfterSince: 0 },
5610
+ journeySummary: null,
5600
5611
  };
5601
5612
 
5602
5613
  function readJsonlSince(p) {
@@ -5644,6 +5655,19 @@ async function addContext(){
5644
5655
  result.funnel.truncated = all.length > limit;
5645
5656
  }
5646
5657
 
5658
+ try {
5659
+ const { buildVisitorJourneySummary } = require('../../scripts/visitor-journey');
5660
+ result.journeySummary = buildVisitorJourneySummary({
5661
+ telemetryRows: wantTelemetry ? result.telemetry.rows : [],
5662
+ funnelRows: wantFunnel ? result.funnel.rows : [],
5663
+ limit: Math.min(limit, 500),
5664
+ });
5665
+ } catch (err) {
5666
+ result.journeySummary = {
5667
+ error: err && err.message ? err.message : 'journey_summary_unavailable',
5668
+ };
5669
+ }
5670
+
5647
5671
  sendJson(res, 200, result);
5648
5672
  return;
5649
5673
  }
@@ -6059,7 +6083,19 @@ async function addContext(){
6059
6083
 
6060
6084
  // Public OpenAPI spec — no auth required (needed for ChatGPT GPT Store import)
6061
6085
  if (isGetLikeRequest && (pathname === '/openapi.json' || pathname === '/openapi.yaml')) {
6062
- const specPath = path.join(__dirname, '../../adapters/chatgpt/openapi.yaml');
6086
+ const specPath = [
6087
+ path.join(__dirname, '../../openapi/openapi.yaml'),
6088
+ path.join(__dirname, '../../adapters/chatgpt/openapi.yaml'),
6089
+ ].find((candidate) => fs.existsSync(candidate));
6090
+ if (!specPath) {
6091
+ sendProblem(res, {
6092
+ type: PROBLEM_TYPES.NOT_FOUND,
6093
+ title: 'Not Found',
6094
+ status: 404,
6095
+ detail: 'OpenAPI spec not found.',
6096
+ });
6097
+ return;
6098
+ }
6063
6099
  try {
6064
6100
  const yaml = renderOpenApiYamlForRequest(fs.readFileSync(specPath, 'utf8'), req);
6065
6101
  if (pathname === '/openapi.yaml') {