thumbgate 1.27.20 → 1.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +124 -129
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +18 -33
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +95 -78
- package/bin/postinstall.js +1 -1
- package/config/entitlement-public-keys.json +6 -0
- package/config/gates/default.json +3 -3
- package/config/github-about.json +2 -5
- package/config/merge-quality-checks.json +3 -0
- package/config/post-deploy-marketing-pages.json +1 -1
- package/hooks/hooks.json +38 -0
- package/package.json +28 -10
- package/public/about.html +1 -4
- package/public/agent-manager.html +6 -6
- package/public/agents-cost-savings.html +3 -3
- package/public/ai-malpractice-prevention.html +1 -1
- package/public/blog.html +12 -9
- package/public/chatgpt-app.html +3 -3
- package/public/codex-enterprise.html +3 -3
- package/public/codex-plugin.html +5 -5
- package/public/compare.html +10 -10
- package/public/dashboard.html +1 -1
- package/public/diagnostic.html +23 -1
- package/public/federal.html +2 -2
- package/public/guide.html +14 -14
- package/public/index.html +202 -220
- package/public/install.html +14 -14
- package/public/learn.html +4 -17
- package/public/numbers.html +2 -2
- package/public/partner-intake.html +198 -0
- package/public/pricing.html +61 -31
- package/public/pro.html +2 -2
- package/scripts/agent-reward-model.js +13 -0
- package/scripts/bayes-optimal-gate.js +6 -1
- package/scripts/billing.js +815 -137
- package/scripts/checkout-attribution-reference.js +85 -0
- package/scripts/claude-feedback-sync.js +23 -6
- package/scripts/cli-feedback.js +33 -8
- package/scripts/commercial-offer.js +3 -3
- package/scripts/entitlement.js +250 -0
- package/scripts/export-databricks-bundle.js +5 -0
- package/scripts/export-dpo-pairs.js +6 -0
- package/scripts/export-hf-dataset.js +5 -0
- package/scripts/feedback-loop.js +290 -1
- package/scripts/feedback-quality.js +25 -26
- package/scripts/feedback-sanitizer.js +151 -3
- package/scripts/gates-engine.js +134 -43
- package/scripts/hosted-config.js +9 -2
- package/scripts/imperative-detector.js +85 -0
- package/scripts/intervention-policy.js +13 -0
- package/scripts/mailer/resend-mailer.js +11 -5
- package/scripts/pr-manager.js +9 -22
- package/scripts/pro-local-dashboard.js +198 -0
- package/scripts/risk-scorer.js +6 -0
- package/scripts/secret-scanner.js +363 -68
- package/scripts/self-protection.js +21 -36
- package/scripts/seo-gsd.js +2 -2
- package/scripts/thompson-sampling.js +11 -2
- package/src/api/server.js +202 -22
- package/public/assets/brand/github-social-preview.png +0 -0
package/src/api/server.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
createUnavailableAsyncOperation,
|
|
11
11
|
loadOptionalModule,
|
|
12
12
|
} = require('../../scripts/private-core-boundary');
|
|
13
|
+
const { redactSecrets, redactSecretsDeep } = require('../../scripts/secret-redaction');
|
|
13
14
|
|
|
14
15
|
const POSTHOG_API_PATHS = new Set(['/capture', '/batch', '/decide', '/e', '/engage']);
|
|
15
16
|
const POSTHOG_INGEST_HOST = 'us.i.posthog.com';
|
|
@@ -20,11 +21,16 @@ const POSTHOG_STATIC_PATH_PREFIX = '/static/';
|
|
|
20
21
|
// Stripe catalog, with the per-tier thumbnails wired in. Re-run the
|
|
21
22
|
// bootstrap workflow to regenerate; the new URLs surface in the workflow
|
|
22
23
|
// summary log.
|
|
24
|
+
const PRO_CHECKOUT_URL = 'https://buy.stripe.com/8x2dR91M84r4cSd9uj3sI3f';
|
|
23
25
|
const FIRST_FAILURE_RULE_CHECKOUT_URL = 'https://buy.stripe.com/7sY6oHaiEbTw6tP5e33sI3e';
|
|
24
26
|
const QUICK_READ_CHECKOUT_URL = 'https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H';
|
|
25
27
|
const WORKFLOW_TEARDOWN_CHECKOUT_URL = 'https://buy.stripe.com/8x214n2Qc4r44lHayn3sI2I';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// Verified live amounts (2026-07-13): diagnostic Stripe page contains $499;
|
|
29
|
+
// sprint code-default Stripe slug previously pointed at a $499 diagnostic product.
|
|
30
|
+
// Production sprint rail is PayPal at $1500 — pin that as the code fallback so a
|
|
31
|
+
// missing Railway env cannot silently charge $499 under a $1500 label.
|
|
32
|
+
const SPRINT_DIAGNOSTIC_CHECKOUT_URL = 'https://buy.stripe.com/9B69ATbmI4r4aK5eOD3sI3k';
|
|
33
|
+
const WORKFLOW_SPRINT_CHECKOUT_URL = 'https://www.paypal.com/ncp/payment/LTQFR7P9AR3QG';
|
|
28
34
|
|
|
29
35
|
function getPosthogProxyPath(pathname) {
|
|
30
36
|
return pathname.slice('/ingest'.length) || '/';
|
|
@@ -213,12 +219,17 @@ const {
|
|
|
213
219
|
const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
|
|
214
220
|
const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
|
|
215
221
|
const mcpOauth = require('../../scripts/mcp-oauth');
|
|
222
|
+
const { packCheckoutReference } = require('../../scripts/checkout-attribution-reference');
|
|
216
223
|
// OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
|
|
217
224
|
// (Claude Connectors Directory requires OAuth for authenticated services).
|
|
218
225
|
const oauthStore = mcpOauth.createStore();
|
|
219
226
|
const pendingOauthAuthorizeRequests = new Map();
|
|
220
227
|
const OAUTH_AUTHORIZE_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
221
228
|
const resendMailer = require('../../scripts/mailer/resend-mailer');
|
|
229
|
+
const {
|
|
230
|
+
fingerprintProKey,
|
|
231
|
+
sendProActivationAlert,
|
|
232
|
+
} = require('../../scripts/pro-local-dashboard');
|
|
222
233
|
const {
|
|
223
234
|
buildContextFootprintReport,
|
|
224
235
|
} = require('../../scripts/context-footprint');
|
|
@@ -242,6 +253,7 @@ const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
|
|
|
242
253
|
const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
|
|
243
254
|
const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
|
|
244
255
|
const DIAGNOSTIC_PAGE_PATH = path.resolve(__dirname, '../../public/diagnostic.html');
|
|
256
|
+
const PARTNER_INTAKE_PAGE_PATH = path.resolve(__dirname, '../../public/partner-intake.html');
|
|
245
257
|
const INSTALL_PAGE_PATH = path.resolve(__dirname, '../../public/install.html');
|
|
246
258
|
const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
247
259
|
const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
|
|
@@ -1426,7 +1438,6 @@ function buildCheckoutAttributionMetadata(body, req, traceId) {
|
|
|
1426
1438
|
: 1;
|
|
1427
1439
|
|
|
1428
1440
|
return {
|
|
1429
|
-
...rawMetadata,
|
|
1430
1441
|
traceId,
|
|
1431
1442
|
acquisitionId: pickFirstText(rawMetadata.acquisitionId, body.acquisitionId),
|
|
1432
1443
|
visitorId: pickFirstText(rawMetadata.visitorId, body.visitorId),
|
|
@@ -1760,8 +1771,10 @@ function appendFeedbackListLines(lines, { entries, signal, intent }) {
|
|
|
1760
1771
|
lines.push(`No ${signal || 'feedback'} entries found ${intent.windowLabel}.`);
|
|
1761
1772
|
return;
|
|
1762
1773
|
}
|
|
1763
|
-
lines.push(
|
|
1764
|
-
|
|
1774
|
+
lines.push(
|
|
1775
|
+
`${FEEDBACK_LIST_LABELS[signal] || 'Recent feedback'} (${intent.windowLabel}):`,
|
|
1776
|
+
...entries.map(formatFeedbackEntry)
|
|
1777
|
+
);
|
|
1765
1778
|
}
|
|
1766
1779
|
|
|
1767
1780
|
function buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeline }) {
|
|
@@ -2095,6 +2108,10 @@ function buildCheckoutFallbackUrl(baseUrl, metadata = {}) {
|
|
|
2095
2108
|
appendQueryParam(url, 'seat_count', metadata.seatCount);
|
|
2096
2109
|
appendQueryParam(url, 'landing_path', metadata.landingPath);
|
|
2097
2110
|
appendQueryParam(url, 'referrer_host', metadata.referrerHost);
|
|
2111
|
+
// External Stripe Payment Links drop utm_* query params but preserve
|
|
2112
|
+
// client_reference_id — pack attribution into it so marketplace-attributed paid
|
|
2113
|
+
// checkouts (e.g. utm_source=aiventyx) survive to the webhook and can be credited.
|
|
2114
|
+
appendQueryParam(url, 'client_reference_id', packCheckoutReference(metadata));
|
|
2098
2115
|
return restoreStripeCheckoutPlaceholder(url.toString());
|
|
2099
2116
|
}
|
|
2100
2117
|
|
|
@@ -2244,8 +2261,8 @@ a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:
|
|
|
2244
2261
|
<div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div>
|
|
2245
2262
|
<h1>Start ThumbGate Pro</h1>
|
|
2246
2263
|
<div class="price">$19<small>/mo</small></div>
|
|
2247
|
-
<p>The npm package runs your gates locally. <strong>Pro</strong>
|
|
2248
|
-
<form action="
|
|
2264
|
+
<p>The npm package runs your gates locally. <strong>Pro</strong> removes solo caps and adds personal recall, proof, exports, and adapter maintenance. Shared hosted lessons and org dashboards are Enterprise.</p>
|
|
2265
|
+
<form action="${PRO_CHECKOUT_URL}" method="GET" data-i="pro_checkout_confirmed">
|
|
2249
2266
|
${hiddenInputs}
|
|
2250
2267
|
<input type="email" name="prefilled_email" value="${escapeHtmlAttribute(prefilledEmail)}" placeholder="you@company.com" autocomplete="email">
|
|
2251
2268
|
<p class="email-note">Optional. Stripe can collect your email on the secure checkout page.</p>
|
|
@@ -2264,7 +2281,7 @@ ${hiddenInputs}
|
|
|
2264
2281
|
</div>
|
|
2265
2282
|
<div class="feedback-saved" id="feedback-saved">Feedback saved.</div>
|
|
2266
2283
|
</div>
|
|
2267
|
-
<div class="trust"><div class="trust-item">
|
|
2284
|
+
<div class="trust"><div class="trust-item">Personal recall and proof exports without free-tier caps</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode</div><div class="trust-item">Personal dashboard: gate stats, rule evidence, DPO export</div><div class="trust-item">Enterprise adds shared hosted lessons, org visibility, rollout support</div></div>
|
|
2268
2285
|
<p class="back"><a href="/">← Back to thumbgate.ai</a></p>
|
|
2269
2286
|
</main>
|
|
2270
2287
|
<script>
|
|
@@ -2539,10 +2556,18 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
|
|
|
2539
2556
|
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
2540
2557
|
const journeyState = resolveJourneyState(req, parsed);
|
|
2541
2558
|
const destinationUrl = buildTrackedLinkDestination(target, hostedConfig, parsed);
|
|
2559
|
+
const attribution = buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl);
|
|
2560
|
+
if (target.external) {
|
|
2561
|
+
appendQueryParam(
|
|
2562
|
+
destinationUrl,
|
|
2563
|
+
'client_reference_id',
|
|
2564
|
+
packCheckoutReference(attribution)
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2542
2567
|
if (!isHeadRequest) {
|
|
2543
2568
|
appendBestEffortTelemetry(
|
|
2544
2569
|
FEEDBACK_DIR,
|
|
2545
|
-
|
|
2570
|
+
attribution,
|
|
2546
2571
|
req.headers,
|
|
2547
2572
|
`tracked_link_redirect:${target.slug}`
|
|
2548
2573
|
);
|
|
@@ -2789,9 +2814,17 @@ function fillTemplate(template, replacements) {
|
|
|
2789
2814
|
}
|
|
2790
2815
|
|
|
2791
2816
|
function escapeHtmlAttribute(value) {
|
|
2817
|
+
// Complete HTML-entity encoder for attribute contexts. Escapes single quotes
|
|
2818
|
+
// and backticks in addition to & " < > so the output is safe in single- AND
|
|
2819
|
+
// double-quoted attributes (and not just the double-quoted case). Fixes
|
|
2820
|
+
// CodeQL js/reflected-xss #252 (search-param `email` reflected into the
|
|
2821
|
+
// checkout page's value="..." attribute) — the prior version omitted ' which
|
|
2822
|
+
// left it context-fragile and unrecognized as a sanitizer.
|
|
2792
2823
|
return String(value)
|
|
2793
2824
|
.replaceAll('&', '&')
|
|
2794
2825
|
.replaceAll('"', '"')
|
|
2826
|
+
.replaceAll("'", ''')
|
|
2827
|
+
.replaceAll('`', '`')
|
|
2795
2828
|
.replaceAll('<', '<')
|
|
2796
2829
|
.replaceAll('>', '>');
|
|
2797
2830
|
}
|
|
@@ -2862,6 +2895,11 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
2862
2895
|
'__SERVER_SESSION_ID__': pageContext.serverSessionId || '',
|
|
2863
2896
|
'__SERVER_ACQUISITION_ID__': pageContext.serverAcquisitionId || '',
|
|
2864
2897
|
'__SERVER_TELEMETRY_CAPTURED__': pageContext.serverTelemetryCaptured ? 'true' : 'false',
|
|
2898
|
+
'__SERVER_ATTRIBUTION_SOURCE__': escapeHtmlAttribute(pageContext.serverAttributionSource || 'partner'),
|
|
2899
|
+
'__SERVER_UTM_SOURCE__': escapeHtmlAttribute(pageContext.serverUtmSource || 'partner'),
|
|
2900
|
+
'__SERVER_UTM_MEDIUM__': escapeHtmlAttribute(pageContext.serverUtmMedium || 'partner'),
|
|
2901
|
+
'__SERVER_UTM_CAMPAIGN__': escapeHtmlAttribute(pageContext.serverUtmCampaign || 'hosted_listing'),
|
|
2902
|
+
'__SERVER_UTM_CONTENT__': escapeHtmlAttribute(pageContext.serverUtmContent || 'partner_intake'),
|
|
2865
2903
|
'__VERIFICATION_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
|
|
2866
2904
|
'__COMPATIBILITY_REPORT_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
|
|
2867
2905
|
'__AUTOMATION_REPORT_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
|
|
@@ -2883,6 +2921,10 @@ function loadDiagnosticPageHtml(runtimeConfig, pageContext = {}) {
|
|
|
2883
2921
|
return loadPublicMarketingTemplateHtml(DIAGNOSTIC_PAGE_PATH, runtimeConfig, pageContext);
|
|
2884
2922
|
}
|
|
2885
2923
|
|
|
2924
|
+
function loadPartnerIntakePageHtml(runtimeConfig, pageContext = {}) {
|
|
2925
|
+
return loadPublicMarketingTemplateHtml(PARTNER_INTAKE_PAGE_PATH, runtimeConfig, pageContext);
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2886
2928
|
function loadInstallPageHtml(runtimeConfig, pageContext = {}) {
|
|
2887
2929
|
return loadPublicMarketingTemplateHtml(INSTALL_PAGE_PATH, runtimeConfig, pageContext);
|
|
2888
2930
|
}
|
|
@@ -3702,6 +3744,11 @@ function servePublicMarketingPage({
|
|
|
3702
3744
|
serverSessionId: journeyState.sessionId,
|
|
3703
3745
|
serverAcquisitionId: journeyState.acquisitionId,
|
|
3704
3746
|
serverTelemetryCaptured: landingTelemetryCaptured,
|
|
3747
|
+
serverAttributionSource: landingAttribution.source,
|
|
3748
|
+
serverUtmSource: landingAttribution.utmSource,
|
|
3749
|
+
serverUtmMedium: landingAttribution.utmMedium,
|
|
3750
|
+
serverUtmCampaign: landingAttribution.utmCampaign,
|
|
3751
|
+
serverUtmContent: landingAttribution.utmContent,
|
|
3705
3752
|
requestHost,
|
|
3706
3753
|
});
|
|
3707
3754
|
|
|
@@ -4506,6 +4553,19 @@ function normalizeLeadEmail(value) {
|
|
|
4506
4553
|
return email.toLowerCase();
|
|
4507
4554
|
}
|
|
4508
4555
|
|
|
4556
|
+
function buildIntakeAlertRateLimitKey(req, env = process.env) {
|
|
4557
|
+
const transportPeer = String(req.socket?.remoteAddress || 'unknown');
|
|
4558
|
+
const railwayRealIp = String(req.headers['x-real-ip'] || '').trim();
|
|
4559
|
+
const runningBehindRailway = Boolean(env.RAILWAY_ENVIRONMENT_ID || env.RAILWAY_PROJECT_ID);
|
|
4560
|
+
const hasValidRailwayIp = /^[0-9a-f:.]{3,64}$/i.test(railwayRealIp);
|
|
4561
|
+
// Railway overwrites X-Real-IP at its ingress. Outside that explicit trust
|
|
4562
|
+
// boundary, forwarding headers remain caller-controlled and are ignored.
|
|
4563
|
+
const clientAddress = runningBehindRailway && hasValidRailwayIp
|
|
4564
|
+
? railwayRealIp
|
|
4565
|
+
: transportPeer;
|
|
4566
|
+
return crypto.createHash('sha256').update(clientAddress).digest('hex');
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4509
4569
|
function normalizeHttpUrl(value, fieldName) {
|
|
4510
4570
|
const raw = normalizeNullableText(value);
|
|
4511
4571
|
if (!raw) throw createHttpError(400, `${fieldName} is required`);
|
|
@@ -5221,7 +5281,14 @@ function createApiServer() {
|
|
|
5221
5281
|
if (req.method === 'POST' && pathname === '/api/event') {
|
|
5222
5282
|
// Filter bots from analytics to keep Plausible data clean
|
|
5223
5283
|
let _botDetector;
|
|
5224
|
-
try {
|
|
5284
|
+
try {
|
|
5285
|
+
_botDetector = require('../../scripts/bot-detection');
|
|
5286
|
+
} catch (err) {
|
|
5287
|
+
_botDetector = null;
|
|
5288
|
+
if (process.env.THUMBGATE_DEBUG_BOT_DETECTION === '1') {
|
|
5289
|
+
console.warn(`Optional bot-detection module unavailable: ${err.message}`);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5225
5292
|
if (_botDetector && _botDetector.shouldExcludeFromAnalytics(req)) {
|
|
5226
5293
|
sendJson(res, 202, { status: 'filtered', reason: 'bot' });
|
|
5227
5294
|
return;
|
|
@@ -5607,6 +5674,16 @@ async function addContext(){
|
|
|
5607
5674
|
return;
|
|
5608
5675
|
}
|
|
5609
5676
|
|
|
5677
|
+
// /docs is a common buyer URL; route to the public setup guide instead of 404.
|
|
5678
|
+
if (isGetLikeRequest && (pathname === '/docs' || pathname === '/docs/' || pathname === '/documentation' || pathname === '/documentation/')) {
|
|
5679
|
+
res.writeHead(302, {
|
|
5680
|
+
Location: '/guide',
|
|
5681
|
+
'Cache-Control': 'public, max-age=300',
|
|
5682
|
+
});
|
|
5683
|
+
res.end();
|
|
5684
|
+
return;
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5610
5687
|
if (isGetLikeRequest && (pathname === '/guide' || pathname === '/guide.html')) {
|
|
5611
5688
|
try {
|
|
5612
5689
|
servePublicMarketingPage({
|
|
@@ -5742,6 +5819,25 @@ async function addContext(){
|
|
|
5742
5819
|
return;
|
|
5743
5820
|
}
|
|
5744
5821
|
|
|
5822
|
+
if (isGetLikeRequest && (pathname === '/partner-intake' || pathname === '/partner-intake.html')) {
|
|
5823
|
+
try {
|
|
5824
|
+
servePublicMarketingPage({
|
|
5825
|
+
req,
|
|
5826
|
+
res,
|
|
5827
|
+
parsed,
|
|
5828
|
+
hostedConfig,
|
|
5829
|
+
isHeadRequest,
|
|
5830
|
+
renderHtml: loadPartnerIntakePageHtml,
|
|
5831
|
+
extraTelemetry: {
|
|
5832
|
+
pageType: 'partner_intake',
|
|
5833
|
+
},
|
|
5834
|
+
});
|
|
5835
|
+
} catch (err) {
|
|
5836
|
+
sendText(res, 500, err.message || 'Partner intake page unavailable');
|
|
5837
|
+
}
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5745
5841
|
// Natural marketing URLs for the Workflow Hardening Diagnostic/Sprint.
|
|
5746
5842
|
// High-intent outreach refers to "the diagnostic", "the sprint", or
|
|
5747
5843
|
// "workflow hardening"; serve a focused buyer page instead of sending
|
|
@@ -6116,7 +6212,7 @@ async function addContext(){
|
|
|
6116
6212
|
// THUMBGATE_CHECKOUT_PRO_STRIPE_URL is supported for future
|
|
6117
6213
|
// price-link rotation without a redeploy.
|
|
6118
6214
|
const bypassTarget = process.env.THUMBGATE_CHECKOUT_PRO_STRIPE_URL
|
|
6119
|
-
||
|
|
6215
|
+
|| PRO_CHECKOUT_URL;
|
|
6120
6216
|
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
6121
6217
|
eventType: 'checkout_interstitial_bypass_redirect',
|
|
6122
6218
|
clientType: 'web',
|
|
@@ -6180,6 +6276,12 @@ async function addContext(){
|
|
|
6180
6276
|
? 'checkout_bot_deflected'
|
|
6181
6277
|
: 'checkout_interstitial_view';
|
|
6182
6278
|
const missingConfirmedEmail = hasConfirmFlag && !hasValidCustomerEmailHint;
|
|
6279
|
+
let checkoutDeflectionReason = botClassification.reason;
|
|
6280
|
+
if (missingConfirmedEmail) {
|
|
6281
|
+
checkoutDeflectionReason = hasCustomerEmailHint
|
|
6282
|
+
? 'invalid_customer_email'
|
|
6283
|
+
: 'missing_customer_email';
|
|
6284
|
+
}
|
|
6183
6285
|
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
6184
6286
|
eventType,
|
|
6185
6287
|
clientType: 'web',
|
|
@@ -6203,9 +6305,7 @@ async function addContext(){
|
|
|
6203
6305
|
isBot: botClassification.isBot ? 'true' : 'false',
|
|
6204
6306
|
interstitialSampled: interstitialSampled ? 'true' : 'false',
|
|
6205
6307
|
interstitialSampleRate,
|
|
6206
|
-
reason:
|
|
6207
|
-
? (hasCustomerEmailHint ? 'invalid_customer_email' : 'missing_customer_email')
|
|
6208
|
-
: botClassification.reason,
|
|
6308
|
+
reason: checkoutDeflectionReason,
|
|
6209
6309
|
confirmEmailRequired: missingConfirmedEmail ? 'true' : 'false',
|
|
6210
6310
|
}, req.headers, eventType);
|
|
6211
6311
|
const prefilledEmail = parsed?.searchParams?.get('customer_email') || '';
|
|
@@ -7411,7 +7511,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7411
7511
|
? await parseFormBody(req, 24 * 1024)
|
|
7412
7512
|
: await parseJsonBody(req, 24 * 1024);
|
|
7413
7513
|
const workflowSprintIntake = requirePrivateApiModule('workflowSprintIntake', 'Workflow sprint intake');
|
|
7414
|
-
const
|
|
7514
|
+
const leadPayload = {
|
|
7415
7515
|
...body,
|
|
7416
7516
|
traceId: body.traceId || traceId,
|
|
7417
7517
|
acquisitionId: body.acquisitionId || journeyState.acquisitionId,
|
|
@@ -7436,7 +7536,17 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7436
7536
|
offerCode: body.offerCode || referrerAttribution.offerCode || null,
|
|
7437
7537
|
referrerHost: body.referrerHost || referrerAttribution.referrerHost || null,
|
|
7438
7538
|
referrer: body.referrer || referrerAttribution.referrer || null,
|
|
7439
|
-
}
|
|
7539
|
+
};
|
|
7540
|
+
const intakeReservation = workflowSprintIntake.reserveWorkflowSprintIntake(leadPayload, {
|
|
7541
|
+
feedbackDir: FEEDBACK_DIR,
|
|
7542
|
+
rateLimitKey: buildIntakeAlertRateLimitKey(req),
|
|
7543
|
+
});
|
|
7544
|
+
if (!intakeReservation.allowed) {
|
|
7545
|
+
const error = new Error('Too many workflow-intake submissions. Try again later.');
|
|
7546
|
+
error.statusCode = intakeReservation.statusCode || 429;
|
|
7547
|
+
throw error;
|
|
7548
|
+
}
|
|
7549
|
+
const lead = workflowSprintIntake.appendWorkflowSprintLead(leadPayload, { feedbackDir: FEEDBACK_DIR });
|
|
7440
7550
|
|
|
7441
7551
|
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
7442
7552
|
eventType: 'workflow_sprint_lead_submitted',
|
|
@@ -7467,6 +7577,11 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7467
7577
|
referrer: lead.attribution.referrer,
|
|
7468
7578
|
}, req.headers, 'workflow_sprint_lead_submitted');
|
|
7469
7579
|
|
|
7580
|
+
const leadNotification = await workflowSprintIntake.notifyWorkflowSprintLead(lead, {
|
|
7581
|
+
sendEmailImpl: resendMailer.sendEmail,
|
|
7582
|
+
rateLimitKey: buildIntakeAlertRateLimitKey(req),
|
|
7583
|
+
});
|
|
7584
|
+
|
|
7470
7585
|
if (isFormSubmission && !wantsJson(req, parsed)) {
|
|
7471
7586
|
sendHtml(
|
|
7472
7587
|
res,
|
|
@@ -7487,6 +7602,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7487
7602
|
status: lead.status,
|
|
7488
7603
|
offer: lead.offer,
|
|
7489
7604
|
nextStep: 'review_proof_pack',
|
|
7605
|
+
operatorAlertSent: leadNotification.sent === true,
|
|
7606
|
+
operatorAlertReason: leadNotification.reason || null,
|
|
7490
7607
|
proofPackUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
|
|
7491
7608
|
sprintBriefUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/WORKFLOW_HARDENING_SPRINT.md',
|
|
7492
7609
|
}, {
|
|
@@ -7494,7 +7611,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7494
7611
|
...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
|
|
7495
7612
|
});
|
|
7496
7613
|
} catch (err) {
|
|
7497
|
-
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
7614
|
+
appendBestEffortTelemetry(FEEDBACK_DIR, redactSecretsDeep({
|
|
7498
7615
|
eventType: 'workflow_sprint_lead_failed',
|
|
7499
7616
|
clientType: 'web',
|
|
7500
7617
|
traceId,
|
|
@@ -7517,10 +7634,10 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7517
7634
|
page: referrerAttribution.page || '/#workflow-sprint-intake',
|
|
7518
7635
|
landingPath: referrerAttribution.landingPath || '/',
|
|
7519
7636
|
referrerHost: referrerAttribution.referrerHost,
|
|
7520
|
-
referrer: referrerAttribution.referrer,
|
|
7521
|
-
failureCode: err?.message ? err.message : 'workflow_sprint_lead_failed',
|
|
7637
|
+
referrer: redactSecrets(referrerAttribution.referrer),
|
|
7638
|
+
failureCode: redactSecrets(err?.message ? err.message : 'workflow_sprint_lead_failed'),
|
|
7522
7639
|
httpStatus: err && err.statusCode ? err.statusCode : null,
|
|
7523
|
-
}, req.headers, 'workflow_sprint_lead_failed');
|
|
7640
|
+
}), redactSecretsDeep(req.headers), 'workflow_sprint_lead_failed');
|
|
7524
7641
|
if (isFormSubmission && !wantsJson(req, parsed)) {
|
|
7525
7642
|
sendHtml(
|
|
7526
7643
|
res,
|
|
@@ -7806,6 +7923,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7806
7923
|
});
|
|
7807
7924
|
return;
|
|
7808
7925
|
}
|
|
7926
|
+
if (result && result.retryable === true) {
|
|
7927
|
+
sendProblem(res, {
|
|
7928
|
+
type: PROBLEM_TYPES.INTERNAL,
|
|
7929
|
+
title: 'Fulfillment temporarily unavailable',
|
|
7930
|
+
status: 503,
|
|
7931
|
+
detail: 'The payment was received but fulfillment did not complete. Stripe should retry this event.',
|
|
7932
|
+
});
|
|
7933
|
+
return;
|
|
7934
|
+
}
|
|
7809
7935
|
sendJson(res, 200, result);
|
|
7810
7936
|
|
|
7811
7937
|
} catch (err) {
|
|
@@ -7938,16 +8064,24 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7938
8064
|
}
|
|
7939
8065
|
|
|
7940
8066
|
const resolvedTraceId = result.traceId || requestedTraceId;
|
|
8067
|
+
const nextSteps = result.apiKey
|
|
8068
|
+
? {
|
|
8069
|
+
env: `THUMBGATE_API_KEY=${result.apiKey}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
|
|
8070
|
+
curl: `curl -X POST ${hostedConfig.billingApiBaseUrl}/v1/feedback/capture \\\n+ -H 'Authorization: Bearer ${result.apiKey}' \\\n+ -H 'Content-Type: application/json' \\\n+ -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
|
|
8071
|
+
}
|
|
8072
|
+
: result.offerKind === 'workflow_hardening_diagnostic'
|
|
8073
|
+
? { fulfillment: 'Payment confirmed. Diagnostic fulfillment is processing; an email will arrive when it completes.' }
|
|
8074
|
+
: {};
|
|
7941
8075
|
|
|
7942
8076
|
sendJson(res, 200, {
|
|
7943
8077
|
...result,
|
|
7944
8078
|
traceId: resolvedTraceId || null,
|
|
7945
8079
|
appOrigin: hostedConfig.appOrigin,
|
|
7946
8080
|
apiBaseUrl: hostedConfig.billingApiBaseUrl,
|
|
7947
|
-
nextSteps: {
|
|
8081
|
+
nextSteps: result.apiKey ? {
|
|
7948
8082
|
env: `THUMBGATE_API_KEY=${result.apiKey || ''}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
|
|
7949
8083
|
curl: `curl -X POST ${hostedConfig.billingApiBaseUrl}/v1/feedback/capture \\\n -H 'Authorization: Bearer ${result.apiKey || ''}' \\\n -H 'Content-Type: application/json' \\\n -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
|
|
7950
|
-
},
|
|
8084
|
+
} : nextSteps,
|
|
7951
8085
|
}, getPublicBillingHeaders(resolvedTraceId));
|
|
7952
8086
|
} catch (err) {
|
|
7953
8087
|
const requestedTraceId = parsed.searchParams.get('traceId') || '';
|
|
@@ -9206,6 +9340,48 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9206
9340
|
return;
|
|
9207
9341
|
}
|
|
9208
9342
|
|
|
9343
|
+
// POST /v1/billing/pro-activation — customer key activation alert.
|
|
9344
|
+
if (req.method === 'POST' && pathname === '/v1/billing/pro-activation') {
|
|
9345
|
+
const token = extractBearerToken(req);
|
|
9346
|
+
const validation = validateApiKey(token);
|
|
9347
|
+
if (!validation.valid) {
|
|
9348
|
+
sendProblem(res, {
|
|
9349
|
+
type: PROBLEM_TYPES.UNAUTHORIZED,
|
|
9350
|
+
title: 'Unauthorized',
|
|
9351
|
+
status: 401,
|
|
9352
|
+
detail: 'A valid API key is required to access this endpoint.',
|
|
9353
|
+
});
|
|
9354
|
+
return;
|
|
9355
|
+
}
|
|
9356
|
+
|
|
9357
|
+
const body = await parseJsonBody(req);
|
|
9358
|
+
const keyFingerprint = fingerprintProKey(token);
|
|
9359
|
+
const alert = await sendProActivationAlert({
|
|
9360
|
+
key: token,
|
|
9361
|
+
source: body.source || 'hosted_pro_activation',
|
|
9362
|
+
version: body.version || null,
|
|
9363
|
+
customerId: validation.customerId,
|
|
9364
|
+
installId: validation.installId,
|
|
9365
|
+
usageCount: validation.usageCount,
|
|
9366
|
+
env: process.env,
|
|
9367
|
+
});
|
|
9368
|
+
|
|
9369
|
+
sendJson(res, 200, {
|
|
9370
|
+
ok: true,
|
|
9371
|
+
customerId: validation.customerId,
|
|
9372
|
+
installId: validation.installId,
|
|
9373
|
+
usageCount: validation.usageCount,
|
|
9374
|
+
keyFingerprint,
|
|
9375
|
+
keyFingerprintMatchesClient: body.keyFingerprint ? body.keyFingerprint === keyFingerprint : null,
|
|
9376
|
+
alert: {
|
|
9377
|
+
sent: Boolean(alert?.sent),
|
|
9378
|
+
reason: alert?.reason || null,
|
|
9379
|
+
id: alert?.id || null,
|
|
9380
|
+
},
|
|
9381
|
+
});
|
|
9382
|
+
return;
|
|
9383
|
+
}
|
|
9384
|
+
|
|
9209
9385
|
// POST /v1/billing/provision — manually provision key (admin)
|
|
9210
9386
|
if (req.method === 'POST' && pathname === '/v1/billing/provision') {
|
|
9211
9387
|
if (!isStaticAdminAuthorized(req, expectedApiKey)) {
|
|
@@ -9765,6 +9941,9 @@ module.exports = {
|
|
|
9765
9941
|
createApiServer,
|
|
9766
9942
|
startServer,
|
|
9767
9943
|
__test__: {
|
|
9944
|
+
PRO_CHECKOUT_URL,
|
|
9945
|
+
escapeHtmlAttribute,
|
|
9946
|
+
renderCheckoutIntentPage,
|
|
9768
9947
|
buildCheckoutFallbackUrl,
|
|
9769
9948
|
createPrivateCoreUnavailableError,
|
|
9770
9949
|
buildPosthogProxyRequestOptions,
|
|
@@ -9786,6 +9965,7 @@ module.exports = {
|
|
|
9786
9965
|
answerEnterpriseDataChat,
|
|
9787
9966
|
answerEnterpriseDialogflowChat,
|
|
9788
9967
|
buildLossAnalyticsResponse,
|
|
9968
|
+
buildIntakeAlertRateLimitKey,
|
|
9789
9969
|
sanitizeHtmlUnsafeJsonValue,
|
|
9790
9970
|
},
|
|
9791
9971
|
};
|
|
Binary file
|