thumbgate 1.27.20 → 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.
Files changed (63) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +124 -129
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/mcp/server-stdio.js +18 -33
  6. package/adapters/opencode/opencode.json +1 -1
  7. package/bin/cli.js +95 -78
  8. package/bin/postinstall.js +1 -1
  9. package/config/entitlement-public-keys.json +6 -0
  10. package/config/gates/default.json +3 -3
  11. package/config/github-about.json +2 -5
  12. package/config/merge-quality-checks.json +3 -0
  13. package/config/post-deploy-marketing-pages.json +1 -1
  14. package/hooks/hooks.json +38 -0
  15. package/package.json +28 -10
  16. package/public/about.html +1 -4
  17. package/public/agent-manager.html +6 -6
  18. package/public/agents-cost-savings.html +3 -3
  19. package/public/ai-malpractice-prevention.html +1 -1
  20. package/public/blog.html +12 -9
  21. package/public/chatgpt-app.html +3 -3
  22. package/public/codex-enterprise.html +3 -3
  23. package/public/codex-plugin.html +5 -5
  24. package/public/compare.html +10 -10
  25. package/public/dashboard.html +1 -1
  26. package/public/diagnostic.html +23 -1
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +14 -14
  29. package/public/index.html +202 -220
  30. package/public/install.html +14 -14
  31. package/public/learn.html +4 -17
  32. package/public/numbers.html +2 -2
  33. package/public/partner-intake.html +198 -0
  34. package/public/pricing.html +61 -31
  35. package/public/pro.html +2 -2
  36. package/scripts/agent-reward-model.js +13 -0
  37. package/scripts/bayes-optimal-gate.js +6 -1
  38. package/scripts/billing.js +815 -137
  39. package/scripts/checkout-attribution-reference.js +85 -0
  40. package/scripts/claude-feedback-sync.js +23 -6
  41. package/scripts/cli-feedback.js +33 -8
  42. package/scripts/commercial-offer.js +3 -3
  43. package/scripts/entitlement.js +250 -0
  44. package/scripts/export-databricks-bundle.js +5 -0
  45. package/scripts/export-dpo-pairs.js +6 -0
  46. package/scripts/export-hf-dataset.js +5 -0
  47. package/scripts/feedback-loop.js +290 -1
  48. package/scripts/feedback-quality.js +25 -26
  49. package/scripts/feedback-sanitizer.js +151 -3
  50. package/scripts/gates-engine.js +134 -43
  51. package/scripts/hosted-config.js +9 -2
  52. package/scripts/imperative-detector.js +85 -0
  53. package/scripts/intervention-policy.js +13 -0
  54. package/scripts/mailer/resend-mailer.js +11 -5
  55. package/scripts/pr-manager.js +9 -22
  56. package/scripts/pro-local-dashboard.js +198 -0
  57. package/scripts/risk-scorer.js +6 -0
  58. package/scripts/secret-scanner.js +363 -68
  59. package/scripts/self-protection.js +21 -36
  60. package/scripts/seo-gsd.js +2 -2
  61. package/scripts/thompson-sampling.js +11 -2
  62. package/src/api/server.js +202 -22
  63. package/public/assets/brand/github-social-preview.png +0 -0
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';
@@ -20,11 +21,16 @@ const POSTHOG_STATIC_PATH_PREFIX = '/static/';
20
21
  // Stripe catalog, with the per-tier thumbnails wired in. Re-run the
21
22
  // bootstrap workflow to regenerate; the new URLs surface in the workflow
22
23
  // summary log.
24
+ const PRO_CHECKOUT_URL = 'https://buy.stripe.com/8x2dR91M84r4cSd9uj3sI3f';
23
25
  const FIRST_FAILURE_RULE_CHECKOUT_URL = 'https://buy.stripe.com/7sY6oHaiEbTw6tP5e33sI3e';
24
26
  const QUICK_READ_CHECKOUT_URL = 'https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H';
25
27
  const WORKFLOW_TEARDOWN_CHECKOUT_URL = 'https://buy.stripe.com/8x214n2Qc4r44lHayn3sI2I';
26
- const SPRINT_DIAGNOSTIC_CHECKOUT_URL = 'https://buy.stripe.com/28E00j3Uge1E2dzgWL3sI2J';
27
- 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';
28
34
 
29
35
  function getPosthogProxyPath(pathname) {
30
36
  return pathname.slice('/ingest'.length) || '/';
@@ -213,12 +219,17 @@ const {
213
219
  const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
214
220
  const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
215
221
  const mcpOauth = require('../../scripts/mcp-oauth');
222
+ const { packCheckoutReference } = require('../../scripts/checkout-attribution-reference');
216
223
  // OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
217
224
  // (Claude Connectors Directory requires OAuth for authenticated services).
218
225
  const oauthStore = mcpOauth.createStore();
219
226
  const pendingOauthAuthorizeRequests = new Map();
220
227
  const OAUTH_AUTHORIZE_REQUEST_TTL_MS = 10 * 60 * 1000;
221
228
  const resendMailer = require('../../scripts/mailer/resend-mailer');
229
+ const {
230
+ fingerprintProKey,
231
+ sendProActivationAlert,
232
+ } = require('../../scripts/pro-local-dashboard');
222
233
  const {
223
234
  buildContextFootprintReport,
224
235
  } = require('../../scripts/context-footprint');
@@ -242,6 +253,7 @@ const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
242
253
  const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
243
254
  const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
244
255
  const DIAGNOSTIC_PAGE_PATH = path.resolve(__dirname, '../../public/diagnostic.html');
256
+ const PARTNER_INTAKE_PAGE_PATH = path.resolve(__dirname, '../../public/partner-intake.html');
245
257
  const INSTALL_PAGE_PATH = path.resolve(__dirname, '../../public/install.html');
246
258
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
247
259
  const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
@@ -1426,7 +1438,6 @@ function buildCheckoutAttributionMetadata(body, req, traceId) {
1426
1438
  : 1;
1427
1439
 
1428
1440
  return {
1429
- ...rawMetadata,
1430
1441
  traceId,
1431
1442
  acquisitionId: pickFirstText(rawMetadata.acquisitionId, body.acquisitionId),
1432
1443
  visitorId: pickFirstText(rawMetadata.visitorId, body.visitorId),
@@ -1760,8 +1771,10 @@ function appendFeedbackListLines(lines, { entries, signal, intent }) {
1760
1771
  lines.push(`No ${signal || 'feedback'} entries found ${intent.windowLabel}.`);
1761
1772
  return;
1762
1773
  }
1763
- lines.push(`${FEEDBACK_LIST_LABELS[signal] || 'Recent feedback'} (${intent.windowLabel}):`);
1764
- lines.push(...entries.map(formatFeedbackEntry));
1774
+ lines.push(
1775
+ `${FEEDBACK_LIST_LABELS[signal] || 'Recent feedback'} (${intent.windowLabel}):`,
1776
+ ...entries.map(formatFeedbackEntry)
1777
+ );
1765
1778
  }
1766
1779
 
1767
1780
  function buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeline }) {
@@ -2095,6 +2108,10 @@ function buildCheckoutFallbackUrl(baseUrl, metadata = {}) {
2095
2108
  appendQueryParam(url, 'seat_count', metadata.seatCount);
2096
2109
  appendQueryParam(url, 'landing_path', metadata.landingPath);
2097
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));
2098
2115
  return restoreStripeCheckoutPlaceholder(url.toString());
2099
2116
  }
2100
2117
 
@@ -2244,8 +2261,8 @@ a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:
2244
2261
  <div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div>
2245
2262
  <h1>Start ThumbGate Pro</h1>
2246
2263
  <div class="price">$19<small>/mo</small></div>
2247
- <p>The npm package runs your gates locally. <strong>Pro</strong> is what keeps them working across every machine, every agent runtime, and every breaking-change week.</p>
2248
- <form action="https://buy.stripe.com/8x2dR91M84r4cSd9uj3sI3f" method="GET" data-i="pro_checkout_confirmed">
2264
+ <p>The npm package runs your gates locally. <strong>Pro</strong> removes solo caps and adds personal recall, proof, exports, and adapter maintenance. Shared hosted lessons and org dashboards are Enterprise.</p>
2265
+ <form action="${PRO_CHECKOUT_URL}" method="GET" data-i="pro_checkout_confirmed">
2249
2266
  ${hiddenInputs}
2250
2267
  <input type="email" name="prefilled_email" value="${escapeHtmlAttribute(prefilledEmail)}" placeholder="you@company.com" autocomplete="email">
2251
2268
  <p class="email-note">Optional. Stripe can collect your email on the secure checkout page.</p>
@@ -2264,7 +2281,7 @@ ${hiddenInputs}
2264
2281
  </div>
2265
2282
  <div class="feedback-saved" id="feedback-saved">Feedback saved.</div>
2266
2283
  </div>
2267
- <div class="trust"><div class="trust-item">Lessons synced across all your machines no local SQLite to babysit</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode — version drift is our problem, not yours</div><div class="trust-item">Hosted dashboard: gate stats, DPO export, org-wide rule library</div><div class="trust-item">24×7 ops on the rule engine SonarCloud regressions fixed in &lt;24h</div></div>
2284
+ <div class="trust"><div class="trust-item">Personal recall and proof exports without free-tier caps</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode</div><div class="trust-item">Personal dashboard: gate stats, rule evidence, DPO export</div><div class="trust-item">Enterprise adds shared hosted lessons, org visibility, rollout support</div></div>
2268
2285
  <p class="back"><a href="/">← Back to thumbgate.ai</a></p>
2269
2286
  </main>
2270
2287
  <script>
@@ -2539,10 +2556,18 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
2539
2556
  const { FEEDBACK_DIR } = getFeedbackPaths();
2540
2557
  const journeyState = resolveJourneyState(req, parsed);
2541
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
+ }
2542
2567
  if (!isHeadRequest) {
2543
2568
  appendBestEffortTelemetry(
2544
2569
  FEEDBACK_DIR,
2545
- buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl),
2570
+ attribution,
2546
2571
  req.headers,
2547
2572
  `tracked_link_redirect:${target.slug}`
2548
2573
  );
@@ -2789,9 +2814,17 @@ function fillTemplate(template, replacements) {
2789
2814
  }
2790
2815
 
2791
2816
  function escapeHtmlAttribute(value) {
2817
+ // Complete HTML-entity encoder for attribute contexts. Escapes single quotes
2818
+ // and backticks in addition to & " < > so the output is safe in single- AND
2819
+ // double-quoted attributes (and not just the double-quoted case). Fixes
2820
+ // CodeQL js/reflected-xss #252 (search-param `email` reflected into the
2821
+ // checkout page's value="..." attribute) — the prior version omitted ' which
2822
+ // left it context-fragile and unrecognized as a sanitizer.
2792
2823
  return String(value)
2793
2824
  .replaceAll('&', '&amp;')
2794
2825
  .replaceAll('"', '&quot;')
2826
+ .replaceAll("'", '&#39;')
2827
+ .replaceAll('`', '&#96;')
2795
2828
  .replaceAll('<', '&lt;')
2796
2829
  .replaceAll('>', '&gt;');
2797
2830
  }
@@ -2862,6 +2895,11 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
2862
2895
  '__SERVER_SESSION_ID__': pageContext.serverSessionId || '',
2863
2896
  '__SERVER_ACQUISITION_ID__': pageContext.serverAcquisitionId || '',
2864
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'),
2865
2903
  '__VERIFICATION_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
2866
2904
  '__COMPATIBILITY_REPORT_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
2867
2905
  '__AUTOMATION_REPORT_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
@@ -2883,6 +2921,10 @@ function loadDiagnosticPageHtml(runtimeConfig, pageContext = {}) {
2883
2921
  return loadPublicMarketingTemplateHtml(DIAGNOSTIC_PAGE_PATH, runtimeConfig, pageContext);
2884
2922
  }
2885
2923
 
2924
+ function loadPartnerIntakePageHtml(runtimeConfig, pageContext = {}) {
2925
+ return loadPublicMarketingTemplateHtml(PARTNER_INTAKE_PAGE_PATH, runtimeConfig, pageContext);
2926
+ }
2927
+
2886
2928
  function loadInstallPageHtml(runtimeConfig, pageContext = {}) {
2887
2929
  return loadPublicMarketingTemplateHtml(INSTALL_PAGE_PATH, runtimeConfig, pageContext);
2888
2930
  }
@@ -3702,6 +3744,11 @@ function servePublicMarketingPage({
3702
3744
  serverSessionId: journeyState.sessionId,
3703
3745
  serverAcquisitionId: journeyState.acquisitionId,
3704
3746
  serverTelemetryCaptured: landingTelemetryCaptured,
3747
+ serverAttributionSource: landingAttribution.source,
3748
+ serverUtmSource: landingAttribution.utmSource,
3749
+ serverUtmMedium: landingAttribution.utmMedium,
3750
+ serverUtmCampaign: landingAttribution.utmCampaign,
3751
+ serverUtmContent: landingAttribution.utmContent,
3705
3752
  requestHost,
3706
3753
  });
3707
3754
 
@@ -4506,6 +4553,19 @@ function normalizeLeadEmail(value) {
4506
4553
  return email.toLowerCase();
4507
4554
  }
4508
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
+
4509
4569
  function normalizeHttpUrl(value, fieldName) {
4510
4570
  const raw = normalizeNullableText(value);
4511
4571
  if (!raw) throw createHttpError(400, `${fieldName} is required`);
@@ -5221,7 +5281,14 @@ function createApiServer() {
5221
5281
  if (req.method === 'POST' && pathname === '/api/event') {
5222
5282
  // Filter bots from analytics to keep Plausible data clean
5223
5283
  let _botDetector;
5224
- try { _botDetector = require('../../scripts/bot-detection'); } catch (_e) { _botDetector = null; }
5284
+ try {
5285
+ _botDetector = require('../../scripts/bot-detection');
5286
+ } catch (err) {
5287
+ _botDetector = null;
5288
+ if (process.env.THUMBGATE_DEBUG_BOT_DETECTION === '1') {
5289
+ console.warn(`Optional bot-detection module unavailable: ${err.message}`);
5290
+ }
5291
+ }
5225
5292
  if (_botDetector && _botDetector.shouldExcludeFromAnalytics(req)) {
5226
5293
  sendJson(res, 202, { status: 'filtered', reason: 'bot' });
5227
5294
  return;
@@ -5607,6 +5674,16 @@ async function addContext(){
5607
5674
  return;
5608
5675
  }
5609
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
+
5610
5687
  if (isGetLikeRequest && (pathname === '/guide' || pathname === '/guide.html')) {
5611
5688
  try {
5612
5689
  servePublicMarketingPage({
@@ -5742,6 +5819,25 @@ async function addContext(){
5742
5819
  return;
5743
5820
  }
5744
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
+
5745
5841
  // Natural marketing URLs for the Workflow Hardening Diagnostic/Sprint.
5746
5842
  // High-intent outreach refers to "the diagnostic", "the sprint", or
5747
5843
  // "workflow hardening"; serve a focused buyer page instead of sending
@@ -6116,7 +6212,7 @@ async function addContext(){
6116
6212
  // THUMBGATE_CHECKOUT_PRO_STRIPE_URL is supported for future
6117
6213
  // price-link rotation without a redeploy.
6118
6214
  const bypassTarget = process.env.THUMBGATE_CHECKOUT_PRO_STRIPE_URL
6119
- || FIRST_FAILURE_RULE_CHECKOUT_URL;
6215
+ || PRO_CHECKOUT_URL;
6120
6216
  appendBestEffortTelemetry(FEEDBACK_DIR, {
6121
6217
  eventType: 'checkout_interstitial_bypass_redirect',
6122
6218
  clientType: 'web',
@@ -6180,6 +6276,12 @@ async function addContext(){
6180
6276
  ? 'checkout_bot_deflected'
6181
6277
  : 'checkout_interstitial_view';
6182
6278
  const missingConfirmedEmail = hasConfirmFlag && !hasValidCustomerEmailHint;
6279
+ let checkoutDeflectionReason = botClassification.reason;
6280
+ if (missingConfirmedEmail) {
6281
+ checkoutDeflectionReason = hasCustomerEmailHint
6282
+ ? 'invalid_customer_email'
6283
+ : 'missing_customer_email';
6284
+ }
6183
6285
  appendBestEffortTelemetry(FEEDBACK_DIR, {
6184
6286
  eventType,
6185
6287
  clientType: 'web',
@@ -6203,9 +6305,7 @@ async function addContext(){
6203
6305
  isBot: botClassification.isBot ? 'true' : 'false',
6204
6306
  interstitialSampled: interstitialSampled ? 'true' : 'false',
6205
6307
  interstitialSampleRate,
6206
- reason: missingConfirmedEmail
6207
- ? (hasCustomerEmailHint ? 'invalid_customer_email' : 'missing_customer_email')
6208
- : botClassification.reason,
6308
+ reason: checkoutDeflectionReason,
6209
6309
  confirmEmailRequired: missingConfirmedEmail ? 'true' : 'false',
6210
6310
  }, req.headers, eventType);
6211
6311
  const prefilledEmail = parsed?.searchParams?.get('customer_email') || '';
@@ -7411,7 +7511,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7411
7511
  ? await parseFormBody(req, 24 * 1024)
7412
7512
  : await parseJsonBody(req, 24 * 1024);
7413
7513
  const workflowSprintIntake = requirePrivateApiModule('workflowSprintIntake', 'Workflow sprint intake');
7414
- const lead = workflowSprintIntake.appendWorkflowSprintLead({
7514
+ const leadPayload = {
7415
7515
  ...body,
7416
7516
  traceId: body.traceId || traceId,
7417
7517
  acquisitionId: body.acquisitionId || journeyState.acquisitionId,
@@ -7436,7 +7536,17 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7436
7536
  offerCode: body.offerCode || referrerAttribution.offerCode || null,
7437
7537
  referrerHost: body.referrerHost || referrerAttribution.referrerHost || null,
7438
7538
  referrer: body.referrer || referrerAttribution.referrer || null,
7439
- }, { 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 });
7440
7550
 
7441
7551
  appendBestEffortTelemetry(FEEDBACK_DIR, {
7442
7552
  eventType: 'workflow_sprint_lead_submitted',
@@ -7467,6 +7577,11 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7467
7577
  referrer: lead.attribution.referrer,
7468
7578
  }, req.headers, 'workflow_sprint_lead_submitted');
7469
7579
 
7580
+ const leadNotification = await workflowSprintIntake.notifyWorkflowSprintLead(lead, {
7581
+ sendEmailImpl: resendMailer.sendEmail,
7582
+ rateLimitKey: buildIntakeAlertRateLimitKey(req),
7583
+ });
7584
+
7470
7585
  if (isFormSubmission && !wantsJson(req, parsed)) {
7471
7586
  sendHtml(
7472
7587
  res,
@@ -7487,6 +7602,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7487
7602
  status: lead.status,
7488
7603
  offer: lead.offer,
7489
7604
  nextStep: 'review_proof_pack',
7605
+ operatorAlertSent: leadNotification.sent === true,
7606
+ operatorAlertReason: leadNotification.reason || null,
7490
7607
  proofPackUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
7491
7608
  sprintBriefUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/WORKFLOW_HARDENING_SPRINT.md',
7492
7609
  }, {
@@ -7494,7 +7611,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7494
7611
  ...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
7495
7612
  });
7496
7613
  } catch (err) {
7497
- appendBestEffortTelemetry(FEEDBACK_DIR, {
7614
+ appendBestEffortTelemetry(FEEDBACK_DIR, redactSecretsDeep({
7498
7615
  eventType: 'workflow_sprint_lead_failed',
7499
7616
  clientType: 'web',
7500
7617
  traceId,
@@ -7517,10 +7634,10 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7517
7634
  page: referrerAttribution.page || '/#workflow-sprint-intake',
7518
7635
  landingPath: referrerAttribution.landingPath || '/',
7519
7636
  referrerHost: referrerAttribution.referrerHost,
7520
- referrer: referrerAttribution.referrer,
7521
- 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'),
7522
7639
  httpStatus: err && err.statusCode ? err.statusCode : null,
7523
- }, req.headers, 'workflow_sprint_lead_failed');
7640
+ }), redactSecretsDeep(req.headers), 'workflow_sprint_lead_failed');
7524
7641
  if (isFormSubmission && !wantsJson(req, parsed)) {
7525
7642
  sendHtml(
7526
7643
  res,
@@ -7806,6 +7923,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7806
7923
  });
7807
7924
  return;
7808
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
+ }
7809
7935
  sendJson(res, 200, result);
7810
7936
 
7811
7937
  } catch (err) {
@@ -7938,16 +8064,24 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7938
8064
  }
7939
8065
 
7940
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
+ : {};
7941
8075
 
7942
8076
  sendJson(res, 200, {
7943
8077
  ...result,
7944
8078
  traceId: resolvedTraceId || null,
7945
8079
  appOrigin: hostedConfig.appOrigin,
7946
8080
  apiBaseUrl: hostedConfig.billingApiBaseUrl,
7947
- nextSteps: {
8081
+ nextSteps: result.apiKey ? {
7948
8082
  env: `THUMBGATE_API_KEY=${result.apiKey || ''}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
7949
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"}'`,
7950
- },
8084
+ } : nextSteps,
7951
8085
  }, getPublicBillingHeaders(resolvedTraceId));
7952
8086
  } catch (err) {
7953
8087
  const requestedTraceId = parsed.searchParams.get('traceId') || '';
@@ -9206,6 +9340,48 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9206
9340
  return;
9207
9341
  }
9208
9342
 
9343
+ // POST /v1/billing/pro-activation — customer key activation alert.
9344
+ if (req.method === 'POST' && pathname === '/v1/billing/pro-activation') {
9345
+ const token = extractBearerToken(req);
9346
+ const validation = validateApiKey(token);
9347
+ if (!validation.valid) {
9348
+ sendProblem(res, {
9349
+ type: PROBLEM_TYPES.UNAUTHORIZED,
9350
+ title: 'Unauthorized',
9351
+ status: 401,
9352
+ detail: 'A valid API key is required to access this endpoint.',
9353
+ });
9354
+ return;
9355
+ }
9356
+
9357
+ const body = await parseJsonBody(req);
9358
+ const keyFingerprint = fingerprintProKey(token);
9359
+ const alert = await sendProActivationAlert({
9360
+ key: token,
9361
+ source: body.source || 'hosted_pro_activation',
9362
+ version: body.version || null,
9363
+ customerId: validation.customerId,
9364
+ installId: validation.installId,
9365
+ usageCount: validation.usageCount,
9366
+ env: process.env,
9367
+ });
9368
+
9369
+ sendJson(res, 200, {
9370
+ ok: true,
9371
+ customerId: validation.customerId,
9372
+ installId: validation.installId,
9373
+ usageCount: validation.usageCount,
9374
+ keyFingerprint,
9375
+ keyFingerprintMatchesClient: body.keyFingerprint ? body.keyFingerprint === keyFingerprint : null,
9376
+ alert: {
9377
+ sent: Boolean(alert?.sent),
9378
+ reason: alert?.reason || null,
9379
+ id: alert?.id || null,
9380
+ },
9381
+ });
9382
+ return;
9383
+ }
9384
+
9209
9385
  // POST /v1/billing/provision — manually provision key (admin)
9210
9386
  if (req.method === 'POST' && pathname === '/v1/billing/provision') {
9211
9387
  if (!isStaticAdminAuthorized(req, expectedApiKey)) {
@@ -9765,6 +9941,9 @@ module.exports = {
9765
9941
  createApiServer,
9766
9942
  startServer,
9767
9943
  __test__: {
9944
+ PRO_CHECKOUT_URL,
9945
+ escapeHtmlAttribute,
9946
+ renderCheckoutIntentPage,
9768
9947
  buildCheckoutFallbackUrl,
9769
9948
  createPrivateCoreUnavailableError,
9770
9949
  buildPosthogProxyRequestOptions,
@@ -9786,6 +9965,7 @@ module.exports = {
9786
9965
  answerEnterpriseDataChat,
9787
9966
  answerEnterpriseDialogflowChat,
9788
9967
  buildLossAnalyticsResponse,
9968
+ buildIntakeAlertRateLimitKey,
9789
9969
  sanitizeHtmlUnsafeJsonValue,
9790
9970
  },
9791
9971
  };