thumbgate 1.27.20 → 1.28.0
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/mcp/server-card.json +1 -1
- package/README.md +63 -42
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +25 -8
- package/bin/postinstall.js +1 -1
- package/config/entitlement-public-keys.json +6 -0
- package/config/gates/default.json +1 -1
- package/config/github-about.json +2 -2
- package/config/merge-quality-checks.json +3 -0
- package/package.json +12 -4
- 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/federal.html +2 -2
- package/public/guide.html +14 -14
- package/public/index.html +81 -70
- package/public/install.html +14 -14
- package/public/learn.html +4 -17
- package/public/numbers.html +2 -2
- package/public/pricing.html +12 -19
- package/public/pro.html +2 -2
- package/scripts/agent-reward-model.js +13 -0
- package/scripts/bayes-optimal-gate.js +6 -1
- package/scripts/cli-feedback.js +17 -1
- 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/gates-engine.js +15 -3
- package/scripts/imperative-detector.js +85 -0
- package/scripts/intervention-policy.js +13 -0
- package/scripts/pr-manager.js +9 -22
- package/scripts/pro-local-dashboard.js +198 -0
- package/scripts/risk-scorer.js +6 -0
- package/scripts/seo-gsd.js +2 -2
- package/scripts/thompson-sampling.js +11 -2
- package/src/api/server.js +81 -10
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_PRO_API = 'https://thumbgate-production.up.railway.app';
|
|
9
|
+
const DEFAULT_PRO_ACTIVATION_ALERT_EMAIL = 'igor.ganapolsky@gmail.com';
|
|
8
10
|
const CREATOR_BYPASS_VALUE = process.env.THUMBGATE_DEV_SECRET || '';
|
|
9
11
|
const CREATOR_BYPASS_ENV = 'THUMBGATE_DEV_BYPASS';
|
|
10
12
|
const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
|
|
@@ -55,6 +57,195 @@ function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE ||
|
|
|
55
57
|
return path.join(getLicenseDir(homeDir), 'license.json');
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function isTruthyEnv(value) {
|
|
61
|
+
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeText(value) {
|
|
65
|
+
return String(value || '').trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function escapeHtml(value) {
|
|
69
|
+
return String(value || '')
|
|
70
|
+
.replace(/&/g, '&')
|
|
71
|
+
.replace(/</g, '<')
|
|
72
|
+
.replace(/>/g, '>')
|
|
73
|
+
.replace(/"/g, '"')
|
|
74
|
+
.replace(/'/g, ''');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fingerprintProKey(key) {
|
|
78
|
+
const normalized = normalizeText(key);
|
|
79
|
+
if (!normalized) return '';
|
|
80
|
+
return `sha256:${crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 12)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveProActivationAlertRecipient(env = process.env) {
|
|
84
|
+
return normalizeText(
|
|
85
|
+
env.THUMBGATE_PRO_ACTIVATION_ALERT_EMAIL ||
|
|
86
|
+
env.THUMBGATE_OPERATOR_ALERT_EMAIL ||
|
|
87
|
+
env.THUMBGATE_SUPPORT_EMAIL ||
|
|
88
|
+
DEFAULT_PRO_ACTIVATION_ALERT_EMAIL
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function canSendProActivationAlert({ env = process.env, sendEmailImpl } = {}) {
|
|
93
|
+
if (isTruthyEnv(env.THUMBGATE_DISABLE_PRO_ACTIVATION_ALERTS)) return false;
|
|
94
|
+
if (!resolveProActivationAlertRecipient(env)) return false;
|
|
95
|
+
if (sendEmailImpl) return true;
|
|
96
|
+
return Boolean(normalizeText(env.RESEND_API_KEY || env.THUMBGATE_RESEND_API_KEY));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderProActivationAlertBodies({
|
|
100
|
+
keyFingerprint,
|
|
101
|
+
source,
|
|
102
|
+
version,
|
|
103
|
+
activatedAt,
|
|
104
|
+
hostname,
|
|
105
|
+
platform,
|
|
106
|
+
arch,
|
|
107
|
+
nodeVersion,
|
|
108
|
+
customerId,
|
|
109
|
+
installId,
|
|
110
|
+
usageCount,
|
|
111
|
+
} = {}) {
|
|
112
|
+
const occurredAt = activatedAt || new Date().toISOString();
|
|
113
|
+
const runtimePlatform = platform || process.platform;
|
|
114
|
+
const runtimeArch = arch || process.arch;
|
|
115
|
+
const runtimeNode = nodeVersion || process.version;
|
|
116
|
+
const text = [
|
|
117
|
+
'ThumbGate Pro activation detected.',
|
|
118
|
+
'',
|
|
119
|
+
`Activated at: ${occurredAt}`,
|
|
120
|
+
`Key fingerprint: ${keyFingerprint || 'unknown'}`,
|
|
121
|
+
`Source: ${source || 'unknown'}`,
|
|
122
|
+
`Version: ${version || 'unknown'}`,
|
|
123
|
+
`Customer ID: ${customerId || 'unknown'}`,
|
|
124
|
+
`Install ID: ${installId || 'unknown'}`,
|
|
125
|
+
`Usage count: ${usageCount ?? 'unknown'}`,
|
|
126
|
+
`Host: ${hostname || 'unknown'}`,
|
|
127
|
+
`Runtime: ${runtimePlatform}/${runtimeArch} on ${runtimeNode}`,
|
|
128
|
+
'',
|
|
129
|
+
'Secret hygiene: this alert intentionally does not include the raw Pro key.',
|
|
130
|
+
].join('\n');
|
|
131
|
+
const html = `<!doctype html>
|
|
132
|
+
<html>
|
|
133
|
+
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#17212b;line-height:1.5;">
|
|
134
|
+
<h1 style="font-size:20px;margin:0 0 12px;">ThumbGate Pro activation detected</h1>
|
|
135
|
+
<table role="presentation" cellspacing="0" cellpadding="0" style="border-collapse:collapse;">
|
|
136
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Activated at</td><td style="padding:4px 0;">${escapeHtml(occurredAt)}</td></tr>
|
|
137
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Key fingerprint</td><td style="padding:4px 0;"><code>${escapeHtml(keyFingerprint || 'unknown')}</code></td></tr>
|
|
138
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Source</td><td style="padding:4px 0;">${escapeHtml(source || 'unknown')}</td></tr>
|
|
139
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Version</td><td style="padding:4px 0;">${escapeHtml(version || 'unknown')}</td></tr>
|
|
140
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Customer ID</td><td style="padding:4px 0;">${escapeHtml(customerId || 'unknown')}</td></tr>
|
|
141
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Install ID</td><td style="padding:4px 0;">${escapeHtml(installId || 'unknown')}</td></tr>
|
|
142
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Usage count</td><td style="padding:4px 0;">${escapeHtml(usageCount ?? 'unknown')}</td></tr>
|
|
143
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Host</td><td style="padding:4px 0;">${escapeHtml(hostname || 'unknown')}</td></tr>
|
|
144
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Runtime</td><td style="padding:4px 0;">${escapeHtml(runtimePlatform)}/${escapeHtml(runtimeArch)} on ${escapeHtml(runtimeNode)}</td></tr>
|
|
145
|
+
</table>
|
|
146
|
+
<p style="margin-top:16px;color:#64748b;">Secret hygiene: this alert intentionally does not include the raw Pro key.</p>
|
|
147
|
+
</body>
|
|
148
|
+
</html>`;
|
|
149
|
+
return { text, html };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function sendProActivationAlert({
|
|
153
|
+
key,
|
|
154
|
+
source = 'unknown',
|
|
155
|
+
version,
|
|
156
|
+
customerId,
|
|
157
|
+
installId,
|
|
158
|
+
usageCount,
|
|
159
|
+
env = process.env,
|
|
160
|
+
sendEmailImpl,
|
|
161
|
+
} = {}) {
|
|
162
|
+
const keyFingerprint = fingerprintProKey(key);
|
|
163
|
+
if (!keyFingerprint) return { sent: false, reason: 'missing_key' };
|
|
164
|
+
if (!canSendProActivationAlert({ env, sendEmailImpl })) {
|
|
165
|
+
return { sent: false, reason: 'activation_alert_disabled_or_unconfigured' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const to = resolveProActivationAlertRecipient(env);
|
|
169
|
+
const { html, text } = renderProActivationAlertBodies({
|
|
170
|
+
keyFingerprint,
|
|
171
|
+
source,
|
|
172
|
+
version,
|
|
173
|
+
customerId,
|
|
174
|
+
installId,
|
|
175
|
+
usageCount,
|
|
176
|
+
activatedAt: new Date().toISOString(),
|
|
177
|
+
hostname: os.hostname(),
|
|
178
|
+
platform: process.platform,
|
|
179
|
+
arch: process.arch,
|
|
180
|
+
nodeVersion: process.version,
|
|
181
|
+
});
|
|
182
|
+
const sender = sendEmailImpl || require('./mailer').sendEmail;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
return await sender({
|
|
186
|
+
to,
|
|
187
|
+
subject: 'ThumbGate Pro activated',
|
|
188
|
+
html,
|
|
189
|
+
text,
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
sent: false,
|
|
194
|
+
reason: 'exception',
|
|
195
|
+
error: error && error.message ? error.message : String(error),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function notifyHostedProActivation({
|
|
201
|
+
key,
|
|
202
|
+
source = 'cli_pro_activate',
|
|
203
|
+
version,
|
|
204
|
+
apiBaseUrl = process.env.THUMBGATE_API_BASE_URL || DEFAULT_PRO_API,
|
|
205
|
+
fetchImpl = globalThis.fetch,
|
|
206
|
+
} = {}) {
|
|
207
|
+
const normalizedKey = normalizeText(key);
|
|
208
|
+
if (!normalizedKey) return { notified: false, reason: 'missing_key' };
|
|
209
|
+
if (typeof fetchImpl !== 'function') return { notified: false, reason: 'no_fetch' };
|
|
210
|
+
|
|
211
|
+
const keyFingerprint = fingerprintProKey(normalizedKey);
|
|
212
|
+
const endpoint = new URL('/v1/billing/pro-activation', apiBaseUrl).toString();
|
|
213
|
+
try {
|
|
214
|
+
const response = await fetchImpl(endpoint, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: {
|
|
217
|
+
authorization: `Bearer ${normalizedKey}`,
|
|
218
|
+
'content-type': 'application/json',
|
|
219
|
+
},
|
|
220
|
+
body: JSON.stringify({
|
|
221
|
+
keyFingerprint,
|
|
222
|
+
source,
|
|
223
|
+
version: version || null,
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
const body = await response.json().catch(() => ({}));
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
return {
|
|
229
|
+
notified: false,
|
|
230
|
+
reason: body && body.detail ? body.detail : `http_${response.status}`,
|
|
231
|
+
status: response.status,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
notified: true,
|
|
236
|
+
status: response.status,
|
|
237
|
+
alert: body.alert || null,
|
|
238
|
+
keyFingerprint: body.keyFingerprint || keyFingerprint,
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
notified: false,
|
|
243
|
+
reason: 'exception',
|
|
244
|
+
error: error && error.message ? error.message : String(error),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
58
249
|
function readLicense({ homeDir } = {}) {
|
|
59
250
|
try {
|
|
60
251
|
return JSON.parse(fs.readFileSync(getLicensePath(homeDir), 'utf8'));
|
|
@@ -162,12 +353,19 @@ module.exports = {
|
|
|
162
353
|
CREATOR_BYPASS_VALUE,
|
|
163
354
|
CREATOR_SYNTHETIC_KEY,
|
|
164
355
|
DEFAULT_PRO_API,
|
|
356
|
+
DEFAULT_PRO_ACTIVATION_ALERT_EMAIL,
|
|
357
|
+
canSendProActivationAlert,
|
|
358
|
+
fingerprintProKey,
|
|
165
359
|
getLicenseDir,
|
|
166
360
|
getLicensePath,
|
|
167
361
|
hasDevOverride,
|
|
168
362
|
isCreatorDev,
|
|
169
363
|
readLicense,
|
|
364
|
+
renderProActivationAlertBodies,
|
|
365
|
+
notifyHostedProActivation,
|
|
366
|
+
resolveProActivationAlertRecipient,
|
|
170
367
|
saveLicense,
|
|
368
|
+
sendProActivationAlert,
|
|
171
369
|
resolveProKey,
|
|
172
370
|
validateProKey,
|
|
173
371
|
startLocalProDashboard,
|
package/scripts/risk-scorer.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { resolveFeedbackDir: resolveSharedFeedbackDir } = require('./feedback-paths');
|
|
7
|
+
const { requireLearnedModelsEntitlement } = require('./entitlement');
|
|
7
8
|
|
|
8
9
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
9
10
|
const DEFAULT_FEEDBACK_DIR = resolveSharedFeedbackDir();
|
|
@@ -268,6 +269,10 @@ function buildPatternSummary(rows) {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
function trainRiskModel(rows, options = {}) {
|
|
272
|
+
requireLearnedModelsEntitlement({
|
|
273
|
+
...(options.entitlement || {}),
|
|
274
|
+
label: 'risk-scorer AdaBoost training',
|
|
275
|
+
});
|
|
271
276
|
const registry = buildFeatureRegistry(rows, options);
|
|
272
277
|
const examples = rows.map((row) => ({
|
|
273
278
|
row,
|
|
@@ -419,6 +424,7 @@ function trainAndPersistRiskModel(feedbackDir, options = {}) {
|
|
|
419
424
|
}
|
|
420
425
|
|
|
421
426
|
function getRiskSummary(feedbackDir) {
|
|
427
|
+
requireLearnedModelsEntitlement({ label: 'risk-scorer summary' });
|
|
422
428
|
const resolvedDir = resolveFeedbackDir(feedbackDir);
|
|
423
429
|
const rows = readJSONL(sequencePathFor(resolvedDir));
|
|
424
430
|
if (rows.length === 0) return null;
|
package/scripts/seo-gsd.js
CHANGED
|
@@ -562,7 +562,7 @@ const PRETOOLUSE_HOOK_GUIDE_SPEC = Object.freeze({
|
|
|
562
562
|
],
|
|
563
563
|
[
|
|
564
564
|
'Does this run locally or call a cloud service?',
|
|
565
|
-
'Local-first. The PreToolUse decision happens in the hook process on your machine in milliseconds — no network round-trip, no cloud dependency, no data leaving the laptop.
|
|
565
|
+
'Local-first. The PreToolUse decision happens in the hook process on your machine in milliseconds — no network round-trip, no cloud dependency, no data leaving the laptop. Pro adds personal recall, exports, dashboard proof, and managed adapter coverage. Enterprise adds hosted sharing for teams that want to share rules across seats.',
|
|
566
566
|
],
|
|
567
567
|
],
|
|
568
568
|
relatedPaths: ['/guides/mcp-tool-governance', '/guides/ai-agent-pre-action-approval-gates', '/guides/ai-coding-agent-zero-trust'],
|
|
@@ -1972,7 +1972,7 @@ const PAGE_BLUEPRINTS = [
|
|
|
1972
1972
|
faq: [
|
|
1973
1973
|
{
|
|
1974
1974
|
question: 'Why pay $19/mo for ThumbGate Pro when disler hooks are free?',
|
|
1975
|
-
answer: 'Free disler hooks are static patterns you maintain per machine — re-copying them when they change, debugging false positives alone, and re-applying them in every new project. ThumbGate Pro adds the learning loop (thumbs-
|
|
1975
|
+
answer: 'Free disler hooks are static patterns you maintain per machine — re-copying them when they change, debugging false positives alone, and re-applying them in every new project. ThumbGate Pro adds the learning loop (repeated thumbs-downs → cross-session prevention rule), the personal dashboard, lesson recall/search, DPO export, and adapter maintenance across the weekly breaking-change cycle of Claude Code, Cursor, and Cline. Free disler is the right answer if you only ever work in one project on one machine and never want to learn from past mistakes. Pro is the right answer when those assumptions stop holding; Enterprise adds shared hosted lessons across seats.',
|
|
1976
1976
|
},
|
|
1977
1977
|
{
|
|
1978
1978
|
question: 'Is ThumbGate just a packaged version of disler/claude-code-hooks-mastery?',
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const { parseTimestamp } = require('./feedback-schema');
|
|
24
24
|
const { getEffectiveSetting } = require('./evolution-state');
|
|
25
|
+
const { requireLearnedModelsEntitlement } = require('./entitlement');
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Constants
|
|
@@ -179,7 +180,11 @@ function saveModel(model, modelPath) {
|
|
|
179
180
|
* @param {number} [params.weightMultiplier] - Optional multiplier for stronger/slower updates
|
|
180
181
|
* @returns {Object} The mutated model
|
|
181
182
|
*/
|
|
182
|
-
function updateModel(model, { signal, timestamp, categories, weightMultiplier, failureType }) {
|
|
183
|
+
function updateModel(model, { signal, timestamp, categories, weightMultiplier, failureType, entitlement }) {
|
|
184
|
+
requireLearnedModelsEntitlement({
|
|
185
|
+
...(entitlement || {}),
|
|
186
|
+
label: 'Thompson Sampling model update',
|
|
187
|
+
});
|
|
183
188
|
const multiplier = Number.isFinite(weightMultiplier) && weightMultiplier > 0 ? weightMultiplier : 1;
|
|
184
189
|
const weight = timeDecayWeight(timestamp) * multiplier;
|
|
185
190
|
const isPositive = signal === 'positive';
|
|
@@ -334,7 +339,11 @@ function getTemperatureScaledPosteriorParams(params, temperature = 1.0) {
|
|
|
334
339
|
* @param {number} temperature - Scaling factor (default 1.0)
|
|
335
340
|
* @returns {Object} Map of category → float sample in [0, 1]
|
|
336
341
|
*/
|
|
337
|
-
function samplePosteriors(model, temperature = 1.0) {
|
|
342
|
+
function samplePosteriors(model, temperature = 1.0, options = {}) {
|
|
343
|
+
requireLearnedModelsEntitlement({
|
|
344
|
+
...(options.entitlement || {}),
|
|
345
|
+
label: 'Thompson Sampling posterior sampling',
|
|
346
|
+
});
|
|
338
347
|
const samples = {};
|
|
339
348
|
|
|
340
349
|
for (const [cat, params] of Object.entries(model.categories || {})) {
|
package/src/api/server.js
CHANGED
|
@@ -20,6 +20,7 @@ const POSTHOG_STATIC_PATH_PREFIX = '/static/';
|
|
|
20
20
|
// Stripe catalog, with the per-tier thumbnails wired in. Re-run the
|
|
21
21
|
// bootstrap workflow to regenerate; the new URLs surface in the workflow
|
|
22
22
|
// summary log.
|
|
23
|
+
const PRO_CHECKOUT_URL = 'https://buy.stripe.com/8x2dR91M84r4cSd9uj3sI3f';
|
|
23
24
|
const FIRST_FAILURE_RULE_CHECKOUT_URL = 'https://buy.stripe.com/7sY6oHaiEbTw6tP5e33sI3e';
|
|
24
25
|
const QUICK_READ_CHECKOUT_URL = 'https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H';
|
|
25
26
|
const WORKFLOW_TEARDOWN_CHECKOUT_URL = 'https://buy.stripe.com/8x214n2Qc4r44lHayn3sI2I';
|
|
@@ -219,6 +220,10 @@ const oauthStore = mcpOauth.createStore();
|
|
|
219
220
|
const pendingOauthAuthorizeRequests = new Map();
|
|
220
221
|
const OAUTH_AUTHORIZE_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
221
222
|
const resendMailer = require('../../scripts/mailer/resend-mailer');
|
|
223
|
+
const {
|
|
224
|
+
fingerprintProKey,
|
|
225
|
+
sendProActivationAlert,
|
|
226
|
+
} = require('../../scripts/pro-local-dashboard');
|
|
222
227
|
const {
|
|
223
228
|
buildContextFootprintReport,
|
|
224
229
|
} = require('../../scripts/context-footprint');
|
|
@@ -1760,8 +1765,10 @@ function appendFeedbackListLines(lines, { entries, signal, intent }) {
|
|
|
1760
1765
|
lines.push(`No ${signal || 'feedback'} entries found ${intent.windowLabel}.`);
|
|
1761
1766
|
return;
|
|
1762
1767
|
}
|
|
1763
|
-
lines.push(
|
|
1764
|
-
|
|
1768
|
+
lines.push(
|
|
1769
|
+
`${FEEDBACK_LIST_LABELS[signal] || 'Recent feedback'} (${intent.windowLabel}):`,
|
|
1770
|
+
...entries.map(formatFeedbackEntry)
|
|
1771
|
+
);
|
|
1765
1772
|
}
|
|
1766
1773
|
|
|
1767
1774
|
function buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeline }) {
|
|
@@ -2244,8 +2251,8 @@ a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:
|
|
|
2244
2251
|
<div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div>
|
|
2245
2252
|
<h1>Start ThumbGate Pro</h1>
|
|
2246
2253
|
<div class="price">$19<small>/mo</small></div>
|
|
2247
|
-
<p>The npm package runs your gates locally. <strong>Pro</strong>
|
|
2248
|
-
<form action="
|
|
2254
|
+
<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>
|
|
2255
|
+
<form action="${PRO_CHECKOUT_URL}" method="GET" data-i="pro_checkout_confirmed">
|
|
2249
2256
|
${hiddenInputs}
|
|
2250
2257
|
<input type="email" name="prefilled_email" value="${escapeHtmlAttribute(prefilledEmail)}" placeholder="you@company.com" autocomplete="email">
|
|
2251
2258
|
<p class="email-note">Optional. Stripe can collect your email on the secure checkout page.</p>
|
|
@@ -2264,7 +2271,7 @@ ${hiddenInputs}
|
|
|
2264
2271
|
</div>
|
|
2265
2272
|
<div class="feedback-saved" id="feedback-saved">Feedback saved.</div>
|
|
2266
2273
|
</div>
|
|
2267
|
-
<div class="trust"><div class="trust-item">
|
|
2274
|
+
<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
2275
|
<p class="back"><a href="/">← Back to thumbgate.ai</a></p>
|
|
2269
2276
|
</main>
|
|
2270
2277
|
<script>
|
|
@@ -2789,9 +2796,17 @@ function fillTemplate(template, replacements) {
|
|
|
2789
2796
|
}
|
|
2790
2797
|
|
|
2791
2798
|
function escapeHtmlAttribute(value) {
|
|
2799
|
+
// Complete HTML-entity encoder for attribute contexts. Escapes single quotes
|
|
2800
|
+
// and backticks in addition to & " < > so the output is safe in single- AND
|
|
2801
|
+
// double-quoted attributes (and not just the double-quoted case). Fixes
|
|
2802
|
+
// CodeQL js/reflected-xss #252 (search-param `email` reflected into the
|
|
2803
|
+
// checkout page's value="..." attribute) — the prior version omitted ' which
|
|
2804
|
+
// left it context-fragile and unrecognized as a sanitizer.
|
|
2792
2805
|
return String(value)
|
|
2793
2806
|
.replaceAll('&', '&')
|
|
2794
2807
|
.replaceAll('"', '"')
|
|
2808
|
+
.replaceAll("'", ''')
|
|
2809
|
+
.replaceAll('`', '`')
|
|
2795
2810
|
.replaceAll('<', '<')
|
|
2796
2811
|
.replaceAll('>', '>');
|
|
2797
2812
|
}
|
|
@@ -5221,7 +5236,14 @@ function createApiServer() {
|
|
|
5221
5236
|
if (req.method === 'POST' && pathname === '/api/event') {
|
|
5222
5237
|
// Filter bots from analytics to keep Plausible data clean
|
|
5223
5238
|
let _botDetector;
|
|
5224
|
-
try {
|
|
5239
|
+
try {
|
|
5240
|
+
_botDetector = require('../../scripts/bot-detection');
|
|
5241
|
+
} catch (err) {
|
|
5242
|
+
_botDetector = null;
|
|
5243
|
+
if (process.env.THUMBGATE_DEBUG_BOT_DETECTION === '1') {
|
|
5244
|
+
console.warn(`Optional bot-detection module unavailable: ${err.message}`);
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5225
5247
|
if (_botDetector && _botDetector.shouldExcludeFromAnalytics(req)) {
|
|
5226
5248
|
sendJson(res, 202, { status: 'filtered', reason: 'bot' });
|
|
5227
5249
|
return;
|
|
@@ -6116,7 +6138,7 @@ async function addContext(){
|
|
|
6116
6138
|
// THUMBGATE_CHECKOUT_PRO_STRIPE_URL is supported for future
|
|
6117
6139
|
// price-link rotation without a redeploy.
|
|
6118
6140
|
const bypassTarget = process.env.THUMBGATE_CHECKOUT_PRO_STRIPE_URL
|
|
6119
|
-
||
|
|
6141
|
+
|| PRO_CHECKOUT_URL;
|
|
6120
6142
|
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
6121
6143
|
eventType: 'checkout_interstitial_bypass_redirect',
|
|
6122
6144
|
clientType: 'web',
|
|
@@ -6180,6 +6202,12 @@ async function addContext(){
|
|
|
6180
6202
|
? 'checkout_bot_deflected'
|
|
6181
6203
|
: 'checkout_interstitial_view';
|
|
6182
6204
|
const missingConfirmedEmail = hasConfirmFlag && !hasValidCustomerEmailHint;
|
|
6205
|
+
let checkoutDeflectionReason = botClassification.reason;
|
|
6206
|
+
if (missingConfirmedEmail) {
|
|
6207
|
+
checkoutDeflectionReason = hasCustomerEmailHint
|
|
6208
|
+
? 'invalid_customer_email'
|
|
6209
|
+
: 'missing_customer_email';
|
|
6210
|
+
}
|
|
6183
6211
|
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
6184
6212
|
eventType,
|
|
6185
6213
|
clientType: 'web',
|
|
@@ -6203,9 +6231,7 @@ async function addContext(){
|
|
|
6203
6231
|
isBot: botClassification.isBot ? 'true' : 'false',
|
|
6204
6232
|
interstitialSampled: interstitialSampled ? 'true' : 'false',
|
|
6205
6233
|
interstitialSampleRate,
|
|
6206
|
-
reason:
|
|
6207
|
-
? (hasCustomerEmailHint ? 'invalid_customer_email' : 'missing_customer_email')
|
|
6208
|
-
: botClassification.reason,
|
|
6234
|
+
reason: checkoutDeflectionReason,
|
|
6209
6235
|
confirmEmailRequired: missingConfirmedEmail ? 'true' : 'false',
|
|
6210
6236
|
}, req.headers, eventType);
|
|
6211
6237
|
const prefilledEmail = parsed?.searchParams?.get('customer_email') || '';
|
|
@@ -9206,6 +9232,48 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9206
9232
|
return;
|
|
9207
9233
|
}
|
|
9208
9234
|
|
|
9235
|
+
// POST /v1/billing/pro-activation — customer key activation alert.
|
|
9236
|
+
if (req.method === 'POST' && pathname === '/v1/billing/pro-activation') {
|
|
9237
|
+
const token = extractBearerToken(req);
|
|
9238
|
+
const validation = validateApiKey(token);
|
|
9239
|
+
if (!validation.valid) {
|
|
9240
|
+
sendProblem(res, {
|
|
9241
|
+
type: PROBLEM_TYPES.UNAUTHORIZED,
|
|
9242
|
+
title: 'Unauthorized',
|
|
9243
|
+
status: 401,
|
|
9244
|
+
detail: 'A valid API key is required to access this endpoint.',
|
|
9245
|
+
});
|
|
9246
|
+
return;
|
|
9247
|
+
}
|
|
9248
|
+
|
|
9249
|
+
const body = await parseJsonBody(req);
|
|
9250
|
+
const keyFingerprint = fingerprintProKey(token);
|
|
9251
|
+
const alert = await sendProActivationAlert({
|
|
9252
|
+
key: token,
|
|
9253
|
+
source: body.source || 'hosted_pro_activation',
|
|
9254
|
+
version: body.version || null,
|
|
9255
|
+
customerId: validation.customerId,
|
|
9256
|
+
installId: validation.installId,
|
|
9257
|
+
usageCount: validation.usageCount,
|
|
9258
|
+
env: process.env,
|
|
9259
|
+
});
|
|
9260
|
+
|
|
9261
|
+
sendJson(res, 200, {
|
|
9262
|
+
ok: true,
|
|
9263
|
+
customerId: validation.customerId,
|
|
9264
|
+
installId: validation.installId,
|
|
9265
|
+
usageCount: validation.usageCount,
|
|
9266
|
+
keyFingerprint,
|
|
9267
|
+
keyFingerprintMatchesClient: body.keyFingerprint ? body.keyFingerprint === keyFingerprint : null,
|
|
9268
|
+
alert: {
|
|
9269
|
+
sent: Boolean(alert?.sent),
|
|
9270
|
+
reason: alert?.reason || null,
|
|
9271
|
+
id: alert?.id || null,
|
|
9272
|
+
},
|
|
9273
|
+
});
|
|
9274
|
+
return;
|
|
9275
|
+
}
|
|
9276
|
+
|
|
9209
9277
|
// POST /v1/billing/provision — manually provision key (admin)
|
|
9210
9278
|
if (req.method === 'POST' && pathname === '/v1/billing/provision') {
|
|
9211
9279
|
if (!isStaticAdminAuthorized(req, expectedApiKey)) {
|
|
@@ -9765,6 +9833,9 @@ module.exports = {
|
|
|
9765
9833
|
createApiServer,
|
|
9766
9834
|
startServer,
|
|
9767
9835
|
__test__: {
|
|
9836
|
+
PRO_CHECKOUT_URL,
|
|
9837
|
+
escapeHtmlAttribute,
|
|
9838
|
+
renderCheckoutIntentPage,
|
|
9768
9839
|
buildCheckoutFallbackUrl,
|
|
9769
9840
|
createPrivateCoreUnavailableError,
|
|
9770
9841
|
buildPosthogProxyRequestOptions,
|