thumbgate 1.28.4 → 1.29.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/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +18 -10
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +5 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +8 -8
- package/bin/postinstall.js +4 -13
- package/config/github-about.json +5 -4
- package/config/post-deploy-marketing-pages.json +6 -6
- package/docs/integrations/grafana/README.md +109 -0
- package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
- package/openapi/openapi.yaml +240 -5
- package/package.json +59 -19
- package/public/agent-manager.html +10 -11
- package/public/agents-cost-savings.html +2 -2
- package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
- package/public/assets/brand/thumbgate-mark.svg +10 -11
- package/public/blog/inside-your-boundary.html +114 -0
- package/public/blog/process-over-outcome-gates.html +119 -0
- package/public/blog.html +296 -402
- package/public/brand/thumbgate-mark.svg +5 -9
- package/public/codex-enterprise.html +2 -2
- package/public/compare.html +12 -3
- package/public/diagnostic.html +79 -29
- package/public/guide.html +4 -4
- package/public/index.html +1088 -2098
- package/public/install.html +3 -3
- package/public/js/buyer-intent.js +33 -18
- package/public/numbers.html +2 -2
- package/public/pricing.html +268 -408
- package/public/pro.html +4 -4
- package/scripts/billing.js +456 -126
- package/scripts/buyer-paths.js +102 -0
- package/scripts/cli-feedback.js +2 -2
- package/scripts/commercial-offer.js +18 -10
- package/scripts/external-customer-audit.js +881 -0
- package/scripts/feedback-loop.js +26 -0
- package/scripts/gates-engine.js +123 -1
- package/scripts/grafana-revenue-evidence.js +856 -0
- package/scripts/jsonl-window.js +89 -0
- package/scripts/lesson-embedding-index.js +3 -7
- package/scripts/meta-agent-loop.js +20 -2
- package/scripts/observability-env.js +139 -0
- package/scripts/observability-setup.js +55 -0
- package/scripts/plausible-domain-config.js +4 -0
- package/scripts/provider-live-evidence.js +1290 -0
- package/scripts/provider-payment-reconciler.js +442 -0
- package/scripts/provider-revenue-evidence.js +249 -0
- package/scripts/rate-limiter.js +1 -5
- package/scripts/revenue-action-eligibility.js +414 -0
- package/scripts/revenue-evidence-remediation.js +694 -0
- package/scripts/revenue-offer-system.js +709 -0
- package/scripts/sales-pipeline.js +1117 -0
- package/scripts/seo-gsd.js +8 -4
- package/scripts/stripe-credentials.js +37 -0
- package/scripts/stripe-revenue-catalog-audit.js +363 -0
- package/scripts/stripe-revenue-catalog.js +164 -0
- package/scripts/telemetry-analytics.js +23 -3
- package/scripts/vector-store.js +83 -7
- package/scripts/workflow-intake-queue.js +483 -0
- package/src/api/server.js +521 -114
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
|
-
|
|
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,
|
|
@@ -259,12 +263,14 @@ const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
|
259
263
|
const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
|
|
260
264
|
const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
|
|
261
265
|
const USE_CASES_DIR = path.resolve(__dirname, '../../public/use-cases');
|
|
266
|
+
const BLOG_DIR = path.resolve(__dirname, '../../public/blog');
|
|
262
267
|
const PUBLIC_DIR = path.resolve(__dirname, '../../public');
|
|
263
268
|
const PUBLIC_ASSETS_DIR = path.resolve(__dirname, '../../public/assets');
|
|
264
269
|
const LEARN_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(LEARN_DIR);
|
|
265
270
|
const GUIDE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(GUIDES_DIR);
|
|
266
271
|
const COMPARE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(COMPARE_DIR);
|
|
267
272
|
const USE_CASE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(USE_CASES_DIR);
|
|
273
|
+
const BLOG_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(BLOG_DIR);
|
|
268
274
|
const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
|
|
269
275
|
const STATIC_MIME_BY_EXT = Object.freeze({
|
|
270
276
|
'.png': 'image/png',
|
|
@@ -482,34 +488,46 @@ const TRACKED_LINK_TARGETS = Object.freeze({
|
|
|
482
488
|
allowCustomerEmail: true,
|
|
483
489
|
},
|
|
484
490
|
diagnostic: {
|
|
485
|
-
|
|
486
|
-
fallbackHref: SPRINT_DIAGNOSTIC_CHECKOUT_URL,
|
|
487
|
-
external: true,
|
|
491
|
+
path: '/diagnostic',
|
|
488
492
|
ctaId: 'go_diagnostic',
|
|
489
493
|
ctaPlacement: 'link_router',
|
|
490
|
-
eventType: '
|
|
494
|
+
eventType: 'diagnostic_intent_open',
|
|
491
495
|
defaults: {
|
|
492
496
|
utm_source: 'website',
|
|
493
497
|
utm_medium: 'link_router',
|
|
494
498
|
utm_campaign: 'sprint_diagnostic',
|
|
495
499
|
plan_id: 'sprint_diagnostic',
|
|
496
500
|
},
|
|
501
|
+
},
|
|
502
|
+
'diagnostic-pay': {
|
|
503
|
+
configUrlKey: 'sprintDiagnosticCheckoutUrl',
|
|
504
|
+
fallbackHref: SPRINT_DIAGNOSTIC_CHECKOUT_URL,
|
|
505
|
+
external: true,
|
|
506
|
+
ctaId: 'go_diagnostic_pay',
|
|
507
|
+
ctaPlacement: 'diagnostic_confirmation',
|
|
508
|
+
eventType: 'diagnostic_checkout_confirmed',
|
|
509
|
+
requiresPost: true,
|
|
510
|
+
requiresBuyerEmail: true,
|
|
511
|
+
prefillStripeEmail: true,
|
|
512
|
+
defaults: {
|
|
513
|
+
utm_source: 'diagnostic_page',
|
|
514
|
+
utm_medium: 'diagnostic_confirmation',
|
|
515
|
+
utm_campaign: 'sprint_diagnostic',
|
|
516
|
+
plan_id: 'sprint_diagnostic',
|
|
517
|
+
},
|
|
497
518
|
allowCustomerEmail: true,
|
|
498
519
|
},
|
|
499
520
|
sprint: {
|
|
500
|
-
|
|
501
|
-
fallbackHref: WORKFLOW_SPRINT_CHECKOUT_URL,
|
|
502
|
-
external: true,
|
|
521
|
+
path: '/diagnostic',
|
|
503
522
|
ctaId: 'go_sprint',
|
|
504
523
|
ctaPlacement: 'link_router',
|
|
505
|
-
eventType: '
|
|
524
|
+
eventType: 'workflow_sprint_scope_open',
|
|
506
525
|
defaults: {
|
|
507
526
|
utm_source: 'website',
|
|
508
527
|
utm_medium: 'link_router',
|
|
509
528
|
utm_campaign: 'workflow_sprint',
|
|
510
529
|
plan_id: 'workflow_sprint',
|
|
511
530
|
},
|
|
512
|
-
allowCustomerEmail: true,
|
|
513
531
|
},
|
|
514
532
|
trial: {
|
|
515
533
|
path: '/guide',
|
|
@@ -1519,6 +1537,180 @@ function resolveBillingSummaryOptions(parsed) {
|
|
|
1519
1537
|
});
|
|
1520
1538
|
}
|
|
1521
1539
|
|
|
1540
|
+
const BILLING_SUMMARY_BATCH_WINDOWS = Object.freeze(['today', '30d', 'lifetime']);
|
|
1541
|
+
const WORKFLOW_INTAKE_QUEUE_DEFAULT_LIMIT = 50;
|
|
1542
|
+
const WORKFLOW_INTAKE_QUEUE_MAX_LIMIT = 100;
|
|
1543
|
+
const PRIVATE_OPERATOR_RESPONSE_HEADERS = Object.freeze({
|
|
1544
|
+
'Cache-Control': 'private, no-store, max-age=0',
|
|
1545
|
+
Pragma: 'no-cache',
|
|
1546
|
+
Vary: 'Authorization',
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
function resolveBillingSummaryBatchOptions(parsed) {
|
|
1550
|
+
return Object.fromEntries(BILLING_SUMMARY_BATCH_WINDOWS.map((window) => [
|
|
1551
|
+
window,
|
|
1552
|
+
resolveAnalyticsWindow({
|
|
1553
|
+
window,
|
|
1554
|
+
timeZone: parsed.searchParams.get('timezone'),
|
|
1555
|
+
now: parsed.searchParams.get('now'),
|
|
1556
|
+
}),
|
|
1557
|
+
]));
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function wantsWorkflowIntakeQueue(parsed) {
|
|
1561
|
+
return ['1', 'true', 'yes'].includes(
|
|
1562
|
+
String(parsed.searchParams.get('include_intake_queue') || '').trim().toLowerCase()
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function resolveWorkflowIntakeQueueOptions(parsed, workflowSprintIntake) {
|
|
1567
|
+
const rawLimit = Number(
|
|
1568
|
+
parsed.searchParams.get('intake_limit') ||
|
|
1569
|
+
parsed.searchParams.get('limit') ||
|
|
1570
|
+
WORKFLOW_INTAKE_QUEUE_DEFAULT_LIMIT
|
|
1571
|
+
);
|
|
1572
|
+
if (!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > WORKFLOW_INTAKE_QUEUE_MAX_LIMIT) {
|
|
1573
|
+
throw createHttpError(
|
|
1574
|
+
400,
|
|
1575
|
+
`intake_limit must be an integer from 1 to ${WORKFLOW_INTAKE_QUEUE_MAX_LIMIT}`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const allowedStatuses = new Set(workflowSprintIntake.WORKFLOW_SPRINT_STATUS_FLOW || []);
|
|
1580
|
+
const rawStatuses = String(
|
|
1581
|
+
parsed.searchParams.get('intake_status') ||
|
|
1582
|
+
parsed.searchParams.get('status') ||
|
|
1583
|
+
'new,qualified'
|
|
1584
|
+
)
|
|
1585
|
+
.split(',')
|
|
1586
|
+
.map((value) => value.trim().toLowerCase())
|
|
1587
|
+
.filter(Boolean);
|
|
1588
|
+
const allRequested = rawStatuses.length === 1 && rawStatuses[0] === 'all';
|
|
1589
|
+
const statuses = allRequested ? [] : [...new Set(rawStatuses)];
|
|
1590
|
+
const invalid = statuses.filter((status) => !allowedStatuses.has(status));
|
|
1591
|
+
if (invalid.length > 0 || (!allRequested && statuses.length === 0)) {
|
|
1592
|
+
throw createHttpError(
|
|
1593
|
+
400,
|
|
1594
|
+
`intake_status must be all or a comma-separated subset of: ${[...allowedStatuses].join(', ')}`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
return { limit: rawLimit, statuses };
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function projectWorkflowIntakeQueueLead(lead = {}, options = {}) {
|
|
1602
|
+
const contact = lead.contact || {};
|
|
1603
|
+
const qualification = lead.qualification || {};
|
|
1604
|
+
const attribution = lead.attribution || {};
|
|
1605
|
+
return {
|
|
1606
|
+
leadId: lead.leadId || null,
|
|
1607
|
+
submittedAt: lead.submittedAt || null,
|
|
1608
|
+
updatedAt: lead.updatedAt || null,
|
|
1609
|
+
status: lead.status || null,
|
|
1610
|
+
offer: lead.offer || null,
|
|
1611
|
+
contact: {
|
|
1612
|
+
name: contact.name || null,
|
|
1613
|
+
email: contact.email || null,
|
|
1614
|
+
company: contact.company || null,
|
|
1615
|
+
},
|
|
1616
|
+
qualification: {
|
|
1617
|
+
workflow: qualification.workflow || null,
|
|
1618
|
+
owner: qualification.owner || null,
|
|
1619
|
+
blocker: qualification.blocker || null,
|
|
1620
|
+
runtime: qualification.runtime || null,
|
|
1621
|
+
urgency: qualification.urgency || null,
|
|
1622
|
+
note: qualification.note || null,
|
|
1623
|
+
},
|
|
1624
|
+
attribution: {
|
|
1625
|
+
source: attribution.source || null,
|
|
1626
|
+
utmSource: attribution.utmSource || null,
|
|
1627
|
+
utmMedium: attribution.utmMedium || null,
|
|
1628
|
+
utmCampaign: attribution.utmCampaign || null,
|
|
1629
|
+
creator: attribution.creator || null,
|
|
1630
|
+
community: attribution.community || null,
|
|
1631
|
+
planId: attribution.planId || null,
|
|
1632
|
+
page: attribution.page || null,
|
|
1633
|
+
landingPath: attribution.landingPath || null,
|
|
1634
|
+
},
|
|
1635
|
+
nextOperatorStep: options.closePacket?.status === 'approval_ready_not_authorized'
|
|
1636
|
+
? 'request_action_time_approval'
|
|
1637
|
+
: options.discoveryPacket?.status === 'approval_ready_not_authorized'
|
|
1638
|
+
? 'request_action_time_approval_for_discovery'
|
|
1639
|
+
: lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold',
|
|
1640
|
+
qualificationCard: options.qualificationCard || buildIntakeQualificationCard(lead, options),
|
|
1641
|
+
discoveryPacket: options.discoveryPacket || buildIntakeDiscoveryPacket(lead, options),
|
|
1642
|
+
closePacket: options.closePacket || buildIntakeClosePacket(lead, options),
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function buildWorkflowIntakeQueue(parsed, workflowSprintIntake, feedbackDir, options = {}) {
|
|
1647
|
+
const { limit, statuses } = resolveWorkflowIntakeQueueOptions(parsed, workflowSprintIntake);
|
|
1648
|
+
const leads = workflowSprintIntake.loadWorkflowSprintLeads(feedbackDir);
|
|
1649
|
+
const generatedAt = new Date().toISOString();
|
|
1650
|
+
const byStatus = {};
|
|
1651
|
+
for (const lead of leads) {
|
|
1652
|
+
const status = lead.status || 'unknown';
|
|
1653
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
1654
|
+
}
|
|
1655
|
+
const eligible = leads
|
|
1656
|
+
.filter((lead) => statuses.length === 0 || statuses.includes(lead.status))
|
|
1657
|
+
.map((lead) => {
|
|
1658
|
+
const qualificationReviewVerified =
|
|
1659
|
+
typeof workflowSprintIntake.isEvidenceBasedQualificationReview === 'function' &&
|
|
1660
|
+
workflowSprintIntake.isEvidenceBasedQualificationReview(lead.qualificationReview);
|
|
1661
|
+
const cardOptions = {
|
|
1662
|
+
now: generatedAt,
|
|
1663
|
+
qualificationReviewVerified,
|
|
1664
|
+
publicOrigin: options.publicOrigin,
|
|
1665
|
+
};
|
|
1666
|
+
return {
|
|
1667
|
+
lead,
|
|
1668
|
+
qualificationCard: buildIntakeQualificationCard(lead, cardOptions),
|
|
1669
|
+
discoveryPacket: buildIntakeDiscoveryPacket(lead, cardOptions),
|
|
1670
|
+
closePacket: buildIntakeClosePacket(lead, cardOptions),
|
|
1671
|
+
};
|
|
1672
|
+
})
|
|
1673
|
+
.sort((a, b) =>
|
|
1674
|
+
b.qualificationCard.priorityScore - a.qualificationCard.priorityScore ||
|
|
1675
|
+
String(b.lead.submittedAt || '').localeCompare(String(a.lead.submittedAt || '')));
|
|
1676
|
+
const selected = eligible.slice(0, limit);
|
|
1677
|
+
const approvalReady = eligible.filter(
|
|
1678
|
+
(entry) => entry.closePacket.status === 'approval_ready_not_authorized'
|
|
1679
|
+
);
|
|
1680
|
+
const discoveryReady = eligible.filter(
|
|
1681
|
+
(entry) => entry.discoveryPacket.status === 'approval_ready_not_authorized'
|
|
1682
|
+
);
|
|
1683
|
+
const chronological = eligible
|
|
1684
|
+
.map((entry) => entry.lead)
|
|
1685
|
+
.sort((a, b) => String(b.submittedAt || '').localeCompare(String(a.submittedAt || '')));
|
|
1686
|
+
return {
|
|
1687
|
+
generatedAt,
|
|
1688
|
+
total: leads.length,
|
|
1689
|
+
eligibleTotal: eligible.length,
|
|
1690
|
+
returned: selected.length,
|
|
1691
|
+
approvalReadyTotal: approvalReady.length,
|
|
1692
|
+
discoveryReadyTotal: discoveryReady.length,
|
|
1693
|
+
primaryApprovalAction: approvalReady[0]?.closePacket || null,
|
|
1694
|
+
primaryDiscoveryAction: discoveryReady[0]?.discoveryPacket || null,
|
|
1695
|
+
byStatus,
|
|
1696
|
+
filters: {
|
|
1697
|
+
statuses: statuses.length === 0 ? ['all'] : statuses,
|
|
1698
|
+
limit,
|
|
1699
|
+
},
|
|
1700
|
+
latestSubmittedAt: chronological[0] ? chronological[0].submittedAt || null : null,
|
|
1701
|
+
oldestSubmittedAt: chronological.length ? chronological[chronological.length - 1].submittedAt || null : null,
|
|
1702
|
+
leads: selected.map((entry, index) => ({
|
|
1703
|
+
...projectWorkflowIntakeQueueLead(entry.lead, {
|
|
1704
|
+
now: generatedAt,
|
|
1705
|
+
qualificationCard: entry.qualificationCard,
|
|
1706
|
+
discoveryPacket: entry.discoveryPacket,
|
|
1707
|
+
closePacket: entry.closePacket,
|
|
1708
|
+
}),
|
|
1709
|
+
priorityRank: index + 1,
|
|
1710
|
+
})),
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1522
1714
|
function sendInvalidAnalyticsWindowProblem(res, title, err) {
|
|
1523
1715
|
sendProblem(res, {
|
|
1524
1716
|
type: PROBLEM_TYPES.INVALID_REQUEST,
|
|
@@ -1528,6 +1720,18 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
|
|
|
1528
1720
|
});
|
|
1529
1721
|
}
|
|
1530
1722
|
|
|
1723
|
+
function sendWorkflowIntakeQueueProblem(res, err) {
|
|
1724
|
+
const status = Number(err && err.statusCode) || 400;
|
|
1725
|
+
sendProblem(res, {
|
|
1726
|
+
type: status >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.INVALID_REQUEST,
|
|
1727
|
+
title: status >= 500
|
|
1728
|
+
? 'Workflow intake queue unavailable'
|
|
1729
|
+
: 'Invalid workflow intake queue query',
|
|
1730
|
+
status,
|
|
1731
|
+
detail: err?.message ? err.message : 'Workflow intake queue request failed.',
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1531
1735
|
function resolveBillingSummaryOptionsOrRespondProblem(res, parsed, invalidTitle) {
|
|
1532
1736
|
try {
|
|
1533
1737
|
return resolveBillingSummaryOptions(parsed);
|
|
@@ -2261,11 +2465,12 @@ a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:
|
|
|
2261
2465
|
<div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div>
|
|
2262
2466
|
<h1>Start ThumbGate Pro</h1>
|
|
2263
2467
|
<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.
|
|
2265
|
-
<form action="
|
|
2468
|
+
<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>
|
|
2469
|
+
<form action="/checkout/pro" method="POST" data-i="pro_checkout_confirmed">
|
|
2266
2470
|
${hiddenInputs}
|
|
2267
|
-
<input type="
|
|
2268
|
-
<
|
|
2471
|
+
<input type="hidden" name="confirm" value="1">
|
|
2472
|
+
<input type="email" name="customer_email" value="${escapeHtmlAttribute(prefilledEmail)}" placeholder="you@company.com" autocomplete="email" required>
|
|
2473
|
+
<p class="email-note">Required so the checkout has attributable buyer intent and your receipt can be recovered.</p>
|
|
2269
2474
|
<button type="submit" class="primary">Pay $19/mo with Stripe →</button>
|
|
2270
2475
|
</form>
|
|
2271
2476
|
<a class="secondary" data-i="workflow_sprint_intake" href="/#workflow-sprint-intake">Not sure yet? Send the workflow first</a>
|
|
@@ -2281,7 +2486,7 @@ ${hiddenInputs}
|
|
|
2281
2486
|
</div>
|
|
2282
2487
|
<div class="feedback-saved" id="feedback-saved">Feedback saved.</div>
|
|
2283
2488
|
</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
|
|
2489
|
+
<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
2490
|
<p class="back"><a href="/">← Back to thumbgate.ai</a></p>
|
|
2286
2491
|
</main>
|
|
2287
2492
|
<script>
|
|
@@ -2475,7 +2680,14 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
|
2475
2680
|
}
|
|
2476
2681
|
}
|
|
2477
2682
|
if (target.allowCustomerEmail) {
|
|
2478
|
-
|
|
2683
|
+
const customerEmail = normalizeCheckoutCustomerEmail(params.get('customer_email'));
|
|
2684
|
+
if (customerEmail) {
|
|
2685
|
+
appendQueryParam(
|
|
2686
|
+
destinationUrl,
|
|
2687
|
+
target.prefillStripeEmail ? 'prefilled_email' : 'customer_email',
|
|
2688
|
+
customerEmail
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2479
2691
|
}
|
|
2480
2692
|
if (!destinationUrl.searchParams.has('cta_id')) {
|
|
2481
2693
|
appendQueryParam(destinationUrl, 'cta_id', target.ctaId);
|
|
@@ -2553,6 +2765,31 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
|
|
|
2553
2765
|
return;
|
|
2554
2766
|
}
|
|
2555
2767
|
|
|
2768
|
+
if (target.requiresPost && req.method !== 'POST') {
|
|
2769
|
+
sendJson(res, 405, {
|
|
2770
|
+
error: 'Explicit checkout confirmation is required',
|
|
2771
|
+
next: '/diagnostic',
|
|
2772
|
+
}, {
|
|
2773
|
+
Allow: 'POST',
|
|
2774
|
+
'Cache-Control': 'no-store',
|
|
2775
|
+
}, {
|
|
2776
|
+
headOnly: isHeadRequest,
|
|
2777
|
+
});
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
if (
|
|
2781
|
+
target.requiresBuyerEmail
|
|
2782
|
+
&& !normalizeCheckoutCustomerEmail(parsed.searchParams.get('customer_email'))
|
|
2783
|
+
) {
|
|
2784
|
+
sendJson(res, 400, {
|
|
2785
|
+
error: 'A valid buyer email is required before checkout',
|
|
2786
|
+
next: '/diagnostic',
|
|
2787
|
+
}, {
|
|
2788
|
+
'Cache-Control': 'no-store',
|
|
2789
|
+
});
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2556
2793
|
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
2557
2794
|
const journeyState = resolveJourneyState(req, parsed);
|
|
2558
2795
|
const destinationUrl = buildTrackedLinkDestination(target, hostedConfig, parsed);
|
|
@@ -2573,7 +2810,7 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
|
|
|
2573
2810
|
);
|
|
2574
2811
|
}
|
|
2575
2812
|
|
|
2576
|
-
res.writeHead(302, {
|
|
2813
|
+
res.writeHead(req.method === 'POST' ? 303 : 302, {
|
|
2577
2814
|
...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
|
|
2578
2815
|
'Cache-Control': 'no-store',
|
|
2579
2816
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
@@ -3599,6 +3836,7 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3599
3836
|
const seoDirectories = [
|
|
3600
3837
|
{ dir: 'compare', route: '/compare', priority: '0.85' },
|
|
3601
3838
|
{ dir: 'guides', route: '/guides', priority: '0.85' },
|
|
3839
|
+
{ dir: 'blog', route: '/blog', priority: '0.8' },
|
|
3602
3840
|
];
|
|
3603
3841
|
for (const catalog of seoDirectories) {
|
|
3604
3842
|
const files = fs.readdirSync(path.join(PUBLIC_DIR, catalog.dir)).sort((a, b) => a.localeCompare(b));
|
|
@@ -4113,30 +4351,16 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
4113
4351
|
}
|
|
4114
4352
|
|
|
4115
4353
|
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
4354
|
const sprintDiagnosticPriceDollars = runtimeConfig.sprintDiagnosticPriceDollars || 499;
|
|
4126
4355
|
const workflowSprintPriceDollars = runtimeConfig.workflowSprintPriceDollars || 1500;
|
|
4127
|
-
const
|
|
4356
|
+
const diagnosticUrl = escapeHtmlAttribute(new URL('/diagnostic', runtimeConfig.appOrigin).toString());
|
|
4357
|
+
const workflowSprintUrl = escapeHtmlAttribute(new URL('/go/sprint', runtimeConfig.appOrigin).toString());
|
|
4358
|
+
const workflowSprintIntakeUrl = escapeHtmlAttribute(new URL('/#workflow-sprint-intake', runtimeConfig.appOrigin).toString());
|
|
4128
4359
|
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
4360
|
`<a id="send-workflow-first" href="${workflowSprintIntakeUrl}" data-recovery-offer="workflow_sprint_intake" data-offer-price="0">Send workflow first</a>`,
|
|
4139
|
-
|
|
4361
|
+
`<a href="${diagnosticUrl}" data-recovery-offer="sprint_diagnostic" data-offer-price="${sprintDiagnosticPriceDollars}">Review $${sprintDiagnosticPriceDollars} diagnostic</a>`,
|
|
4362
|
+
`<a href="${workflowSprintUrl}" data-recovery-offer="workflow_sprint" data-offer-price="${workflowSprintPriceDollars}">Scope $${workflowSprintPriceDollars} sprint</a>`,
|
|
4363
|
+
].join('\n ');
|
|
4140
4364
|
const recoveryOfferCard = recoveryOfferLinks
|
|
4141
4365
|
? `<div class="card recovery-card">
|
|
4142
4366
|
<h2>Need help deciding?</h2>
|
|
@@ -4504,19 +4728,24 @@ function renderBrokerAuditIntakeResultPage(runtimeConfig, { title, detail, leadI
|
|
|
4504
4728
|
function readBodyBuffer(req, maxBytes = 1024 * 1024) {
|
|
4505
4729
|
return new Promise((resolve, reject) => {
|
|
4506
4730
|
let total = 0;
|
|
4731
|
+
let oversize = false;
|
|
4507
4732
|
const chunks = [];
|
|
4508
4733
|
|
|
4509
4734
|
req.on('data', (chunk) => {
|
|
4510
4735
|
total += chunk.length;
|
|
4511
4736
|
if (total > maxBytes) {
|
|
4512
|
-
|
|
4513
|
-
|
|
4737
|
+
oversize = true;
|
|
4738
|
+
chunks.length = 0;
|
|
4514
4739
|
return;
|
|
4515
4740
|
}
|
|
4516
|
-
chunks.push(chunk);
|
|
4741
|
+
if (!oversize) chunks.push(chunk);
|
|
4517
4742
|
});
|
|
4518
4743
|
|
|
4519
4744
|
req.on('end', () => {
|
|
4745
|
+
if (oversize) {
|
|
4746
|
+
reject(createHttpError(413, 'Request body too large'));
|
|
4747
|
+
return;
|
|
4748
|
+
}
|
|
4520
4749
|
if (chunks.length === 0) {
|
|
4521
4750
|
resolve(Buffer.alloc(0));
|
|
4522
4751
|
return;
|
|
@@ -5320,11 +5549,38 @@ function createApiServer() {
|
|
|
5320
5549
|
|
|
5321
5550
|
// Public endpoints — no auth required
|
|
5322
5551
|
const trackedLinkMatch = pathname.match(/^\/go\/([^/]+)$/);
|
|
5323
|
-
if (isGetLikeRequest && trackedLinkMatch) {
|
|
5552
|
+
if ((isGetLikeRequest || req.method === 'POST') && trackedLinkMatch) {
|
|
5553
|
+
let trackedParsed = parsed;
|
|
5554
|
+
if (req.method === 'POST') {
|
|
5555
|
+
const target = getTrackedLinkTarget(trackedLinkMatch[1]);
|
|
5556
|
+
if (!target || !target.requiresPost) {
|
|
5557
|
+
sendJson(res, target ? 405 : 404, target
|
|
5558
|
+
? { error: 'POST is not supported for this tracked link' }
|
|
5559
|
+
: { error: 'Tracked link not found', allowed: Object.keys(TRACKED_LINK_TARGETS) }, {
|
|
5560
|
+
Allow: 'GET, HEAD',
|
|
5561
|
+
'Cache-Control': 'no-store',
|
|
5562
|
+
});
|
|
5563
|
+
return;
|
|
5564
|
+
}
|
|
5565
|
+
const contentType = String(req.headers['content-type'] || '').toLowerCase();
|
|
5566
|
+
if (!contentType.includes('application/x-www-form-urlencoded')) {
|
|
5567
|
+
sendJson(res, 415, { error: 'Form-encoded checkout confirmation is required' }, {
|
|
5568
|
+
'Cache-Control': 'no-store',
|
|
5569
|
+
});
|
|
5570
|
+
return;
|
|
5571
|
+
}
|
|
5572
|
+
const form = await parseFormBody(req, 16 * 1024);
|
|
5573
|
+
trackedParsed = new URL(parsed.toString());
|
|
5574
|
+
for (const [key, value] of Object.entries(form)) {
|
|
5575
|
+
if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.includes(key)) {
|
|
5576
|
+
trackedParsed.searchParams.set(key, String(value || '').trim());
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5324
5580
|
serveTrackedLinkRedirect({
|
|
5325
5581
|
req,
|
|
5326
5582
|
res,
|
|
5327
|
-
parsed,
|
|
5583
|
+
parsed: trackedParsed,
|
|
5328
5584
|
hostedConfig,
|
|
5329
5585
|
isHeadRequest,
|
|
5330
5586
|
slug: trackedLinkMatch[1],
|
|
@@ -5782,6 +6038,22 @@ async function addContext(){
|
|
|
5782
6038
|
return;
|
|
5783
6039
|
}
|
|
5784
6040
|
|
|
6041
|
+
if (isGetLikeRequest && pathname.startsWith('/blog/') && pathname !== '/blog/') {
|
|
6042
|
+
try {
|
|
6043
|
+
const blogPostPath = resolvePublicHtmlFile(BLOG_PAGE_PATHS_BY_SLUG, pathname.replace('/blog/', ''));
|
|
6044
|
+
if (!blogPostPath) {
|
|
6045
|
+
sendJson(res, 404, { error: 'Post not found' });
|
|
6046
|
+
return;
|
|
6047
|
+
}
|
|
6048
|
+
const requestHost = getSafePublicRequestHost(req);
|
|
6049
|
+
const html = normalizePublicMarketingHtml(fs.readFileSync(blogPostPath, 'utf-8'), hostedConfig, requestHost);
|
|
6050
|
+
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
6051
|
+
} catch {
|
|
6052
|
+
sendJson(res, 404, { error: 'Post not found' });
|
|
6053
|
+
}
|
|
6054
|
+
return;
|
|
6055
|
+
}
|
|
6056
|
+
|
|
5785
6057
|
if (isGetLikeRequest && (pathname === '/learn' || pathname === '/learn.html')) {
|
|
5786
6058
|
try {
|
|
5787
6059
|
servePublicMarketingPage({
|
|
@@ -6132,11 +6404,11 @@ async function addContext(){
|
|
|
6132
6404
|
return;
|
|
6133
6405
|
}
|
|
6134
6406
|
|
|
6135
|
-
// HOTFIX 2026-06-04 — accept
|
|
6407
|
+
// HOTFIX 2026-06-04 — accept GET, HEAD, and POST on /checkout/pro
|
|
6136
6408
|
// to prevent the API-key guard from 401'ing real prospective customers
|
|
6137
|
-
// whose forms
|
|
6138
|
-
//
|
|
6139
|
-
//
|
|
6409
|
+
// whose forms land via POST. Audit: 69 emails submitted → 0 paid because
|
|
6410
|
+
// POST hit the auth gate. Form bodies are now allowlisted into the same
|
|
6411
|
+
// attribution path as query parameters before checkout evaluation.
|
|
6140
6412
|
if ((isGetLikeRequest || req.method === 'POST') && pathname === '/checkout/pro') {
|
|
6141
6413
|
if (isHeadRequest) {
|
|
6142
6414
|
sendHtml(res, 200, '', {}, {
|
|
@@ -6145,6 +6417,26 @@ async function addContext(){
|
|
|
6145
6417
|
return;
|
|
6146
6418
|
}
|
|
6147
6419
|
|
|
6420
|
+
if (req.method === 'POST') {
|
|
6421
|
+
const contentType = String(req.headers['content-type'] || '').toLowerCase();
|
|
6422
|
+
if (!contentType.includes('application/x-www-form-urlencoded')) {
|
|
6423
|
+
sendJson(res, 415, { error: 'Form-encoded checkout confirmation is required' }, {
|
|
6424
|
+
'Cache-Control': 'no-store',
|
|
6425
|
+
});
|
|
6426
|
+
return;
|
|
6427
|
+
}
|
|
6428
|
+
const form = await parseFormBody(req, 16 * 1024);
|
|
6429
|
+
for (const [key, value] of Object.entries(form)) {
|
|
6430
|
+
if (
|
|
6431
|
+
key === 'confirm'
|
|
6432
|
+
|| key === 'customer_email'
|
|
6433
|
+
|| TRACKED_LINK_QUERY_KEYS.includes(key)
|
|
6434
|
+
) {
|
|
6435
|
+
parsed.searchParams.set(key, String(value || '').trim());
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
|
|
6148
6440
|
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
6149
6441
|
const journeyState = resolveJourneyState(req, parsed);
|
|
6150
6442
|
const bootstrapBody = buildCheckoutBootstrapBody(parsed, req, journeyState);
|
|
@@ -6163,33 +6455,26 @@ async function addContext(){
|
|
|
6163
6455
|
// it and followed it, bypassing the bot deflection. A bot crawl never
|
|
6164
6456
|
// reaches the payment form, so the session is guaranteed waste.
|
|
6165
6457
|
//
|
|
6166
|
-
// Fix: bot
|
|
6167
|
-
//
|
|
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.
|
|
6458
|
+
// Fix: bot-classified traffic never reaches session creation, and a
|
|
6459
|
+
// browser must provide both explicit confirmation and a valid email.
|
|
6171
6460
|
const rawCheckoutEmail = normalizeNullableText(bootstrapBody.customerEmail);
|
|
6172
6461
|
const normalizedCheckoutEmail = normalizeCheckoutCustomerEmail(rawCheckoutEmail);
|
|
6173
6462
|
const hasCustomerEmailHint = !!parsed?.searchParams?.has('customer_email');
|
|
6174
6463
|
const hasValidCustomerEmailHint = !!normalizedCheckoutEmail;
|
|
6175
|
-
const botShouldBypass = !botClassification.isBot
|
|
6176
|
-
//
|
|
6177
|
-
//
|
|
6178
|
-
//
|
|
6179
|
-
// Stripe
|
|
6180
|
-
// still need a valid email hint to bypass deflection.
|
|
6464
|
+
const botShouldBypass = !botClassification.isBot;
|
|
6465
|
+
// A valid email is the explicit human-intent proof for both GET
|
|
6466
|
+
// confirmation and form POST. This prevents browser-shaped monitors
|
|
6467
|
+
// from minting anonymous sessions while preserving a one-field path to
|
|
6468
|
+
// Stripe for real buyers.
|
|
6181
6469
|
const isConfirmedCheckout = (
|
|
6182
6470
|
(req.method === 'POST' && hasValidCustomerEmailHint)
|
|
6183
|
-
|| hasConfirmFlag
|
|
6471
|
+
|| (hasConfirmFlag && hasValidCustomerEmailHint)
|
|
6184
6472
|
) && botShouldBypass;
|
|
6185
|
-
//
|
|
6186
|
-
//
|
|
6187
|
-
//
|
|
6188
|
-
//
|
|
6189
|
-
|
|
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';
|
|
6473
|
+
// Legacy env-gated bypass. It is default-off and still requires a valid
|
|
6474
|
+
// email plus a non-bot request; enabling it must never re-open anonymous
|
|
6475
|
+
// Stripe-session creation. Attribution is preserved via
|
|
6476
|
+
// buildCheckoutFallbackUrl.
|
|
6477
|
+
const interstitialBypassEnabled = process.env.THUMBGATE_CHECKOUT_INTERSTITIAL_BYPASS === '1';
|
|
6193
6478
|
const interstitialSampleRate = normalizeCheckoutInterstitialSampleRate(
|
|
6194
6479
|
process.env.THUMBGATE_CHECKOUT_INTERSTITIAL_SAMPLE_RATE
|
|
6195
6480
|
);
|
|
@@ -6203,6 +6488,7 @@ async function addContext(){
|
|
|
6203
6488
|
&& interstitialBypassEnabled
|
|
6204
6489
|
&& req.method !== 'POST'
|
|
6205
6490
|
&& botShouldBypass
|
|
6491
|
+
&& hasValidCustomerEmailHint
|
|
6206
6492
|
&& !interstitialSampled
|
|
6207
6493
|
) {
|
|
6208
6494
|
// Always target the pro Stripe Payment Link directly. The
|
|
@@ -6258,19 +6544,10 @@ async function addContext(){
|
|
|
6258
6544
|
planId: analyticsMetadata.planId,
|
|
6259
6545
|
},
|
|
6260
6546
|
}).catch(() => {});
|
|
6261
|
-
// Render the
|
|
6262
|
-
//
|
|
6263
|
-
//
|
|
6264
|
-
//
|
|
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.
|
|
6547
|
+
// Render the intent form for every non-confirmed request. A raw GET can
|
|
6548
|
+
// come from a crawler, link preview, monitor, or visitor who has not yet
|
|
6549
|
+
// supplied buyer identity. The form requires email-backed confirmation
|
|
6550
|
+
// before any Stripe session is created.
|
|
6274
6551
|
if (!isConfirmedCheckout) {
|
|
6275
6552
|
const eventType = botClassification.isBot
|
|
6276
6553
|
? 'checkout_bot_deflected'
|
|
@@ -6316,16 +6593,6 @@ async function addContext(){
|
|
|
6316
6593
|
return;
|
|
6317
6594
|
}
|
|
6318
6595
|
|
|
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
6596
|
bootstrapBody.customerEmail = normalizedCheckoutEmail || undefined;
|
|
6330
6597
|
|
|
6331
6598
|
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
@@ -7172,9 +7439,28 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7172
7439
|
journeySummary: null,
|
|
7173
7440
|
};
|
|
7174
7441
|
|
|
7442
|
+
// Bounded reverse scan — full-file read of multi-MB ledgers was timing
|
|
7443
|
+
// out operator dashboards and revenue:doctor probes.
|
|
7444
|
+
let readJsonlSinceTail;
|
|
7445
|
+
try {
|
|
7446
|
+
({ readJsonlSinceTail } = require('../../scripts/jsonl-window'));
|
|
7447
|
+
} catch {
|
|
7448
|
+
readJsonlSinceTail = null;
|
|
7449
|
+
}
|
|
7450
|
+
|
|
7175
7451
|
function readJsonlSince(p) {
|
|
7452
|
+
if (typeof readJsonlSinceTail === 'function') {
|
|
7453
|
+
const windowed = readJsonlSinceTail(p, {
|
|
7454
|
+
sinceMs: since,
|
|
7455
|
+
limit,
|
|
7456
|
+
maxBytes: 8 * 1024 * 1024,
|
|
7457
|
+
});
|
|
7458
|
+
return windowed;
|
|
7459
|
+
}
|
|
7176
7460
|
try {
|
|
7177
|
-
if (!fs.existsSync(p))
|
|
7461
|
+
if (!fs.existsSync(p)) {
|
|
7462
|
+
return { rows: [], totalAfterSince: 0, truncated: false };
|
|
7463
|
+
}
|
|
7178
7464
|
const text = fs.readFileSync(p, 'utf8');
|
|
7179
7465
|
const rows = [];
|
|
7180
7466
|
for (const line of text.split('\n')) {
|
|
@@ -7187,16 +7473,22 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7187
7473
|
rows.push(obj);
|
|
7188
7474
|
}
|
|
7189
7475
|
}
|
|
7190
|
-
return
|
|
7191
|
-
|
|
7476
|
+
return {
|
|
7477
|
+
rows: rows.slice(-limit),
|
|
7478
|
+
totalAfterSince: rows.length,
|
|
7479
|
+
truncated: rows.length > limit,
|
|
7480
|
+
};
|
|
7481
|
+
} catch {
|
|
7482
|
+
return { rows: [], totalAfterSince: 0, truncated: false };
|
|
7483
|
+
}
|
|
7192
7484
|
}
|
|
7193
7485
|
|
|
7194
7486
|
if (wantTelemetry) {
|
|
7195
7487
|
const tp = path.join(exportDir, 'telemetry-pings.jsonl');
|
|
7196
|
-
const
|
|
7197
|
-
result.telemetry.totalAfterSince =
|
|
7198
|
-
result.telemetry.rows =
|
|
7199
|
-
result.telemetry.truncated =
|
|
7488
|
+
const windowed = readJsonlSince(tp);
|
|
7489
|
+
result.telemetry.totalAfterSince = windowed.totalAfterSince;
|
|
7490
|
+
result.telemetry.rows = windowed.rows;
|
|
7491
|
+
result.telemetry.truncated = windowed.truncated;
|
|
7200
7492
|
}
|
|
7201
7493
|
|
|
7202
7494
|
if (wantFunnel) {
|
|
@@ -7211,10 +7503,10 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7211
7503
|
} catch {
|
|
7212
7504
|
funnelPath = path.join(exportDir, 'funnel-events.jsonl');
|
|
7213
7505
|
}
|
|
7214
|
-
const
|
|
7215
|
-
result.funnel.totalAfterSince =
|
|
7216
|
-
result.funnel.rows =
|
|
7217
|
-
result.funnel.truncated =
|
|
7506
|
+
const windowed = readJsonlSince(funnelPath);
|
|
7507
|
+
result.funnel.totalAfterSince = windowed.totalAfterSince;
|
|
7508
|
+
result.funnel.rows = windowed.rows;
|
|
7509
|
+
result.funnel.truncated = windowed.truncated;
|
|
7218
7510
|
}
|
|
7219
7511
|
|
|
7220
7512
|
try {
|
|
@@ -7952,6 +8244,45 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7952
8244
|
return;
|
|
7953
8245
|
}
|
|
7954
8246
|
|
|
8247
|
+
// PayPal webhook evidence. PayPal authenticates the delivery through its
|
|
8248
|
+
// verify-webhook-signature API before raw proof is persisted locally.
|
|
8249
|
+
if (req.method === 'POST' && pathname === '/v1/billing/paypal-webhook') {
|
|
8250
|
+
try {
|
|
8251
|
+
const rawBody = await readBodyBuffer(req);
|
|
8252
|
+
const delivery = await recordPayPalWebhookDelivery({
|
|
8253
|
+
rawBody,
|
|
8254
|
+
headers: req.headers,
|
|
8255
|
+
});
|
|
8256
|
+
if (!delivery.verified || !delivery.recorded) {
|
|
8257
|
+
sendProblem(res, {
|
|
8258
|
+
type: PROBLEM_TYPES.WEBHOOK_INVALID,
|
|
8259
|
+
title: 'Invalid PayPal webhook delivery',
|
|
8260
|
+
status: delivery.verified ? 503 : 400,
|
|
8261
|
+
detail: delivery.verified
|
|
8262
|
+
? 'The PayPal-verified webhook could not be written to the evidence ledger.'
|
|
8263
|
+
: 'The PayPal transmission headers, event, or remote signature verification were invalid.',
|
|
8264
|
+
});
|
|
8265
|
+
return;
|
|
8266
|
+
}
|
|
8267
|
+
sendJson(res, 200, {
|
|
8268
|
+
verified: true,
|
|
8269
|
+
recorded: true,
|
|
8270
|
+
duplicate: delivery.duplicate === true,
|
|
8271
|
+
eventId: delivery.eventId,
|
|
8272
|
+
eventType: delivery.eventType,
|
|
8273
|
+
payloadSha256: delivery.payloadSha256,
|
|
8274
|
+
});
|
|
8275
|
+
} catch (err) {
|
|
8276
|
+
sendProblem(res, {
|
|
8277
|
+
type: !err.statusCode || err.statusCode >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.BAD_REQUEST,
|
|
8278
|
+
title: !err.statusCode || err.statusCode >= 500 ? 'Internal Server Error' : 'Request Error',
|
|
8279
|
+
status: err.statusCode || 500,
|
|
8280
|
+
detail: err.message,
|
|
8281
|
+
});
|
|
8282
|
+
}
|
|
8283
|
+
return;
|
|
8284
|
+
}
|
|
8285
|
+
|
|
7955
8286
|
// GitHub Marketplace webhook
|
|
7956
8287
|
if (req.method === 'POST' && pathname === '/v1/billing/github-webhook') {
|
|
7957
8288
|
try {
|
|
@@ -7963,12 +8294,20 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7963
8294
|
});
|
|
7964
8295
|
|
|
7965
8296
|
const sig = req.headers['x-hub-signature-256'] || '';
|
|
7966
|
-
|
|
8297
|
+
const delivery = recordGithubMarketplaceWebhookDelivery({
|
|
8298
|
+
rawBody,
|
|
8299
|
+
signature: sig,
|
|
8300
|
+
deliveryId: req.headers['x-github-delivery'] || '',
|
|
8301
|
+
eventName: req.headers['x-github-event'] || '',
|
|
8302
|
+
});
|
|
8303
|
+
if (!delivery.verified || !delivery.recorded) {
|
|
7967
8304
|
sendProblem(res, {
|
|
7968
8305
|
type: PROBLEM_TYPES.WEBHOOK_INVALID,
|
|
7969
|
-
title: 'Invalid webhook
|
|
7970
|
-
status: 400,
|
|
7971
|
-
detail:
|
|
8306
|
+
title: 'Invalid GitHub webhook delivery',
|
|
8307
|
+
status: delivery.verified ? 503 : 400,
|
|
8308
|
+
detail: delivery.verified
|
|
8309
|
+
? 'The verified webhook could not be written to the evidence ledger.'
|
|
8310
|
+
: 'The webhook signature, delivery headers, or payload could not be verified.',
|
|
7972
8311
|
});
|
|
7973
8312
|
return;
|
|
7974
8313
|
}
|
|
@@ -7986,7 +8325,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7986
8325
|
return;
|
|
7987
8326
|
}
|
|
7988
8327
|
|
|
7989
|
-
const result = handleGithubWebhook(event);
|
|
8328
|
+
const result = handleGithubWebhook(event, delivery);
|
|
7990
8329
|
sendJson(res, 200, result);
|
|
7991
8330
|
} catch (err) {
|
|
7992
8331
|
sendProblem(res, {
|
|
@@ -8108,7 +8447,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8108
8447
|
const isOperatorBillingRequest = Boolean(expectedOperatorKey)
|
|
8109
8448
|
&& _reqToken === expectedOperatorKey
|
|
8110
8449
|
&& req.method === 'GET'
|
|
8111
|
-
&&
|
|
8450
|
+
&& ['/v1/billing/summary', '/v1/intake/workflow-sprint/queue'].includes(pathname);
|
|
8112
8451
|
|
|
8113
8452
|
if (!isOperatorBillingRequest && !isAuthorized(req, expectedApiKey)) {
|
|
8114
8453
|
sendProblem(res, {
|
|
@@ -9406,6 +9745,33 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9406
9745
|
return;
|
|
9407
9746
|
}
|
|
9408
9747
|
|
|
9748
|
+
// GET /v1/intake/workflow-sprint/queue — authenticated, no-store operator close queue
|
|
9749
|
+
if (req.method === 'GET' && pathname === '/v1/intake/workflow-sprint/queue') {
|
|
9750
|
+
if (!isBillingSummaryAuthorized(req, expectedApiKey, expectedOperatorKey)) {
|
|
9751
|
+
sendProblem(res, {
|
|
9752
|
+
type: PROBLEM_TYPES.FORBIDDEN,
|
|
9753
|
+
title: 'Forbidden',
|
|
9754
|
+
status: 403,
|
|
9755
|
+
detail: 'Admin or operator API key required for this endpoint.',
|
|
9756
|
+
});
|
|
9757
|
+
return;
|
|
9758
|
+
}
|
|
9759
|
+
try {
|
|
9760
|
+
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
9761
|
+
const workflowSprintIntake = requirePrivateApiModule(
|
|
9762
|
+
'workflowSprintIntake',
|
|
9763
|
+
'Workflow sprint intake'
|
|
9764
|
+
);
|
|
9765
|
+
const queue = buildWorkflowIntakeQueue(parsed, workflowSprintIntake, FEEDBACK_DIR, {
|
|
9766
|
+
publicOrigin: hostedConfig.appOrigin,
|
|
9767
|
+
});
|
|
9768
|
+
sendJson(res, 200, queue, PRIVATE_OPERATOR_RESPONSE_HEADERS);
|
|
9769
|
+
} catch (err) {
|
|
9770
|
+
sendWorkflowIntakeQueueProblem(res, err);
|
|
9771
|
+
}
|
|
9772
|
+
return;
|
|
9773
|
+
}
|
|
9774
|
+
|
|
9409
9775
|
// GET /v1/billing/summary — operator billing summary (admin key or operator key)
|
|
9410
9776
|
if (req.method === 'GET' && pathname === '/v1/billing/summary') {
|
|
9411
9777
|
if (!isBillingSummaryAuthorized(req, expectedApiKey, expectedOperatorKey)) {
|
|
@@ -9418,6 +9784,48 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9418
9784
|
return;
|
|
9419
9785
|
}
|
|
9420
9786
|
|
|
9787
|
+
const includeIntakeQueue = wantsWorkflowIntakeQueue(parsed);
|
|
9788
|
+
let intakeQueue = null;
|
|
9789
|
+
if (includeIntakeQueue) {
|
|
9790
|
+
try {
|
|
9791
|
+
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
9792
|
+
const workflowSprintIntake = requirePrivateApiModule(
|
|
9793
|
+
'workflowSprintIntake',
|
|
9794
|
+
'Workflow sprint intake'
|
|
9795
|
+
);
|
|
9796
|
+
intakeQueue = buildWorkflowIntakeQueue(parsed, workflowSprintIntake, FEEDBACK_DIR, {
|
|
9797
|
+
publicOrigin: hostedConfig.appOrigin,
|
|
9798
|
+
});
|
|
9799
|
+
} catch (err) {
|
|
9800
|
+
sendWorkflowIntakeQueueProblem(res, err);
|
|
9801
|
+
return;
|
|
9802
|
+
}
|
|
9803
|
+
}
|
|
9804
|
+
|
|
9805
|
+
const runtimePresence = buildHostedRuntimePresence(hostedConfig, {
|
|
9806
|
+
expectedApiKey,
|
|
9807
|
+
expectedOperatorKey,
|
|
9808
|
+
});
|
|
9809
|
+
const requestedWindow = String(parsed.searchParams.get('window') || '').trim().toLowerCase();
|
|
9810
|
+
if (requestedWindow === 'all') {
|
|
9811
|
+
let summaryOptionsByWindow;
|
|
9812
|
+
try {
|
|
9813
|
+
summaryOptionsByWindow = resolveBillingSummaryBatchOptions(parsed);
|
|
9814
|
+
} catch (err) {
|
|
9815
|
+
sendInvalidAnalyticsWindowProblem(res, 'Invalid billing summary query', err);
|
|
9816
|
+
return;
|
|
9817
|
+
}
|
|
9818
|
+
const summaries = await getBillingSummariesLive(summaryOptionsByWindow);
|
|
9819
|
+
sendJson(res, 200, {
|
|
9820
|
+
generatedAt: new Date().toISOString(),
|
|
9821
|
+
windows: BILLING_SUMMARY_BATCH_WINDOWS,
|
|
9822
|
+
summaries,
|
|
9823
|
+
runtimePresence,
|
|
9824
|
+
...(intakeQueue ? { intakeQueue } : {}),
|
|
9825
|
+
}, PRIVATE_OPERATOR_RESPONSE_HEADERS);
|
|
9826
|
+
return;
|
|
9827
|
+
}
|
|
9828
|
+
|
|
9421
9829
|
const summaryOptions = resolveBillingSummaryOptionsOrRespondProblem(
|
|
9422
9830
|
res,
|
|
9423
9831
|
parsed,
|
|
@@ -9430,11 +9838,9 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9430
9838
|
const summary = await getBillingSummaryLive(summaryOptions);
|
|
9431
9839
|
sendJson(res, 200, {
|
|
9432
9840
|
...summary,
|
|
9433
|
-
runtimePresence
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
}),
|
|
9437
|
-
});
|
|
9841
|
+
runtimePresence,
|
|
9842
|
+
...(intakeQueue ? { intakeQueue } : {}),
|
|
9843
|
+
}, PRIVATE_OPERATOR_RESPONSE_HEADERS);
|
|
9438
9844
|
return;
|
|
9439
9845
|
}
|
|
9440
9846
|
|
|
@@ -9887,6 +10293,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9887
10293
|
function startServer({ port, host } = {}) {
|
|
9888
10294
|
const listenPort = Number(port ?? process.env.PORT ?? 8787);
|
|
9889
10295
|
const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
|
|
10296
|
+
fs.mkdirSync(getFeedbackPaths().FEEDBACK_DIR, { recursive: true });
|
|
9890
10297
|
const server = createApiServer();
|
|
9891
10298
|
registerGracefulShutdown(server);
|
|
9892
10299
|
return new Promise((resolve) => {
|