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.
- package/.claude/commands/dashboard.md +11 -1
- package/.claude/commands/thumbgate-dashboard.md +23 -8
- 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 +66 -3
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +88 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +8 -8
- package/bin/postinstall.js +4 -13
- package/commands/dashboard.md +11 -1
- package/commands/thumbgate-dashboard.md +23 -8
- package/config/agent-outcome-monitor-thresholds.json +63 -0
- package/config/evals/agent-outcomes-baseline.json +17 -0
- package/config/evals/agent-outcomes-golden.json +412 -0
- package/config/evals/prompt-eval-baseline.json +23 -0
- package/config/github-about.json +5 -4
- package/config/post-deploy-marketing-pages.json +6 -6
- package/config/schemas/task-outcome-receipt.schema.json +296 -0
- package/docs/integrations/grafana/README.md +109 -0
- package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
- package/openapi/openapi.yaml +475 -5
- package/package.json +75 -22
- 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 +1090 -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/agent-outcome-eval.js +130 -0
- package/scripts/agent-outcome-monitor.js +261 -0
- package/scripts/agent-reasoning-traces.js +8 -9
- package/scripts/async-job-runner.js +107 -13
- 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/durability/step.js +121 -12
- package/scripts/external-customer-audit.js +881 -0
- package/scripts/feedback-loop.js +26 -0
- package/scripts/gates-engine.js +554 -19
- package/scripts/grafana-revenue-evidence.js +856 -0
- package/scripts/human-escalation.js +265 -0
- package/scripts/hybrid-feedback-context.js +93 -50
- package/scripts/jsonl-window.js +89 -0
- package/scripts/judge-reward-function.js +30 -18
- 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/prompt-eval.js +81 -4
- 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/schedule-manager.js +249 -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/task-outcomes.js +425 -0
- package/scripts/telemetry-analytics.js +23 -3
- package/scripts/tool-contract-validator.js +287 -59
- package/scripts/tool-registry.js +143 -0
- package/scripts/vector-store.js +83 -7
- package/scripts/workflow-intake-queue.js +483 -0
- 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
|
-
|
|
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
|
-
|
|
486
|
-
fallbackHref: SPRINT_DIAGNOSTIC_CHECKOUT_URL,
|
|
487
|
-
external: true,
|
|
504
|
+
path: '/diagnostic',
|
|
488
505
|
ctaId: 'go_diagnostic',
|
|
489
506
|
ctaPlacement: 'link_router',
|
|
490
|
-
eventType: '
|
|
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
|
-
|
|
501
|
-
fallbackHref: WORKFLOW_SPRINT_CHECKOUT_URL,
|
|
502
|
-
external: true,
|
|
534
|
+
path: '/diagnostic',
|
|
503
535
|
ctaId: 'go_sprint',
|
|
504
536
|
ctaPlacement: 'link_router',
|
|
505
|
-
eventType: '
|
|
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.
|
|
2265
|
-
<form action="
|
|
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="
|
|
2268
|
-
<
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4513
|
-
|
|
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
|
|
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
|
|
6138
|
-
//
|
|
6139
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
6176
|
-
//
|
|
6177
|
-
//
|
|
6178
|
-
//
|
|
6179
|
-
// Stripe
|
|
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
|
-
//
|
|
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';
|
|
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
|
|
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.
|
|
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))
|
|
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
|
|
7191
|
-
|
|
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
|
|
7197
|
-
result.telemetry.totalAfterSince =
|
|
7198
|
-
result.telemetry.rows =
|
|
7199
|
-
result.telemetry.truncated =
|
|
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
|
|
7215
|
-
result.funnel.totalAfterSince =
|
|
7216
|
-
result.funnel.rows =
|
|
7217
|
-
result.funnel.truncated =
|
|
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
|
-
|
|
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
|
|
7970
|
-
status: 400,
|
|
7971
|
-
detail:
|
|
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
|
|
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
|
|
8109
|
-
&& _reqToken
|
|
8488
|
+
const isOperatorReadRequest = Boolean(expectedOperatorKey)
|
|
8489
|
+
&& safeKeyEqual(_reqToken, expectedOperatorKey)
|
|
8110
8490
|
&& req.method === 'GET'
|
|
8111
|
-
&&
|
|
8491
|
+
&& [
|
|
8492
|
+
'/v1/billing/summary',
|
|
8493
|
+
'/v1/intake/workflow-sprint/queue',
|
|
8494
|
+
'/v1/task-outcomes/monitor',
|
|
8495
|
+
].includes(pathname);
|
|
8112
8496
|
|
|
8113
|
-
if (!
|
|
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
|
|
9434
|
-
|
|
9435
|
-
|
|
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) => {
|