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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +3 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +26 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +420 -1
- package/bin/postinstall.js +2 -2
- package/config/gate-templates.json +372 -0
- package/config/mcp-allowlists.json +25 -0
- package/config/model-candidates.json +59 -2
- package/config/model-tiers.json +4 -1
- package/package.json +79 -22
- package/public/compare.html +6 -0
- package/public/index.html +153 -20
- package/public/numbers.html +6 -6
- package/public/pro.html +25 -27
- package/scripts/agent-design-governance.js +211 -0
- package/scripts/agent-reasoning-traces.js +683 -0
- package/scripts/agent-reward-model.js +438 -0
- package/scripts/agent-stack-survival-audit.js +231 -0
- package/scripts/ai-engineering-stack-guardrails.js +256 -0
- package/scripts/billing.js +33 -5
- package/scripts/chatgpt-ads-readiness-pack.js +195 -0
- package/scripts/cli-schema.js +277 -0
- package/scripts/code-graph-guardrails.js +176 -0
- package/scripts/commercial-offer.js +1 -1
- package/scripts/deepseek-v4-runtime-guardrails.js +253 -0
- package/scripts/gemini-embedding-policy.js +198 -0
- package/scripts/inference-cache-policy.js +39 -0
- package/scripts/judge-reward-function.js +396 -0
- package/scripts/llm-behavior-monitor.js +251 -0
- package/scripts/long-running-agent-context-guardrails.js +176 -0
- package/scripts/multimodal-retrieval-plan.js +31 -11
- package/scripts/oss-pr-opportunity-scout.js +240 -0
- package/scripts/proactive-agent-eval-guardrails.js +230 -0
- package/scripts/profile-router.js +5 -4
- package/scripts/prompting-operating-system.js +273 -0
- package/scripts/proxy-pointer-rag-guardrails.js +189 -0
- package/scripts/rag-precision-guardrails.js +202 -0
- package/scripts/rate-limiter.js +1 -1
- package/scripts/reasoning-efficiency-guardrails.js +176 -0
- package/scripts/reward-hacking-guardrails.js +251 -0
- package/scripts/seo-gsd.js +1201 -11
- package/scripts/single-use-credential-gate.js +182 -0
- package/scripts/structured-prompt-driven.js +226 -0
- package/scripts/telemetry-analytics.js +108 -6
- package/scripts/tool-registry.js +92 -0
- package/scripts/upstream-contribution-engine.js +379 -0
- package/scripts/vector-store.js +119 -4
- package/src/api/server.js +455 -143
- package/scripts/agents-sdk-sandbox-plan.js +0 -57
- package/scripts/ai-org-governance.js +0 -98
- package/scripts/artifact-agent-plan.js +0 -81
- package/scripts/enterprise-agent-rollout.js +0 -34
- package/scripts/experience-replay-governance.js +0 -69
- package/scripts/inference-economics.js +0 -53
- package/scripts/knowledge-layer-plan.js +0 -108
- package/scripts/memory-store-governance.js +0 -60
- package/scripts/post-training-governance.js +0 -34
- package/scripts/production-agent-readiness.js +0 -40
- package/scripts/scaling-law-claims.js +0 -60
- 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('>', '>');
|
|
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}'
|
|
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
|
-
|
|
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">
|
|
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
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
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 (
|
|
4416
|
+
if (!isConfirmedCheckout) {
|
|
4417
|
+
const eventType = botClassification.isBot ? 'checkout_bot_deflected' : 'checkout_interstitial_view';
|
|
4102
4418
|
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
4103
|
-
eventType
|
|
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,
|
|
4115
|
-
const
|
|
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,
|
|
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 {
|