thumbgate 1.28.4 → 1.29.2

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 (87) hide show
  1. package/.claude/commands/dashboard.md +11 -1
  2. package/.claude/commands/thumbgate-dashboard.md +23 -8
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/llms.txt +18 -10
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +66 -3
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/forge/forge.yaml +3 -3
  9. package/adapters/mcp/server-stdio.js +88 -2
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bin/cli.js +8 -8
  12. package/bin/postinstall.js +4 -13
  13. package/commands/dashboard.md +11 -1
  14. package/commands/thumbgate-dashboard.md +23 -8
  15. package/config/agent-outcome-monitor-thresholds.json +63 -0
  16. package/config/evals/agent-outcomes-baseline.json +17 -0
  17. package/config/evals/agent-outcomes-golden.json +412 -0
  18. package/config/evals/prompt-eval-baseline.json +23 -0
  19. package/config/github-about.json +5 -4
  20. package/config/post-deploy-marketing-pages.json +6 -6
  21. package/config/schemas/task-outcome-receipt.schema.json +296 -0
  22. package/docs/integrations/grafana/README.md +109 -0
  23. package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
  24. package/openapi/openapi.yaml +475 -5
  25. package/package.json +75 -22
  26. package/public/agent-manager.html +10 -11
  27. package/public/agents-cost-savings.html +2 -2
  28. package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
  29. package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
  30. package/public/assets/brand/thumbgate-mark.svg +10 -11
  31. package/public/blog/inside-your-boundary.html +114 -0
  32. package/public/blog/process-over-outcome-gates.html +119 -0
  33. package/public/blog.html +296 -402
  34. package/public/brand/thumbgate-mark.svg +5 -9
  35. package/public/codex-enterprise.html +2 -2
  36. package/public/compare.html +12 -3
  37. package/public/diagnostic.html +79 -29
  38. package/public/guide.html +4 -4
  39. package/public/index.html +1090 -2098
  40. package/public/install.html +3 -3
  41. package/public/js/buyer-intent.js +33 -18
  42. package/public/numbers.html +2 -2
  43. package/public/pricing.html +268 -408
  44. package/public/pro.html +4 -4
  45. package/scripts/agent-outcome-eval.js +130 -0
  46. package/scripts/agent-outcome-monitor.js +261 -0
  47. package/scripts/agent-reasoning-traces.js +8 -9
  48. package/scripts/async-job-runner.js +107 -13
  49. package/scripts/billing.js +456 -126
  50. package/scripts/buyer-paths.js +102 -0
  51. package/scripts/cli-feedback.js +2 -2
  52. package/scripts/commercial-offer.js +18 -10
  53. package/scripts/durability/step.js +121 -12
  54. package/scripts/external-customer-audit.js +881 -0
  55. package/scripts/feedback-loop.js +26 -0
  56. package/scripts/gates-engine.js +554 -19
  57. package/scripts/grafana-revenue-evidence.js +856 -0
  58. package/scripts/human-escalation.js +265 -0
  59. package/scripts/hybrid-feedback-context.js +93 -50
  60. package/scripts/jsonl-window.js +89 -0
  61. package/scripts/judge-reward-function.js +30 -18
  62. package/scripts/lesson-embedding-index.js +3 -7
  63. package/scripts/meta-agent-loop.js +20 -2
  64. package/scripts/observability-env.js +139 -0
  65. package/scripts/observability-setup.js +55 -0
  66. package/scripts/plausible-domain-config.js +4 -0
  67. package/scripts/prompt-eval.js +81 -4
  68. package/scripts/provider-live-evidence.js +1290 -0
  69. package/scripts/provider-payment-reconciler.js +442 -0
  70. package/scripts/provider-revenue-evidence.js +249 -0
  71. package/scripts/rate-limiter.js +1 -5
  72. package/scripts/revenue-action-eligibility.js +414 -0
  73. package/scripts/revenue-evidence-remediation.js +694 -0
  74. package/scripts/revenue-offer-system.js +709 -0
  75. package/scripts/sales-pipeline.js +1117 -0
  76. package/scripts/schedule-manager.js +249 -0
  77. package/scripts/seo-gsd.js +8 -4
  78. package/scripts/stripe-credentials.js +37 -0
  79. package/scripts/stripe-revenue-catalog-audit.js +363 -0
  80. package/scripts/stripe-revenue-catalog.js +164 -0
  81. package/scripts/task-outcomes.js +425 -0
  82. package/scripts/telemetry-analytics.js +23 -3
  83. package/scripts/tool-contract-validator.js +287 -59
  84. package/scripts/tool-registry.js +143 -0
  85. package/scripts/vector-store.js +83 -7
  86. package/scripts/workflow-intake-queue.js +483 -0
  87. package/src/api/server.js +647 -118
package/src/api/server.js CHANGED
@@ -11,6 +11,11 @@ const {
11
11
  loadOptionalModule,
12
12
  } = require('../../scripts/private-core-boundary');
13
13
  const { redactSecrets, redactSecretsDeep } = require('../../scripts/secret-redaction');
14
+ const {
15
+ buildIntakeClosePacket,
16
+ buildIntakeDiscoveryPacket,
17
+ buildIntakeQualificationCard,
18
+ } = require('../../scripts/revenue-offer-system');
14
19
 
15
20
  const POSTHOG_API_PATHS = new Set(['/capture', '/batch', '/decide', '/e', '/engage']);
16
21
  const POSTHOG_INGEST_HOST = 'us.i.posthog.com';
@@ -22,9 +27,6 @@ const POSTHOG_STATIC_PATH_PREFIX = '/static/';
22
27
  // bootstrap workflow to regenerate; the new URLs surface in the workflow
23
28
  // summary log.
24
29
  const PRO_CHECKOUT_URL = 'https://buy.stripe.com/8x2dR91M84r4cSd9uj3sI3f';
25
- const FIRST_FAILURE_RULE_CHECKOUT_URL = 'https://buy.stripe.com/7sY6oHaiEbTw6tP5e33sI3e';
26
- const QUICK_READ_CHECKOUT_URL = 'https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H';
27
- const WORKFLOW_TEARDOWN_CHECKOUT_URL = 'https://buy.stripe.com/8x214n2Qc4r44lHayn3sI2I';
28
30
  // Verified live amounts (2026-07-13): diagnostic Stripe page contains $499;
29
31
  // sprint code-default Stripe slug previously pointed at a $499 diagnostic product.
30
32
  // Production sprint rail is PayPal at $1500 — pin that as the code fallback so a
@@ -129,11 +131,13 @@ const {
129
131
  rotateApiKey,
130
132
  handleWebhook,
131
133
  verifyWebhookSignature,
132
- verifyGithubWebhookSignature,
134
+ recordGithubMarketplaceWebhookDelivery,
135
+ recordPayPalWebhookDelivery,
133
136
  handleGithubWebhook,
134
137
  getFunnelAnalytics,
135
138
  getBillingSummary,
136
139
  getBillingSummaryLive,
140
+ getBillingSummariesLive,
137
141
  } = require('../../scripts/billing');
138
142
  const {
139
143
  DEFAULT_PUBLIC_APP_ORIGIN,
@@ -162,6 +166,19 @@ const {
162
166
  const {
163
167
  evaluateWorkflowSentinel,
164
168
  } = require('../../scripts/workflow-sentinel');
169
+ const {
170
+ calculateTaskOutcomeMetrics,
171
+ getTaskOutcome,
172
+ readTaskOutcomes,
173
+ recordTaskOutcome,
174
+ } = require('../../scripts/task-outcomes');
175
+ const { monitorTaskOutcomes } = require('../../scripts/agent-outcome-monitor');
176
+ const {
177
+ calculateEscalationMetrics,
178
+ decideEscalation,
179
+ listEscalations,
180
+ requestEscalation,
181
+ } = require('../../scripts/human-escalation');
165
182
  const {
166
183
  normalizeProviderAction,
167
184
  } = require('../../scripts/provider-action-normalizer');
@@ -259,12 +276,14 @@ const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
259
276
  const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
260
277
  const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
261
278
  const USE_CASES_DIR = path.resolve(__dirname, '../../public/use-cases');
279
+ const BLOG_DIR = path.resolve(__dirname, '../../public/blog');
262
280
  const PUBLIC_DIR = path.resolve(__dirname, '../../public');
263
281
  const PUBLIC_ASSETS_DIR = path.resolve(__dirname, '../../public/assets');
264
282
  const LEARN_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(LEARN_DIR);
265
283
  const GUIDE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(GUIDES_DIR);
266
284
  const COMPARE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(COMPARE_DIR);
267
285
  const USE_CASE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(USE_CASES_DIR);
286
+ const BLOG_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(BLOG_DIR);
268
287
  const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
269
288
  const STATIC_MIME_BY_EXT = Object.freeze({
270
289
  '.png': 'image/png',
@@ -482,34 +501,46 @@ const TRACKED_LINK_TARGETS = Object.freeze({
482
501
  allowCustomerEmail: true,
483
502
  },
484
503
  diagnostic: {
485
- configUrlKey: 'sprintDiagnosticCheckoutUrl',
486
- fallbackHref: SPRINT_DIAGNOSTIC_CHECKOUT_URL,
487
- external: true,
504
+ path: '/diagnostic',
488
505
  ctaId: 'go_diagnostic',
489
506
  ctaPlacement: 'link_router',
490
- eventType: 'cta_click',
507
+ eventType: 'diagnostic_intent_open',
491
508
  defaults: {
492
509
  utm_source: 'website',
493
510
  utm_medium: 'link_router',
494
511
  utm_campaign: 'sprint_diagnostic',
495
512
  plan_id: 'sprint_diagnostic',
496
513
  },
514
+ },
515
+ 'diagnostic-pay': {
516
+ configUrlKey: 'sprintDiagnosticCheckoutUrl',
517
+ fallbackHref: SPRINT_DIAGNOSTIC_CHECKOUT_URL,
518
+ external: true,
519
+ ctaId: 'go_diagnostic_pay',
520
+ ctaPlacement: 'diagnostic_confirmation',
521
+ eventType: 'diagnostic_checkout_confirmed',
522
+ requiresPost: true,
523
+ requiresBuyerEmail: true,
524
+ prefillStripeEmail: true,
525
+ defaults: {
526
+ utm_source: 'diagnostic_page',
527
+ utm_medium: 'diagnostic_confirmation',
528
+ utm_campaign: 'sprint_diagnostic',
529
+ plan_id: 'sprint_diagnostic',
530
+ },
497
531
  allowCustomerEmail: true,
498
532
  },
499
533
  sprint: {
500
- configUrlKey: 'workflowSprintCheckoutUrl',
501
- fallbackHref: WORKFLOW_SPRINT_CHECKOUT_URL,
502
- external: true,
534
+ path: '/diagnostic',
503
535
  ctaId: 'go_sprint',
504
536
  ctaPlacement: 'link_router',
505
- eventType: 'cta_click',
537
+ eventType: 'workflow_sprint_scope_open',
506
538
  defaults: {
507
539
  utm_source: 'website',
508
540
  utm_medium: 'link_router',
509
541
  utm_campaign: 'workflow_sprint',
510
542
  plan_id: 'workflow_sprint',
511
543
  },
512
- allowCustomerEmail: true,
513
544
  },
514
545
  trial: {
515
546
  path: '/guide',
@@ -1519,6 +1550,180 @@ function resolveBillingSummaryOptions(parsed) {
1519
1550
  });
1520
1551
  }
1521
1552
 
1553
+ const BILLING_SUMMARY_BATCH_WINDOWS = Object.freeze(['today', '30d', 'lifetime']);
1554
+ const WORKFLOW_INTAKE_QUEUE_DEFAULT_LIMIT = 50;
1555
+ const WORKFLOW_INTAKE_QUEUE_MAX_LIMIT = 100;
1556
+ const PRIVATE_OPERATOR_RESPONSE_HEADERS = Object.freeze({
1557
+ 'Cache-Control': 'private, no-store, max-age=0',
1558
+ Pragma: 'no-cache',
1559
+ Vary: 'Authorization',
1560
+ });
1561
+
1562
+ function resolveBillingSummaryBatchOptions(parsed) {
1563
+ return Object.fromEntries(BILLING_SUMMARY_BATCH_WINDOWS.map((window) => [
1564
+ window,
1565
+ resolveAnalyticsWindow({
1566
+ window,
1567
+ timeZone: parsed.searchParams.get('timezone'),
1568
+ now: parsed.searchParams.get('now'),
1569
+ }),
1570
+ ]));
1571
+ }
1572
+
1573
+ function wantsWorkflowIntakeQueue(parsed) {
1574
+ return ['1', 'true', 'yes'].includes(
1575
+ String(parsed.searchParams.get('include_intake_queue') || '').trim().toLowerCase()
1576
+ );
1577
+ }
1578
+
1579
+ function resolveWorkflowIntakeQueueOptions(parsed, workflowSprintIntake) {
1580
+ const rawLimit = Number(
1581
+ parsed.searchParams.get('intake_limit') ||
1582
+ parsed.searchParams.get('limit') ||
1583
+ WORKFLOW_INTAKE_QUEUE_DEFAULT_LIMIT
1584
+ );
1585
+ if (!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > WORKFLOW_INTAKE_QUEUE_MAX_LIMIT) {
1586
+ throw createHttpError(
1587
+ 400,
1588
+ `intake_limit must be an integer from 1 to ${WORKFLOW_INTAKE_QUEUE_MAX_LIMIT}`
1589
+ );
1590
+ }
1591
+
1592
+ const allowedStatuses = new Set(workflowSprintIntake.WORKFLOW_SPRINT_STATUS_FLOW || []);
1593
+ const rawStatuses = String(
1594
+ parsed.searchParams.get('intake_status') ||
1595
+ parsed.searchParams.get('status') ||
1596
+ 'new,qualified'
1597
+ )
1598
+ .split(',')
1599
+ .map((value) => value.trim().toLowerCase())
1600
+ .filter(Boolean);
1601
+ const allRequested = rawStatuses.length === 1 && rawStatuses[0] === 'all';
1602
+ const statuses = allRequested ? [] : [...new Set(rawStatuses)];
1603
+ const invalid = statuses.filter((status) => !allowedStatuses.has(status));
1604
+ if (invalid.length > 0 || (!allRequested && statuses.length === 0)) {
1605
+ throw createHttpError(
1606
+ 400,
1607
+ `intake_status must be all or a comma-separated subset of: ${[...allowedStatuses].join(', ')}`
1608
+ );
1609
+ }
1610
+
1611
+ return { limit: rawLimit, statuses };
1612
+ }
1613
+
1614
+ function projectWorkflowIntakeQueueLead(lead = {}, options = {}) {
1615
+ const contact = lead.contact || {};
1616
+ const qualification = lead.qualification || {};
1617
+ const attribution = lead.attribution || {};
1618
+ return {
1619
+ leadId: lead.leadId || null,
1620
+ submittedAt: lead.submittedAt || null,
1621
+ updatedAt: lead.updatedAt || null,
1622
+ status: lead.status || null,
1623
+ offer: lead.offer || null,
1624
+ contact: {
1625
+ name: contact.name || null,
1626
+ email: contact.email || null,
1627
+ company: contact.company || null,
1628
+ },
1629
+ qualification: {
1630
+ workflow: qualification.workflow || null,
1631
+ owner: qualification.owner || null,
1632
+ blocker: qualification.blocker || null,
1633
+ runtime: qualification.runtime || null,
1634
+ urgency: qualification.urgency || null,
1635
+ note: qualification.note || null,
1636
+ },
1637
+ attribution: {
1638
+ source: attribution.source || null,
1639
+ utmSource: attribution.utmSource || null,
1640
+ utmMedium: attribution.utmMedium || null,
1641
+ utmCampaign: attribution.utmCampaign || null,
1642
+ creator: attribution.creator || null,
1643
+ community: attribution.community || null,
1644
+ planId: attribution.planId || null,
1645
+ page: attribution.page || null,
1646
+ landingPath: attribution.landingPath || null,
1647
+ },
1648
+ nextOperatorStep: options.closePacket?.status === 'approval_ready_not_authorized'
1649
+ ? 'request_action_time_approval'
1650
+ : options.discoveryPacket?.status === 'approval_ready_not_authorized'
1651
+ ? 'request_action_time_approval_for_discovery'
1652
+ : lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold',
1653
+ qualificationCard: options.qualificationCard || buildIntakeQualificationCard(lead, options),
1654
+ discoveryPacket: options.discoveryPacket || buildIntakeDiscoveryPacket(lead, options),
1655
+ closePacket: options.closePacket || buildIntakeClosePacket(lead, options),
1656
+ };
1657
+ }
1658
+
1659
+ function buildWorkflowIntakeQueue(parsed, workflowSprintIntake, feedbackDir, options = {}) {
1660
+ const { limit, statuses } = resolveWorkflowIntakeQueueOptions(parsed, workflowSprintIntake);
1661
+ const leads = workflowSprintIntake.loadWorkflowSprintLeads(feedbackDir);
1662
+ const generatedAt = new Date().toISOString();
1663
+ const byStatus = {};
1664
+ for (const lead of leads) {
1665
+ const status = lead.status || 'unknown';
1666
+ byStatus[status] = (byStatus[status] || 0) + 1;
1667
+ }
1668
+ const eligible = leads
1669
+ .filter((lead) => statuses.length === 0 || statuses.includes(lead.status))
1670
+ .map((lead) => {
1671
+ const qualificationReviewVerified =
1672
+ typeof workflowSprintIntake.isEvidenceBasedQualificationReview === 'function' &&
1673
+ workflowSprintIntake.isEvidenceBasedQualificationReview(lead.qualificationReview);
1674
+ const cardOptions = {
1675
+ now: generatedAt,
1676
+ qualificationReviewVerified,
1677
+ publicOrigin: options.publicOrigin,
1678
+ };
1679
+ return {
1680
+ lead,
1681
+ qualificationCard: buildIntakeQualificationCard(lead, cardOptions),
1682
+ discoveryPacket: buildIntakeDiscoveryPacket(lead, cardOptions),
1683
+ closePacket: buildIntakeClosePacket(lead, cardOptions),
1684
+ };
1685
+ })
1686
+ .sort((a, b) =>
1687
+ b.qualificationCard.priorityScore - a.qualificationCard.priorityScore ||
1688
+ String(b.lead.submittedAt || '').localeCompare(String(a.lead.submittedAt || '')));
1689
+ const selected = eligible.slice(0, limit);
1690
+ const approvalReady = eligible.filter(
1691
+ (entry) => entry.closePacket.status === 'approval_ready_not_authorized'
1692
+ );
1693
+ const discoveryReady = eligible.filter(
1694
+ (entry) => entry.discoveryPacket.status === 'approval_ready_not_authorized'
1695
+ );
1696
+ const chronological = eligible
1697
+ .map((entry) => entry.lead)
1698
+ .sort((a, b) => String(b.submittedAt || '').localeCompare(String(a.submittedAt || '')));
1699
+ return {
1700
+ generatedAt,
1701
+ total: leads.length,
1702
+ eligibleTotal: eligible.length,
1703
+ returned: selected.length,
1704
+ approvalReadyTotal: approvalReady.length,
1705
+ discoveryReadyTotal: discoveryReady.length,
1706
+ primaryApprovalAction: approvalReady[0]?.closePacket || null,
1707
+ primaryDiscoveryAction: discoveryReady[0]?.discoveryPacket || null,
1708
+ byStatus,
1709
+ filters: {
1710
+ statuses: statuses.length === 0 ? ['all'] : statuses,
1711
+ limit,
1712
+ },
1713
+ latestSubmittedAt: chronological[0] ? chronological[0].submittedAt || null : null,
1714
+ oldestSubmittedAt: chronological.length ? chronological[chronological.length - 1].submittedAt || null : null,
1715
+ leads: selected.map((entry, index) => ({
1716
+ ...projectWorkflowIntakeQueueLead(entry.lead, {
1717
+ now: generatedAt,
1718
+ qualificationCard: entry.qualificationCard,
1719
+ discoveryPacket: entry.discoveryPacket,
1720
+ closePacket: entry.closePacket,
1721
+ }),
1722
+ priorityRank: index + 1,
1723
+ })),
1724
+ };
1725
+ }
1726
+
1522
1727
  function sendInvalidAnalyticsWindowProblem(res, title, err) {
1523
1728
  sendProblem(res, {
1524
1729
  type: PROBLEM_TYPES.INVALID_REQUEST,
@@ -1528,6 +1733,18 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
1528
1733
  });
1529
1734
  }
1530
1735
 
1736
+ function sendWorkflowIntakeQueueProblem(res, err) {
1737
+ const status = Number(err && err.statusCode) || 400;
1738
+ sendProblem(res, {
1739
+ type: status >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.INVALID_REQUEST,
1740
+ title: status >= 500
1741
+ ? 'Workflow intake queue unavailable'
1742
+ : 'Invalid workflow intake queue query',
1743
+ status,
1744
+ detail: err?.message ? err.message : 'Workflow intake queue request failed.',
1745
+ });
1746
+ }
1747
+
1531
1748
  function resolveBillingSummaryOptionsOrRespondProblem(res, parsed, invalidTitle) {
1532
1749
  try {
1533
1750
  return resolveBillingSummaryOptions(parsed);
@@ -2261,11 +2478,12 @@ a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:
2261
2478
  <div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div>
2262
2479
  <h1>Start ThumbGate Pro</h1>
2263
2480
  <div class="price">$19<small>/mo</small></div>
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">
2481
+ <p>The npm package runs your gates locally. <strong>Pro</strong> removes solo caps and adds personal recall, proof, exports, and adapter maintenance. Enterprise service work is qualified after intake; hosted team sync and a hosted org dashboard are not generally available.</p>
2482
+ <form action="/checkout/pro" method="POST" data-i="pro_checkout_confirmed">
2266
2483
  ${hiddenInputs}
2267
- <input type="email" name="prefilled_email" value="${escapeHtmlAttribute(prefilledEmail)}" placeholder="you@company.com" autocomplete="email">
2268
- <p class="email-note">Optional. Stripe can collect your email on the secure checkout page.</p>
2484
+ <input type="hidden" name="confirm" value="1">
2485
+ <input type="email" name="customer_email" value="${escapeHtmlAttribute(prefilledEmail)}" placeholder="you@company.com" autocomplete="email" required>
2486
+ <p class="email-note">Required so the checkout has attributable buyer intent and your receipt can be recovered.</p>
2269
2487
  <button type="submit" class="primary">Pay $19/mo with Stripe →</button>
2270
2488
  </form>
2271
2489
  <a class="secondary" data-i="workflow_sprint_intake" href="/#workflow-sprint-intake">Not sure yet? Send the workflow first</a>
@@ -2281,7 +2499,7 @@ ${hiddenInputs}
2281
2499
  </div>
2282
2500
  <div class="feedback-saved" id="feedback-saved">Feedback saved.</div>
2283
2501
  </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>
2502
+ <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 service work is qualified after intake; hosted team sync and a hosted org dashboard are not generally available</div></div>
2285
2503
  <p class="back"><a href="/">← Back to thumbgate.ai</a></p>
2286
2504
  </main>
2287
2505
  <script>
@@ -2475,7 +2693,14 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
2475
2693
  }
2476
2694
  }
2477
2695
  if (target.allowCustomerEmail) {
2478
- appendQueryParam(destinationUrl, 'customer_email', params.get('customer_email'));
2696
+ const customerEmail = normalizeCheckoutCustomerEmail(params.get('customer_email'));
2697
+ if (customerEmail) {
2698
+ appendQueryParam(
2699
+ destinationUrl,
2700
+ target.prefillStripeEmail ? 'prefilled_email' : 'customer_email',
2701
+ customerEmail
2702
+ );
2703
+ }
2479
2704
  }
2480
2705
  if (!destinationUrl.searchParams.has('cta_id')) {
2481
2706
  appendQueryParam(destinationUrl, 'cta_id', target.ctaId);
@@ -2553,6 +2778,31 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
2553
2778
  return;
2554
2779
  }
2555
2780
 
2781
+ if (target.requiresPost && req.method !== 'POST') {
2782
+ sendJson(res, 405, {
2783
+ error: 'Explicit checkout confirmation is required',
2784
+ next: '/diagnostic',
2785
+ }, {
2786
+ Allow: 'POST',
2787
+ 'Cache-Control': 'no-store',
2788
+ }, {
2789
+ headOnly: isHeadRequest,
2790
+ });
2791
+ return;
2792
+ }
2793
+ if (
2794
+ target.requiresBuyerEmail
2795
+ && !normalizeCheckoutCustomerEmail(parsed.searchParams.get('customer_email'))
2796
+ ) {
2797
+ sendJson(res, 400, {
2798
+ error: 'A valid buyer email is required before checkout',
2799
+ next: '/diagnostic',
2800
+ }, {
2801
+ 'Cache-Control': 'no-store',
2802
+ });
2803
+ return;
2804
+ }
2805
+
2556
2806
  const { FEEDBACK_DIR } = getFeedbackPaths();
2557
2807
  const journeyState = resolveJourneyState(req, parsed);
2558
2808
  const destinationUrl = buildTrackedLinkDestination(target, hostedConfig, parsed);
@@ -2573,7 +2823,7 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
2573
2823
  );
2574
2824
  }
2575
2825
 
2576
- res.writeHead(302, {
2826
+ res.writeHead(req.method === 'POST' ? 303 : 302, {
2577
2827
  ...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
2578
2828
  'Cache-Control': 'no-store',
2579
2829
  'Referrer-Policy': 'strict-origin-when-cross-origin',
@@ -3599,6 +3849,7 @@ function renderSitemapXml(runtimeConfig) {
3599
3849
  const seoDirectories = [
3600
3850
  { dir: 'compare', route: '/compare', priority: '0.85' },
3601
3851
  { dir: 'guides', route: '/guides', priority: '0.85' },
3852
+ { dir: 'blog', route: '/blog', priority: '0.8' },
3602
3853
  ];
3603
3854
  for (const catalog of seoDirectories) {
3604
3855
  const files = fs.readdirSync(path.join(PUBLIC_DIR, catalog.dir)).sort((a, b) => a.localeCompare(b));
@@ -3637,6 +3888,8 @@ function buildHostedRuntimePresence(hostedConfig, { expectedApiKey, expectedOper
3637
3888
  THUMBGATE_FEEDBACK_DIR: Boolean(process.env.THUMBGATE_FEEDBACK_DIR),
3638
3889
  THUMBGATE_OPERATOR_KEY: Boolean(expectedOperatorKey),
3639
3890
  THUMBGATE_API_KEY: Boolean(expectedApiKey),
3891
+ THUMBGATE_HUMAN_REVIEWER_KEY: Boolean(process.env.THUMBGATE_HUMAN_REVIEWER_KEY),
3892
+ THUMBGATE_HUMAN_REVIEWER_ID: Boolean(process.env.THUMBGATE_HUMAN_REVIEWER_ID),
3640
3893
  THUMBGATE_PUBLIC_APP_ORIGIN: Boolean(hostedConfig.appOrigin),
3641
3894
  THUMBGATE_BILLING_API_BASE_URL: Boolean(hostedConfig.billingApiBaseUrl),
3642
3895
  THUMBGATE_GA_MEASUREMENT_ID: Boolean(hostedConfig.gaMeasurementId),
@@ -4113,30 +4366,16 @@ function renderCheckoutSuccessPage(runtimeConfig) {
4113
4366
  }
4114
4367
 
4115
4368
  function renderCheckoutCancelledPage(runtimeConfig) {
4116
- const firstFailureRuleCheckoutUrl = escapeHtmlAttribute(FIRST_FAILURE_RULE_CHECKOUT_URL);
4117
- const quickReadCheckoutUrl = escapeHtmlAttribute(QUICK_READ_CHECKOUT_URL);
4118
- const workflowTeardownCheckoutUrl = escapeHtmlAttribute(WORKFLOW_TEARDOWN_CHECKOUT_URL);
4119
- const diagnosticCheckoutUrl = runtimeConfig.sprintDiagnosticCheckoutUrl
4120
- ? escapeHtmlAttribute(runtimeConfig.sprintDiagnosticCheckoutUrl)
4121
- : '';
4122
- const workflowSprintCheckoutUrl = runtimeConfig.workflowSprintCheckoutUrl
4123
- ? escapeHtmlAttribute(runtimeConfig.workflowSprintCheckoutUrl)
4124
- : '';
4125
4369
  const sprintDiagnosticPriceDollars = runtimeConfig.sprintDiagnosticPriceDollars || 499;
4126
4370
  const workflowSprintPriceDollars = runtimeConfig.workflowSprintPriceDollars || 1500;
4127
- const workflowSprintIntakeUrl = `${escapeHtmlAttribute(runtimeConfig.appOrigin)}/#workflow-sprint-intake`;
4371
+ const diagnosticUrl = escapeHtmlAttribute(new URL('/diagnostic', runtimeConfig.appOrigin).toString());
4372
+ const workflowSprintUrl = escapeHtmlAttribute(new URL('/go/sprint', runtimeConfig.appOrigin).toString());
4373
+ const workflowSprintIntakeUrl = escapeHtmlAttribute(new URL('/#workflow-sprint-intake', runtimeConfig.appOrigin).toString());
4128
4374
  const recoveryOfferLinks = [
4129
- diagnosticCheckoutUrl
4130
- ? `<a href="${diagnosticCheckoutUrl}" data-recovery-offer="sprint_diagnostic" data-offer-price="${sprintDiagnosticPriceDollars}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
4131
- : '',
4132
- workflowSprintCheckoutUrl
4133
- ? `<a href="${workflowSprintCheckoutUrl}" data-recovery-offer="workflow_sprint" data-offer-price="${workflowSprintPriceDollars}">Start $${workflowSprintPriceDollars} sprint</a>`
4134
- : '',
4135
- `<a href="${workflowTeardownCheckoutUrl}" data-recovery-offer="workflow_teardown" data-offer-price="99">Pay $99 teardown</a>`,
4136
- `<a href="${quickReadCheckoutUrl}" data-recovery-offer="quick_read" data-offer-price="19">Pay $19 quick read</a>`,
4137
- `<a href="${firstFailureRuleCheckoutUrl}" data-recovery-offer="first_failure_rule" data-offer-price="1">Pay $1 first rule</a>`,
4138
4375
  `<a id="send-workflow-first" href="${workflowSprintIntakeUrl}" data-recovery-offer="workflow_sprint_intake" data-offer-price="0">Send workflow first</a>`,
4139
- ].filter(Boolean).join('\n ');
4376
+ `<a href="${diagnosticUrl}" data-recovery-offer="sprint_diagnostic" data-offer-price="${sprintDiagnosticPriceDollars}">Review $${sprintDiagnosticPriceDollars} diagnostic</a>`,
4377
+ `<a href="${workflowSprintUrl}" data-recovery-offer="workflow_sprint" data-offer-price="${workflowSprintPriceDollars}">Scope $${workflowSprintPriceDollars} sprint</a>`,
4378
+ ].join('\n ');
4140
4379
  const recoveryOfferCard = recoveryOfferLinks
4141
4380
  ? `<div class="card recovery-card">
4142
4381
  <h2>Need help deciding?</h2>
@@ -4504,19 +4743,24 @@ function renderBrokerAuditIntakeResultPage(runtimeConfig, { title, detail, leadI
4504
4743
  function readBodyBuffer(req, maxBytes = 1024 * 1024) {
4505
4744
  return new Promise((resolve, reject) => {
4506
4745
  let total = 0;
4746
+ let oversize = false;
4507
4747
  const chunks = [];
4508
4748
 
4509
4749
  req.on('data', (chunk) => {
4510
4750
  total += chunk.length;
4511
4751
  if (total > maxBytes) {
4512
- reject(createHttpError(413, 'Request body too large'));
4513
- req.destroy();
4752
+ oversize = true;
4753
+ chunks.length = 0;
4514
4754
  return;
4515
4755
  }
4516
- chunks.push(chunk);
4756
+ if (!oversize) chunks.push(chunk);
4517
4757
  });
4518
4758
 
4519
4759
  req.on('end', () => {
4760
+ if (oversize) {
4761
+ reject(createHttpError(413, 'Request body too large'));
4762
+ return;
4763
+ }
4520
4764
  if (chunks.length === 0) {
4521
4765
  resolve(Buffer.alloc(0));
4522
4766
  return;
@@ -4661,6 +4905,15 @@ function getExpectedOperatorKey() {
4661
4905
  return key || null;
4662
4906
  }
4663
4907
 
4908
+ function getHumanReviewerConfig(expectedApiKey, expectedOperatorKey) {
4909
+ const key = String(process.env.THUMBGATE_HUMAN_REVIEWER_KEY || '').trim();
4910
+ const id = String(process.env.THUMBGATE_HUMAN_REVIEWER_ID || '').trim();
4911
+ const independent = Boolean(key)
4912
+ && !safeKeyEqual(key, expectedApiKey)
4913
+ && !safeKeyEqual(key, expectedOperatorKey);
4914
+ return { key: key || null, id: id || null, independent };
4915
+ }
4916
+
4664
4917
  function isAuthorized(req, expected) {
4665
4918
  if (!expected) return true;
4666
4919
  const token = extractApiKey(req);
@@ -4728,6 +4981,21 @@ function safeKeyEqual(a, b) {
4728
4981
  return require('crypto').timingSafeEqual(x, y);
4729
4982
  }
4730
4983
 
4984
+ function authenticateHumanReviewer(req, reviewerConfig) {
4985
+ if (!reviewerConfig.key || !reviewerConfig.id || !reviewerConfig.independent) {
4986
+ throw createHttpError(503, 'Human escalation decisions are unavailable until an independent reviewer key and identity are configured');
4987
+ }
4988
+ const header = req.headers['x-thumbgate-human-reviewer-key'];
4989
+ const presented = Array.isArray(header) ? header[0] : header;
4990
+ if (!safeKeyEqual(presented, reviewerConfig.key)) {
4991
+ throw createHttpError(403, 'A valid human reviewer credential is required to decide an escalation');
4992
+ }
4993
+ return {
4994
+ id: reviewerConfig.id,
4995
+ kind: 'human',
4996
+ };
4997
+ }
4998
+
4731
4999
  function resolveKeyRole(key) {
4732
5000
  const k = String(key || '').trim();
4733
5001
  const adminKey = String(process.env.THUMBGATE_API_KEY || '').trim();
@@ -4914,6 +5182,7 @@ function resolveDocumentImportFilePath(inputPath, options = {}) {
4914
5182
  function createApiServer() {
4915
5183
  const expectedApiKey = getExpectedApiKey();
4916
5184
  const expectedOperatorKey = getExpectedOperatorKey();
5185
+ const humanReviewerConfig = getHumanReviewerConfig(expectedApiKey, expectedOperatorKey);
4917
5186
 
4918
5187
  const eventBus = new EventEmitter();
4919
5188
  eventBus.setMaxListeners(200);
@@ -5320,11 +5589,38 @@ function createApiServer() {
5320
5589
 
5321
5590
  // Public endpoints — no auth required
5322
5591
  const trackedLinkMatch = pathname.match(/^\/go\/([^/]+)$/);
5323
- if (isGetLikeRequest && trackedLinkMatch) {
5592
+ if ((isGetLikeRequest || req.method === 'POST') && trackedLinkMatch) {
5593
+ let trackedParsed = parsed;
5594
+ if (req.method === 'POST') {
5595
+ const target = getTrackedLinkTarget(trackedLinkMatch[1]);
5596
+ if (!target || !target.requiresPost) {
5597
+ sendJson(res, target ? 405 : 404, target
5598
+ ? { error: 'POST is not supported for this tracked link' }
5599
+ : { error: 'Tracked link not found', allowed: Object.keys(TRACKED_LINK_TARGETS) }, {
5600
+ Allow: 'GET, HEAD',
5601
+ 'Cache-Control': 'no-store',
5602
+ });
5603
+ return;
5604
+ }
5605
+ const contentType = String(req.headers['content-type'] || '').toLowerCase();
5606
+ if (!contentType.includes('application/x-www-form-urlencoded')) {
5607
+ sendJson(res, 415, { error: 'Form-encoded checkout confirmation is required' }, {
5608
+ 'Cache-Control': 'no-store',
5609
+ });
5610
+ return;
5611
+ }
5612
+ const form = await parseFormBody(req, 16 * 1024);
5613
+ trackedParsed = new URL(parsed.toString());
5614
+ for (const [key, value] of Object.entries(form)) {
5615
+ if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.includes(key)) {
5616
+ trackedParsed.searchParams.set(key, String(value || '').trim());
5617
+ }
5618
+ }
5619
+ }
5324
5620
  serveTrackedLinkRedirect({
5325
5621
  req,
5326
5622
  res,
5327
- parsed,
5623
+ parsed: trackedParsed,
5328
5624
  hostedConfig,
5329
5625
  isHeadRequest,
5330
5626
  slug: trackedLinkMatch[1],
@@ -5782,6 +6078,22 @@ async function addContext(){
5782
6078
  return;
5783
6079
  }
5784
6080
 
6081
+ if (isGetLikeRequest && pathname.startsWith('/blog/') && pathname !== '/blog/') {
6082
+ try {
6083
+ const blogPostPath = resolvePublicHtmlFile(BLOG_PAGE_PATHS_BY_SLUG, pathname.replace('/blog/', ''));
6084
+ if (!blogPostPath) {
6085
+ sendJson(res, 404, { error: 'Post not found' });
6086
+ return;
6087
+ }
6088
+ const requestHost = getSafePublicRequestHost(req);
6089
+ const html = normalizePublicMarketingHtml(fs.readFileSync(blogPostPath, 'utf-8'), hostedConfig, requestHost);
6090
+ sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
6091
+ } catch {
6092
+ sendJson(res, 404, { error: 'Post not found' });
6093
+ }
6094
+ return;
6095
+ }
6096
+
5785
6097
  if (isGetLikeRequest && (pathname === '/learn' || pathname === '/learn.html')) {
5786
6098
  try {
5787
6099
  servePublicMarketingPage({
@@ -6132,11 +6444,11 @@ async function addContext(){
6132
6444
  return;
6133
6445
  }
6134
6446
 
6135
- // HOTFIX 2026-06-04 — accept ANY method (GET/HEAD/POST) on /checkout/pro
6447
+ // HOTFIX 2026-06-04 — accept GET, HEAD, and POST on /checkout/pro
6136
6448
  // to prevent the API-key guard from 401'ing real prospective customers
6137
- // whose forms or fetch() calls land via POST. Audit: 69 emails submitted
6138
- // → 0 paid because POST hit the auth gate. Query params still drive the
6139
- // Stripe session creation; POST bodies are ignored harmlessly.
6449
+ // whose forms land via POST. Audit: 69 emails submitted → 0 paid because
6450
+ // POST hit the auth gate. Form bodies are now allowlisted into the same
6451
+ // attribution path as query parameters before checkout evaluation.
6140
6452
  if ((isGetLikeRequest || req.method === 'POST') && pathname === '/checkout/pro') {
6141
6453
  if (isHeadRequest) {
6142
6454
  sendHtml(res, 200, '', {}, {
@@ -6145,6 +6457,26 @@ async function addContext(){
6145
6457
  return;
6146
6458
  }
6147
6459
 
6460
+ if (req.method === 'POST') {
6461
+ const contentType = String(req.headers['content-type'] || '').toLowerCase();
6462
+ if (!contentType.includes('application/x-www-form-urlencoded')) {
6463
+ sendJson(res, 415, { error: 'Form-encoded checkout confirmation is required' }, {
6464
+ 'Cache-Control': 'no-store',
6465
+ });
6466
+ return;
6467
+ }
6468
+ const form = await parseFormBody(req, 16 * 1024);
6469
+ for (const [key, value] of Object.entries(form)) {
6470
+ if (
6471
+ key === 'confirm'
6472
+ || key === 'customer_email'
6473
+ || TRACKED_LINK_QUERY_KEYS.includes(key)
6474
+ ) {
6475
+ parsed.searchParams.set(key, String(value || '').trim());
6476
+ }
6477
+ }
6478
+ }
6479
+
6148
6480
  const { FEEDBACK_DIR } = getFeedbackPaths();
6149
6481
  const journeyState = resolveJourneyState(req, parsed);
6150
6482
  const bootstrapBody = buildCheckoutBootstrapBody(parsed, req, journeyState);
@@ -6163,33 +6495,26 @@ async function addContext(){
6163
6495
  // it and followed it, bypassing the bot deflection. A bot crawl never
6164
6496
  // reaches the payment form, so the session is guaranteed waste.
6165
6497
  //
6166
- // Fix: bot + confirm=1 (alone) deflects back to the interstitial. Two
6167
- // escape hatches preserve real-user flows: (a) POST always proceeds
6168
- // (form submission JS-less bots don't do), (b) a `customer_email`
6169
- // query param treats the request as confirmed even from a bot UA,
6170
- // because no real crawler appends customer_email to discovered URLs.
6498
+ // Fix: bot-classified traffic never reaches session creation, and a
6499
+ // browser must provide both explicit confirmation and a valid email.
6171
6500
  const rawCheckoutEmail = normalizeNullableText(bootstrapBody.customerEmail);
6172
6501
  const normalizedCheckoutEmail = normalizeCheckoutCustomerEmail(rawCheckoutEmail);
6173
6502
  const hasCustomerEmailHint = !!parsed?.searchParams?.has('customer_email');
6174
6503
  const hasValidCustomerEmailHint = !!normalizedCheckoutEmail;
6175
- const botShouldBypass = !botClassification.isBot || hasValidCustomerEmailHint;
6176
- // 2026-06-29 conversion audit: requiring email before Stripe gave us a
6177
- // recoverable abandoned-session theory, but the hosted funnel showed
6178
- // traffic with 0 checkout starts. Let confirmed human clicks reach
6179
- // Stripe; Stripe can collect email on the secure checkout page. Bots
6180
- // still need a valid email hint to bypass deflection.
6504
+ const botShouldBypass = !botClassification.isBot;
6505
+ // A valid email is the explicit human-intent proof for both GET
6506
+ // confirmation and form POST. This prevents browser-shaped monitors
6507
+ // from minting anonymous sessions while preserving a one-field path to
6508
+ // Stripe for real buyers.
6181
6509
  const isConfirmedCheckout = (
6182
6510
  (req.method === 'POST' && hasValidCustomerEmailHint)
6183
- || hasConfirmFlag
6511
+ || (hasConfirmFlag && hasValidCustomerEmailHint)
6184
6512
  ) && botShouldBypass;
6185
- // 2026-06-05 revenue bypass: env-gated direct-to-Stripe redirect.
6186
- // Live 30d billing showed 254 interstitial views 1 Stripe click-through
6187
- // 0 paid. When THUMBGATE_CHECKOUT_INTERSTITIAL_BYPASS=1 is set we
6188
- // route raw /checkout/pro GETs (no confirm=1, no POST) straight to the
6189
- // pro Stripe Payment Link, preserving UTM + attribution metadata via
6190
- // buildCheckoutFallbackUrl. Default-off; bot-deflection still applies
6191
- // (bot + no email hint still falls through to the existing interstitial).
6192
- const interstitialBypassEnabled = process.env.THUMBGATE_CHECKOUT_INTERSTITIAL_BYPASS !== '0';
6513
+ // Legacy env-gated bypass. It is default-off and still requires a valid
6514
+ // email plus a non-bot request; enabling it must never re-open anonymous
6515
+ // Stripe-session creation. Attribution is preserved via
6516
+ // buildCheckoutFallbackUrl.
6517
+ const interstitialBypassEnabled = process.env.THUMBGATE_CHECKOUT_INTERSTITIAL_BYPASS === '1';
6193
6518
  const interstitialSampleRate = normalizeCheckoutInterstitialSampleRate(
6194
6519
  process.env.THUMBGATE_CHECKOUT_INTERSTITIAL_SAMPLE_RATE
6195
6520
  );
@@ -6203,6 +6528,7 @@ async function addContext(){
6203
6528
  && interstitialBypassEnabled
6204
6529
  && req.method !== 'POST'
6205
6530
  && botShouldBypass
6531
+ && hasValidCustomerEmailHint
6206
6532
  && !interstitialSampled
6207
6533
  ) {
6208
6534
  // Always target the pro Stripe Payment Link directly. The
@@ -6258,19 +6584,10 @@ async function addContext(){
6258
6584
  planId: analyticsMetadata.planId,
6259
6585
  },
6260
6586
  }).catch(() => {});
6261
- // Render the interstitial for ALL non-confirmed GETs (bot or human),
6262
- // not just bot traffic. Rationale: a raw GET on /checkout/pro currently
6263
- // 302s straight to a fresh Stripe `cs_live_*` session, regardless of
6264
- // whether the visitor is a search crawler, a link-preview fetcher, or
6265
- // a confused human who doesn't yet know what they're paying for.
6266
- // That created the 50-zombie-sessions / 0-paid pattern the CEO flagged
6267
- // 2026-05-13. Every visitor now sees the $19/mo confirmation page first
6268
- // and must click "Pay $19/mo with Stripe →" (which sets confirm=1) to
6269
- // trigger the Stripe-session creation + redirect. Counter-risk: one
6270
- // extra click on the human path. Mitigated because the interstitial
6271
- // also serves as a value-preview ("5,200+ npm installs/mo, MIT open source,
6272
- // cancel anytime"), which typically lifts conversion more than the
6273
- // click-friction costs.
6587
+ // Render the intent form for every non-confirmed request. A raw GET can
6588
+ // come from a crawler, link preview, monitor, or visitor who has not yet
6589
+ // supplied buyer identity. The form requires email-backed confirmation
6590
+ // before any Stripe session is created.
6274
6591
  if (!isConfirmedCheckout) {
6275
6592
  const eventType = botClassification.isBot
6276
6593
  ? 'checkout_bot_deflected'
@@ -6316,16 +6633,6 @@ async function addContext(){
6316
6633
  return;
6317
6634
  }
6318
6635
 
6319
- if (!normalizedCheckoutEmail) {
6320
- appendBestEffortTelemetry(FEEDBACK_DIR, {
6321
- eventType: 'checkout_email_deferred_to_stripe',
6322
- clientType: 'web',
6323
- traceId,
6324
- page: '/checkout/pro',
6325
- planId: analyticsMetadata.planId,
6326
- reason: rawCheckoutEmail ? 'invalid_customer_email' : 'missing_customer_email',
6327
- }, req.headers, 'checkout_email_deferred_to_stripe');
6328
- }
6329
6636
  bootstrapBody.customerEmail = normalizedCheckoutEmail || undefined;
6330
6637
 
6331
6638
  appendBestEffortTelemetry(FEEDBACK_DIR, {
@@ -7172,9 +7479,28 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7172
7479
  journeySummary: null,
7173
7480
  };
7174
7481
 
7482
+ // Bounded reverse scan — full-file read of multi-MB ledgers was timing
7483
+ // out operator dashboards and revenue:doctor probes.
7484
+ let readJsonlSinceTail;
7485
+ try {
7486
+ ({ readJsonlSinceTail } = require('../../scripts/jsonl-window'));
7487
+ } catch {
7488
+ readJsonlSinceTail = null;
7489
+ }
7490
+
7175
7491
  function readJsonlSince(p) {
7492
+ if (typeof readJsonlSinceTail === 'function') {
7493
+ const windowed = readJsonlSinceTail(p, {
7494
+ sinceMs: since,
7495
+ limit,
7496
+ maxBytes: 8 * 1024 * 1024,
7497
+ });
7498
+ return windowed;
7499
+ }
7176
7500
  try {
7177
- if (!fs.existsSync(p)) return [];
7501
+ if (!fs.existsSync(p)) {
7502
+ return { rows: [], totalAfterSince: 0, truncated: false };
7503
+ }
7178
7504
  const text = fs.readFileSync(p, 'utf8');
7179
7505
  const rows = [];
7180
7506
  for (const line of text.split('\n')) {
@@ -7187,16 +7513,22 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7187
7513
  rows.push(obj);
7188
7514
  }
7189
7515
  }
7190
- return rows;
7191
- } catch { return []; }
7516
+ return {
7517
+ rows: rows.slice(-limit),
7518
+ totalAfterSince: rows.length,
7519
+ truncated: rows.length > limit,
7520
+ };
7521
+ } catch {
7522
+ return { rows: [], totalAfterSince: 0, truncated: false };
7523
+ }
7192
7524
  }
7193
7525
 
7194
7526
  if (wantTelemetry) {
7195
7527
  const tp = path.join(exportDir, 'telemetry-pings.jsonl');
7196
- const all = readJsonlSince(tp);
7197
- result.telemetry.totalAfterSince = all.length;
7198
- result.telemetry.rows = all.slice(-limit);
7199
- result.telemetry.truncated = all.length > limit;
7528
+ const windowed = readJsonlSince(tp);
7529
+ result.telemetry.totalAfterSince = windowed.totalAfterSince;
7530
+ result.telemetry.rows = windowed.rows;
7531
+ result.telemetry.truncated = windowed.truncated;
7200
7532
  }
7201
7533
 
7202
7534
  if (wantFunnel) {
@@ -7211,10 +7543,10 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7211
7543
  } catch {
7212
7544
  funnelPath = path.join(exportDir, 'funnel-events.jsonl');
7213
7545
  }
7214
- const all = readJsonlSince(funnelPath);
7215
- result.funnel.totalAfterSince = all.length;
7216
- result.funnel.rows = all.slice(-limit);
7217
- result.funnel.truncated = all.length > limit;
7546
+ const windowed = readJsonlSince(funnelPath);
7547
+ result.funnel.totalAfterSince = windowed.totalAfterSince;
7548
+ result.funnel.rows = windowed.rows;
7549
+ result.funnel.truncated = windowed.truncated;
7218
7550
  }
7219
7551
 
7220
7552
  try {
@@ -7952,6 +8284,45 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7952
8284
  return;
7953
8285
  }
7954
8286
 
8287
+ // PayPal webhook evidence. PayPal authenticates the delivery through its
8288
+ // verify-webhook-signature API before raw proof is persisted locally.
8289
+ if (req.method === 'POST' && pathname === '/v1/billing/paypal-webhook') {
8290
+ try {
8291
+ const rawBody = await readBodyBuffer(req);
8292
+ const delivery = await recordPayPalWebhookDelivery({
8293
+ rawBody,
8294
+ headers: req.headers,
8295
+ });
8296
+ if (!delivery.verified || !delivery.recorded) {
8297
+ sendProblem(res, {
8298
+ type: PROBLEM_TYPES.WEBHOOK_INVALID,
8299
+ title: 'Invalid PayPal webhook delivery',
8300
+ status: delivery.verified ? 503 : 400,
8301
+ detail: delivery.verified
8302
+ ? 'The PayPal-verified webhook could not be written to the evidence ledger.'
8303
+ : 'The PayPal transmission headers, event, or remote signature verification were invalid.',
8304
+ });
8305
+ return;
8306
+ }
8307
+ sendJson(res, 200, {
8308
+ verified: true,
8309
+ recorded: true,
8310
+ duplicate: delivery.duplicate === true,
8311
+ eventId: delivery.eventId,
8312
+ eventType: delivery.eventType,
8313
+ payloadSha256: delivery.payloadSha256,
8314
+ });
8315
+ } catch (err) {
8316
+ sendProblem(res, {
8317
+ type: !err.statusCode || err.statusCode >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.BAD_REQUEST,
8318
+ title: !err.statusCode || err.statusCode >= 500 ? 'Internal Server Error' : 'Request Error',
8319
+ status: err.statusCode || 500,
8320
+ detail: err.message,
8321
+ });
8322
+ }
8323
+ return;
8324
+ }
8325
+
7955
8326
  // GitHub Marketplace webhook
7956
8327
  if (req.method === 'POST' && pathname === '/v1/billing/github-webhook') {
7957
8328
  try {
@@ -7963,12 +8334,20 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7963
8334
  });
7964
8335
 
7965
8336
  const sig = req.headers['x-hub-signature-256'] || '';
7966
- if (!verifyGithubWebhookSignature(rawBody, sig)) {
8337
+ const delivery = recordGithubMarketplaceWebhookDelivery({
8338
+ rawBody,
8339
+ signature: sig,
8340
+ deliveryId: req.headers['x-github-delivery'] || '',
8341
+ eventName: req.headers['x-github-event'] || '',
8342
+ });
8343
+ if (!delivery.verified || !delivery.recorded) {
7967
8344
  sendProblem(res, {
7968
8345
  type: PROBLEM_TYPES.WEBHOOK_INVALID,
7969
- title: 'Invalid webhook signature',
7970
- status: 400,
7971
- detail: 'The webhook signature could not be verified.',
8346
+ title: 'Invalid GitHub webhook delivery',
8347
+ status: delivery.verified ? 503 : 400,
8348
+ detail: delivery.verified
8349
+ ? 'The verified webhook could not be written to the evidence ledger.'
8350
+ : 'The webhook signature, delivery headers, or payload could not be verified.',
7972
8351
  });
7973
8352
  return;
7974
8353
  }
@@ -7986,7 +8365,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7986
8365
  return;
7987
8366
  }
7988
8367
 
7989
- const result = handleGithubWebhook(event);
8368
+ const result = handleGithubWebhook(event, delivery);
7990
8369
  sendJson(res, 200, result);
7991
8370
  } catch (err) {
7992
8371
  sendProblem(res, {
@@ -8103,14 +8482,19 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8103
8482
  return;
8104
8483
  }
8105
8484
 
8106
- // Operator key is allowed to bypass the general admin gate for its dedicated endpoint
8485
+ // Operator key is allowed to bypass the general admin gate for dedicated
8486
+ // read-only operational endpoints.
8107
8487
  const _reqToken = extractApiKey(req);
8108
- const isOperatorBillingRequest = Boolean(expectedOperatorKey)
8109
- && _reqToken === expectedOperatorKey
8488
+ const isOperatorReadRequest = Boolean(expectedOperatorKey)
8489
+ && safeKeyEqual(_reqToken, expectedOperatorKey)
8110
8490
  && req.method === 'GET'
8111
- && pathname === '/v1/billing/summary';
8491
+ && [
8492
+ '/v1/billing/summary',
8493
+ '/v1/intake/workflow-sprint/queue',
8494
+ '/v1/task-outcomes/monitor',
8495
+ ].includes(pathname);
8112
8496
 
8113
- if (!isOperatorBillingRequest && !isAuthorized(req, expectedApiKey)) {
8497
+ if (!isOperatorReadRequest && !isAuthorized(req, expectedApiKey)) {
8114
8498
  sendProblem(res, {
8115
8499
  type: PROBLEM_TYPES.UNAUTHORIZED,
8116
8500
  title: 'Unauthorized',
@@ -8698,6 +9082,83 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8698
9082
  return;
8699
9083
  }
8700
9084
 
9085
+ if (req.method === 'POST' && pathname === '/v1/task-outcomes') {
9086
+ const body = await parseJsonBody(req);
9087
+ try {
9088
+ const result = recordTaskOutcome(body, { feedbackDir: requestFeedbackDir });
9089
+ sendJson(res, result.recorded ? 201 : 200, result);
9090
+ } catch (error) {
9091
+ throw createHttpError(400, error.message);
9092
+ }
9093
+ return;
9094
+ }
9095
+
9096
+ if (req.method === 'GET' && pathname === '/v1/task-outcomes') {
9097
+ const taskId = parsed.searchParams.get('taskId');
9098
+ const limit = Math.max(1, Math.min(100, Number(parsed.searchParams.get('limit') || 20)));
9099
+ if (taskId) {
9100
+ const outcome = getTaskOutcome(taskId, { feedbackDir: requestFeedbackDir });
9101
+ if (!outcome) throw createHttpError(404, `Task outcome not found: ${taskId}`);
9102
+ sendJson(res, 200, outcome);
9103
+ } else {
9104
+ sendJson(res, 200, {
9105
+ outcomes: readTaskOutcomes({ feedbackDir: requestFeedbackDir }).slice(-limit),
9106
+ });
9107
+ }
9108
+ return;
9109
+ }
9110
+
9111
+ if (req.method === 'GET' && pathname === '/v1/task-outcomes/metrics') {
9112
+ const outcomes = readTaskOutcomes({ feedbackDir: requestFeedbackDir });
9113
+ sendJson(res, 200, calculateTaskOutcomeMetrics(outcomes));
9114
+ return;
9115
+ }
9116
+
9117
+ if (req.method === 'GET' && pathname === '/v1/task-outcomes/monitor') {
9118
+ const outcomes = readTaskOutcomes({ feedbackDir: requestFeedbackDir });
9119
+ sendJson(res, 200, monitorTaskOutcomes(outcomes));
9120
+ return;
9121
+ }
9122
+
9123
+ if (req.method === 'POST' && pathname === '/v1/escalations') {
9124
+ const body = await parseJsonBody(req);
9125
+ try {
9126
+ const result = requestEscalation(body, { feedbackDir: requestFeedbackDir });
9127
+ sendJson(res, result.recorded ? 201 : 200, result);
9128
+ } catch (error) {
9129
+ throw createHttpError(400, error.message);
9130
+ }
9131
+ return;
9132
+ }
9133
+
9134
+ if (req.method === 'GET' && pathname === '/v1/escalations') {
9135
+ const status = parsed.searchParams.get('status') || undefined;
9136
+ const escalations = listEscalations({ feedbackDir: requestFeedbackDir, status });
9137
+ sendJson(res, 200, {
9138
+ escalations,
9139
+ metrics: calculateEscalationMetrics(escalations),
9140
+ });
9141
+ return;
9142
+ }
9143
+
9144
+ if (req.method === 'POST' && /^\/v1\/escalations\/[^/]+\/decision$/.test(pathname)) {
9145
+ const escalationId = decodeURIComponent(pathname.split('/')[3]);
9146
+ const authenticatedActor = authenticateHumanReviewer(req, humanReviewerConfig);
9147
+ const body = await parseJsonBody(req);
9148
+ try {
9149
+ sendJson(res, 200, decideEscalation({
9150
+ ...body,
9151
+ escalationId,
9152
+ }, {
9153
+ authenticatedActor,
9154
+ feedbackDir: requestFeedbackDir,
9155
+ }));
9156
+ } catch (error) {
9157
+ throw createHttpError(400, error.message);
9158
+ }
9159
+ return;
9160
+ }
9161
+
8701
9162
  if (req.method === 'GET' && pathname === '/v1/ops/integrity') {
8702
9163
  const command = parsed.searchParams.get('command') || undefined;
8703
9164
  const baseBranch = parsed.searchParams.get('baseBranch') || undefined;
@@ -9406,6 +9867,33 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9406
9867
  return;
9407
9868
  }
9408
9869
 
9870
+ // GET /v1/intake/workflow-sprint/queue — authenticated, no-store operator close queue
9871
+ if (req.method === 'GET' && pathname === '/v1/intake/workflow-sprint/queue') {
9872
+ if (!isBillingSummaryAuthorized(req, expectedApiKey, expectedOperatorKey)) {
9873
+ sendProblem(res, {
9874
+ type: PROBLEM_TYPES.FORBIDDEN,
9875
+ title: 'Forbidden',
9876
+ status: 403,
9877
+ detail: 'Admin or operator API key required for this endpoint.',
9878
+ });
9879
+ return;
9880
+ }
9881
+ try {
9882
+ const { FEEDBACK_DIR } = getFeedbackPaths();
9883
+ const workflowSprintIntake = requirePrivateApiModule(
9884
+ 'workflowSprintIntake',
9885
+ 'Workflow sprint intake'
9886
+ );
9887
+ const queue = buildWorkflowIntakeQueue(parsed, workflowSprintIntake, FEEDBACK_DIR, {
9888
+ publicOrigin: hostedConfig.appOrigin,
9889
+ });
9890
+ sendJson(res, 200, queue, PRIVATE_OPERATOR_RESPONSE_HEADERS);
9891
+ } catch (err) {
9892
+ sendWorkflowIntakeQueueProblem(res, err);
9893
+ }
9894
+ return;
9895
+ }
9896
+
9409
9897
  // GET /v1/billing/summary — operator billing summary (admin key or operator key)
9410
9898
  if (req.method === 'GET' && pathname === '/v1/billing/summary') {
9411
9899
  if (!isBillingSummaryAuthorized(req, expectedApiKey, expectedOperatorKey)) {
@@ -9418,6 +9906,48 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9418
9906
  return;
9419
9907
  }
9420
9908
 
9909
+ const includeIntakeQueue = wantsWorkflowIntakeQueue(parsed);
9910
+ let intakeQueue = null;
9911
+ if (includeIntakeQueue) {
9912
+ try {
9913
+ const { FEEDBACK_DIR } = getFeedbackPaths();
9914
+ const workflowSprintIntake = requirePrivateApiModule(
9915
+ 'workflowSprintIntake',
9916
+ 'Workflow sprint intake'
9917
+ );
9918
+ intakeQueue = buildWorkflowIntakeQueue(parsed, workflowSprintIntake, FEEDBACK_DIR, {
9919
+ publicOrigin: hostedConfig.appOrigin,
9920
+ });
9921
+ } catch (err) {
9922
+ sendWorkflowIntakeQueueProblem(res, err);
9923
+ return;
9924
+ }
9925
+ }
9926
+
9927
+ const runtimePresence = buildHostedRuntimePresence(hostedConfig, {
9928
+ expectedApiKey,
9929
+ expectedOperatorKey,
9930
+ });
9931
+ const requestedWindow = String(parsed.searchParams.get('window') || '').trim().toLowerCase();
9932
+ if (requestedWindow === 'all') {
9933
+ let summaryOptionsByWindow;
9934
+ try {
9935
+ summaryOptionsByWindow = resolveBillingSummaryBatchOptions(parsed);
9936
+ } catch (err) {
9937
+ sendInvalidAnalyticsWindowProblem(res, 'Invalid billing summary query', err);
9938
+ return;
9939
+ }
9940
+ const summaries = await getBillingSummariesLive(summaryOptionsByWindow);
9941
+ sendJson(res, 200, {
9942
+ generatedAt: new Date().toISOString(),
9943
+ windows: BILLING_SUMMARY_BATCH_WINDOWS,
9944
+ summaries,
9945
+ runtimePresence,
9946
+ ...(intakeQueue ? { intakeQueue } : {}),
9947
+ }, PRIVATE_OPERATOR_RESPONSE_HEADERS);
9948
+ return;
9949
+ }
9950
+
9421
9951
  const summaryOptions = resolveBillingSummaryOptionsOrRespondProblem(
9422
9952
  res,
9423
9953
  parsed,
@@ -9430,11 +9960,9 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9430
9960
  const summary = await getBillingSummaryLive(summaryOptions);
9431
9961
  sendJson(res, 200, {
9432
9962
  ...summary,
9433
- runtimePresence: buildHostedRuntimePresence(hostedConfig, {
9434
- expectedApiKey,
9435
- expectedOperatorKey,
9436
- }),
9437
- });
9963
+ runtimePresence,
9964
+ ...(intakeQueue ? { intakeQueue } : {}),
9965
+ }, PRIVATE_OPERATOR_RESPONSE_HEADERS);
9438
9966
  return;
9439
9967
  }
9440
9968
 
@@ -9887,6 +10415,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9887
10415
  function startServer({ port, host } = {}) {
9888
10416
  const listenPort = Number(port ?? process.env.PORT ?? 8787);
9889
10417
  const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
10418
+ fs.mkdirSync(getFeedbackPaths().FEEDBACK_DIR, { recursive: true });
9890
10419
  const server = createApiServer();
9891
10420
  registerGracefulShutdown(server);
9892
10421
  return new Promise((resolve) => {