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.
Files changed (63) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +124 -129
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/mcp/server-stdio.js +18 -33
  6. package/adapters/opencode/opencode.json +1 -1
  7. package/bin/cli.js +95 -78
  8. package/bin/postinstall.js +1 -1
  9. package/config/entitlement-public-keys.json +6 -0
  10. package/config/gates/default.json +3 -3
  11. package/config/github-about.json +2 -5
  12. package/config/merge-quality-checks.json +3 -0
  13. package/config/post-deploy-marketing-pages.json +1 -1
  14. package/hooks/hooks.json +38 -0
  15. package/package.json +28 -10
  16. package/public/about.html +1 -4
  17. package/public/agent-manager.html +6 -6
  18. package/public/agents-cost-savings.html +3 -3
  19. package/public/ai-malpractice-prevention.html +1 -1
  20. package/public/blog.html +12 -9
  21. package/public/chatgpt-app.html +3 -3
  22. package/public/codex-enterprise.html +3 -3
  23. package/public/codex-plugin.html +5 -5
  24. package/public/compare.html +10 -10
  25. package/public/dashboard.html +1 -1
  26. package/public/diagnostic.html +23 -1
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +14 -14
  29. package/public/index.html +202 -220
  30. package/public/install.html +14 -14
  31. package/public/learn.html +4 -17
  32. package/public/numbers.html +2 -2
  33. package/public/partner-intake.html +198 -0
  34. package/public/pricing.html +61 -31
  35. package/public/pro.html +2 -2
  36. package/scripts/agent-reward-model.js +13 -0
  37. package/scripts/bayes-optimal-gate.js +6 -1
  38. package/scripts/billing.js +815 -137
  39. package/scripts/checkout-attribution-reference.js +85 -0
  40. package/scripts/claude-feedback-sync.js +23 -6
  41. package/scripts/cli-feedback.js +33 -8
  42. package/scripts/commercial-offer.js +3 -3
  43. package/scripts/entitlement.js +250 -0
  44. package/scripts/export-databricks-bundle.js +5 -0
  45. package/scripts/export-dpo-pairs.js +6 -0
  46. package/scripts/export-hf-dataset.js +5 -0
  47. package/scripts/feedback-loop.js +290 -1
  48. package/scripts/feedback-quality.js +25 -26
  49. package/scripts/feedback-sanitizer.js +151 -3
  50. package/scripts/gates-engine.js +134 -43
  51. package/scripts/hosted-config.js +9 -2
  52. package/scripts/imperative-detector.js +85 -0
  53. package/scripts/intervention-policy.js +13 -0
  54. package/scripts/mailer/resend-mailer.js +11 -5
  55. package/scripts/pr-manager.js +9 -22
  56. package/scripts/pro-local-dashboard.js +198 -0
  57. package/scripts/risk-scorer.js +6 -0
  58. package/scripts/secret-scanner.js +363 -68
  59. package/scripts/self-protection.js +21 -36
  60. package/scripts/seo-gsd.js +2 -2
  61. package/scripts/thompson-sampling.js +11 -2
  62. package/src/api/server.js +202 -22
  63. package/public/assets/brand/github-social-preview.png +0 -0
@@ -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, '&lt;')
72
+ .replace(/>/g, '&gt;')
73
+ .replace(/"/g, '&quot;')
74
+ .replace(/'/g, '&#39;');
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,
@@ -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;