thumbgate 1.27.19 → 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.
Files changed (116) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +1 -1
  9. package/.well-known/mcp/server-card.json +1 -1
  10. package/README.md +63 -45
  11. package/adapters/claude/.mcp.json +2 -2
  12. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  13. package/adapters/letta/README.md +41 -0
  14. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  15. package/adapters/mcp/server-stdio.js +1 -1
  16. package/adapters/opencode/opencode.json +1 -1
  17. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  18. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  19. package/bench/observability-eval-suite.json +26 -0
  20. package/bin/cli.js +52 -9
  21. package/bin/dashboard-cli.js +7 -0
  22. package/bin/postinstall.js +14 -23
  23. package/commands/dashboard.md +15 -0
  24. package/commands/thumbgate-dashboard.md +15 -0
  25. package/config/entitlement-public-keys.json +6 -0
  26. package/config/gates/default.json +1 -1
  27. package/config/github-about.json +2 -2
  28. package/config/merge-quality-checks.json +3 -0
  29. package/package.json +233 -100
  30. package/public/about.html +159 -0
  31. package/public/agent-manager.html +179 -0
  32. package/public/agents-cost-savings.html +153 -0
  33. package/public/ai-malpractice-prevention.html +818 -0
  34. package/public/assets/brand/github-social-preview.png +0 -0
  35. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  36. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  37. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  38. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  39. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  40. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  41. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  42. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  43. package/public/assets/brand/thumbgate-mark.svg +21 -0
  44. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  45. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  46. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  47. package/public/assets/legal-intake-control-flow.svg +66 -0
  48. package/public/blog.html +12 -9
  49. package/public/brand/thumbgate-mark.svg +19 -0
  50. package/public/brand/thumbgate-og.svg +16 -0
  51. package/public/chatgpt-app.html +330 -0
  52. package/public/codex-enterprise.html +123 -0
  53. package/public/codex-plugin.html +5 -5
  54. package/public/compare.html +10 -10
  55. package/public/dashboard.html +1 -1
  56. package/public/diagnostic.html +345 -0
  57. package/public/federal.html +2 -2
  58. package/public/guide.html +14 -14
  59. package/public/index.html +81 -70
  60. package/public/install.html +193 -0
  61. package/public/js/buyer-intent.js +672 -0
  62. package/public/learn.html +4 -17
  63. package/public/numbers.html +2 -2
  64. package/public/pricing.html +392 -0
  65. package/public/pro.html +2 -2
  66. package/scripts/action-receipts.js +324 -0
  67. package/scripts/activation-quickstart.js +187 -0
  68. package/scripts/agent-operations-planner.js +621 -0
  69. package/scripts/agent-reward-model.js +13 -0
  70. package/scripts/ai-component-inventory.js +367 -0
  71. package/scripts/async-eval-observability.js +236 -0
  72. package/scripts/audit.js +65 -0
  73. package/scripts/aws-blocks-guardrails.js +272 -0
  74. package/scripts/bayes-optimal-gate.js +6 -1
  75. package/scripts/classifier-routing.js +130 -0
  76. package/scripts/cli-feedback.js +17 -1
  77. package/scripts/commercial-offer.js +3 -3
  78. package/scripts/dashboard-chat.js +332 -0
  79. package/scripts/entitlement.js +250 -0
  80. package/scripts/export-databricks-bundle.js +5 -0
  81. package/scripts/export-dpo-pairs.js +6 -0
  82. package/scripts/export-hf-dataset.js +5 -0
  83. package/scripts/feedback-aggregate.js +281 -0
  84. package/scripts/feedback-sanitizer.js +105 -0
  85. package/scripts/gates-engine.js +15 -3
  86. package/scripts/hook-stop-anti-claim.js +301 -0
  87. package/scripts/imperative-detector.js +85 -0
  88. package/scripts/install-shim.js +87 -0
  89. package/scripts/intervention-policy.js +13 -0
  90. package/scripts/mcp-oauth.js +293 -0
  91. package/scripts/noop-detect.js +285 -0
  92. package/scripts/parallel-workflow-orchestrator.js +293 -0
  93. package/scripts/plan-gate.js +243 -0
  94. package/scripts/plausible-domain-config.js +99 -0
  95. package/scripts/pr-manager.js +9 -22
  96. package/scripts/pro-local-dashboard.js +198 -0
  97. package/scripts/qa-scenario-planner.js +136 -0
  98. package/scripts/repeat-metric.js +137 -0
  99. package/scripts/risk-scorer.js +6 -0
  100. package/scripts/secret-fixture-tokens.js +61 -0
  101. package/scripts/secret-redaction.js +166 -0
  102. package/scripts/self-harness-optimizer.js +141 -0
  103. package/scripts/self-healing-check.js +193 -0
  104. package/scripts/self-protection.js +90 -0
  105. package/scripts/seo-gsd.js +2 -2
  106. package/scripts/silent-failure-cluster.js +531 -0
  107. package/scripts/statusline-cache-read.js +57 -0
  108. package/scripts/sync-telemetry-from-prod.js +374 -0
  109. package/scripts/thompson-sampling.js +11 -2
  110. package/scripts/tool-contract-validator.js +76 -0
  111. package/scripts/trajectory-scorer.js +63 -0
  112. package/scripts/verify-marketing-pages-deployed.js +212 -0
  113. package/scripts/visitor-journey.js +172 -0
  114. package/src/api/server.js +81 -10
  115. package/.claude-plugin/marketplace.json +0 -85
  116. package/adapters/chatgpt/openapi.yaml +0 -1707
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * verify-marketing-pages-deployed.js — post-deploy probe for top-level
6
+ * marketing pages.
7
+ *
8
+ * Reads `config/post-deploy-marketing-pages.json` and curls every entry
9
+ * against the live production URL (default
10
+ * https://thumbgate-production.up.railway.app, overridable via
11
+ * THUMBGATE_PROD_URL env or --prod-url=…). Each response body must
12
+ * contain the configured sentinel string and must not contain any configured
13
+ * `mustNotContain` strings. Any mismatch fails the run and the route is
14
+ * included in the failure summary.
15
+ *
16
+ * Intended to run as a workflow step in .github/workflows/deploy-verify.yml
17
+ * after the version sentinel check, so the marketing surface is verified
18
+ * in the same gate as /health and /dashboard. Adding a new marketing
19
+ * page? Add it to the JSON manifest — no workflow edit required.
20
+ *
21
+ * Modes:
22
+ * --json machine-readable report on stdout, suitable for piping
23
+ * into the GitHub Actions PR comment step.
24
+ * --quiet suppress per-route lines; only print the final summary.
25
+ *
26
+ * Exit code is 0 when every page passes, 1 if any sentinel is missing
27
+ * or any route returns non-200.
28
+ */
29
+
30
+ const fs = require('node:fs');
31
+ const path = require('node:path');
32
+
33
+ const DEFAULT_PROD_URL = 'https://thumbgate-production.up.railway.app';
34
+ const DEFAULT_TIMEOUT_MS = 12000;
35
+ const DEFAULT_MANIFEST_PATH = path.resolve(__dirname, '..', 'config', 'post-deploy-marketing-pages.json');
36
+
37
+ function parseArgs(argv = []) {
38
+ const out = {
39
+ prodUrl: process.env.THUMBGATE_PROD_URL || DEFAULT_PROD_URL,
40
+ manifestPath: DEFAULT_MANIFEST_PATH,
41
+ json: false,
42
+ quiet: false,
43
+ timeoutMs: DEFAULT_TIMEOUT_MS,
44
+ };
45
+ for (const arg of argv) {
46
+ if (arg === '--json') out.json = true;
47
+ else if (arg === '--quiet') out.quiet = true;
48
+ else if (arg.startsWith('--prod-url=')) out.prodUrl = arg.slice('--prod-url='.length);
49
+ else if (arg.startsWith('--manifest=')) out.manifestPath = arg.slice('--manifest='.length);
50
+ else if (arg.startsWith('--timeout-ms=')) {
51
+ const n = Number(arg.slice('--timeout-ms='.length));
52
+ if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function loadManifest(manifestPath) {
59
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
60
+ const parsed = JSON.parse(raw);
61
+ if (!Array.isArray(parsed.pages) || parsed.pages.length === 0) {
62
+ throw new Error(`Manifest has no pages: ${manifestPath}`);
63
+ }
64
+ for (const entry of parsed.pages) {
65
+ if (typeof entry.route !== 'string' || !entry.route.startsWith('/')) {
66
+ throw new Error(`Invalid route in manifest: ${JSON.stringify(entry)}`);
67
+ }
68
+ if (typeof entry.sentinel !== 'string' || entry.sentinel.length === 0) {
69
+ throw new Error(`Invalid sentinel for route ${entry.route}`);
70
+ }
71
+ if (entry.mustNotContain != null) {
72
+ if (!Array.isArray(entry.mustNotContain) || entry.mustNotContain.some((value) => typeof value !== 'string' || value.length === 0)) {
73
+ throw new Error(`Invalid mustNotContain for route ${entry.route}`);
74
+ }
75
+ }
76
+ if (entry.userAgent != null && (typeof entry.userAgent !== 'string' || entry.userAgent.length === 0)) {
77
+ throw new Error(`Invalid userAgent for route ${entry.route}`);
78
+ }
79
+ }
80
+ return parsed;
81
+ }
82
+
83
+ async function probePage({ prodUrl, route, sentinel, mustNotContain = [], userAgent, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS }) {
84
+ if (typeof fetchImpl !== 'function') {
85
+ return { route, ok: false, error: 'fetch_unavailable' };
86
+ }
87
+ const controller = new AbortController();
88
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
89
+ try {
90
+ const url = `${prodUrl.replace(/\/$/, '')}${route}`;
91
+ const res = await fetchImpl(url, {
92
+ signal: controller.signal,
93
+ headers: {
94
+ // A real browser-shaped UA so bot-deflection interstitials do not
95
+ // trigger; this probe is meant to verify the buyer-facing surface.
96
+ 'User-Agent': userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
97
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
98
+ },
99
+ });
100
+ const body = await res.text().catch(() => '');
101
+ const sentinelPresent = body.includes(sentinel);
102
+ const forbiddenPresent = Array.isArray(mustNotContain)
103
+ ? mustNotContain.filter((value) => body.includes(value))
104
+ : [];
105
+ return {
106
+ route,
107
+ url,
108
+ status: res.status,
109
+ ok: res.ok && sentinelPresent && forbiddenPresent.length === 0,
110
+ sentinelPresent,
111
+ forbiddenPresent,
112
+ bytes: body.length,
113
+ };
114
+ } catch (error) {
115
+ return {
116
+ route,
117
+ ok: false,
118
+ error: error?.name === 'AbortError' ? `timeout_after_${timeoutMs}ms` : (error?.message || String(error)),
119
+ };
120
+ } finally {
121
+ clearTimeout(timer);
122
+ }
123
+ }
124
+
125
+ async function runVerification({ prodUrl, manifestPath, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
126
+ const manifest = loadManifest(manifestPath);
127
+ const results = [];
128
+ // Sequential is fine — manifest has <20 entries; parallelism would
129
+ // be over-engineered and would risk masking a per-route rate-limit signal.
130
+ for (const entry of manifest.pages) {
131
+ // eslint-disable-next-line no-await-in-loop
132
+ const result = await probePage({
133
+ prodUrl,
134
+ route: entry.route,
135
+ sentinel: entry.sentinel,
136
+ mustNotContain: entry.mustNotContain,
137
+ userAgent: entry.userAgent,
138
+ fetchImpl,
139
+ timeoutMs,
140
+ });
141
+ results.push({ ...result, sentinel: entry.sentinel, description: entry.description });
142
+ }
143
+ const passed = results.filter((r) => r.ok);
144
+ const failed = results.filter((r) => !r.ok);
145
+ return {
146
+ generatedAt: new Date().toISOString(),
147
+ prodUrl,
148
+ manifestVersion: manifest.version,
149
+ totalRoutes: results.length,
150
+ passedCount: passed.length,
151
+ failedCount: failed.length,
152
+ verdict: failed.length === 0 ? 'pass' : 'fail',
153
+ results,
154
+ };
155
+ }
156
+
157
+ function renderHuman(report, { quiet = false } = {}) {
158
+ const lines = [];
159
+ if (!quiet) {
160
+ for (const r of report.results) {
161
+ if (r.ok) {
162
+ lines.push(`✅ ${r.route.padEnd(20)} HTTP ${r.status} bytes=${r.bytes} sentinel-OK`);
163
+ } else if (r.error) {
164
+ lines.push(`❌ ${r.route.padEnd(20)} ERROR ${r.error}`);
165
+ } else if (!r.sentinelPresent) {
166
+ lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status} sentinel MISSING (expected: ${JSON.stringify(r.sentinel)})`);
167
+ } else if (Array.isArray(r.forbiddenPresent) && r.forbiddenPresent.length > 0) {
168
+ lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status} forbidden content PRESENT (${r.forbiddenPresent.map((value) => JSON.stringify(value)).join(', ')})`);
169
+ } else {
170
+ lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status}`);
171
+ }
172
+ }
173
+ }
174
+ lines.push(`\nSummary: ${report.passedCount}/${report.totalRoutes} pages passed against ${report.prodUrl} (verdict: ${report.verdict.toUpperCase()})`);
175
+ return lines.join('\n');
176
+ }
177
+
178
+ async function main(argv) {
179
+ const args = parseArgs(argv);
180
+ let report;
181
+ try {
182
+ report = await runVerification({
183
+ prodUrl: args.prodUrl,
184
+ manifestPath: args.manifestPath,
185
+ timeoutMs: args.timeoutMs,
186
+ });
187
+ } catch (error) {
188
+ process.stderr.write(`verify-marketing-pages-deployed FAILED: ${error.message}\n`);
189
+ process.exit(2);
190
+ }
191
+ if (args.json) {
192
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
193
+ } else {
194
+ process.stdout.write(`${renderHuman(report, { quiet: args.quiet })}\n`);
195
+ }
196
+ process.exitCode = report.verdict === 'pass' ? 0 : 1;
197
+ }
198
+
199
+ module.exports = {
200
+ DEFAULT_PROD_URL,
201
+ DEFAULT_TIMEOUT_MS,
202
+ DEFAULT_MANIFEST_PATH,
203
+ parseArgs,
204
+ loadManifest,
205
+ probePage,
206
+ runVerification,
207
+ renderHuman,
208
+ };
209
+
210
+ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
211
+ main(process.argv.slice(2));
212
+ }
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const STAGE_ORDER = [
4
+ 'landing',
5
+ 'pricing',
6
+ 'checkout_viewed',
7
+ 'email_submitted',
8
+ 'stripe_redirect',
9
+ 'purchase',
10
+ ];
11
+
12
+ function parseTimestamp(row) {
13
+ const raw = row && (row.timestamp || row.receivedAt || row.ts);
14
+ const ms = raw ? Date.parse(raw) : NaN;
15
+ return Number.isFinite(ms) ? ms : null;
16
+ }
17
+
18
+ function pickText(...values) {
19
+ for (const value of values) {
20
+ const text = String(value || '').trim();
21
+ if (text) return text;
22
+ }
23
+ return '';
24
+ }
25
+
26
+ function extractSessionId(row) {
27
+ return pickText(
28
+ row.sessionId,
29
+ row.visitorSessionId,
30
+ row.visitor_session_id,
31
+ row.attribution && row.attribution.sessionId,
32
+ row.metadata && row.metadata.sessionId,
33
+ row.stripeSessionId,
34
+ row.traceId,
35
+ row.acquisitionId,
36
+ );
37
+ }
38
+
39
+ function extractVisitorId(row) {
40
+ return pickText(
41
+ row.visitorId,
42
+ row.visitor_id,
43
+ row.installId,
44
+ row.attribution && row.attribution.visitorId,
45
+ row.metadata && row.metadata.visitorId,
46
+ );
47
+ }
48
+
49
+ function extractPath(row) {
50
+ return pickText(
51
+ row.path,
52
+ row.page,
53
+ row.landingPath,
54
+ row.landing_path,
55
+ row.attribution && row.attribution.landingPath,
56
+ );
57
+ }
58
+
59
+ function classifyStage(row) {
60
+ const event = String(row.eventType || row.event || '').toLowerCase();
61
+ const stage = String(row.stage || '').toLowerCase();
62
+ const path = extractPath(row).toLowerCase();
63
+ const cta = String(row.ctaId || row.cta_id || '').toLowerCase();
64
+
65
+ if (/purchase|paid|checkout\.session\.completed/.test(event) || stage === 'purchase') return 'purchase';
66
+ if (/stripe_redirect|stripe redirect|redirect started/.test(event) || stage === 'stripe_redirect') return 'stripe_redirect';
67
+ if (/email_submitted|email submitted|checkout_interstitial_cta_clicked/.test(event)) return 'email_submitted';
68
+ if (/checkout/.test(path) || /checkout.*view|checkout pro viewed/.test(event)) return 'checkout_viewed';
69
+ if (/pricing/.test(path) || /pricing/.test(cta) || /pricing_cta/.test(event)) return 'pricing';
70
+ if (/landing|page_view|landing_page_view|discovery/.test(event) || stage === 'discovery') return 'landing';
71
+ return null;
72
+ }
73
+
74
+ function stageRank(stage) {
75
+ const index = STAGE_ORDER.indexOf(stage);
76
+ return index === -1 ? -1 : index;
77
+ }
78
+
79
+ function nextMissingStage(maxStage) {
80
+ const index = stageRank(maxStage);
81
+ if (index < 0) return 'unknown';
82
+ if (maxStage === 'purchase') return 'converted';
83
+ return STAGE_ORDER[index + 1] || 'unknown';
84
+ }
85
+
86
+ function buildVisitorJourneySummary({ telemetryRows = [], funnelRows = [], limit = 100 } = {}) {
87
+ const sessions = new Map();
88
+ const stageCounts = Object.fromEntries(STAGE_ORDER.map((stage) => [stage, 0]));
89
+
90
+ function ingest(row, source) {
91
+ const ts = parseTimestamp(row);
92
+ if (ts == null) return;
93
+ const sessionId = extractSessionId(row) || `anonymous:${extractVisitorId(row) || ts}`;
94
+ const visitorId = extractVisitorId(row) || null;
95
+ const stage = classifyStage(row);
96
+ const path = extractPath(row);
97
+ const existing = sessions.get(sessionId) || {
98
+ sessionId,
99
+ visitorId,
100
+ firstSeen: new Date(ts).toISOString(),
101
+ lastSeen: new Date(ts).toISOString(),
102
+ maxStage: null,
103
+ dropoffStage: 'unknown',
104
+ events: [],
105
+ paths: [],
106
+ ctas: [],
107
+ sources: [],
108
+ visitorType: row.visitorType || null,
109
+ utm: {},
110
+ referrerHost: pickText(row.referrerHost, row.referrer_host, row.referrer),
111
+ };
112
+
113
+ existing.visitorId = existing.visitorId || visitorId;
114
+ existing.firstSeen = new Date(Math.min(Date.parse(existing.firstSeen), ts)).toISOString();
115
+ existing.lastSeen = new Date(Math.max(Date.parse(existing.lastSeen), ts)).toISOString();
116
+ existing.sources.push(source);
117
+ if (stage) {
118
+ stageCounts[stage] += 1;
119
+ if (stageRank(stage) > stageRank(existing.maxStage)) existing.maxStage = stage;
120
+ }
121
+ if (path && !existing.paths.includes(path)) existing.paths.push(path);
122
+ const cta = pickText(row.ctaId, row.cta_id);
123
+ if (cta && !existing.ctas.includes(cta)) existing.ctas.push(cta);
124
+ for (const key of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) {
125
+ const value = pickText(row[key], row.attribution && row.attribution[key]);
126
+ if (value && !existing.utm[key]) existing.utm[key] = value;
127
+ }
128
+ existing.events.push({
129
+ timestamp: new Date(ts).toISOString(),
130
+ source,
131
+ eventType: pickText(row.eventType, row.event, row.stage, 'unknown'),
132
+ stage,
133
+ path: path || null,
134
+ ctaId: cta || null,
135
+ });
136
+ sessions.set(sessionId, existing);
137
+ }
138
+
139
+ for (const row of telemetryRows) ingest(row, 'telemetry');
140
+ for (const row of funnelRows) ingest(row, 'funnel');
141
+
142
+ const journeys = [...sessions.values()].map((session) => {
143
+ session.sources = [...new Set(session.sources)];
144
+ session.events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
145
+ session.maxStage = session.maxStage || 'unknown';
146
+ session.dropoffStage = nextMissingStage(session.maxStage);
147
+ session.eventCount = session.events.length;
148
+ return session;
149
+ }).sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen));
150
+
151
+ const dropoffCounts = {};
152
+ for (const journey of journeys) {
153
+ dropoffCounts[journey.dropoffStage] = (dropoffCounts[journey.dropoffStage] || 0) + 1;
154
+ }
155
+
156
+ return {
157
+ generatedAt: new Date().toISOString(),
158
+ sessionCount: journeys.length,
159
+ stageCounts,
160
+ dropoffCounts,
161
+ journeys: journeys.slice(0, Math.max(1, Math.min(Number(limit) || 100, 500))),
162
+ truncated: journeys.length > Math.max(1, Math.min(Number(limit) || 100, 500)),
163
+ };
164
+ }
165
+
166
+ module.exports = {
167
+ STAGE_ORDER,
168
+ buildVisitorJourneySummary,
169
+ classifyStage,
170
+ extractSessionId,
171
+ extractVisitorId,
172
+ };
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(`${FEEDBACK_LIST_LABELS[signal] || 'Recent feedback'} (${intent.windowLabel}):`);
1764
- lines.push(...entries.map(formatFeedbackEntry));
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> is what keeps them working across every machine, every agent runtime, and every breaking-change week.</p>
2248
- <form action="https://buy.stripe.com/8x2dR91M84r4cSd9uj3sI3f" method="GET" data-i="pro_checkout_confirmed">
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">Lessons synced across all your machines no local SQLite to babysit</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode — version drift is our problem, not yours</div><div class="trust-item">Hosted dashboard: gate stats, DPO export, org-wide rule library</div><div class="trust-item">24×7 ops on the rule engine SonarCloud regressions fixed in &lt;24h</div></div>
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('&', '&amp;')
2794
2807
  .replaceAll('"', '&quot;')
2808
+ .replaceAll("'", '&#39;')
2809
+ .replaceAll('`', '&#96;')
2795
2810
  .replaceAll('<', '&lt;')
2796
2811
  .replaceAll('>', '&gt;');
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 { _botDetector = require('../../scripts/bot-detection'); } catch (_e) { _botDetector = null; }
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
- || FIRST_FAILURE_RULE_CHECKOUT_URL;
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: missingConfirmedEmail
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,
@@ -1,85 +0,0 @@
1
- {
2
- "name": "thumbgate-marketplace",
3
- "version": "1.27.15",
4
- "owner": {
5
- "name": "Igor Ganapolsky",
6
- "email": "ig5973700@gmail.com"
7
- },
8
- "plugins": [
9
- {
10
- "name": "thumbgate",
11
- "description": "One 👎 becomes a hard rule the agent cannot bypass. Captures thumbs-down feedback, distills it into PreToolUse Pre-Action Checks, enforced across every future Claude Code session.",
12
- "longDescription": "ThumbGate is a tripwire, not a memory layer. When you thumbs-down an agent action — a force-push that overwrote a teammate's commit, a destructive SQL drop, a refactor you already rejected — ThumbGate captures the pattern, distills a one-sentence lesson, and installs it as a PreToolUse block. The next time the agent reaches for the same tool call shape, the hook fires before execution and Claude sees your own words back as the reason.\n\nThe enforcement lives in the hook, not in the model's context, so the agent cannot bypass it by forgetting, by being prompted around it, or by switching sessions. Same rule works in Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, and OpenCode.\n\n33 pre-action checks shipped (force-push, destructive SQL, mass-delete, npm publish from wrong branch, deploy without verification). Budget enforcement (action count + time limits) prevents runaway agent sessions. NIST / SOC2 / OWASP / CWE compliance tags on every rule for enterprise teams. DPO training-pair export for teams running their own fine-tunes.\n\nFree tier: 2 feedback captures/day, 10 total captures, 3 active auto-promoted prevention rules, fully offline. Pro ($19/mo): hosted sync across machines and agent runtimes, dashboard, audit export.",
13
- "source": {
14
- "source": "npm",
15
- "package": "thumbgate"
16
- },
17
- "version": "1.27.15",
18
- "author": {
19
- "name": "Igor Ganapolsky",
20
- "email": "ig5973700@gmail.com",
21
- "url": "https://thumbgate.ai"
22
- },
23
- "homepage": "https://thumbgate.ai",
24
- "repository": "https://github.com/IgorGanapolsky/ThumbGate",
25
- "license": "MIT",
26
- "category": "developer-tools",
27
- "tags": [
28
- "guardrails",
29
- "pretooluse",
30
- "hooks",
31
- "feedback",
32
- "rlhf",
33
- "dpo",
34
- "agent-safety",
35
- "workflow-hardening"
36
- ],
37
- "keywords": [
38
- "guardrails",
39
- "pretooluse",
40
- "hooks",
41
- "feedback",
42
- "rlhf",
43
- "dpo",
44
- "agent-safety",
45
- "workflow-hardening",
46
- "claude-desktop",
47
- "desktop-extension",
48
- "pre-action-checks",
49
- "ai-agent-safety",
50
- "mcp",
51
- "memory"
52
- ],
53
- "capabilities": {
54
- "skills": 2,
55
- "commands": 5,
56
- "agents": 1,
57
- "hooks": 3,
58
- "mcpServer": "thumbgate serve"
59
- },
60
- "installCommand": "/plugin install thumbgate@claude-plugins-official",
61
- "metadata": {
62
- "author": "Igor Ganapolsky",
63
- "homepage": "https://thumbgate.ai",
64
- "license": "MIT",
65
- "keywords": [
66
- "guardrails",
67
- "pretooluse",
68
- "hooks",
69
- "feedback",
70
- "rlhf",
71
- "dpo",
72
- "agent-safety",
73
- "workflow-hardening",
74
- "claude-desktop",
75
- "desktop-extension",
76
- "pre-action-checks",
77
- "ai-agent-safety",
78
- "mcp",
79
- "memory"
80
- ],
81
- "category": "developer-tools"
82
- }
83
- }
84
- ]
85
- }