thumbgate 1.28.0 → 1.28.1

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
@@ -10,6 +10,7 @@ const {
10
10
  createUnavailableAsyncOperation,
11
11
  loadOptionalModule,
12
12
  } = require('../../scripts/private-core-boundary');
13
+ const { redactSecrets, redactSecretsDeep } = require('../../scripts/secret-redaction');
13
14
 
14
15
  const POSTHOG_API_PATHS = new Set(['/capture', '/batch', '/decide', '/e', '/engage']);
15
16
  const POSTHOG_INGEST_HOST = 'us.i.posthog.com';
@@ -24,8 +25,12 @@ const PRO_CHECKOUT_URL = 'https://buy.stripe.com/8x2dR91M84r4cSd9uj3sI3f';
24
25
  const FIRST_FAILURE_RULE_CHECKOUT_URL = 'https://buy.stripe.com/7sY6oHaiEbTw6tP5e33sI3e';
25
26
  const QUICK_READ_CHECKOUT_URL = 'https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H';
26
27
  const WORKFLOW_TEARDOWN_CHECKOUT_URL = 'https://buy.stripe.com/8x214n2Qc4r44lHayn3sI2I';
27
- const SPRINT_DIAGNOSTIC_CHECKOUT_URL = 'https://buy.stripe.com/28E00j3Uge1E2dzgWL3sI2J';
28
- const WORKFLOW_SPRINT_CHECKOUT_URL = 'https://buy.stripe.com/6oU00j8aw2iWdWh9uj3sI2K';
28
+ // Verified live amounts (2026-07-13): diagnostic Stripe page contains $499;
29
+ // sprint code-default Stripe slug previously pointed at a $499 diagnostic product.
30
+ // Production sprint rail is PayPal at $1500 — pin that as the code fallback so a
31
+ // missing Railway env cannot silently charge $499 under a $1500 label.
32
+ const SPRINT_DIAGNOSTIC_CHECKOUT_URL = 'https://buy.stripe.com/9B69ATbmI4r4aK5eOD3sI3k';
33
+ const WORKFLOW_SPRINT_CHECKOUT_URL = 'https://www.paypal.com/ncp/payment/LTQFR7P9AR3QG';
29
34
 
30
35
  function getPosthogProxyPath(pathname) {
31
36
  return pathname.slice('/ingest'.length) || '/';
@@ -214,6 +219,7 @@ const {
214
219
  const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
215
220
  const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
216
221
  const mcpOauth = require('../../scripts/mcp-oauth');
222
+ const { packCheckoutReference } = require('../../scripts/checkout-attribution-reference');
217
223
  // OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
218
224
  // (Claude Connectors Directory requires OAuth for authenticated services).
219
225
  const oauthStore = mcpOauth.createStore();
@@ -247,6 +253,7 @@ const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
247
253
  const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
248
254
  const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
249
255
  const DIAGNOSTIC_PAGE_PATH = path.resolve(__dirname, '../../public/diagnostic.html');
256
+ const PARTNER_INTAKE_PAGE_PATH = path.resolve(__dirname, '../../public/partner-intake.html');
250
257
  const INSTALL_PAGE_PATH = path.resolve(__dirname, '../../public/install.html');
251
258
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
252
259
  const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
@@ -1431,7 +1438,6 @@ function buildCheckoutAttributionMetadata(body, req, traceId) {
1431
1438
  : 1;
1432
1439
 
1433
1440
  return {
1434
- ...rawMetadata,
1435
1441
  traceId,
1436
1442
  acquisitionId: pickFirstText(rawMetadata.acquisitionId, body.acquisitionId),
1437
1443
  visitorId: pickFirstText(rawMetadata.visitorId, body.visitorId),
@@ -2102,6 +2108,10 @@ function buildCheckoutFallbackUrl(baseUrl, metadata = {}) {
2102
2108
  appendQueryParam(url, 'seat_count', metadata.seatCount);
2103
2109
  appendQueryParam(url, 'landing_path', metadata.landingPath);
2104
2110
  appendQueryParam(url, 'referrer_host', metadata.referrerHost);
2111
+ // External Stripe Payment Links drop utm_* query params but preserve
2112
+ // client_reference_id — pack attribution into it so marketplace-attributed paid
2113
+ // checkouts (e.g. utm_source=aiventyx) survive to the webhook and can be credited.
2114
+ appendQueryParam(url, 'client_reference_id', packCheckoutReference(metadata));
2105
2115
  return restoreStripeCheckoutPlaceholder(url.toString());
2106
2116
  }
2107
2117
 
@@ -2546,10 +2556,18 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
2546
2556
  const { FEEDBACK_DIR } = getFeedbackPaths();
2547
2557
  const journeyState = resolveJourneyState(req, parsed);
2548
2558
  const destinationUrl = buildTrackedLinkDestination(target, hostedConfig, parsed);
2559
+ const attribution = buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl);
2560
+ if (target.external) {
2561
+ appendQueryParam(
2562
+ destinationUrl,
2563
+ 'client_reference_id',
2564
+ packCheckoutReference(attribution)
2565
+ );
2566
+ }
2549
2567
  if (!isHeadRequest) {
2550
2568
  appendBestEffortTelemetry(
2551
2569
  FEEDBACK_DIR,
2552
- buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl),
2570
+ attribution,
2553
2571
  req.headers,
2554
2572
  `tracked_link_redirect:${target.slug}`
2555
2573
  );
@@ -2877,6 +2895,11 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
2877
2895
  '__SERVER_SESSION_ID__': pageContext.serverSessionId || '',
2878
2896
  '__SERVER_ACQUISITION_ID__': pageContext.serverAcquisitionId || '',
2879
2897
  '__SERVER_TELEMETRY_CAPTURED__': pageContext.serverTelemetryCaptured ? 'true' : 'false',
2898
+ '__SERVER_ATTRIBUTION_SOURCE__': escapeHtmlAttribute(pageContext.serverAttributionSource || 'partner'),
2899
+ '__SERVER_UTM_SOURCE__': escapeHtmlAttribute(pageContext.serverUtmSource || 'partner'),
2900
+ '__SERVER_UTM_MEDIUM__': escapeHtmlAttribute(pageContext.serverUtmMedium || 'partner'),
2901
+ '__SERVER_UTM_CAMPAIGN__': escapeHtmlAttribute(pageContext.serverUtmCampaign || 'hosted_listing'),
2902
+ '__SERVER_UTM_CONTENT__': escapeHtmlAttribute(pageContext.serverUtmContent || 'partner_intake'),
2880
2903
  '__VERIFICATION_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
2881
2904
  '__COMPATIBILITY_REPORT_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
2882
2905
  '__AUTOMATION_REPORT_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
@@ -2898,6 +2921,10 @@ function loadDiagnosticPageHtml(runtimeConfig, pageContext = {}) {
2898
2921
  return loadPublicMarketingTemplateHtml(DIAGNOSTIC_PAGE_PATH, runtimeConfig, pageContext);
2899
2922
  }
2900
2923
 
2924
+ function loadPartnerIntakePageHtml(runtimeConfig, pageContext = {}) {
2925
+ return loadPublicMarketingTemplateHtml(PARTNER_INTAKE_PAGE_PATH, runtimeConfig, pageContext);
2926
+ }
2927
+
2901
2928
  function loadInstallPageHtml(runtimeConfig, pageContext = {}) {
2902
2929
  return loadPublicMarketingTemplateHtml(INSTALL_PAGE_PATH, runtimeConfig, pageContext);
2903
2930
  }
@@ -3717,6 +3744,11 @@ function servePublicMarketingPage({
3717
3744
  serverSessionId: journeyState.sessionId,
3718
3745
  serverAcquisitionId: journeyState.acquisitionId,
3719
3746
  serverTelemetryCaptured: landingTelemetryCaptured,
3747
+ serverAttributionSource: landingAttribution.source,
3748
+ serverUtmSource: landingAttribution.utmSource,
3749
+ serverUtmMedium: landingAttribution.utmMedium,
3750
+ serverUtmCampaign: landingAttribution.utmCampaign,
3751
+ serverUtmContent: landingAttribution.utmContent,
3720
3752
  requestHost,
3721
3753
  });
3722
3754
 
@@ -4521,6 +4553,19 @@ function normalizeLeadEmail(value) {
4521
4553
  return email.toLowerCase();
4522
4554
  }
4523
4555
 
4556
+ function buildIntakeAlertRateLimitKey(req, env = process.env) {
4557
+ const transportPeer = String(req.socket?.remoteAddress || 'unknown');
4558
+ const railwayRealIp = String(req.headers['x-real-ip'] || '').trim();
4559
+ const runningBehindRailway = Boolean(env.RAILWAY_ENVIRONMENT_ID || env.RAILWAY_PROJECT_ID);
4560
+ const hasValidRailwayIp = /^[0-9a-f:.]{3,64}$/i.test(railwayRealIp);
4561
+ // Railway overwrites X-Real-IP at its ingress. Outside that explicit trust
4562
+ // boundary, forwarding headers remain caller-controlled and are ignored.
4563
+ const clientAddress = runningBehindRailway && hasValidRailwayIp
4564
+ ? railwayRealIp
4565
+ : transportPeer;
4566
+ return crypto.createHash('sha256').update(clientAddress).digest('hex');
4567
+ }
4568
+
4524
4569
  function normalizeHttpUrl(value, fieldName) {
4525
4570
  const raw = normalizeNullableText(value);
4526
4571
  if (!raw) throw createHttpError(400, `${fieldName} is required`);
@@ -5629,6 +5674,16 @@ async function addContext(){
5629
5674
  return;
5630
5675
  }
5631
5676
 
5677
+ // /docs is a common buyer URL; route to the public setup guide instead of 404.
5678
+ if (isGetLikeRequest && (pathname === '/docs' || pathname === '/docs/' || pathname === '/documentation' || pathname === '/documentation/')) {
5679
+ res.writeHead(302, {
5680
+ Location: '/guide',
5681
+ 'Cache-Control': 'public, max-age=300',
5682
+ });
5683
+ res.end();
5684
+ return;
5685
+ }
5686
+
5632
5687
  if (isGetLikeRequest && (pathname === '/guide' || pathname === '/guide.html')) {
5633
5688
  try {
5634
5689
  servePublicMarketingPage({
@@ -5764,6 +5819,25 @@ async function addContext(){
5764
5819
  return;
5765
5820
  }
5766
5821
 
5822
+ if (isGetLikeRequest && (pathname === '/partner-intake' || pathname === '/partner-intake.html')) {
5823
+ try {
5824
+ servePublicMarketingPage({
5825
+ req,
5826
+ res,
5827
+ parsed,
5828
+ hostedConfig,
5829
+ isHeadRequest,
5830
+ renderHtml: loadPartnerIntakePageHtml,
5831
+ extraTelemetry: {
5832
+ pageType: 'partner_intake',
5833
+ },
5834
+ });
5835
+ } catch (err) {
5836
+ sendText(res, 500, err.message || 'Partner intake page unavailable');
5837
+ }
5838
+ return;
5839
+ }
5840
+
5767
5841
  // Natural marketing URLs for the Workflow Hardening Diagnostic/Sprint.
5768
5842
  // High-intent outreach refers to "the diagnostic", "the sprint", or
5769
5843
  // "workflow hardening"; serve a focused buyer page instead of sending
@@ -7437,7 +7511,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7437
7511
  ? await parseFormBody(req, 24 * 1024)
7438
7512
  : await parseJsonBody(req, 24 * 1024);
7439
7513
  const workflowSprintIntake = requirePrivateApiModule('workflowSprintIntake', 'Workflow sprint intake');
7440
- const lead = workflowSprintIntake.appendWorkflowSprintLead({
7514
+ const leadPayload = {
7441
7515
  ...body,
7442
7516
  traceId: body.traceId || traceId,
7443
7517
  acquisitionId: body.acquisitionId || journeyState.acquisitionId,
@@ -7462,7 +7536,17 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7462
7536
  offerCode: body.offerCode || referrerAttribution.offerCode || null,
7463
7537
  referrerHost: body.referrerHost || referrerAttribution.referrerHost || null,
7464
7538
  referrer: body.referrer || referrerAttribution.referrer || null,
7465
- }, { feedbackDir: FEEDBACK_DIR });
7539
+ };
7540
+ const intakeReservation = workflowSprintIntake.reserveWorkflowSprintIntake(leadPayload, {
7541
+ feedbackDir: FEEDBACK_DIR,
7542
+ rateLimitKey: buildIntakeAlertRateLimitKey(req),
7543
+ });
7544
+ if (!intakeReservation.allowed) {
7545
+ const error = new Error('Too many workflow-intake submissions. Try again later.');
7546
+ error.statusCode = intakeReservation.statusCode || 429;
7547
+ throw error;
7548
+ }
7549
+ const lead = workflowSprintIntake.appendWorkflowSprintLead(leadPayload, { feedbackDir: FEEDBACK_DIR });
7466
7550
 
7467
7551
  appendBestEffortTelemetry(FEEDBACK_DIR, {
7468
7552
  eventType: 'workflow_sprint_lead_submitted',
@@ -7493,6 +7577,11 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7493
7577
  referrer: lead.attribution.referrer,
7494
7578
  }, req.headers, 'workflow_sprint_lead_submitted');
7495
7579
 
7580
+ const leadNotification = await workflowSprintIntake.notifyWorkflowSprintLead(lead, {
7581
+ sendEmailImpl: resendMailer.sendEmail,
7582
+ rateLimitKey: buildIntakeAlertRateLimitKey(req),
7583
+ });
7584
+
7496
7585
  if (isFormSubmission && !wantsJson(req, parsed)) {
7497
7586
  sendHtml(
7498
7587
  res,
@@ -7513,6 +7602,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7513
7602
  status: lead.status,
7514
7603
  offer: lead.offer,
7515
7604
  nextStep: 'review_proof_pack',
7605
+ operatorAlertSent: leadNotification.sent === true,
7606
+ operatorAlertReason: leadNotification.reason || null,
7516
7607
  proofPackUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
7517
7608
  sprintBriefUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/WORKFLOW_HARDENING_SPRINT.md',
7518
7609
  }, {
@@ -7520,7 +7611,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7520
7611
  ...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
7521
7612
  });
7522
7613
  } catch (err) {
7523
- appendBestEffortTelemetry(FEEDBACK_DIR, {
7614
+ appendBestEffortTelemetry(FEEDBACK_DIR, redactSecretsDeep({
7524
7615
  eventType: 'workflow_sprint_lead_failed',
7525
7616
  clientType: 'web',
7526
7617
  traceId,
@@ -7543,10 +7634,10 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7543
7634
  page: referrerAttribution.page || '/#workflow-sprint-intake',
7544
7635
  landingPath: referrerAttribution.landingPath || '/',
7545
7636
  referrerHost: referrerAttribution.referrerHost,
7546
- referrer: referrerAttribution.referrer,
7547
- failureCode: err?.message ? err.message : 'workflow_sprint_lead_failed',
7637
+ referrer: redactSecrets(referrerAttribution.referrer),
7638
+ failureCode: redactSecrets(err?.message ? err.message : 'workflow_sprint_lead_failed'),
7548
7639
  httpStatus: err && err.statusCode ? err.statusCode : null,
7549
- }, req.headers, 'workflow_sprint_lead_failed');
7640
+ }), redactSecretsDeep(req.headers), 'workflow_sprint_lead_failed');
7550
7641
  if (isFormSubmission && !wantsJson(req, parsed)) {
7551
7642
  sendHtml(
7552
7643
  res,
@@ -7832,6 +7923,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7832
7923
  });
7833
7924
  return;
7834
7925
  }
7926
+ if (result && result.retryable === true) {
7927
+ sendProblem(res, {
7928
+ type: PROBLEM_TYPES.INTERNAL,
7929
+ title: 'Fulfillment temporarily unavailable',
7930
+ status: 503,
7931
+ detail: 'The payment was received but fulfillment did not complete. Stripe should retry this event.',
7932
+ });
7933
+ return;
7934
+ }
7835
7935
  sendJson(res, 200, result);
7836
7936
 
7837
7937
  } catch (err) {
@@ -7964,16 +8064,24 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7964
8064
  }
7965
8065
 
7966
8066
  const resolvedTraceId = result.traceId || requestedTraceId;
8067
+ const nextSteps = result.apiKey
8068
+ ? {
8069
+ env: `THUMBGATE_API_KEY=${result.apiKey}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
8070
+ curl: `curl -X POST ${hostedConfig.billingApiBaseUrl}/v1/feedback/capture \\\n+ -H 'Authorization: Bearer ${result.apiKey}' \\\n+ -H 'Content-Type: application/json' \\\n+ -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
8071
+ }
8072
+ : result.offerKind === 'workflow_hardening_diagnostic'
8073
+ ? { fulfillment: 'Payment confirmed. Diagnostic fulfillment is processing; an email will arrive when it completes.' }
8074
+ : {};
7967
8075
 
7968
8076
  sendJson(res, 200, {
7969
8077
  ...result,
7970
8078
  traceId: resolvedTraceId || null,
7971
8079
  appOrigin: hostedConfig.appOrigin,
7972
8080
  apiBaseUrl: hostedConfig.billingApiBaseUrl,
7973
- nextSteps: {
8081
+ nextSteps: result.apiKey ? {
7974
8082
  env: `THUMBGATE_API_KEY=${result.apiKey || ''}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
7975
8083
  curl: `curl -X POST ${hostedConfig.billingApiBaseUrl}/v1/feedback/capture \\\n -H 'Authorization: Bearer ${result.apiKey || ''}' \\\n -H 'Content-Type: application/json' \\\n -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
7976
- },
8084
+ } : nextSteps,
7977
8085
  }, getPublicBillingHeaders(resolvedTraceId));
7978
8086
  } catch (err) {
7979
8087
  const requestedTraceId = parsed.searchParams.get('traceId') || '';
@@ -9857,6 +9965,7 @@ module.exports = {
9857
9965
  answerEnterpriseDataChat,
9858
9966
  answerEnterpriseDialogflowChat,
9859
9967
  buildLossAnalyticsResponse,
9968
+ buildIntakeAlertRateLimitKey,
9860
9969
  sanitizeHtmlUnsafeJsonValue,
9861
9970
  },
9862
9971
  };