thumbgate 1.16.19 → 1.16.21

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/src/api/server.js CHANGED
@@ -13,6 +13,9 @@ const {
13
13
  const POSTHOG_API_PATHS = new Set(['/capture', '/batch', '/decide', '/e', '/engage']);
14
14
  const POSTHOG_INGEST_HOST = 'us.i.posthog.com';
15
15
  const POSTHOG_STATIC_PATH_PREFIX = '/static/';
16
+ const FIRST_FAILURE_RULE_CHECKOUT_URL = 'https://buy.stripe.com/4gM6oHgH2bTw4lH6i73sI0z';
17
+ const QUICK_READ_CHECKOUT_URL = 'https://buy.stripe.com/aFa8wPgH29Lo4lH35V3sI0w';
18
+ const WORKFLOW_TEARDOWN_CHECKOUT_URL = 'https://buy.stripe.com/7sYfZhgH29LodWhdKz3sI0v';
16
19
 
17
20
  function getPosthogProxyPath(pathname) {
18
21
  return pathname.slice('/ingest'.length) || '/';
@@ -1453,6 +1456,61 @@ function buildCheckoutFallbackUrl(baseUrl, metadata = {}) {
1453
1456
  return restoreStripeCheckoutPlaceholder(url.toString());
1454
1457
  }
1455
1458
 
1459
+ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
1460
+ return buildCheckoutFallbackUrl(baseUrl, {
1461
+ ...metadata,
1462
+ ...overrides,
1463
+ });
1464
+ }
1465
+
1466
+ function renderCheckoutIntentPage({
1467
+ confirmHref,
1468
+ firstRuleCheckoutHref,
1469
+ quickReadCheckoutHref,
1470
+ workflowTeardownCheckoutHref,
1471
+ workflowIntakeHref,
1472
+ teamOptionsHref,
1473
+ diagnosticCheckoutHref,
1474
+ sprintCheckoutHref,
1475
+ sprintDiagnosticPriceDollars = 499,
1476
+ workflowSprintPriceDollars = 1500,
1477
+ }) {
1478
+ const safeConfirmHref = escapeHtmlAttribute(confirmHref);
1479
+ const safeFirstRuleCheckoutHref = firstRuleCheckoutHref
1480
+ ? escapeHtmlAttribute(firstRuleCheckoutHref)
1481
+ : '';
1482
+ const safeQuickReadCheckoutHref = quickReadCheckoutHref
1483
+ ? escapeHtmlAttribute(quickReadCheckoutHref)
1484
+ : '';
1485
+ const safeWorkflowTeardownCheckoutHref = workflowTeardownCheckoutHref
1486
+ ? escapeHtmlAttribute(workflowTeardownCheckoutHref)
1487
+ : '';
1488
+ const safeWorkflowIntakeHref = escapeHtmlAttribute(workflowIntakeHref);
1489
+ const safeTeamOptionsHref = escapeHtmlAttribute(teamOptionsHref);
1490
+ const safeDiagnosticCheckoutHref = diagnosticCheckoutHref
1491
+ ? escapeHtmlAttribute(diagnosticCheckoutHref)
1492
+ : '';
1493
+ const safeSprintCheckoutHref = sprintCheckoutHref
1494
+ ? escapeHtmlAttribute(sprintCheckoutHref)
1495
+ : '';
1496
+ const diagnosticAction = safeDiagnosticCheckoutHref
1497
+ ? `<a data-i="sprint_diagnostic_checkout" href="${safeDiagnosticCheckoutHref}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
1498
+ : '';
1499
+ const sprintAction = safeSprintCheckoutHref
1500
+ ? `<a data-i="workflow_sprint_checkout" href="${safeSprintCheckoutHref}">Start $${workflowSprintPriceDollars} sprint</a>`
1501
+ : '';
1502
+ const firstRuleAction = safeFirstRuleCheckoutHref
1503
+ ? `<a data-i="first_failure_rule_checkout" href="${safeFirstRuleCheckoutHref}">Pay $1 first rule</a>`
1504
+ : '';
1505
+ const quickReadAction = safeQuickReadCheckoutHref
1506
+ ? `<a data-i="quick_read_checkout" href="${safeQuickReadCheckoutHref}">Pay $19 quick read</a>`
1507
+ : '';
1508
+ const teardownAction = safeWorkflowTeardownCheckoutHref
1509
+ ? `<a data-i="workflow_teardown_checkout" href="${safeWorkflowTeardownCheckoutHref}">Pay $99 teardown</a>`
1510
+ : '';
1511
+ 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}.high-ticket{border-color:#4ade80;color:#4ade80}</style><div><h1>Choose the right paid path.</h1><p>For one repeated workflow failure, start with the diagnostic or sprint. Use Pro only when you need the local self-serve dashboard.</p>${diagnosticAction.replace('<a ', '<a class="high-ticket" ')}${sprintAction.replace('<a ', '<a class="high-ticket" ')}<a class="primary" data-i="pro_checkout_confirmed" href="${safeConfirmHref}">Pay in Stripe</a>${teardownAction}${quickReadAction}${firstRuleAction}<a data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Send workflow first</a><a data-i="team_paid_path" href="${safeTeamOptionsHref}">See 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>`;
1512
+ }
1513
+
1456
1514
  function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
1457
1515
  const params = parsed.searchParams;
1458
1516
  const traceId = pickFirstText(params.get('trace_id')) || createJourneyId('checkout');
@@ -1505,6 +1563,26 @@ function buildCheckoutConfirmHref(parsed) {
1505
1563
  return `${confirmUrl.pathname}${confirmUrl.search}`;
1506
1564
  }
1507
1565
 
1566
+ function normalizeCheckoutCustomerEmail(value) {
1567
+ const email = (normalizeNullableText(value) || '').toLowerCase();
1568
+ const atIndex = email.indexOf('@');
1569
+ const domain = email.slice(atIndex + 1);
1570
+ if (!email || email.length > 254 || atIndex <= 0 || atIndex !== email.lastIndexOf('@') || !domain || !domain.includes('.') || domain.startsWith('.') || domain.endsWith('.') || domain.includes('..')) return null;
1571
+ for (const ch of email) if (ch <= ' ' || ch === '<' || ch === '>' || ch === '"') return null;
1572
+ return email;
1573
+ }
1574
+
1575
+ function renderCheckoutIntentGate(parsed, responseHeaders = {}) {
1576
+ let hiddenInputs = '';
1577
+ for (const [key, value] of parsed.searchParams.entries()) {
1578
+ if (key !== 'confirm' && key !== 'customer_email') hiddenInputs += `<input type=hidden name=${escapeHtmlAttribute(key)} value=${escapeHtmlAttribute(value)}>`;
1579
+ }
1580
+ return {
1581
+ 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>`,
1582
+ headers: responseHeaders,
1583
+ };
1584
+ }
1585
+
1508
1586
  function normalizeTrackedLinkSlug(value) {
1509
1587
  return String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
1510
1588
  }
@@ -1746,9 +1824,7 @@ function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
1746
1824
  evidence: [err && err.message ? err.message : 'unknown_error'],
1747
1825
  },
1748
1826
  });
1749
- } catch (_) {
1750
- // Public telemetry remains best-effort even when diagnostics fail.
1751
- }
1827
+ } catch (_) {}
1752
1828
  return false;
1753
1829
  }
1754
1830
  }
@@ -2055,7 +2131,6 @@ a{color:#22d3ee;text-decoration:none}</style></head><body>
2055
2131
  const timestamp = merged.timestamp ? new Date(merged.timestamp).toLocaleString() : '';
2056
2132
  const isoTimestamp = merged.timestamp || '';
2057
2133
 
2058
- // Technical metadata
2059
2134
  const failureType = merged.failureType || null;
2060
2135
  const skill = merged.skill || null;
2061
2136
  const source = merged.source || fb.source || null;
@@ -2066,19 +2141,12 @@ a{color:#22d3ee;text-decoration:none}</style></head><body>
2066
2141
  const guardrails = merged.guardrails || null;
2067
2142
  const rubricScores = merged.rubricScores || null;
2068
2143
 
2069
- // Structured rule
2070
2144
  const rule = merged.structuredRule || merged.rule || null;
2071
- // Conversation window
2072
2145
  const convoWindow = merged.conversationWindow || merged.chatHistory || [];
2073
- // Reflector analysis
2074
2146
  const reflector = merged.reflectorAnalysis || merged.reflector || null;
2075
- // Diagnosis
2076
2147
  const diagnosis = merged.diagnosis || null;
2077
- // Rubric
2078
2148
  const rubric = merged.rubricEvaluation || merged.rubric || null;
2079
- // Synthesis
2080
2149
  const synthesis = merged.synthesis || null;
2081
- // Bayesian
2082
2150
  const bayesian = merged.bayesianBelief || merged.bayesian || null;
2083
2151
 
2084
2152
  function sectionCard(titleText, content, id) {
@@ -2501,14 +2569,6 @@ function servePublicMarketingPage({
2501
2569
  'landing_page_view'
2502
2570
  );
2503
2571
 
2504
- // Funnel-ledger write (2026-04-21): populate funnel-events.jsonl with a
2505
- // discovery-stage event on every landing-page view so UTM-tagged social
2506
- // traffic becomes visible in `npm run feedback:summary` and
2507
- // `bin/cli.js cfo --today`. Prior to this wire, landing views wrote only
2508
- // to telemetry-pings.jsonl (invisible to the CEO-facing revenue surface),
2509
- // leaving funnel-events.jsonl empty despite 404 published Zernio posts.
2510
- // Best-effort: wrapped in try/catch so a billing-ledger hiccup never
2511
- // breaks a page render.
2512
2572
  try {
2513
2573
  appendFunnelEvent({
2514
2574
  stage: 'discovery',
@@ -2927,6 +2987,9 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2927
2987
  }
2928
2988
 
2929
2989
  function renderCheckoutCancelledPage(runtimeConfig) {
2990
+ const firstFailureRuleCheckoutUrl = escapeHtmlAttribute(FIRST_FAILURE_RULE_CHECKOUT_URL);
2991
+ const quickReadCheckoutUrl = escapeHtmlAttribute(QUICK_READ_CHECKOUT_URL);
2992
+ const workflowTeardownCheckoutUrl = escapeHtmlAttribute(WORKFLOW_TEARDOWN_CHECKOUT_URL);
2930
2993
  const diagnosticCheckoutUrl = runtimeConfig.sprintDiagnosticCheckoutUrl
2931
2994
  ? escapeHtmlAttribute(runtimeConfig.sprintDiagnosticCheckoutUrl)
2932
2995
  : '';
@@ -2937,18 +3000,21 @@ function renderCheckoutCancelledPage(runtimeConfig) {
2937
3000
  const workflowSprintPriceDollars = runtimeConfig.workflowSprintPriceDollars || 1500;
2938
3001
  const workflowSprintIntakeUrl = `${escapeHtmlAttribute(runtimeConfig.appOrigin)}/#workflow-sprint-intake`;
2939
3002
  const recoveryOfferLinks = [
2940
- `<a id="send-workflow-first" href="${workflowSprintIntakeUrl}" data-recovery-offer="workflow_sprint_intake" data-offer-price="0">Send workflow first</a>`,
2941
3003
  diagnosticCheckoutUrl
2942
3004
  ? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
2943
3005
  : '',
2944
3006
  workflowSprintCheckoutUrl
2945
3007
  ? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="workflow_sprint" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint</a>`
2946
3008
  : '',
3009
+ `<a href="${workflowTeardownCheckoutUrl}" data-recovery-offer="workflow_teardown" data-offer-price="99">Pay $99 teardown</a>`,
3010
+ `<a href="${quickReadCheckoutUrl}" data-recovery-offer="quick_read" data-offer-price="19">Pay $19 quick read</a>`,
3011
+ `<a href="${firstFailureRuleCheckoutUrl}" data-recovery-offer="first_failure_rule" data-offer-price="1">Pay $1 first rule</a>`,
3012
+ `<a id="send-workflow-first" href="${workflowSprintIntakeUrl}" data-recovery-offer="workflow_sprint_intake" data-offer-price="0">Send workflow first</a>`,
2947
3013
  ].filter(Boolean).join('\n ');
2948
3014
  const recoveryOfferCard = recoveryOfferLinks
2949
3015
  ? `<div class="card recovery-card">
2950
3016
  <h2>Need help deciding?</h2>
2951
- <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>
3017
+ <p>If Pro is not the right next step, start smaller or 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>
2952
3018
  <div class="actions">
2953
3019
  ${recoveryOfferLinks}
2954
3020
  </div>
@@ -3070,7 +3136,7 @@ function renderCheckoutCancelledPage(runtimeConfig) {
3070
3136
  </div>
3071
3137
  <div class="actions">
3072
3138
  <button type="button" id="submit-reason">Send feedback</button>
3073
- <a id="retry-checkout" href="/checkout/pro" class="secondary" data-recovery-offer="pro_trial_retry" data-offer-price="19">Restart $19 Pro trial</a>
3139
+ <a id="retry-checkout" href="/checkout/pro" class="secondary" data-recovery-offer="pro_pay_now_retry" data-offer-price="19">Restart $19 Pro checkout</a>
3074
3140
  <a href="${runtimeConfig.appOrigin}" class="secondary">Return to Context Gateway</a>
3075
3141
  </div>
3076
3142
  <p class="note" id="status">No feedback sent yet.</p>
@@ -3361,10 +3427,8 @@ function isAuthorized(req, expected) {
3361
3427
  if (!expected) return true;
3362
3428
  const token = extractApiKey(req);
3363
3429
 
3364
- // Check static THUMBGATE_API_KEY first
3365
3430
  if (token === expected) return true;
3366
3431
 
3367
- // Also accept any valid provisioned billing key
3368
3432
  if (token) {
3369
3433
  const result = validateApiKey(token);
3370
3434
  return result.valid === true;
@@ -3373,9 +3437,6 @@ function isAuthorized(req, expected) {
3373
3437
  return false;
3374
3438
  }
3375
3439
 
3376
- /**
3377
- * Extract the Bearer token from a request (returns '' if absent).
3378
- */
3379
3440
  function extractBearerToken(req) {
3380
3441
  const auth = req.headers.authorization || '';
3381
3442
  return auth.startsWith('Bearer ') ? auth.slice(7) : '';
@@ -3537,15 +3598,6 @@ function createApiServer() {
3537
3598
  const expectedApiKey = getExpectedApiKey();
3538
3599
  const expectedOperatorKey = getExpectedOperatorKey();
3539
3600
 
3540
- // Live-event bus. Feedback captures, prevention-rule regenerations, and
3541
- // gate decisions push to this emitter; the /v1/events SSE endpoint streams
3542
- // those events to connected dashboard clients so they render in real time
3543
- // instead of waiting for the next manual refresh.
3544
- //
3545
- // See .changeset/dashboard-sse-live.md for the ROI rationale — this is a
3546
- // direct application of the "persistent channel beats per-turn HTTP" pattern
3547
- // to ThumbGate's dashboard surface (the primary UI for watching team
3548
- // feedback flow).
3549
3601
  const eventBus = new EventEmitter();
3550
3602
  eventBus.setMaxListeners(200);
3551
3603
 
@@ -4386,37 +4438,117 @@ async function addContext(){
4386
4438
  ? { 'Set-Cookie': journeyState.setCookieHeaders }
4387
4439
  : {};
4388
4440
 
4389
- // ── Bot guard ────────────────────────────────────────────────────
4390
- // Creating a Stripe Checkout session on every GET means crawlers,
4391
- // link-preview fetchers, and LLM scrapers inflate "sessions opened"
4392
- // while completions stay at zero. Serve bots an interstitial HTML
4393
- // page instead — no Stripe session created, no funnel pollution.
4394
- // The `?confirm=1` query param or POST below is the real-user path.
4395
4441
  const botClassification = classifyRequester(req.headers);
4396
4442
  const confirmParam = parsed?.searchParams?.get('confirm') ?? null;
4397
4443
  const isConfirmedCheckout = confirmParam === '1'
4398
4444
  || confirmParam === 'true'
4399
4445
  || req.method === 'POST';
4400
- if (botClassification.isBot && !isConfirmedCheckout) {
4446
+ if (!isConfirmedCheckout && botClassification.isBot) {
4447
+ const eventType = 'checkout_bot_deflected';
4401
4448
  appendBestEffortTelemetry(FEEDBACK_DIR, {
4402
- eventType: 'checkout_bot_deflected',
4449
+ eventType,
4403
4450
  clientType: 'web',
4404
4451
  traceId,
4452
+ acquisitionId: analyticsMetadata.acquisitionId,
4453
+ visitorId: analyticsMetadata.visitorId,
4454
+ sessionId: analyticsMetadata.sessionId,
4405
4455
  utmSource: analyticsMetadata.utmSource,
4406
4456
  utmMedium: analyticsMetadata.utmMedium,
4407
4457
  utmCampaign: analyticsMetadata.utmCampaign,
4458
+ utmContent: analyticsMetadata.utmContent,
4459
+ utmTerm: analyticsMetadata.utmTerm,
4408
4460
  referrer: analyticsMetadata.referrer,
4409
4461
  referrerHost: analyticsMetadata.referrerHost,
4410
4462
  page: '/checkout/pro',
4463
+ ctaId: analyticsMetadata.ctaId,
4464
+ ctaPlacement: analyticsMetadata.ctaPlacement,
4411
4465
  planId: analyticsMetadata.planId,
4412
4466
  reason: botClassification.reason,
4413
- }, req.headers, 'checkout_bot_deflected');
4414
- const confirmHref = escapeHtmlAttribute(buildCheckoutConfirmHref(parsed));
4415
- 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="${confirmHref}" rel="noopener">Continue to Stripe \u2192</a><div class="sub">Payments handled by Stripe. 7-day trial. Card required; no charge today. Cancel anytime.</div><div class="sub"><a class="back" href="/">\u2190 Back to homepage</a></div></div></body></html>`;
4467
+ }, req.headers, eventType);
4468
+ const workflowIntakeHref = buildCheckoutIntentHref(`${hostedConfig.appOrigin}/#workflow-sprint-intake`, analyticsMetadata, {
4469
+ utmMedium: 'checkout_interstitial_recovery',
4470
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_sprint',
4471
+ ctaId: 'checkout_interstitial_workflow_sprint_intake',
4472
+ ctaPlacement: 'checkout_interstitial',
4473
+ planId: 'team',
4474
+ });
4475
+ const teamOptionsHref = buildCheckoutIntentHref(`${hostedConfig.appOrigin}/guides/ai-agent-governance-sprint`, analyticsMetadata, {
4476
+ utmMedium: 'checkout_interstitial_paid_path',
4477
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_team_paid_path',
4478
+ ctaId: 'checkout_interstitial_team_paid_path',
4479
+ ctaPlacement: 'checkout_interstitial',
4480
+ planId: 'team',
4481
+ });
4482
+ const firstRuleCheckoutHref = buildCheckoutIntentHref(FIRST_FAILURE_RULE_CHECKOUT_URL, analyticsMetadata, {
4483
+ utmMedium: 'checkout_interstitial_paid_path',
4484
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_first_failure_rule',
4485
+ ctaId: 'checkout_interstitial_first_failure_rule_checkout',
4486
+ ctaPlacement: 'checkout_interstitial',
4487
+ planId: 'first_failure_rule',
4488
+ });
4489
+ const quickReadCheckoutHref = buildCheckoutIntentHref(QUICK_READ_CHECKOUT_URL, analyticsMetadata, {
4490
+ utmMedium: 'checkout_interstitial_paid_path',
4491
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_quick_read',
4492
+ ctaId: 'checkout_interstitial_quick_read_checkout',
4493
+ ctaPlacement: 'checkout_interstitial',
4494
+ planId: 'quick_read',
4495
+ });
4496
+ const workflowTeardownCheckoutHref = buildCheckoutIntentHref(WORKFLOW_TEARDOWN_CHECKOUT_URL, analyticsMetadata, {
4497
+ utmMedium: 'checkout_interstitial_paid_path',
4498
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_teardown',
4499
+ ctaId: 'checkout_interstitial_workflow_teardown_checkout',
4500
+ ctaPlacement: 'checkout_interstitial',
4501
+ planId: 'workflow_teardown',
4502
+ });
4503
+ const diagnosticCheckoutHref = hostedConfig.sprintDiagnosticCheckoutUrl
4504
+ ? buildCheckoutIntentHref(hostedConfig.sprintDiagnosticCheckoutUrl, analyticsMetadata, {
4505
+ utmMedium: 'checkout_interstitial_paid_path',
4506
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_diagnostic',
4507
+ ctaId: 'checkout_interstitial_sprint_diagnostic_checkout',
4508
+ ctaPlacement: 'checkout_interstitial',
4509
+ planId: 'sprint_diagnostic',
4510
+ })
4511
+ : '';
4512
+ const sprintCheckoutHref = hostedConfig.workflowSprintCheckoutUrl
4513
+ ? buildCheckoutIntentHref(hostedConfig.workflowSprintCheckoutUrl, analyticsMetadata, {
4514
+ utmMedium: 'checkout_interstitial_paid_path',
4515
+ utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_sprint',
4516
+ ctaId: 'checkout_interstitial_workflow_sprint_checkout',
4517
+ ctaPlacement: 'checkout_interstitial',
4518
+ planId: 'workflow_sprint',
4519
+ })
4520
+ : '';
4521
+ const html = renderCheckoutIntentPage({
4522
+ confirmHref: buildCheckoutConfirmHref(parsed),
4523
+ firstRuleCheckoutHref,
4524
+ quickReadCheckoutHref,
4525
+ workflowTeardownCheckoutHref,
4526
+ workflowIntakeHref,
4527
+ teamOptionsHref,
4528
+ diagnosticCheckoutHref,
4529
+ sprintCheckoutHref,
4530
+ sprintDiagnosticPriceDollars: hostedConfig.sprintDiagnosticPriceDollars || 499,
4531
+ workflowSprintPriceDollars: hostedConfig.workflowSprintPriceDollars || 1500,
4532
+ botClassification,
4533
+ });
4416
4534
  sendHtml(res, 200, html, responseHeaders);
4417
4535
  return;
4418
4536
  }
4419
4537
 
4538
+ const rawCheckoutEmail = normalizeNullableText(bootstrapBody.customerEmail);
4539
+ const normalizedCheckoutEmail = normalizeCheckoutCustomerEmail(rawCheckoutEmail);
4540
+ if (!normalizedCheckoutEmail) {
4541
+ appendBestEffortTelemetry(FEEDBACK_DIR, {
4542
+ eventType: 'checkout_email_deferred_to_stripe',
4543
+ clientType: 'web',
4544
+ traceId,
4545
+ page: '/checkout/pro',
4546
+ planId: analyticsMetadata.planId,
4547
+ reason: rawCheckoutEmail ? 'invalid_customer_email' : 'missing_customer_email',
4548
+ }, req.headers, 'checkout_email_deferred_to_stripe');
4549
+ }
4550
+ bootstrapBody.customerEmail = normalizedCheckoutEmail || undefined;
4551
+
4420
4552
  appendBestEffortTelemetry(FEEDBACK_DIR, {
4421
4553
  eventType: 'checkout_bootstrap',
4422
4554
  clientType: 'web',
@@ -4792,7 +4924,7 @@ async function addContext(){
4792
4924
  .map(l => { try { return JSON.parse(l); } catch(_e) { return null; } })
4793
4925
  .filter(Boolean);
4794
4926
  }
4795
- } catch (_) {}
4927
+ } catch { entries = []; }
4796
4928
 
4797
4929
  const now = Date.now();
4798
4930
  const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;