thumbgate 1.16.13 → 1.16.20

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 (64) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +3 -1
  5. package/adapters/claude/.mcp.json +2 -2
  6. package/adapters/mcp/server-stdio.js +26 -1
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bin/cli.js +420 -1
  9. package/bin/postinstall.js +2 -2
  10. package/config/gate-templates.json +372 -0
  11. package/config/mcp-allowlists.json +25 -0
  12. package/config/model-candidates.json +59 -2
  13. package/config/model-tiers.json +4 -1
  14. package/package.json +79 -22
  15. package/public/compare.html +6 -0
  16. package/public/index.html +153 -20
  17. package/public/numbers.html +6 -6
  18. package/public/pro.html +25 -27
  19. package/scripts/agent-design-governance.js +211 -0
  20. package/scripts/agent-reasoning-traces.js +683 -0
  21. package/scripts/agent-reward-model.js +438 -0
  22. package/scripts/agent-stack-survival-audit.js +231 -0
  23. package/scripts/ai-engineering-stack-guardrails.js +256 -0
  24. package/scripts/billing.js +33 -5
  25. package/scripts/chatgpt-ads-readiness-pack.js +195 -0
  26. package/scripts/cli-schema.js +277 -0
  27. package/scripts/code-graph-guardrails.js +176 -0
  28. package/scripts/commercial-offer.js +1 -1
  29. package/scripts/deepseek-v4-runtime-guardrails.js +253 -0
  30. package/scripts/gemini-embedding-policy.js +198 -0
  31. package/scripts/inference-cache-policy.js +39 -0
  32. package/scripts/judge-reward-function.js +396 -0
  33. package/scripts/llm-behavior-monitor.js +251 -0
  34. package/scripts/long-running-agent-context-guardrails.js +176 -0
  35. package/scripts/multimodal-retrieval-plan.js +31 -11
  36. package/scripts/oss-pr-opportunity-scout.js +240 -0
  37. package/scripts/proactive-agent-eval-guardrails.js +230 -0
  38. package/scripts/profile-router.js +5 -4
  39. package/scripts/prompting-operating-system.js +273 -0
  40. package/scripts/proxy-pointer-rag-guardrails.js +189 -0
  41. package/scripts/rag-precision-guardrails.js +202 -0
  42. package/scripts/rate-limiter.js +1 -1
  43. package/scripts/reasoning-efficiency-guardrails.js +176 -0
  44. package/scripts/reward-hacking-guardrails.js +251 -0
  45. package/scripts/seo-gsd.js +1201 -11
  46. package/scripts/single-use-credential-gate.js +182 -0
  47. package/scripts/structured-prompt-driven.js +226 -0
  48. package/scripts/telemetry-analytics.js +108 -6
  49. package/scripts/tool-registry.js +92 -0
  50. package/scripts/upstream-contribution-engine.js +379 -0
  51. package/scripts/vector-store.js +119 -4
  52. package/src/api/server.js +455 -143
  53. package/scripts/agents-sdk-sandbox-plan.js +0 -57
  54. package/scripts/ai-org-governance.js +0 -98
  55. package/scripts/artifact-agent-plan.js +0 -81
  56. package/scripts/enterprise-agent-rollout.js +0 -34
  57. package/scripts/experience-replay-governance.js +0 -69
  58. package/scripts/inference-economics.js +0 -53
  59. package/scripts/knowledge-layer-plan.js +0 -108
  60. package/scripts/memory-store-governance.js +0 -60
  61. package/scripts/post-training-governance.js +0 -34
  62. package/scripts/production-agent-readiness.js +0 -40
  63. package/scripts/scaling-law-claims.js +0 -60
  64. package/scripts/student-consistent-training.js +0 -73
package/src/api/server.js CHANGED
@@ -112,6 +112,7 @@ const {
112
112
  getBillingSummaryLive,
113
113
  } = require('../../scripts/billing');
114
114
  const {
115
+ DEFAULT_PUBLIC_APP_ORIGIN,
115
116
  resolveHostedBillingConfig,
116
117
  createTraceId,
117
118
  buildHostedSuccessUrl,
@@ -462,6 +463,12 @@ const TRACKED_LINK_TARGETS = Object.freeze({
462
463
  // Stripe event tracking helpers
463
464
  // ---------------------------------------------------------------------------
464
465
  const STRIPE_EVENTS_PATH = path.resolve(__dirname, '../../.thumbgate/stripe-events.jsonl');
466
+ const LEGACY_STRIPE_EVENT_TYPES = new Set([
467
+ 'checkout.session.completed',
468
+ 'customer.subscription.created',
469
+ 'customer.subscription.updated',
470
+ 'customer.subscription.deleted',
471
+ ]);
465
472
 
466
473
  function ensureStripeEventsDir() {
467
474
  const dir = path.dirname(STRIPE_EVENTS_PATH);
@@ -473,6 +480,81 @@ function appendStripeEvent(record) {
473
480
  fs.appendFileSync(STRIPE_EVENTS_PATH, JSON.stringify(record) + '\n', 'utf8');
474
481
  }
475
482
 
483
+ function buildLegacyStripeEventRecord(event) {
484
+ const obj = event.data && event.data.object ? event.data.object : {};
485
+ return {
486
+ timestamp: new Date().toISOString(),
487
+ event_type: event.type,
488
+ event_id: event.id || null,
489
+ customer_email:
490
+ obj.customer_email ||
491
+ obj.email ||
492
+ (obj.customer_details && obj.customer_details.email) ||
493
+ null,
494
+ plan:
495
+ obj.plan
496
+ ? (obj.plan.nickname || obj.plan.id || null)
497
+ : (
498
+ obj.items &&
499
+ obj.items.data &&
500
+ obj.items.data[0] &&
501
+ obj.items.data[0].plan
502
+ ? (obj.items.data[0].plan.nickname || obj.items.data[0].plan.id)
503
+ : null
504
+ ),
505
+ amount_cents: obj.amount_total || (obj.plan && obj.plan.amount) || null,
506
+ currency: obj.currency || null,
507
+ subscription_id: obj.subscription || obj.id || null,
508
+ };
509
+ }
510
+
511
+ async function handleLegacyStripeWebhook(req, res) {
512
+ try {
513
+ const rawBody = await new Promise((resolve, reject) => {
514
+ const chunks = [];
515
+ req.on('data', (c) => chunks.push(c));
516
+ req.on('end', () => resolve(Buffer.concat(chunks)));
517
+ req.on('error', reject);
518
+ });
519
+
520
+ const sig = req.headers['stripe-signature'] || '';
521
+ if (!verifyWebhookSignature(rawBody, sig)) {
522
+ sendProblem(res, {
523
+ type: PROBLEM_TYPES.WEBHOOK_INVALID,
524
+ title: 'Invalid webhook signature',
525
+ status: 400,
526
+ detail: 'The webhook signature could not be verified.',
527
+ });
528
+ return;
529
+ }
530
+
531
+ let event;
532
+ try {
533
+ event = JSON.parse(rawBody.toString('utf-8'));
534
+ } catch {
535
+ sendProblem(res, {
536
+ type: PROBLEM_TYPES.INVALID_JSON,
537
+ title: 'Invalid JSON',
538
+ status: 400,
539
+ detail: 'Invalid JSON in webhook body.',
540
+ });
541
+ return;
542
+ }
543
+
544
+ if (LEGACY_STRIPE_EVENT_TYPES.has(event.type)) {
545
+ appendStripeEvent(buildLegacyStripeEventRecord(event));
546
+ }
547
+ sendJson(res, 200, { received: true, event_type: event.type });
548
+ } catch (err) {
549
+ sendProblem(res, {
550
+ type: PROBLEM_TYPES.INTERNAL,
551
+ title: 'Internal Server Error',
552
+ status: 500,
553
+ detail: err.message,
554
+ });
555
+ }
556
+ }
557
+
476
558
  function readStripeEvents() {
477
559
  ensureStripeEventsDir();
478
560
  if (!fs.existsSync(STRIPE_EVENTS_PATH)) return [];
@@ -688,6 +770,76 @@ function getMcpSkillManifests(hostedConfig) {
688
770
  footprintUrl: buildPublicUrl(hostedConfig, '/.well-known/mcp/footprint.json'),
689
771
  proofUrl: VERIFICATION_EVIDENCE_URL,
690
772
  },
773
+ {
774
+ name: 'agent-design-governance',
775
+ title: 'Agent Design Governance',
776
+ description: 'Decide when to stay single-agent, when to split into manager or handoff patterns, and which eval/tool safeguards are required first.',
777
+ triggers: ['agent architecture', 'multi-agent', 'tool overload', 'agent evals', 'agent instructions'],
778
+ recommendedFlow: [
779
+ 'Start with a single agent plus clear tools and instructions.',
780
+ 'Split only when instruction complexity or tool overload is measured.',
781
+ 'Require baseline evals before adding autonomy or subagents.',
782
+ 'Classify tool risk before allowing writes, money movement, production changes, or outbound actions.',
783
+ ],
784
+ contextUrl: buildPublicUrl(hostedConfig, '/public/llm-context.md'),
785
+ proofUrl: VERIFICATION_EVIDENCE_URL,
786
+ },
787
+ {
788
+ name: 'proactive-agent-eval-guardrails',
789
+ title: 'Proactive Agent Eval Guardrails',
790
+ description: 'Require state-machine modeling, active user simulation, goal inference, intervention timing, and multi-app orchestration proof before proactive agents write or interrupt users.',
791
+ triggers: ['proactive agents', 'PARE', 'active user simulation', 'intervention timing', 'multi-app orchestration'],
792
+ recommendedFlow: [
793
+ 'Model each app as states, actions, and valid transitions.',
794
+ 'Simulate active users before enabling anticipatory interventions.',
795
+ 'Grade goal inference separately from intervention timing.',
796
+ 'Block multi-app proactive writes until rollback and orchestration evidence exists.',
797
+ ],
798
+ contextUrl: buildPublicUrl(hostedConfig, '/public/llm-context.md'),
799
+ proofUrl: VERIFICATION_EVIDENCE_URL,
800
+ },
801
+ {
802
+ name: 'reward-hacking-guardrails',
803
+ title: 'Reward Hacking Guardrails',
804
+ description: 'Catch proxy-optimization failures such as unsupported completion claims, sycophancy, verbosity-as-proof, benchmark overfitting, and evaluator manipulation.',
805
+ triggers: ['reward hacking', 'benchmark overfitting', 'unsupported claims', 'sycophancy', 'verifier theater'],
806
+ recommendedFlow: [
807
+ 'Inspect candidate claims for completion, safety, test, or deployment language.',
808
+ 'Require proof artifacts before accepting done, fixed, safe, or ready-to-merge claims.',
809
+ 'Map every proxy metric to the real user objective.',
810
+ 'Require holdout or regression proof before treating benchmark gains as product gains.',
811
+ ],
812
+ contextUrl: buildPublicUrl(hostedConfig, '/public/llm-context.md'),
813
+ proofUrl: VERIFICATION_EVIDENCE_URL,
814
+ },
815
+ {
816
+ name: 'oss-pr-opportunity-scout',
817
+ title: 'OSS PR Opportunity Scout',
818
+ description: 'Find upstream GitHub repositories ThumbGate actually depends on, then rank issue, bounty, and proof-backed PR opportunities without spam.',
819
+ triggers: ['GitHub issues', 'bug bounty', 'upstream PR', 'open source promotion', 'maintainer outreach'],
820
+ recommendedFlow: [
821
+ 'Map package dependencies to upstream repositories.',
822
+ 'Search only maintainer-visible issues, help-wanted labels, regressions, and bounty surfaces.',
823
+ 'Reproduce locally before claiming a fix.',
824
+ 'Open one focused PR with tests, proof, and transparent ThumbGate context only when relevant.',
825
+ ],
826
+ contextUrl: buildPublicUrl(hostedConfig, '/public/llm-context.md'),
827
+ proofUrl: VERIFICATION_EVIDENCE_URL,
828
+ },
829
+ {
830
+ name: 'chatgpt-ads-readiness-pack',
831
+ title: 'ChatGPT Ads Readiness Pack',
832
+ description: 'Prepare ThumbGate intent clusters, proof-backed copy, landing routes, and measurement before ChatGPT Ads Manager becomes broadly self-serve.',
833
+ triggers: ['ChatGPT ads', 'AI ads', 'paid AI search', 'Ads Manager', 'agent governance advertising'],
834
+ recommendedFlow: [
835
+ 'Submit advertiser interest when eligible.',
836
+ 'Cluster high-intent conversational queries around agent governance and repeated workflow failures.',
837
+ 'Route self-serve intent to the guide and team pain to Workflow Hardening Sprint intake.',
838
+ 'Block unsupported ad and landing-page claims before spend scales.',
839
+ ],
840
+ contextUrl: buildPublicUrl(hostedConfig, '/public/llm-context.md'),
841
+ proofUrl: VERIFICATION_EVIDENCE_URL,
842
+ },
691
843
  ];
692
844
  }
693
845
 
@@ -788,6 +940,31 @@ function getMcpDiscoveryManifest(hostedConfig) {
788
940
  description: 'Measure MCP schema and feedback-context footprint before loading large manifests into model context.',
789
941
  tools: ['plan_context_footprint', 'construct_context_pack', 'context_provenance'],
790
942
  },
943
+ {
944
+ name: 'agent-design-governance',
945
+ description: 'Evaluate agent architecture, instruction quality, tool risk, and baseline eval readiness before adding subagents or autonomy.',
946
+ tools: ['plan_agent_design_governance', 'search_lessons', 'diagnose_failure', 'require_evidence_for_claim'],
947
+ },
948
+ {
949
+ name: 'proactive-agent-eval-guardrails',
950
+ description: 'Evaluate proactive-agent state modeling, active-user simulation, goal inference, timing, and multi-app write readiness.',
951
+ tools: ['plan_proactive_agent_eval_guardrails', 'require_evidence_for_claim', 'workflow_sentinel'],
952
+ },
953
+ {
954
+ name: 'reward-hacking-guardrails',
955
+ description: 'Detect proxy-optimization failures before accepting completion claims, benchmark wins, verifier approvals, or multimodal assertions.',
956
+ tools: ['plan_reward_hacking_guardrails', 'require_evidence_for_claim', 'verify_claim'],
957
+ },
958
+ {
959
+ name: 'oss-pr-opportunity-scout',
960
+ description: 'Rank upstream repositories for proof-backed issue fixes and PR opportunities using ThumbGate dependency evidence.',
961
+ tools: ['plan_oss_pr_opportunity_scout', 'require_evidence_for_claim', 'track_action'],
962
+ },
963
+ {
964
+ name: 'chatgpt-ads-readiness-pack',
965
+ description: 'Prepare AI-ads intent clusters, copy, proof links, and measurement gates for ThumbGate campaigns.',
966
+ tools: ['plan_chatgpt_ads_readiness', 'require_evidence_for_claim', 'get_business_metrics'],
967
+ },
791
968
  ],
792
969
  skills: getMcpSkillManifests(hostedConfig),
793
970
  applications: getMcpApplications(hostedConfig),
@@ -1276,6 +1453,40 @@ function buildCheckoutFallbackUrl(baseUrl, metadata = {}) {
1276
1453
  return restoreStripeCheckoutPlaceholder(url.toString());
1277
1454
  }
1278
1455
 
1456
+ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
1457
+ return buildCheckoutFallbackUrl(baseUrl, {
1458
+ ...metadata,
1459
+ ...overrides,
1460
+ });
1461
+ }
1462
+
1463
+ function renderCheckoutIntentPage({
1464
+ confirmHref,
1465
+ workflowIntakeHref,
1466
+ teamOptionsHref,
1467
+ diagnosticCheckoutHref,
1468
+ sprintCheckoutHref,
1469
+ sprintDiagnosticPriceDollars = 499,
1470
+ workflowSprintPriceDollars = 1500,
1471
+ }) {
1472
+ const safeConfirmHref = escapeHtmlAttribute(confirmHref);
1473
+ const safeWorkflowIntakeHref = escapeHtmlAttribute(workflowIntakeHref);
1474
+ const safeTeamOptionsHref = escapeHtmlAttribute(teamOptionsHref);
1475
+ const safeDiagnosticCheckoutHref = diagnosticCheckoutHref
1476
+ ? escapeHtmlAttribute(diagnosticCheckoutHref)
1477
+ : '';
1478
+ const safeSprintCheckoutHref = sprintCheckoutHref
1479
+ ? escapeHtmlAttribute(sprintCheckoutHref)
1480
+ : '';
1481
+ const diagnosticAction = safeDiagnosticCheckoutHref
1482
+ ? `<a data-i="sprint_diagnostic_checkout" href="${safeDiagnosticCheckoutHref}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
1483
+ : '';
1484
+ const sprintAction = safeSprintCheckoutHref
1485
+ ? `<a data-i="workflow_sprint_checkout" href="${safeSprintCheckoutHref}">Start $${workflowSprintPriceDollars} sprint</a>`
1486
+ : '';
1487
+ return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{background:#0a0a0a;color:#eee;font-family:system-ui,sans-serif}div{max-width:560px;margin:12vh auto}a{display:block;margin:10px 0;padding:12px;border:1px solid #374151;color:inherit;text-align:center}.primary{background:#22d3ee;color:#000}</style><div><h1>Choose the right paid path.</h1><p>Pick Pro, diagnostic, sprint, or intake.</p><a class="primary" data-i="pro_checkout_confirmed" href="${safeConfirmHref}">Continue to Stripe</a>${diagnosticAction}${sprintAction}<a data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Send workflow first</a><a data-i="team_paid_path" href="${safeTeamOptionsHref}">See diagnostic and sprint options</a><p>Stripe checkout.</p><a href="/">Back</a></div><script>addEventListener('click',e=>{let a=e.target.closest('[data-i]');if(a&&navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:a.dataset.i,ctaPlacement:'checkout_interstitial'})],{type:'application/json'}))})</script>`;
1488
+ }
1489
+
1279
1490
  function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
1280
1491
  const params = parsed.searchParams;
1281
1492
  const traceId = pickFirstText(params.get('trace_id')) || createJourneyId('checkout');
@@ -1318,6 +1529,36 @@ function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneySt
1318
1529
  };
1319
1530
  }
1320
1531
 
1532
+ function buildCheckoutConfirmHref(parsed) {
1533
+ const confirmUrl = new URL('/checkout/pro', 'https://thumbgate.invalid');
1534
+ confirmUrl.searchParams.set('confirm', '1');
1535
+ for (const [key, value] of parsed.searchParams.entries()) {
1536
+ if (key === 'confirm') continue;
1537
+ confirmUrl.searchParams.append(key, value);
1538
+ }
1539
+ return `${confirmUrl.pathname}${confirmUrl.search}`;
1540
+ }
1541
+
1542
+ function normalizeCheckoutCustomerEmail(value) {
1543
+ const email = (normalizeNullableText(value) || '').toLowerCase();
1544
+ const atIndex = email.indexOf('@');
1545
+ const domain = email.slice(atIndex + 1);
1546
+ if (!email || email.length > 254 || atIndex <= 0 || atIndex !== email.lastIndexOf('@') || !domain || !domain.includes('.') || domain.startsWith('.') || domain.endsWith('.') || domain.includes('..')) return null;
1547
+ for (const ch of email) if (ch <= ' ' || ch === '<' || ch === '>' || ch === '"') return null;
1548
+ return email;
1549
+ }
1550
+
1551
+ function renderCheckoutIntentGate(parsed, responseHeaders = {}) {
1552
+ let hiddenInputs = '';
1553
+ for (const [key, value] of parsed.searchParams.entries()) {
1554
+ if (key !== 'confirm' && key !== 'customer_email') hiddenInputs += `<input type=hidden name=${escapeHtmlAttribute(key)} value=${escapeHtmlAttribute(value)}>`;
1555
+ }
1556
+ return {
1557
+ 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>`,
1558
+ headers: responseHeaders,
1559
+ };
1560
+ }
1561
+
1321
1562
  function normalizeTrackedLinkSlug(value) {
1322
1563
  return String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
1323
1564
  }
@@ -1559,9 +1800,7 @@ function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
1559
1800
  evidence: [err && err.message ? err.message : 'unknown_error'],
1560
1801
  },
1561
1802
  });
1562
- } catch (_) {
1563
- // Public telemetry remains best-effort even when diagnostics fail.
1564
- }
1803
+ } catch (_) {}
1565
1804
  return false;
1566
1805
  }
1567
1806
  }
@@ -1625,6 +1864,34 @@ function escapeHtmlAttribute(value) {
1625
1864
  .replaceAll('>', '&gt;');
1626
1865
  }
1627
1866
 
1867
+ function stripTrailingSlashes(value) {
1868
+ const input = String(value || '');
1869
+ let end = input.length;
1870
+ while (end > 0 && input[end - 1] === '/') end -= 1;
1871
+ return input.slice(0, end);
1872
+ }
1873
+
1874
+ function normalizePublicMarketingHtml(html, runtimeConfig) {
1875
+ const appOrigin = runtimeConfig?.appOrigin
1876
+ ? stripTrailingSlashes(runtimeConfig.appOrigin)
1877
+ : '';
1878
+ if (!appOrigin) return html;
1879
+
1880
+ let output = String(html);
1881
+ output = output.replaceAll(DEFAULT_PUBLIC_APP_ORIGIN, appOrigin);
1882
+ try {
1883
+ const host = new URL(appOrigin).host;
1884
+ output = output.replaceAll(
1885
+ 'data-domain="thumbgate-production.up.railway.app"',
1886
+ `data-domain="${escapeHtmlAttribute(host)}"`
1887
+ );
1888
+ } catch {
1889
+ // appOrigin is normalized by hosted-config; leave static analytics domains
1890
+ // untouched if a future caller deliberately supplies a non-URL value.
1891
+ }
1892
+ return output;
1893
+ }
1894
+
1628
1895
  function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContext = {}) {
1629
1896
  const template = fs.readFileSync(templatePath, 'utf-8');
1630
1897
  const googleSiteVerificationMeta = runtimeConfig.googleSiteVerification
@@ -1637,11 +1904,11 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
1637
1904
  ' window.dataLayer = window.dataLayer || [];',
1638
1905
  ' function gtag(){dataLayer.push(arguments);}',
1639
1906
  " gtag('js', new Date());",
1640
- ` gtag('config', '${runtimeConfig.gaMeasurementId}', { send_page_view: false });`,
1907
+ ` gtag('config', '${runtimeConfig.gaMeasurementId}');`,
1641
1908
  ' </script>',
1642
1909
  ].join('\n')
1643
1910
  : '';
1644
- return fillTemplate(template, {
1911
+ return normalizePublicMarketingHtml(fillTemplate(template, {
1645
1912
  '__PACKAGE_VERSION__': pkg.version,
1646
1913
  '__APP_ORIGIN__': runtimeConfig.appOrigin,
1647
1914
  '__CHECKOUT_ENDPOINT__': runtimeConfig.checkoutEndpoint,
@@ -1665,7 +1932,7 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
1665
1932
  '__GTM_PLAN_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/GO_TO_MARKET_REVENUE_WEDGE_2026-03.md',
1666
1933
  '__GITHUB_URL__': 'https://github.com/IgorGanapolsky/ThumbGate',
1667
1934
  '__POSTHOG_API_KEY__': runtimeConfig.posthogApiKey || '',
1668
- });
1935
+ }), runtimeConfig);
1669
1936
  }
1670
1937
 
1671
1938
  function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
@@ -1840,7 +2107,6 @@ a{color:#22d3ee;text-decoration:none}</style></head><body>
1840
2107
  const timestamp = merged.timestamp ? new Date(merged.timestamp).toLocaleString() : '';
1841
2108
  const isoTimestamp = merged.timestamp || '';
1842
2109
 
1843
- // Technical metadata
1844
2110
  const failureType = merged.failureType || null;
1845
2111
  const skill = merged.skill || null;
1846
2112
  const source = merged.source || fb.source || null;
@@ -1851,19 +2117,12 @@ a{color:#22d3ee;text-decoration:none}</style></head><body>
1851
2117
  const guardrails = merged.guardrails || null;
1852
2118
  const rubricScores = merged.rubricScores || null;
1853
2119
 
1854
- // Structured rule
1855
2120
  const rule = merged.structuredRule || merged.rule || null;
1856
- // Conversation window
1857
2121
  const convoWindow = merged.conversationWindow || merged.chatHistory || [];
1858
- // Reflector analysis
1859
2122
  const reflector = merged.reflectorAnalysis || merged.reflector || null;
1860
- // Diagnosis
1861
2123
  const diagnosis = merged.diagnosis || null;
1862
- // Rubric
1863
2124
  const rubric = merged.rubricEvaluation || merged.rubric || null;
1864
- // Synthesis
1865
2125
  const synthesis = merged.synthesis || null;
1866
- // Bayesian
1867
2126
  const bayesian = merged.bayesianBelief || merged.bayesian || null;
1868
2127
 
1869
2128
  function sectionCard(titleText, content, id) {
@@ -2208,9 +2467,7 @@ function renderRobotsTxt(runtimeConfig) {
2208
2467
  function renderSitemapXml(runtimeConfig) {
2209
2468
  const entries = [
2210
2469
  { path: '/', changefreq: 'weekly', priority: '1.0' },
2211
- // /pro consolidated into /#pro-pitch (2026-04-16) — removed from sitemap
2212
- // so search engines don't chase the 301 instead of indexing the canonical
2213
- // homepage directly.
2470
+ { path: '/pro', changefreq: 'weekly', priority: '0.9' },
2214
2471
  { path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
2215
2472
  { path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
2216
2473
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
@@ -2234,6 +2491,21 @@ function renderSitemapXml(runtimeConfig) {
2234
2491
  ].join('\n');
2235
2492
  }
2236
2493
 
2494
+ function buildHostedRuntimePresence(hostedConfig, { expectedApiKey, expectedOperatorKey } = {}) {
2495
+ return {
2496
+ THUMBGATE_FEEDBACK_DIR: Boolean(process.env.THUMBGATE_FEEDBACK_DIR),
2497
+ THUMBGATE_OPERATOR_KEY: Boolean(expectedOperatorKey),
2498
+ THUMBGATE_API_KEY: Boolean(expectedApiKey),
2499
+ THUMBGATE_PUBLIC_APP_ORIGIN: Boolean(hostedConfig.appOrigin),
2500
+ THUMBGATE_BILLING_API_BASE_URL: Boolean(hostedConfig.billingApiBaseUrl),
2501
+ THUMBGATE_GA_MEASUREMENT_ID: Boolean(hostedConfig.gaMeasurementId),
2502
+ THUMBGATE_CHECKOUT_FALLBACK_URL: Boolean(hostedConfig.checkoutFallbackUrl),
2503
+ THUMBGATE_SPRINT_DIAGNOSTIC_CHECKOUT_URL: Boolean(hostedConfig.sprintDiagnosticCheckoutUrl),
2504
+ THUMBGATE_WORKFLOW_SPRINT_CHECKOUT_URL: Boolean(hostedConfig.workflowSprintCheckoutUrl),
2505
+ STRIPE_SECRET_KEY: Boolean(process.env.STRIPE_SECRET_KEY),
2506
+ };
2507
+ }
2508
+
2237
2509
  function isSeoAttributionSource(source) {
2238
2510
  return source === 'organic_search' || source === 'ai_search';
2239
2511
  }
@@ -2273,14 +2545,6 @@ function servePublicMarketingPage({
2273
2545
  'landing_page_view'
2274
2546
  );
2275
2547
 
2276
- // Funnel-ledger write (2026-04-21): populate funnel-events.jsonl with a
2277
- // discovery-stage event on every landing-page view so UTM-tagged social
2278
- // traffic becomes visible in `npm run feedback:summary` and
2279
- // `bin/cli.js cfo --today`. Prior to this wire, landing views wrote only
2280
- // to telemetry-pings.jsonl (invisible to the CEO-facing revenue surface),
2281
- // leaving funnel-events.jsonl empty despite 404 published Zernio posts.
2282
- // Best-effort: wrapped in try/catch so a billing-ledger hiccup never
2283
- // breaks a page render.
2284
2548
  try {
2285
2549
  appendFunnelEvent({
2286
2550
  stage: 'discovery',
@@ -2699,6 +2963,33 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2699
2963
  }
2700
2964
 
2701
2965
  function renderCheckoutCancelledPage(runtimeConfig) {
2966
+ const diagnosticCheckoutUrl = runtimeConfig.sprintDiagnosticCheckoutUrl
2967
+ ? escapeHtmlAttribute(runtimeConfig.sprintDiagnosticCheckoutUrl)
2968
+ : '';
2969
+ const workflowSprintCheckoutUrl = runtimeConfig.workflowSprintCheckoutUrl
2970
+ ? escapeHtmlAttribute(runtimeConfig.workflowSprintCheckoutUrl)
2971
+ : '';
2972
+ const sprintDiagnosticPriceDollars = runtimeConfig.sprintDiagnosticPriceDollars || 499;
2973
+ const workflowSprintPriceDollars = runtimeConfig.workflowSprintPriceDollars || 1500;
2974
+ const workflowSprintIntakeUrl = `${escapeHtmlAttribute(runtimeConfig.appOrigin)}/#workflow-sprint-intake`;
2975
+ const recoveryOfferLinks = [
2976
+ `<a id="send-workflow-first" href="${workflowSprintIntakeUrl}" data-recovery-offer="workflow_sprint_intake" data-offer-price="0">Send workflow first</a>`,
2977
+ diagnosticCheckoutUrl
2978
+ ? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
2979
+ : '',
2980
+ workflowSprintCheckoutUrl
2981
+ ? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="workflow_sprint" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint</a>`
2982
+ : '',
2983
+ ].filter(Boolean).join('\n ');
2984
+ const recoveryOfferCard = recoveryOfferLinks
2985
+ ? `<div class="card recovery-card">
2986
+ <h2>Need help deciding?</h2>
2987
+ <p>If Pro is not the right next step, send the workflow first. We can qualify the blocker, confirm the proof plan, and route you to the diagnostic or sprint only when the scope is real.</p>
2988
+ <div class="actions">
2989
+ ${recoveryOfferLinks}
2990
+ </div>
2991
+ </div>`
2992
+ : '';
2702
2993
  return `<!DOCTYPE html>
2703
2994
  <html lang="en">
2704
2995
  <head>
@@ -2784,6 +3075,9 @@ function renderCheckoutCancelledPage(runtimeConfig) {
2784
3075
  gap: 12px;
2785
3076
  margin-top: 18px;
2786
3077
  }
3078
+ .recovery-card {
3079
+ border-color: rgba(184, 92, 45, 0.38);
3080
+ }
2787
3081
  .note {
2788
3082
  font-size: 14px;
2789
3083
  margin-top: 12px;
@@ -2812,17 +3106,19 @@ function renderCheckoutCancelledPage(runtimeConfig) {
2812
3106
  </div>
2813
3107
  <div class="actions">
2814
3108
  <button type="button" id="submit-reason">Send feedback</button>
2815
- <a id="retry-checkout" href="/checkout/pro" class="secondary">Try checkout again</a>
3109
+ <a id="retry-checkout" href="/checkout/pro" class="secondary" data-recovery-offer="pro_trial_retry" data-offer-price="19">Restart $19 Pro trial</a>
2816
3110
  <a href="${runtimeConfig.appOrigin}" class="secondary">Return to Context Gateway</a>
2817
3111
  </div>
2818
3112
  <p class="note" id="status">No feedback sent yet.</p>
2819
3113
  </div>
3114
+ ${recoveryOfferCard}
2820
3115
  <script>
2821
3116
  (function () {
2822
3117
  const params = new URLSearchParams(window.location.search);
2823
3118
  const statusEl = document.getElementById('status');
2824
3119
  const noteEl = document.getElementById('buyer-note');
2825
3120
  const retryLink = document.getElementById('retry-checkout');
3121
+ const workflowIntakeLink = document.getElementById('send-workflow-first');
2826
3122
  let selectedReason = null;
2827
3123
 
2828
3124
  function sendTelemetry(eventType, extra) {
@@ -2875,6 +3171,19 @@ function renderCheckoutCancelledPage(runtimeConfig) {
2875
3171
  });
2876
3172
  retryLink.href = retryUrl.toString();
2877
3173
 
3174
+ if (workflowIntakeLink) {
3175
+ const intakeUrl = new URL(workflowIntakeLink.href, window.location.origin);
3176
+ ['trace_id', 'acquisition_id', 'visitor_id', 'session_id', 'visitor_session_id', 'install_id', 'utm_source', 'utm_campaign', 'utm_content', 'utm_term', 'creator', 'community', 'post_id', 'comment_id', 'campaign_variant', 'offer_code', 'landing_path', 'referrer_host'].forEach(function (key) {
3177
+ const value = params.get(key);
3178
+ if (value) intakeUrl.searchParams.set(key, value);
3179
+ });
3180
+ intakeUrl.searchParams.set('utm_medium', 'checkout_cancel_recovery');
3181
+ intakeUrl.searchParams.set('cta_id', 'checkout_cancel_workflow_sprint_intake');
3182
+ intakeUrl.searchParams.set('cta_placement', 'checkout_cancel_recovery');
3183
+ intakeUrl.searchParams.set('plan_id', 'team');
3184
+ workflowIntakeLink.href = intakeUrl.toString();
3185
+ }
3186
+
2878
3187
  sendTelemetry('checkout_cancelled');
2879
3188
 
2880
3189
  document.querySelectorAll('[data-reason]').forEach(function (button) {
@@ -2893,6 +3202,27 @@ function renderCheckoutCancelledPage(runtimeConfig) {
2893
3202
  ? 'Feedback saved: ' + selectedReason.replaceAll('_', ' ') + '.'
2894
3203
  : 'Feedback saved.';
2895
3204
  });
3205
+
3206
+ document.querySelectorAll('[data-recovery-offer]').forEach(function (link) {
3207
+ link.addEventListener('click', function () {
3208
+ if (link.getAttribute('data-recovery-offer') === 'workflow_sprint_intake') {
3209
+ sendTelemetry('checkout_cancel_workflow_intake_clicked', {
3210
+ ctaId: 'checkout_cancel_workflow_sprint_intake',
3211
+ ctaPlacement: 'checkout_cancel_recovery',
3212
+ offerCode: 'workflow_sprint_intake',
3213
+ planId: 'team',
3214
+ reasonCode: selectedReason || null
3215
+ });
3216
+ return;
3217
+ }
3218
+ sendTelemetry('checkout_recovery_offer_clicked', {
3219
+ ctaId: link.getAttribute('data-recovery-offer'),
3220
+ ctaPlacement: 'checkout_cancel_recovery',
3221
+ offerCode: link.getAttribute('data-recovery-offer'),
3222
+ offerPriceDollars: link.getAttribute('data-offer-price')
3223
+ });
3224
+ });
3225
+ });
2896
3226
  }());
2897
3227
  </script>
2898
3228
  </main>
@@ -3067,10 +3397,8 @@ function isAuthorized(req, expected) {
3067
3397
  if (!expected) return true;
3068
3398
  const token = extractApiKey(req);
3069
3399
 
3070
- // Check static THUMBGATE_API_KEY first
3071
3400
  if (token === expected) return true;
3072
3401
 
3073
- // Also accept any valid provisioned billing key
3074
3402
  if (token) {
3075
3403
  const result = validateApiKey(token);
3076
3404
  return result.valid === true;
@@ -3079,9 +3407,6 @@ function isAuthorized(req, expected) {
3079
3407
  return false;
3080
3408
  }
3081
3409
 
3082
- /**
3083
- * Extract the Bearer token from a request (returns '' if absent).
3084
- */
3085
3410
  function extractBearerToken(req) {
3086
3411
  const auth = req.headers.authorization || '';
3087
3412
  return auth.startsWith('Bearer ') ? auth.slice(7) : '';
@@ -3243,15 +3568,6 @@ function createApiServer() {
3243
3568
  const expectedApiKey = getExpectedApiKey();
3244
3569
  const expectedOperatorKey = getExpectedOperatorKey();
3245
3570
 
3246
- // Live-event bus. Feedback captures, prevention-rule regenerations, and
3247
- // gate decisions push to this emitter; the /v1/events SSE endpoint streams
3248
- // those events to connected dashboard clients so they render in real time
3249
- // instead of waiting for the next manual refresh.
3250
- //
3251
- // See .changeset/dashboard-sse-live.md for the ROI rationale — this is a
3252
- // direct application of the "persistent channel beats per-turn HTTP" pattern
3253
- // to ThumbGate's dashboard surface (the primary UI for watching team
3254
- // feedback flow).
3255
3571
  const eventBus = new EventEmitter();
3256
3572
  eventBus.setMaxListeners(200);
3257
3573
 
@@ -3880,17 +4196,22 @@ async function addContext(){
3880
4196
  }
3881
4197
 
3882
4198
  if (isGetLikeRequest && pathname === '/pro') {
3883
- // Consolidated: /pro content now lives inline on `/` as the #pro-pitch
3884
- // strip (hero-adjacent pricing card). 301 so external links (README,
3885
- // plugin manifests, guides, compare pages) pass link equity onto the
3886
- // single canonical landing page. Query string is preserved so UTM
3887
- // tracking from inbound campaigns still reaches GA/PostHog on `/`.
3888
- const redirectTarget = `/#pro-pitch${parsed.search || ''}`;
3889
- res.writeHead(301, {
3890
- Location: redirectTarget,
3891
- 'Cache-Control': 'public, max-age=3600',
3892
- });
3893
- res.end();
4199
+ try {
4200
+ servePublicMarketingPage({
4201
+ req,
4202
+ res,
4203
+ parsed,
4204
+ hostedConfig,
4205
+ isHeadRequest,
4206
+ renderHtml: loadProPageHtml,
4207
+ extraTelemetry: {
4208
+ pageType: 'pro',
4209
+ planId: 'pro',
4210
+ },
4211
+ });
4212
+ } catch (err) {
4213
+ sendText(res, 500, err.message || 'Pro page unavailable');
4214
+ }
3894
4215
  return;
3895
4216
  }
3896
4217
 
@@ -4087,36 +4408,94 @@ async function addContext(){
4087
4408
  ? { 'Set-Cookie': journeyState.setCookieHeaders }
4088
4409
  : {};
4089
4410
 
4090
- // ── Bot guard ────────────────────────────────────────────────────
4091
- // Creating a Stripe Checkout session on every GET means crawlers,
4092
- // link-preview fetchers, and LLM scrapers inflate "sessions opened"
4093
- // while completions stay at zero. Serve bots an interstitial HTML
4094
- // page instead — no Stripe session created, no funnel pollution.
4095
- // The `?confirm=1` query param or POST below is the real-user path.
4096
4411
  const botClassification = classifyRequester(req.headers);
4097
4412
  const confirmParam = parsed?.searchParams?.get('confirm') ?? null;
4098
4413
  const isConfirmedCheckout = confirmParam === '1'
4099
4414
  || confirmParam === 'true'
4100
4415
  || req.method === 'POST';
4101
- if (botClassification.isBot && !isConfirmedCheckout) {
4416
+ if (!isConfirmedCheckout) {
4417
+ const eventType = botClassification.isBot ? 'checkout_bot_deflected' : 'checkout_interstitial_view';
4102
4418
  appendBestEffortTelemetry(FEEDBACK_DIR, {
4103
- eventType: 'checkout_bot_deflected',
4419
+ eventType,
4104
4420
  clientType: 'web',
4105
4421
  traceId,
4422
+ acquisitionId: analyticsMetadata.acquisitionId,
4423
+ visitorId: analyticsMetadata.visitorId,
4424
+ sessionId: analyticsMetadata.sessionId,
4106
4425
  utmSource: analyticsMetadata.utmSource,
4107
4426
  utmMedium: analyticsMetadata.utmMedium,
4108
4427
  utmCampaign: analyticsMetadata.utmCampaign,
4428
+ utmContent: analyticsMetadata.utmContent,
4429
+ utmTerm: analyticsMetadata.utmTerm,
4109
4430
  referrer: analyticsMetadata.referrer,
4110
4431
  referrerHost: analyticsMetadata.referrerHost,
4111
4432
  page: '/checkout/pro',
4433
+ ctaId: analyticsMetadata.ctaId,
4434
+ ctaPlacement: analyticsMetadata.ctaPlacement,
4112
4435
  planId: analyticsMetadata.planId,
4113
4436
  reason: botClassification.reason,
4114
- }, req.headers, 'checkout_bot_deflected');
4115
- const html = '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>ThumbGate Pro \u2014 Confirm checkout</title><style>*{box-sizing:border-box}body{background:#0a0a0a;color:#e5e5e5;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:20px}.card{background:#141414;border:1px solid #222;border-radius:16px;padding:48px 40px;max-width:460px;width:100%;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.5)}h1{margin:0 0 12px;font-size:22px;color:#22d3ee}p{color:#9ca3af;font-size:14px;line-height:1.55;margin:0 0 24px}.btn{display:inline-block;background:#22d3ee;color:#000;text-decoration:none;font-weight:700;padding:14px 32px;border-radius:999px;font-size:16px;cursor:pointer;border:none}.btn:hover{opacity:.9}.sub{margin-top:16px;font-size:12px;color:#6b7280}a.back{color:#6b7280;font-size:13px;text-decoration:underline}</style></head><body><div class="card"><h1>Continue to secure checkout</h1><p>You\'re one click from ThumbGate Pro at $19/mo. We create the payment session only after you confirm \u2014 keeps your path clean and our funnel honest.</p><a class="btn" href="/checkout/pro?confirm=1" rel="noopener">Continue to Stripe \u2192</a><div class="sub">Payments handled by Stripe. 7-day free trial. Cancel anytime.</div><div class="sub"><a class="back" href="/">\u2190 Back to homepage</a></div></div></body></html>';
4437
+ }, req.headers, eventType);
4438
+ const workflowIntakeHref = buildCheckoutIntentHref(`${hostedConfig.appOrigin}/#workflow-sprint-intake`, analyticsMetadata, {
4439
+ utmMedium: 'checkout_interstitial_recovery',
4440
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_sprint',
4441
+ ctaId: 'checkout_interstitial_workflow_sprint_intake',
4442
+ ctaPlacement: 'checkout_interstitial',
4443
+ planId: 'team',
4444
+ });
4445
+ const teamOptionsHref = buildCheckoutIntentHref(`${hostedConfig.appOrigin}/guides/ai-agent-governance-sprint`, analyticsMetadata, {
4446
+ utmMedium: 'checkout_interstitial_paid_path',
4447
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_team_paid_path',
4448
+ ctaId: 'checkout_interstitial_team_paid_path',
4449
+ ctaPlacement: 'checkout_interstitial',
4450
+ planId: 'team',
4451
+ });
4452
+ const diagnosticCheckoutHref = hostedConfig.sprintDiagnosticCheckoutUrl
4453
+ ? buildCheckoutIntentHref(hostedConfig.sprintDiagnosticCheckoutUrl, analyticsMetadata, {
4454
+ utmMedium: 'checkout_interstitial_paid_path',
4455
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_diagnostic',
4456
+ ctaId: 'checkout_interstitial_sprint_diagnostic_checkout',
4457
+ ctaPlacement: 'checkout_interstitial',
4458
+ planId: 'sprint_diagnostic',
4459
+ })
4460
+ : '';
4461
+ const sprintCheckoutHref = hostedConfig.workflowSprintCheckoutUrl
4462
+ ? buildCheckoutIntentHref(hostedConfig.workflowSprintCheckoutUrl, analyticsMetadata, {
4463
+ utmMedium: 'checkout_interstitial_paid_path',
4464
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_sprint',
4465
+ ctaId: 'checkout_interstitial_workflow_sprint_checkout',
4466
+ ctaPlacement: 'checkout_interstitial',
4467
+ planId: 'workflow_sprint',
4468
+ })
4469
+ : '';
4470
+ const html = renderCheckoutIntentPage({
4471
+ confirmHref: buildCheckoutConfirmHref(parsed),
4472
+ workflowIntakeHref,
4473
+ teamOptionsHref,
4474
+ diagnosticCheckoutHref,
4475
+ sprintCheckoutHref,
4476
+ sprintDiagnosticPriceDollars: hostedConfig.sprintDiagnosticPriceDollars || 499,
4477
+ workflowSprintPriceDollars: hostedConfig.workflowSprintPriceDollars || 1500,
4478
+ botClassification,
4479
+ });
4116
4480
  sendHtml(res, 200, html, responseHeaders);
4117
4481
  return;
4118
4482
  }
4119
4483
 
4484
+ const normalizedCheckoutEmail = normalizeCheckoutCustomerEmail(bootstrapBody.customerEmail);
4485
+ if (!normalizedCheckoutEmail) {
4486
+ appendBestEffortTelemetry(FEEDBACK_DIR, {
4487
+ eventType: 'checkout_email_gate_shown',
4488
+ clientType: 'web',
4489
+ traceId,
4490
+ page: '/checkout/pro',
4491
+ planId: analyticsMetadata.planId,
4492
+ }, req.headers, 'checkout_email_gate_shown');
4493
+ const { html, headers } = renderCheckoutIntentGate(parsed, responseHeaders);
4494
+ sendHtml(res, 200, html, headers);
4495
+ return;
4496
+ }
4497
+ bootstrapBody.customerEmail = normalizedCheckoutEmail;
4498
+
4120
4499
  appendBestEffortTelemetry(FEEDBACK_DIR, {
4121
4500
  eventType: 'checkout_bootstrap',
4122
4501
  clientType: 'web',
@@ -4492,7 +4871,7 @@ async function addContext(){
4492
4871
  .map(l => { try { return JSON.parse(l); } catch(_e) { return null; } })
4493
4872
  .filter(Boolean);
4494
4873
  }
4495
- } catch (_) {}
4874
+ } catch { entries = []; }
4496
4875
 
4497
4876
  const now = Date.now();
4498
4877
  const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
@@ -4809,6 +5188,13 @@ async function addContext(){
4809
5188
  return;
4810
5189
  }
4811
5190
 
5191
+ // POST /webhook/stripe — legacy Stripe event log bridge kept for backward compatibility.
5192
+ // This must remain unauthenticated like /v1/billing/webhook; Stripe auth is the HMAC signature.
5193
+ if (req.method === 'POST' && pathname === '/webhook/stripe') {
5194
+ await handleLegacyStripeWebhook(req, res);
5195
+ return;
5196
+ }
5197
+
4812
5198
  // GitHub Marketplace webhook
4813
5199
  if (req.method === 'POST' && pathname === '/v1/billing/github-webhook') {
4814
5200
  try {
@@ -6010,7 +6396,13 @@ async function addContext(){
6010
6396
  }
6011
6397
 
6012
6398
  const summary = await getBillingSummaryLive(summaryOptions);
6013
- sendJson(res, 200, summary);
6399
+ sendJson(res, 200, {
6400
+ ...summary,
6401
+ runtimePresence: buildHostedRuntimePresence(hostedConfig, {
6402
+ expectedApiKey,
6403
+ expectedOperatorKey,
6404
+ }),
6405
+ });
6014
6406
  return;
6015
6407
  }
6016
6408
 
@@ -6375,86 +6767,6 @@ async function addContext(){
6375
6767
  return;
6376
6768
  }
6377
6769
 
6378
- // POST /webhook/stripe — legacy Stripe event log bridge kept for backward compatibility.
6379
- // When STRIPE_WEBHOOK_SECRET is configured, verify the same Stripe signature used by
6380
- // the /v1/billing/webhook route before touching any payload.
6381
- if (req.method === 'POST' && pathname === '/webhook/stripe') {
6382
- try {
6383
- const rawBody = await new Promise((resolve, reject) => {
6384
- const chunks = [];
6385
- req.on('data', (c) => chunks.push(c));
6386
- req.on('end', () => resolve(Buffer.concat(chunks)));
6387
- req.on('error', reject);
6388
- });
6389
-
6390
- const sig = req.headers['stripe-signature'] || '';
6391
- if (!verifyWebhookSignature(rawBody, sig)) {
6392
- sendProblem(res, {
6393
- type: PROBLEM_TYPES.WEBHOOK_INVALID,
6394
- title: 'Invalid webhook signature',
6395
- status: 400,
6396
- detail: 'The webhook signature could not be verified.',
6397
- });
6398
- return;
6399
- }
6400
-
6401
- let event;
6402
- try {
6403
- event = JSON.parse(rawBody.toString('utf-8'));
6404
- } catch {
6405
- sendProblem(res, {
6406
- type: PROBLEM_TYPES.INVALID_JSON,
6407
- title: 'Invalid JSON',
6408
- status: 400,
6409
- detail: 'Invalid JSON in webhook body.',
6410
- });
6411
- return;
6412
- }
6413
- const TRACKED_STRIPE_EVENTS = new Set([
6414
- 'checkout.session.completed',
6415
- 'customer.subscription.created',
6416
- 'customer.subscription.deleted',
6417
- ]);
6418
- if (TRACKED_STRIPE_EVENTS.has(event.type)) {
6419
- const obj = event.data && event.data.object ? event.data.object : {};
6420
- const record = {
6421
- timestamp: new Date().toISOString(),
6422
- event_type: event.type,
6423
- event_id: event.id || null,
6424
- customer_email:
6425
- obj.customer_email ||
6426
- obj.email ||
6427
- (obj.customer_details && obj.customer_details.email) ||
6428
- null,
6429
- plan:
6430
- obj.plan
6431
- ? (obj.plan.nickname || obj.plan.id || null)
6432
- : (
6433
- obj.items &&
6434
- obj.items.data &&
6435
- obj.items.data[0] &&
6436
- obj.items.data[0].plan
6437
- ? (obj.items.data[0].plan.nickname || obj.items.data[0].plan.id)
6438
- : null
6439
- ),
6440
- amount_cents: obj.amount_total || (obj.plan && obj.plan.amount) || null,
6441
- currency: obj.currency || null,
6442
- subscription_id: obj.subscription || obj.id || null,
6443
- };
6444
- appendStripeEvent(record);
6445
- }
6446
- sendJson(res, 200, { received: true, event_type: event.type });
6447
- } catch (err) {
6448
- sendProblem(res, {
6449
- type: PROBLEM_TYPES.INTERNAL,
6450
- title: 'Internal Server Error',
6451
- status: 500,
6452
- detail: err.message,
6453
- });
6454
- }
6455
- return;
6456
- }
6457
-
6458
6770
  // GET /api/conversions — Conversion stats derived from the Stripe event log
6459
6771
  if (req.method === 'GET' && pathname === '/api/conversions') {
6460
6772
  try {