thumbgate 1.35.0 → 1.37.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/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +336 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +581 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +390 -148
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +810 -63
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
package/src/api/server.js
CHANGED
|
@@ -16,6 +16,10 @@ const {
|
|
|
16
16
|
buildIntakeDiscoveryPacket,
|
|
17
17
|
buildIntakeQualificationCard,
|
|
18
18
|
} = require('../../scripts/revenue-offer-system');
|
|
19
|
+
const {
|
|
20
|
+
getSecurityQuestionnaire,
|
|
21
|
+
renderSecurityOverviewHtml,
|
|
22
|
+
} = require('../security-questionnaire');
|
|
19
23
|
|
|
20
24
|
const POSTHOG_API_PATHS = new Set(['/capture', '/batch', '/decide', '/e', '/engage']);
|
|
21
25
|
const POSTHOG_INGEST_HOST = 'us.i.posthog.com';
|
|
@@ -139,6 +143,10 @@ const {
|
|
|
139
143
|
getBillingSummaryLive,
|
|
140
144
|
getBillingSummariesLive,
|
|
141
145
|
} = require('../../scripts/billing');
|
|
146
|
+
const {
|
|
147
|
+
ensureProductionSearchCorpus,
|
|
148
|
+
shouldEnsureProductionSearchCorpus,
|
|
149
|
+
} = require('../../scripts/ensure-production-search-corpus');
|
|
142
150
|
const {
|
|
143
151
|
DEFAULT_PUBLIC_APP_ORIGIN,
|
|
144
152
|
resolveHostedBillingConfig,
|
|
@@ -188,6 +196,49 @@ const {
|
|
|
188
196
|
recordDecisionOutcome,
|
|
189
197
|
computeDecisionMetrics,
|
|
190
198
|
} = require('../../scripts/decision-journal');
|
|
199
|
+
const {
|
|
200
|
+
HermesPlatformProtocol,
|
|
201
|
+
} = require('../hermes-platform-protocol');
|
|
202
|
+
const {
|
|
203
|
+
HermesSyncPlane,
|
|
204
|
+
} = require('../hermes-sync-plane');
|
|
205
|
+
|
|
206
|
+
// Partition Hermes protocol/sync state by hosted tenant so one customer's
|
|
207
|
+
// connection/thread/approval maps cannot be read or mutated by another.
|
|
208
|
+
const hostedHermesByTenant = new Map();
|
|
209
|
+
|
|
210
|
+
function resolveHermesTenantKey(requestDataIdentity) {
|
|
211
|
+
return requestDataIdentity?.partition || '__default__';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getHostedHermes(requestDataIdentity) {
|
|
215
|
+
const key = resolveHermesTenantKey(requestDataIdentity);
|
|
216
|
+
let entry = hostedHermesByTenant.get(key);
|
|
217
|
+
if (!entry) {
|
|
218
|
+
entry = {
|
|
219
|
+
protocol: new HermesPlatformProtocol(),
|
|
220
|
+
syncPlane: new HermesSyncPlane(),
|
|
221
|
+
};
|
|
222
|
+
hostedHermesByTenant.set(key, entry);
|
|
223
|
+
}
|
|
224
|
+
return entry;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolveHermesApproverId(req, humanReviewerConfig, requestDataIdentity) {
|
|
228
|
+
if (humanReviewerConfig?.key && humanReviewerConfig?.id) {
|
|
229
|
+
const header = req.headers['x-thumbgate-human-reviewer-key'];
|
|
230
|
+
const presented = Array.isArray(header) ? header[0] : header;
|
|
231
|
+
if (safeKeyEqual(presented, humanReviewerConfig.key)) {
|
|
232
|
+
return humanReviewerConfig.id;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (requestDataIdentity?.accessContext?.principalId) {
|
|
236
|
+
return requestDataIdentity.accessContext.principalId;
|
|
237
|
+
}
|
|
238
|
+
const role = resolveKeyRole(extractApiKey(req));
|
|
239
|
+
if (role) return `thumbgate:${role}`;
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
191
242
|
const {
|
|
192
243
|
generateDashboard,
|
|
193
244
|
buildReviewSnapshot,
|
|
@@ -218,6 +269,7 @@ const {
|
|
|
218
269
|
} = require('../../scripts/thumbgate-search');
|
|
219
270
|
const {
|
|
220
271
|
appendTelemetryPing,
|
|
272
|
+
headerHasPrivacyOptOut,
|
|
221
273
|
} = require('../../scripts/telemetry-analytics');
|
|
222
274
|
const {
|
|
223
275
|
buildProductIssueTitle,
|
|
@@ -278,10 +330,13 @@ const EVAL_SCORECARD_PAGE_PATH = path.resolve(__dirname, '../../public/eval-scor
|
|
|
278
330
|
const EVALUATIONS_PAGE_PATH = path.resolve(__dirname, '../../public/evaluations.html');
|
|
279
331
|
const CASE_STUDIES_PAGE_PATH = path.resolve(__dirname, '../../public/case-studies.html');
|
|
280
332
|
const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
|
|
333
|
+
const YT_PAGE_PATH = path.resolve(__dirname, '../../public/yt.html');
|
|
281
334
|
const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
|
|
282
335
|
const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
|
|
283
336
|
const DIAGNOSTIC_PAGE_PATH = path.resolve(__dirname, '../../public/diagnostic.html');
|
|
337
|
+
const FOUNDERS_PAGE_PATH = path.resolve(__dirname, '../../public/founders.html');
|
|
284
338
|
const PARTNER_INTAKE_PAGE_PATH = path.resolve(__dirname, '../../public/partner-intake.html');
|
|
339
|
+
const PETER_PAGE_PATH = path.resolve(__dirname, '../../public/peter.html');
|
|
285
340
|
const INSTALL_PAGE_PATH = path.resolve(__dirname, '../../public/install.html');
|
|
286
341
|
const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
287
342
|
const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
|
|
@@ -296,6 +351,7 @@ const COMPARE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(COMPARE_DIR);
|
|
|
296
351
|
const USE_CASE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(USE_CASES_DIR);
|
|
297
352
|
const BLOG_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(BLOG_DIR);
|
|
298
353
|
const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
|
|
354
|
+
const WEBMCP_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/webmcp.js');
|
|
299
355
|
const STATIC_MIME_BY_EXT = Object.freeze({
|
|
300
356
|
'.png': 'image/png',
|
|
301
357
|
'.jpg': 'image/jpeg',
|
|
@@ -644,6 +700,30 @@ const TRACKED_LINK_TARGETS = Object.freeze({
|
|
|
644
700
|
utm_campaign: 'github_repo',
|
|
645
701
|
},
|
|
646
702
|
},
|
|
703
|
+
npm: {
|
|
704
|
+
href: 'https://www.npmjs.com/package/thumbgate',
|
|
705
|
+
external: true,
|
|
706
|
+
ctaId: 'go_npm',
|
|
707
|
+
ctaPlacement: 'link_router',
|
|
708
|
+
eventType: 'cta_click',
|
|
709
|
+
defaults: {
|
|
710
|
+
utm_source: 'website',
|
|
711
|
+
utm_medium: 'link_router',
|
|
712
|
+
utm_campaign: 'npm_package',
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
marketplace: {
|
|
716
|
+
href: 'https://github.com/marketplace/actions/thumbgate-agent-governance',
|
|
717
|
+
external: true,
|
|
718
|
+
ctaId: 'go_marketplace',
|
|
719
|
+
ctaPlacement: 'link_router',
|
|
720
|
+
eventType: 'cta_click',
|
|
721
|
+
defaults: {
|
|
722
|
+
utm_source: 'website',
|
|
723
|
+
utm_medium: 'link_router',
|
|
724
|
+
utm_campaign: 'github_marketplace_action',
|
|
725
|
+
},
|
|
726
|
+
},
|
|
647
727
|
});
|
|
648
728
|
|
|
649
729
|
// ---------------------------------------------------------------------------
|
|
@@ -1837,7 +1917,7 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
|
|
|
1837
1917
|
type: PROBLEM_TYPES.INVALID_REQUEST,
|
|
1838
1918
|
title,
|
|
1839
1919
|
status: 400,
|
|
1840
|
-
detail: err
|
|
1920
|
+
detail: err?.message || 'Invalid analytics window request.',
|
|
1841
1921
|
});
|
|
1842
1922
|
}
|
|
1843
1923
|
|
|
@@ -1864,19 +1944,103 @@ function resolveBillingSummaryOptionsOrRespondProblem(res, parsed, invalidTitle)
|
|
|
1864
1944
|
|
|
1865
1945
|
async function buildLiveDashboardData(parsed, feedbackDir) {
|
|
1866
1946
|
const summaryOptions = resolveBillingSummaryOptions(parsed);
|
|
1867
|
-
|
|
1947
|
+
// Stripe/live billing can hang longer than the authenticated deploy proof
|
|
1948
|
+
// timeout (12s). Prefer live, but fail closed to local summary so /v1/dashboard
|
|
1949
|
+
// remains usable for operational proof.
|
|
1950
|
+
const liveTimeoutMs = Number(process.env.THUMBGATE_DASHBOARD_BILLING_TIMEOUT_MS || 4000);
|
|
1951
|
+
let billingSummary;
|
|
1952
|
+
let billingSource = 'live';
|
|
1953
|
+
try {
|
|
1954
|
+
billingSummary = await Promise.race([
|
|
1955
|
+
getBillingSummaryLive(summaryOptions),
|
|
1956
|
+
new Promise((_, reject) => {
|
|
1957
|
+
const timer = setTimeout(() => reject(new Error('billing_summary_timeout')), liveTimeoutMs);
|
|
1958
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
1959
|
+
}),
|
|
1960
|
+
]);
|
|
1961
|
+
} catch {
|
|
1962
|
+
billingSummary = getBillingSummary(summaryOptions);
|
|
1963
|
+
billingSource = 'local';
|
|
1964
|
+
}
|
|
1868
1965
|
const data = generateDashboard(feedbackDir, {
|
|
1869
1966
|
analyticsWindow: summaryOptions,
|
|
1870
1967
|
billingSummary,
|
|
1871
|
-
billingSource
|
|
1968
|
+
billingSource,
|
|
1872
1969
|
authContext: { tier: 'pro' },
|
|
1873
1970
|
});
|
|
1874
1971
|
return { summaryOptions, data };
|
|
1875
1972
|
}
|
|
1876
1973
|
|
|
1974
|
+
const DEFAULT_DASHBOARD_CACHE_TTL_MS = 30_000;
|
|
1975
|
+
const DEFAULT_DASHBOARD_CACHE_MAX_ENTRIES = 32;
|
|
1976
|
+
const liveDashboardCache = new Map();
|
|
1977
|
+
|
|
1978
|
+
function parseNonNegativeInteger(value, fallback) {
|
|
1979
|
+
const parsed = Number(value);
|
|
1980
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function buildLiveDashboardCacheKey(parsed, feedbackDir) {
|
|
1984
|
+
const query = new URLSearchParams(parsed.searchParams);
|
|
1985
|
+
query.sort();
|
|
1986
|
+
return `${path.resolve(feedbackDir)}?${query.toString()}`;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
async function loadCachedLiveDashboardData(parsed, feedbackDir, options = {}) {
|
|
1990
|
+
const now = options.now || Date.now;
|
|
1991
|
+
const build = options.build || buildLiveDashboardData;
|
|
1992
|
+
const cache = options.cache || liveDashboardCache;
|
|
1993
|
+
const ttlMs = parseNonNegativeInteger(
|
|
1994
|
+
options.ttlMs ?? process.env.THUMBGATE_DASHBOARD_CACHE_TTL_MS,
|
|
1995
|
+
DEFAULT_DASHBOARD_CACHE_TTL_MS,
|
|
1996
|
+
);
|
|
1997
|
+
const maxEntries = parseNonNegativeInteger(
|
|
1998
|
+
options.maxEntries ?? process.env.THUMBGATE_DASHBOARD_CACHE_MAX_ENTRIES,
|
|
1999
|
+
DEFAULT_DASHBOARD_CACHE_MAX_ENTRIES,
|
|
2000
|
+
);
|
|
2001
|
+
const key = buildLiveDashboardCacheKey(parsed, feedbackDir);
|
|
2002
|
+
const currentTime = now();
|
|
2003
|
+
const cached = cache.get(key);
|
|
2004
|
+
if (cached && (cached.promise || cached.expiresAt > currentTime)) {
|
|
2005
|
+
return cached.promise || cached.value;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
if (maxEntries === 0) {
|
|
2009
|
+
return build(parsed, feedbackDir);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
if (cached) cache.delete(key);
|
|
2013
|
+
for (const [cacheKey, entry] of cache) {
|
|
2014
|
+
if (!entry.promise && entry.expiresAt <= currentTime) cache.delete(cacheKey);
|
|
2015
|
+
}
|
|
2016
|
+
while (cache.size >= maxEntries) {
|
|
2017
|
+
const evictableKey = [...cache].find(([, entry]) => !entry.promise)?.[0];
|
|
2018
|
+
if (!evictableKey) {
|
|
2019
|
+
const error = new Error('Live dashboard cache is at concurrent build capacity.');
|
|
2020
|
+
error.code = 'DASHBOARD_BUILD_CAPACITY';
|
|
2021
|
+
throw error;
|
|
2022
|
+
}
|
|
2023
|
+
cache.delete(evictableKey);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
const promise = Promise.resolve().then(() => build(parsed, feedbackDir));
|
|
2027
|
+
cache.set(key, { promise, expiresAt: 0, value: null });
|
|
2028
|
+
try {
|
|
2029
|
+
const value = await promise;
|
|
2030
|
+
cache.set(key, { promise: null, expiresAt: now() + ttlMs, value });
|
|
2031
|
+
while (cache.size > maxEntries && cache.size > 0) {
|
|
2032
|
+
cache.delete(cache.keys().next().value);
|
|
2033
|
+
}
|
|
2034
|
+
return value;
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
cache.delete(key);
|
|
2037
|
+
throw error;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
1877
2041
|
async function loadLiveDashboardDataOrRespondProblem(res, parsed, feedbackDir, invalidTitle) {
|
|
1878
2042
|
try {
|
|
1879
|
-
return await
|
|
2043
|
+
return await loadCachedLiveDashboardData(parsed, feedbackDir);
|
|
1880
2044
|
} catch (err) {
|
|
1881
2045
|
sendInvalidAnalyticsWindowProblem(res, invalidTitle, err);
|
|
1882
2046
|
return null;
|
|
@@ -1993,14 +2157,22 @@ function readRecentFeedbackEntries(feedbackDir, signal, windowMs, limit = 5, opt
|
|
|
1993
2157
|
try {
|
|
1994
2158
|
if (!feedbackDir) return [];
|
|
1995
2159
|
const rows = shouldAggregateFeedback()
|
|
1996
|
-
? collectAggregateLogEntries('feedback-log.jsonl', {
|
|
2160
|
+
? collectAggregateLogEntries('feedback-log.jsonl', {
|
|
2161
|
+
feedbackDir,
|
|
2162
|
+
maxLines: 20_000,
|
|
2163
|
+
maxBytes: 4 * 1024 * 1024,
|
|
2164
|
+
}).entries
|
|
1997
2165
|
: (() => {
|
|
1998
2166
|
const fsLocal = require('node:fs');
|
|
1999
2167
|
const pathLocal = require('node:path');
|
|
2000
2168
|
const logPath = pathLocal.join(feedbackDir, 'feedback-log.jsonl');
|
|
2001
2169
|
if (!fsLocal.existsSync(logPath)) return [];
|
|
2002
2170
|
const { readJsonl } = require('../../scripts/fs-utils');
|
|
2003
|
-
return readJsonl(logPath
|
|
2171
|
+
return readJsonl(logPath, {
|
|
2172
|
+
maxLines: 20_000,
|
|
2173
|
+
maxBytes: 4 * 1024 * 1024,
|
|
2174
|
+
tail: true,
|
|
2175
|
+
}) || [];
|
|
2004
2176
|
})();
|
|
2005
2177
|
const cutoff = windowMs ? Date.now() - windowMs : 0;
|
|
2006
2178
|
const filtered = rows
|
|
@@ -2799,7 +2971,7 @@ function applyTrackedLinkDefaults(destinationUrl, target) {
|
|
|
2799
2971
|
function applyTrackedLinkQueryKeys(destinationUrl, params) {
|
|
2800
2972
|
for (const key of TRACKED_LINK_QUERY_KEYS) {
|
|
2801
2973
|
const value = params.get(key);
|
|
2802
|
-
if (!value
|
|
2974
|
+
if (!value?.trim()) continue;
|
|
2803
2975
|
const normalizedValue = key === 'utm_campaign'
|
|
2804
2976
|
? normalizeCampaignId(value)
|
|
2805
2977
|
: value.trim();
|
|
@@ -3100,8 +3272,8 @@ function sendPublicBillingPreflight(res) {
|
|
|
3100
3272
|
|
|
3101
3273
|
function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
|
|
3102
3274
|
try {
|
|
3103
|
-
appendTelemetryPing(feedbackDir, payload, headers);
|
|
3104
|
-
return
|
|
3275
|
+
const written = appendTelemetryPing(feedbackDir, payload, headers);
|
|
3276
|
+
return Boolean(written);
|
|
3105
3277
|
} catch (err) {
|
|
3106
3278
|
try {
|
|
3107
3279
|
appendDiagnosticRecord({
|
|
@@ -3288,7 +3460,8 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
3288
3460
|
const googleSiteVerificationMeta = runtimeConfig.googleSiteVerification
|
|
3289
3461
|
? ` <meta name="google-site-verification" content="${escapeHtmlAttribute(runtimeConfig.googleSiteVerification)}" />`
|
|
3290
3462
|
: '';
|
|
3291
|
-
const
|
|
3463
|
+
const privacyOptOut = Boolean(pageContext.privacyOptOut);
|
|
3464
|
+
const gaBootstrap = (!privacyOptOut && runtimeConfig.gaMeasurementId)
|
|
3292
3465
|
? [
|
|
3293
3466
|
` <script async src="https://www.googletagmanager.com/gtag/js?id=${runtimeConfig.gaMeasurementId}"></script>`,
|
|
3294
3467
|
' <script>',
|
|
@@ -3299,7 +3472,7 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
3299
3472
|
' </script>',
|
|
3300
3473
|
].join('\n')
|
|
3301
3474
|
: '';
|
|
3302
|
-
|
|
3475
|
+
let rendered = fillTemplate(template, {
|
|
3303
3476
|
'__PACKAGE_VERSION__': pkg.version,
|
|
3304
3477
|
'__APP_ORIGIN__': runtimeConfig.appOrigin,
|
|
3305
3478
|
'__CHECKOUT_ENDPOINT__': runtimeConfig.checkoutEndpoint,
|
|
@@ -3328,7 +3501,34 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
3328
3501
|
'__GTM_PLAN_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/GO_TO_MARKET_REVENUE_WEDGE_2026-03.md',
|
|
3329
3502
|
'__GITHUB_URL__': 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
3330
3503
|
'__POSTHOG_API_KEY__': runtimeConfig.posthogApiKey || '',
|
|
3331
|
-
})
|
|
3504
|
+
});
|
|
3505
|
+
const normalized = normalizePublicMarketingHtml(rendered, runtimeConfig, pageContext.requestHost);
|
|
3506
|
+
if (!privacyOptOut) return normalized;
|
|
3507
|
+
let rewrittenDomain = '';
|
|
3508
|
+
try {
|
|
3509
|
+
rewrittenDomain = normalizePublicRequestHost(pageContext.requestHost)
|
|
3510
|
+
|| new URL(runtimeConfig.appOrigin).host;
|
|
3511
|
+
} catch {
|
|
3512
|
+
rewrittenDomain = '';
|
|
3513
|
+
}
|
|
3514
|
+
return removeKnownPlausibleScriptTags(normalized, rewrittenDomain);
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
function removeKnownPlausibleScriptTags(html, extraDomain) {
|
|
3518
|
+
const domains = ['thumbgate.ai', 'thumbgate-production.up.railway.app'];
|
|
3519
|
+
if (extraDomain) domains.push(extraDomain);
|
|
3520
|
+
const sources = [
|
|
3521
|
+
'https://plausible.io/js/script.tagged-events.js',
|
|
3522
|
+
'https://plausible.io/js/script.js',
|
|
3523
|
+
];
|
|
3524
|
+
let out = html;
|
|
3525
|
+
for (const domain of domains) {
|
|
3526
|
+
for (const src of sources) {
|
|
3527
|
+
const tag = `<script defer data-domain="${domain}" src="${src}"></script>`;
|
|
3528
|
+
if (out.includes(tag)) out = out.split(tag).join('');
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
return out;
|
|
3332
3532
|
}
|
|
3333
3533
|
|
|
3334
3534
|
function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
|
|
@@ -3343,10 +3543,18 @@ function loadDiagnosticPageHtml(runtimeConfig, pageContext = {}) {
|
|
|
3343
3543
|
return loadPublicMarketingTemplateHtml(DIAGNOSTIC_PAGE_PATH, runtimeConfig, pageContext);
|
|
3344
3544
|
}
|
|
3345
3545
|
|
|
3546
|
+
function loadFoundersPageHtml(runtimeConfig, pageContext = {}) {
|
|
3547
|
+
return loadPublicMarketingTemplateHtml(FOUNDERS_PAGE_PATH, runtimeConfig, pageContext);
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3346
3550
|
function loadPartnerIntakePageHtml(runtimeConfig, pageContext = {}) {
|
|
3347
3551
|
return loadPublicMarketingTemplateHtml(PARTNER_INTAKE_PAGE_PATH, runtimeConfig, pageContext);
|
|
3348
3552
|
}
|
|
3349
3553
|
|
|
3554
|
+
function loadPeterPageHtml(runtimeConfig, pageContext = {}) {
|
|
3555
|
+
return loadPublicMarketingTemplateHtml(PETER_PAGE_PATH, runtimeConfig, pageContext);
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3350
3558
|
function loadInstallPageHtml(runtimeConfig, pageContext = {}) {
|
|
3351
3559
|
return loadPublicMarketingTemplateHtml(INSTALL_PAGE_PATH, runtimeConfig, pageContext);
|
|
3352
3560
|
}
|
|
@@ -3985,8 +4193,20 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3985
4193
|
{ path: '/', changefreq: 'weekly', priority: '1.0' },
|
|
3986
4194
|
{ path: '/pro', changefreq: 'weekly', priority: '0.9' },
|
|
3987
4195
|
{ path: '/diagnostic', changefreq: 'weekly', priority: '0.9' },
|
|
4196
|
+
{ path: '/founders', changefreq: 'weekly', priority: '0.95' },
|
|
4197
|
+
{ path: '/peter', changefreq: 'weekly', priority: '0.9' },
|
|
3988
4198
|
{ path: '/workflow-hardening-sprint', changefreq: 'weekly', priority: '0.9' },
|
|
3989
4199
|
{ path: '/install', changefreq: 'weekly', priority: '0.9' },
|
|
4200
|
+
{ path: '/terms', changefreq: 'monthly', priority: '0.7' },
|
|
4201
|
+
{ path: '/privacy', changefreq: 'monthly', priority: '0.7' },
|
|
4202
|
+
{ path: '/support', changefreq: 'monthly', priority: '0.7' },
|
|
4203
|
+
{ path: '/legal', changefreq: 'monthly', priority: '0.7' },
|
|
4204
|
+
{ path: '/security', changefreq: 'monthly', priority: '0.65' },
|
|
4205
|
+
{ path: '/trust', changefreq: 'monthly', priority: '0.65' },
|
|
4206
|
+
{ path: '/security.json', changefreq: 'monthly', priority: '0.55' },
|
|
4207
|
+
{ path: '/legal/data-flow', changefreq: 'monthly', priority: '0.7' },
|
|
4208
|
+
{ path: '/legal/licensing', changefreq: 'monthly', priority: '0.65' },
|
|
4209
|
+
{ path: '/legal/msa-sow', changefreq: 'monthly', priority: '0.6' },
|
|
3990
4210
|
{ path: '/agent-manager', changefreq: 'weekly', priority: '0.9' },
|
|
3991
4211
|
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
3992
4212
|
{ path: '/chatgpt-app', changefreq: 'weekly', priority: '0.85' },
|
|
@@ -4000,6 +4220,7 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
4000
4220
|
{ path: '/evaluations', changefreq: 'weekly', priority: '0.85' },
|
|
4001
4221
|
{ path: '/case-studies', changefreq: 'weekly', priority: '0.9' },
|
|
4002
4222
|
{ path: '/numbers', changefreq: 'weekly', priority: '0.8' },
|
|
4223
|
+
{ path: '/yt', changefreq: 'weekly', priority: '0.8' },
|
|
4003
4224
|
{ path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
|
|
4004
4225
|
{ path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
|
|
4005
4226
|
{ path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
|
|
@@ -4009,10 +4230,14 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
4009
4230
|
{ path: '/learn/agentic-os-team-governance', changefreq: 'weekly', priority: '0.85' },
|
|
4010
4231
|
{ path: '/learn/cost-aware-agent-gate-routing', changefreq: 'weekly', priority: '0.85' },
|
|
4011
4232
|
{ path: '/learn/pretix-stripe-connect-marketplaces', changefreq: 'weekly', priority: '0.9' },
|
|
4233
|
+
{ path: '/learn/netflix-oci-actor-critic-process-audits', changefreq: 'weekly', priority: '0.85' },
|
|
4234
|
+
{ path: '/learn/future-agi-self-healing-agent-firewall', changefreq: 'weekly', priority: '0.85' },
|
|
4235
|
+
{ path: '/learn/five-walls-enterprise-agent-deployment', changefreq: 'weekly', priority: '0.85' },
|
|
4012
4236
|
{ path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
|
|
4013
4237
|
{ path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },
|
|
4014
4238
|
{ path: '/compare/anthropic-containment', changefreq: 'weekly', priority: '0.85' },
|
|
4015
4239
|
{ path: '/compare/oak-and-sparrow-gatekeeper', changefreq: 'weekly', priority: '0.85' },
|
|
4240
|
+
{ path: '/compare/cloudflare-writeguard', changefreq: 'weekly', priority: '0.85' },
|
|
4016
4241
|
{ path: '/compare/arcjet', changefreq: 'weekly', priority: '0.85' },
|
|
4017
4242
|
{ path: '/compare/anthropic-claude-for-legal', changefreq: 'weekly', priority: '0.9' },
|
|
4018
4243
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
@@ -4181,6 +4406,7 @@ function servePublicMarketingPage({
|
|
|
4181
4406
|
serverUtmCampaign: landingAttribution.utmCampaign,
|
|
4182
4407
|
serverUtmContent: landingAttribution.utmContent,
|
|
4183
4408
|
requestHost,
|
|
4409
|
+
privacyOptOut: headerHasPrivacyOptOut(req.headers),
|
|
4184
4410
|
});
|
|
4185
4411
|
|
|
4186
4412
|
sendHtml(
|
|
@@ -5171,6 +5397,7 @@ function authenticateHumanReviewer(req, reviewerConfig) {
|
|
|
5171
5397
|
return {
|
|
5172
5398
|
id: reviewerConfig.id,
|
|
5173
5399
|
kind: 'human',
|
|
5400
|
+
role: 'admin',
|
|
5174
5401
|
};
|
|
5175
5402
|
}
|
|
5176
5403
|
|
|
@@ -5469,13 +5696,22 @@ function createApiServer() {
|
|
|
5469
5696
|
? mcpOauth.tokenAudienceValid(oauthSession, resourceUrl)
|
|
5470
5697
|
: rawKeyValid;
|
|
5471
5698
|
if (!authed) {
|
|
5699
|
+
const deniedName = msg.params?.name || 'unknown';
|
|
5700
|
+
const deniedTool = MCP_TOOLS.find((t) => t.name === deniedName);
|
|
5472
5701
|
recordToolCall({
|
|
5473
|
-
toolName:
|
|
5702
|
+
toolName: deniedName,
|
|
5474
5703
|
serverName: 'mcp-http',
|
|
5475
5704
|
latencyMs: 0,
|
|
5476
5705
|
success: false,
|
|
5706
|
+
outcome: 'blocked',
|
|
5707
|
+
blocked: true,
|
|
5477
5708
|
agentId: 'unauthenticated',
|
|
5478
|
-
|
|
5709
|
+
writeRiskTier: deniedTool?.annotations?.writeRiskTier || null,
|
|
5710
|
+
metadata: {
|
|
5711
|
+
denied: true,
|
|
5712
|
+
deniedReason: 'authentication_required',
|
|
5713
|
+
writeRiskTier: deniedTool?.annotations?.writeRiskTier || null,
|
|
5714
|
+
},
|
|
5479
5715
|
});
|
|
5480
5716
|
res.writeHead(401, {
|
|
5481
5717
|
'Content-Type': 'application/json',
|
|
@@ -5504,8 +5740,15 @@ function createApiServer() {
|
|
|
5504
5740
|
serverName: 'mcp-http',
|
|
5505
5741
|
latencyMs: 0,
|
|
5506
5742
|
success: false,
|
|
5743
|
+
outcome: 'blocked',
|
|
5744
|
+
blocked: true,
|
|
5507
5745
|
agentId: 'reviewer',
|
|
5508
|
-
|
|
5746
|
+
writeRiskTier: tool?.annotations?.writeRiskTier || null,
|
|
5747
|
+
metadata: {
|
|
5748
|
+
denied: true,
|
|
5749
|
+
deniedReason: 'reviewer_read_only',
|
|
5750
|
+
writeRiskTier: tool?.annotations?.writeRiskTier || null,
|
|
5751
|
+
},
|
|
5509
5752
|
});
|
|
5510
5753
|
sendJson(res, 200, {
|
|
5511
5754
|
jsonrpc: '2.0', id: msg.id,
|
|
@@ -5517,18 +5760,25 @@ function createApiServer() {
|
|
|
5517
5760
|
if (oauthSession) {
|
|
5518
5761
|
const name = msg.params && msg.params.name;
|
|
5519
5762
|
const tool = MCP_TOOLS.find((candidate) => candidate.name === name);
|
|
5520
|
-
|
|
5763
|
+
// Hierarchy: mcp:write implies read/gates/feedback (WorkOS MCP Auth style).
|
|
5764
|
+
const requiredScope = mcpOauth.requiredScopeForTool(tool || {});
|
|
5521
5765
|
if (!mcpOauth.scopeAllows(oauthSession, requiredScope)) {
|
|
5522
5766
|
recordToolCall({
|
|
5523
5767
|
toolName: name || 'unknown',
|
|
5524
5768
|
serverName: 'mcp-http',
|
|
5525
5769
|
latencyMs: 0,
|
|
5526
5770
|
success: false,
|
|
5771
|
+
outcome: 'blocked',
|
|
5772
|
+
blocked: true,
|
|
5527
5773
|
agentId: oauthSession.clientId || 'oauth-client',
|
|
5774
|
+
clientId: oauthSession.clientId || null,
|
|
5775
|
+
sessionId: oauthSession.sessionId || oauthSession.id || null,
|
|
5776
|
+
writeRiskTier: tool?.annotations?.writeRiskTier || null,
|
|
5528
5777
|
metadata: {
|
|
5529
5778
|
denied: true,
|
|
5530
5779
|
deniedReason: 'insufficient_scope',
|
|
5531
5780
|
requiredScope,
|
|
5781
|
+
writeRiskTier: tool?.annotations?.writeRiskTier || null,
|
|
5532
5782
|
},
|
|
5533
5783
|
});
|
|
5534
5784
|
sendJson(res, 200, {
|
|
@@ -5543,7 +5793,14 @@ function createApiServer() {
|
|
|
5543
5793
|
const { callTool } = require('../../adapters/mcp/server-stdio');
|
|
5544
5794
|
const name = msg.params?.name;
|
|
5545
5795
|
const args = (msg.params && msg.params.arguments) || {};
|
|
5546
|
-
|
|
5796
|
+
// Attribution rides as callTool options (not tool args) so KPI
|
|
5797
|
+
// can bind human OAuth client/session without polluting schemas.
|
|
5798
|
+
const result = await callTool(name, args, {
|
|
5799
|
+
clientId: oauthSession?.clientId || null,
|
|
5800
|
+
sessionId: oauthSession?.sessionId || oauthSession?.id || null,
|
|
5801
|
+
agentId: oauthSession?.clientId || (isReviewer ? 'reviewer' : 'mcp-http'),
|
|
5802
|
+
serverName: 'mcp-http',
|
|
5803
|
+
});
|
|
5547
5804
|
sendJson(res, 200, {
|
|
5548
5805
|
jsonrpc: '2.0', id: msg.id,
|
|
5549
5806
|
result,
|
|
@@ -5626,6 +5883,27 @@ function createApiServer() {
|
|
|
5626
5883
|
return;
|
|
5627
5884
|
}
|
|
5628
5885
|
|
|
5886
|
+
// WebMCP read-only agent tools for the product pages. Absent from the
|
|
5887
|
+
// packaged npm runtime by design (tarball exclusion) — the route then
|
|
5888
|
+
// 404s and the deferred script tag degrades to a no-op.
|
|
5889
|
+
if (isGetLikeRequest && pathname === '/js/webmcp.js') {
|
|
5890
|
+
try {
|
|
5891
|
+
const script = fs.readFileSync(WEBMCP_SCRIPT_PATH, 'utf-8');
|
|
5892
|
+
res.writeHead(200, {
|
|
5893
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
5894
|
+
'Cache-Control': 'public, max-age=86400',
|
|
5895
|
+
});
|
|
5896
|
+
if (!isHeadRequest) {
|
|
5897
|
+
res.end(script);
|
|
5898
|
+
} else {
|
|
5899
|
+
res.end();
|
|
5900
|
+
}
|
|
5901
|
+
} catch {
|
|
5902
|
+
sendJson(res, 404, { error: 'WebMCP script not found' });
|
|
5903
|
+
}
|
|
5904
|
+
return;
|
|
5905
|
+
}
|
|
5906
|
+
|
|
5629
5907
|
|
|
5630
5908
|
// User feedback → GitHub Issues
|
|
5631
5909
|
if (req.method === 'POST' && pathname === '/api/feedback/submit') {
|
|
@@ -6393,6 +6671,57 @@ async function addContext(){
|
|
|
6393
6671
|
return;
|
|
6394
6672
|
}
|
|
6395
6673
|
|
|
6674
|
+
|
|
6675
|
+
// Partner co-branded case study (Peter Yang / high-ROI acquisition landing).
|
|
6676
|
+
if (isGetLikeRequest && (pathname === '/peter' || pathname === '/peter.html')) {
|
|
6677
|
+
try {
|
|
6678
|
+
servePublicMarketingPage({
|
|
6679
|
+
req,
|
|
6680
|
+
res,
|
|
6681
|
+
parsed,
|
|
6682
|
+
hostedConfig,
|
|
6683
|
+
isHeadRequest,
|
|
6684
|
+
renderHtml: loadPeterPageHtml,
|
|
6685
|
+
extraTelemetry: {
|
|
6686
|
+
pageType: 'partner_case_study',
|
|
6687
|
+
partner: 'peter-yang',
|
|
6688
|
+
},
|
|
6689
|
+
});
|
|
6690
|
+
} catch (err) {
|
|
6691
|
+
sendText(res, 500, err.message || 'Partner case study page unavailable');
|
|
6692
|
+
}
|
|
6693
|
+
return;
|
|
6694
|
+
}
|
|
6695
|
+
|
|
6696
|
+
// Oceans-style founder conversion landing (high-ROI cash path).
|
|
6697
|
+
// Mirrors personal operator pages (/peter): pain triad, timed process,
|
|
6698
|
+
// comparison table, refund fence, sticky diagnostic checkout.
|
|
6699
|
+
if (isGetLikeRequest && (
|
|
6700
|
+
pathname === '/founders'
|
|
6701
|
+
|| pathname === '/founders.html'
|
|
6702
|
+
|| pathname === '/operators'
|
|
6703
|
+
|| pathname === '/operators.html'
|
|
6704
|
+
|| pathname === '/agent-operators'
|
|
6705
|
+
|| pathname === '/agent-operators.html'
|
|
6706
|
+
)) {
|
|
6707
|
+
try {
|
|
6708
|
+
servePublicMarketingPage({
|
|
6709
|
+
req,
|
|
6710
|
+
res,
|
|
6711
|
+
parsed,
|
|
6712
|
+
hostedConfig,
|
|
6713
|
+
isHeadRequest,
|
|
6714
|
+
renderHtml: loadFoundersPageHtml,
|
|
6715
|
+
extraTelemetry: {
|
|
6716
|
+
pageType: 'founders',
|
|
6717
|
+
},
|
|
6718
|
+
});
|
|
6719
|
+
} catch (err) {
|
|
6720
|
+
sendText(res, 500, err.message || 'Founders page unavailable');
|
|
6721
|
+
}
|
|
6722
|
+
return;
|
|
6723
|
+
}
|
|
6724
|
+
|
|
6396
6725
|
// Natural marketing URLs for the Workflow Hardening Diagnostic/Sprint.
|
|
6397
6726
|
// High-intent outreach refers to "the diagnostic", "the sprint", or
|
|
6398
6727
|
// "workflow hardening"; serve a focused buyer page instead of sending
|
|
@@ -6539,6 +6868,35 @@ async function addContext(){
|
|
|
6539
6868
|
return;
|
|
6540
6869
|
}
|
|
6541
6870
|
|
|
6871
|
+
if (isGetLikeRequest && (
|
|
6872
|
+
pathname === '/yt'
|
|
6873
|
+
|| pathname === '/yt.html'
|
|
6874
|
+
|| pathname === '/aias-registration-yt'
|
|
6875
|
+
)) {
|
|
6876
|
+
// Dedicated YouTube/CPC destination (FORMAT steal from AIAS registration
|
|
6877
|
+
// landings). /aias-registration-yt is an alias so ad paths that copy the
|
|
6878
|
+
// summit slug still hit ThumbGate. Canonical is /yt. Free GitHub/npm/
|
|
6879
|
+
// Marketplace CTAs only — not a $499 hero, not a 6-agent OS SKU.
|
|
6880
|
+
try {
|
|
6881
|
+
servePublicMarketingPage({
|
|
6882
|
+
req,
|
|
6883
|
+
res,
|
|
6884
|
+
parsed,
|
|
6885
|
+
hostedConfig,
|
|
6886
|
+
isHeadRequest,
|
|
6887
|
+
renderHtml: (runtimeConfig, pageContext) => loadPublicMarketingTemplateHtml(
|
|
6888
|
+
YT_PAGE_PATH,
|
|
6889
|
+
runtimeConfig,
|
|
6890
|
+
pageContext
|
|
6891
|
+
),
|
|
6892
|
+
extraTelemetry: { pageType: 'yt' },
|
|
6893
|
+
});
|
|
6894
|
+
} catch {
|
|
6895
|
+
sendJson(res, 404, { error: 'YouTube landing page not found' });
|
|
6896
|
+
}
|
|
6897
|
+
return;
|
|
6898
|
+
}
|
|
6899
|
+
|
|
6542
6900
|
if (isGetLikeRequest && (pathname === '/agent-manager' || pathname === '/agent-manager.html')) {
|
|
6543
6901
|
// ICP landing page for the role Anthropic named (Agent Manager —
|
|
6544
6902
|
// hybrid PM/engineer DRI who owns CLAUDE.md hierarchy, plugin
|
|
@@ -8363,35 +8721,168 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8363
8721
|
// Public terms of service — required by Stripe / Stripe Checkout for the
|
|
8364
8722
|
// "Terms of service URL" field in Business → Public details. Mirrors the
|
|
8365
8723
|
// /privacy + /support pages: thin HTML, no external deps, no DB hit.
|
|
8366
|
-
if (isGetLikeRequest && pathname === '/terms') {
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
<h2>Payment</h2>
|
|
8374
|
-
<p>Paid tiers are billed through Stripe. Subscriptions auto-renew until cancelled. One-off purchases (Sprint Diagnostic, Workflow Sprint, Quick Read, Workflow Teardown, First Failure Rule) are charged once.</p>
|
|
8375
|
-
<h2>Refunds</h2>
|
|
8376
|
-
<p>Pro and Team subscriptions: cancel anytime; we issue a full refund within 7 days of the first charge, prorated thereafter. One-off purchases: refund on request if we cannot deliver the scoped artifact.</p>
|
|
8377
|
-
<h2>Acceptable Use</h2>
|
|
8378
|
-
<p>You may not use ThumbGate to (a) circumvent the safety controls of other AI providers, (b) generate malware or content that violates third-party terms, or (c) resell the hosted tier without written permission.</p>
|
|
8379
|
-
<h2>Disclaimer of Warranty</h2>
|
|
8380
|
-
<p>The service is provided "as is", without warranty of any kind. ThumbGate is a guard rail, not a guarantee. We do not warrant that every AI-agent mistake will be prevented.</p>
|
|
8381
|
-
<h2>Limitation of Liability</h2>
|
|
8382
|
-
<p>Total liability for any claim is limited to the amount you paid in the 12 months preceding the claim.</p>
|
|
8383
|
-
<h2>Governing Law</h2>
|
|
8384
|
-
<p>These terms are governed by the laws of the State of New York, United States.</p>
|
|
8385
|
-
<h2>Changes</h2>
|
|
8386
|
-
<p>We may update these terms; material changes will be announced via the email on file at least 14 days before they take effect.</p>
|
|
8387
|
-
<h2>Contact</h2><p>igor.ganapolsky@gmail.com</p>
|
|
8388
|
-
<p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> · <a href="/privacy">Privacy</a> · <a href="/support">Support</a></p>
|
|
8389
|
-
</body></html>`, {}, {
|
|
8390
|
-
headOnly: isHeadRequest,
|
|
8391
|
-
});
|
|
8724
|
+
if (isGetLikeRequest && (pathname === '/terms' || pathname === '/terms.html' || pathname === '/eula' || pathname === '/eula.html')) {
|
|
8725
|
+
try {
|
|
8726
|
+
const termsHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'terms.html'), 'utf-8');
|
|
8727
|
+
sendHtml(res, 200, termsHtml, {}, { headOnly: isHeadRequest });
|
|
8728
|
+
} catch {
|
|
8729
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Terms of Service — ThumbGate</title></head><body><h1>Terms of Service</h1><p>Contact: <a href="mailto:legal@thumbgate.ai">legal@thumbgate.ai</a></p></body></html>`, {}, { headOnly: isHeadRequest });
|
|
8730
|
+
}
|
|
8392
8731
|
return;
|
|
8393
8732
|
}
|
|
8394
8733
|
|
|
8734
|
+
if (isGetLikeRequest && (pathname === '/privacy' || pathname === '/privacy.html')) {
|
|
8735
|
+
try {
|
|
8736
|
+
const privacyHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'privacy.html'), 'utf-8');
|
|
8737
|
+
sendHtml(res, 200, privacyHtml, {}, { headOnly: isHeadRequest });
|
|
8738
|
+
} catch {
|
|
8739
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Privacy Policy — ThumbGate</title></head><body><h1>Privacy Policy</h1><p>Contact: <a href="mailto:privacy@thumbgate.ai">privacy@thumbgate.ai</a></p></body></html>`, {}, { headOnly: isHeadRequest });
|
|
8740
|
+
}
|
|
8741
|
+
return;
|
|
8742
|
+
}
|
|
8743
|
+
|
|
8744
|
+
if (isGetLikeRequest && (pathname === '/support' || pathname === '/support.html')) {
|
|
8745
|
+
try {
|
|
8746
|
+
const supportHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'support.html'), 'utf-8');
|
|
8747
|
+
sendHtml(res, 200, supportHtml, {}, { headOnly: isHeadRequest });
|
|
8748
|
+
} catch {
|
|
8749
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Support — ThumbGate</title></head><body><h1>Support</h1><p>Contact: <a href="mailto:support@thumbgate.ai">support@thumbgate.ai</a></p></body></html>`, {}, { headOnly: isHeadRequest });
|
|
8750
|
+
}
|
|
8751
|
+
return;
|
|
8752
|
+
}
|
|
8753
|
+
|
|
8754
|
+
if (isGetLikeRequest && (pathname === '/third-party-notices' || pathname === '/third-party-notices.html' || pathname === '/third-party-notices.md')) {
|
|
8755
|
+
try {
|
|
8756
|
+
const noticesHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'third-party-notices.html'), 'utf-8');
|
|
8757
|
+
sendHtml(res, 200, noticesHtml, {}, { headOnly: isHeadRequest });
|
|
8758
|
+
} catch {
|
|
8759
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Third-Party Notices — ThumbGate</title></head><body><h1>Third-Party Notices</h1><p>See <a href="https://github.com/IgorGanapolsky/ThumbGate/blob/main/THIRD_PARTY_NOTICES.md">THIRD_PARTY_NOTICES.md</a>.</p></body></html>`, {}, { headOnly: isHeadRequest });
|
|
8760
|
+
}
|
|
8761
|
+
return;
|
|
8762
|
+
}
|
|
8763
|
+
|
|
8764
|
+
// Public security overview + copy-paste questionnaire (not a certification).
|
|
8765
|
+
if (isGetLikeRequest && (pathname === '/security.json' || pathname === '/v1/security-questionnaire')) {
|
|
8766
|
+
sendJson(res, 200, getSecurityQuestionnaire(), {
|
|
8767
|
+
'Cache-Control': 'public, max-age=300',
|
|
8768
|
+
}, { headOnly: isHeadRequest });
|
|
8769
|
+
return;
|
|
8770
|
+
}
|
|
8771
|
+
if (isGetLikeRequest && (
|
|
8772
|
+
pathname === '/security'
|
|
8773
|
+
|| pathname === '/security.html'
|
|
8774
|
+
|| pathname === '/trust'
|
|
8775
|
+
|| pathname === '/trust.html'
|
|
8776
|
+
)) {
|
|
8777
|
+
sendHtml(res, 200, renderSecurityOverviewHtml(), {}, { headOnly: isHeadRequest });
|
|
8778
|
+
return;
|
|
8779
|
+
}
|
|
8780
|
+
|
|
8781
|
+
// Legal index — entry point for counsel package + public legal URLs.
|
|
8782
|
+
if (isGetLikeRequest && (pathname === '/legal' || pathname === '/legal/')) {
|
|
8783
|
+
sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8784
|
+
<title>Legal — ThumbGate</title>
|
|
8785
|
+
<meta name="description" content="ThumbGate legal index: Terms, Privacy, Security, licensing, data-flow, MSA/SOW.">
|
|
8786
|
+
<style>body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}
|
|
8787
|
+
h1{font-size:28px;margin:0 0 8px}h2{font-size:18px;margin:28px 0 8px}p,li{font-size:15px}.meta{color:#6b7280;font-size:14px;margin:0 0 24px}
|
|
8788
|
+
.draft{border-left:3px solid #f59e0b;background:#fffbeb;padding:10px 14px;border-radius:0 8px 8px 0;margin:16px 0}
|
|
8789
|
+
ul{padding-left:22px}li{margin:6px 0}a{color:#0066cc}
|
|
8790
|
+
footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
|
|
8791
|
+
<h1>Legal</h1>
|
|
8792
|
+
<p class="meta">Public legal surfaces for ThumbGate paid and hosted paths.</p>
|
|
8793
|
+
<div class="draft"><strong>Highest-value first pass:</strong> short data-flow map + subscription terms + privacy policy + open-source boundary. Markdown counsel package lives in <code>docs/legal/</code>.</div>
|
|
8794
|
+
<ul>
|
|
8795
|
+
<li><a href="/terms">Terms of Service</a></li>
|
|
8796
|
+
<li><a href="/privacy">Privacy Policy</a></li>
|
|
8797
|
+
<li><a href="/support">Support</a> — <a href="mailto:support@thumbgate.ai">support@thumbgate.ai</a></li>
|
|
8798
|
+
<li><a href="/security">Security questionnaire</a> — <a href="/security.json">JSON</a></li>
|
|
8799
|
+
<li><a href="/legal/licensing">MIT vs paid licensing boundary</a></li>
|
|
8800
|
+
<li><a href="/legal/data-flow">Data-flow map</a></li>
|
|
8801
|
+
<li><a href="/legal/msa-sow">MSA / SOW template</a></li>
|
|
8802
|
+
<li><a href="https://github.com/IgorGanapolsky/ThumbGate/tree/main/docs/legal">docs/legal on GitHub</a></li>
|
|
8803
|
+
<li><a href="/third-party-notices">Third-party notices</a></li>
|
|
8804
|
+
</ul>
|
|
8805
|
+
<footer><a href="/">Home</a> · <a href="/pricing">Pricing</a> · <a href="mailto:legal@thumbgate.ai">legal@thumbgate.ai</a></footer>
|
|
8806
|
+
</body></html>`, {}, { headOnly: isHeadRequest });
|
|
8807
|
+
return;
|
|
8808
|
+
}
|
|
8809
|
+
|
|
8810
|
+
if (isGetLikeRequest && (pathname === '/legal/licensing' || pathname === '/legal/licensing/')) {
|
|
8811
|
+
sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8812
|
+
<title>Licensing — ThumbGate</title>
|
|
8813
|
+
<style>body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}
|
|
8814
|
+
h1{font-size:28px;margin:0 0 8px}h2{font-size:18px;margin:28px 0 8px}p,li{font-size:15px}ul{padding-left:22px}a{color:#0066cc}
|
|
8815
|
+
footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
|
|
8816
|
+
<h1>Commercial licensing boundary</h1>
|
|
8817
|
+
<p class="meta">MIT vs paid · Last updated 2026-08-12</p>
|
|
8818
|
+
<h2>What is MIT</h2>
|
|
8819
|
+
<p>Unless a file states otherwise, the public repository and published npm package <code>thumbgate</code> are offered under the <strong>MIT</strong> License: local CLI, hooks, public MCP tooling, docs, and tests shipped in that package.</p>
|
|
8820
|
+
<h2>What is not MIT</h2>
|
|
8821
|
+
<ul>
|
|
8822
|
+
<li>Hosted API / production cloud service — proprietary hosted service</li>
|
|
8823
|
+
<li>thumbgate.app pairing, mobile, and cloud runners as offered — proprietary product</li>
|
|
8824
|
+
<li><strong>Pro</strong> subscription entitlement, license keys, hosted dashboard state — subscription license, not transfer of ThumbGate IP</li>
|
|
8825
|
+
<li>Brand and marks — trademark, not MIT code rights to imply endorsement</li>
|
|
8826
|
+
<li>Managed services under an SOW — governed by MSA/SOW</li>
|
|
8827
|
+
</ul>
|
|
8828
|
+
<h2>Customer-specific rules</h2>
|
|
8829
|
+
<p><strong>Customer-specific rules</strong> and configs produced under a paid engagement are owned by the customer as deliverables under the SOW, except that ThumbGate retains pre-existing IP and may reuse generalized non-confidential know-how. <strong>Production approvals</strong> remain the customer’s responsibility; ThumbGate does not assume production change-control ownership by default.</p>
|
|
8830
|
+
<p>Questions: <a href="mailto:legal@thumbgate.ai">legal@thumbgate.ai</a>. Full text: <code>docs/legal/COMMERCIAL_LICENSING_BOUNDARY.md</code>.</p>
|
|
8831
|
+
<footer><a href="/legal">Legal index</a> · <a href="/terms">Terms</a></footer>
|
|
8832
|
+
</body></html>`, {}, { headOnly: isHeadRequest });
|
|
8833
|
+
return;
|
|
8834
|
+
}
|
|
8835
|
+
|
|
8836
|
+
if (isGetLikeRequest && (pathname === '/legal/data-flow' || pathname === '/legal/data-flow/')) {
|
|
8837
|
+
sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8838
|
+
<title>Data-flow map — ThumbGate</title>
|
|
8839
|
+
<style>body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}
|
|
8840
|
+
h1{font-size:28px;margin:0 0 8px}h2{font-size:18px;margin:28px 0 8px}p,li{font-size:15px}ul{padding-left:22px}a{color:#0066cc}
|
|
8841
|
+
footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
|
|
8842
|
+
<h1>Data-flow map</h1>
|
|
8843
|
+
<p>First-pass counsel input: what stays local vs what may leave the machine.</p>
|
|
8844
|
+
<h2>Local MIT CLI</h2>
|
|
8845
|
+
<p><strong>Local MIT CLI</strong> and engine: workspace source, local prevention rules, context packs, and lesson DB stay on disk under paths such as <code>~/.thumbgate/</code> and project <code>.claude/</code>. Workspace source does not leave the machine by default.</p>
|
|
8846
|
+
<h2>What may leave the machine</h2>
|
|
8847
|
+
<ul>
|
|
8848
|
+
<li>Account registration and license checks for Pro / hosted features</li>
|
|
8849
|
+
<li><strong>Stripe</strong> payment tokens and billing metadata when you check out</li>
|
|
8850
|
+
<li>Device pairing identifiers for thumbgate.app</li>
|
|
8851
|
+
<li>Cloud-runner operational logs (timestamps, success/fail, error summaries)</li>
|
|
8852
|
+
<li>Optional CLI telemetry (disable with THUMBGATE_NO_TELEMETRY=1)</li>
|
|
8853
|
+
<li>Support email content you voluntarily send</li>
|
|
8854
|
+
</ul>
|
|
8855
|
+
<p>Full map: <code>docs/legal/DATA_FLOW_MAP.md</code> · <a href="/privacy">Privacy Policy</a></p>
|
|
8856
|
+
<footer><a href="/legal">Legal index</a> · <a href="/privacy">Privacy</a></footer>
|
|
8857
|
+
</body></html>`, {}, { headOnly: isHeadRequest });
|
|
8858
|
+
return;
|
|
8859
|
+
}
|
|
8860
|
+
|
|
8861
|
+
if (isGetLikeRequest && (pathname === '/legal/msa-sow' || pathname === '/legal/msa-sow/')) {
|
|
8862
|
+
sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8863
|
+
<title>MSA / SOW — ThumbGate</title>
|
|
8864
|
+
<style>body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}
|
|
8865
|
+
h1{font-size:28px;margin:0 0 8px}h2{font-size:18px;margin:28px 0 8px}p,li{font-size:15px}ul{padding-left:22px}a{color:#0066cc}
|
|
8866
|
+
footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
|
|
8867
|
+
<h1>Master Services Agreement + Statement of Work</h1>
|
|
8868
|
+
<p>Template for counsel/product use. Not a signed contract. Self-serve Pro is covered by <a href="/terms">Terms</a> + checkout, not this SOW.</p>
|
|
8869
|
+
<h2>Master Services Agreement (summary)</h2>
|
|
8870
|
+
<ul>
|
|
8871
|
+
<li>Services only as described in an executed Statement of Work</li>
|
|
8872
|
+
<li>Fees and taxes as in the SOW; payment via Stripe or invoice</li>
|
|
8873
|
+
<li>Pre-existing ThumbGate IP remains ThumbGate’s; customer owns customer-specific configs under the SOW</li>
|
|
8874
|
+
<li>No guarantee all unsafe agent actions will be blocked</li>
|
|
8875
|
+
<li>Liability capped at fees paid under the applicable SOW in the prior 12 months</li>
|
|
8876
|
+
</ul>
|
|
8877
|
+
<h2>Statement of Work fields</h2>
|
|
8878
|
+
<p>Typical SOW fields: title, supported workflow, access/materials, acceptance criteria, deliverables, exclusions, change requests, approvals, fees, timeline, termination.</p>
|
|
8879
|
+
<p>Contact: <a href="mailto:legal@thumbgate.ai">legal@thumbgate.ai</a>. Full template: <code>docs/legal/MSA_SOW_TEMPLATE.md</code>.</p>
|
|
8880
|
+
<footer><a href="/legal">Legal index</a> · <a href="/terms">Terms</a></footer>
|
|
8881
|
+
</body></html>`, {}, { headOnly: isHeadRequest });
|
|
8882
|
+
return;
|
|
8883
|
+
}
|
|
8884
|
+
|
|
8885
|
+
|
|
8395
8886
|
// Public case studies — dogfood proof narratives (static HTML; no fabricated logos).
|
|
8396
8887
|
if (isGetLikeRequest && (pathname === '/case-studies' || pathname === '/case-studies.html')) {
|
|
8397
8888
|
try {
|
|
@@ -8410,14 +8901,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8410
8901
|
return;
|
|
8411
8902
|
}
|
|
8412
8903
|
|
|
8413
|
-
|
|
8414
|
-
// Public canonical pricing page. The audit flagged "pricing schizophrenia":
|
|
8415
|
-
// sales/pricing.json said $49 / $299, COMMERCIAL_TRUTH.md said $19 / $149,
|
|
8416
|
-
// and there was no buyer-facing surface to reconcile the two. This is now
|
|
8417
|
-
// the single source of truth for what ThumbGate sells, in priority order:
|
|
8418
|
-
// Sprint (proof-pack, sales-led) → Pro (self-serve recurring) → Team
|
|
8419
|
-
// (after qualification). Every paid CTA across the site should funnel
|
|
8420
|
-
// here OR directly into Stripe checkout — never a different price.
|
|
8904
|
+
// Public canonical pricing page.
|
|
8421
8905
|
if (isGetLikeRequest && (pathname === '/pricing' || pathname === '/pricing.html')) {
|
|
8422
8906
|
try {
|
|
8423
8907
|
servePublicMarketingPage({
|
|
@@ -8437,100 +8921,14 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8437
8921
|
return;
|
|
8438
8922
|
}
|
|
8439
8923
|
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
sendHtml(res, 200, `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Remote MCP Connector — ThumbGate</title><meta name="description" content="Connect ThumbGate to Claude as a remote MCP server: OAuth 2.1 (PKCE) authorization, available tools, and the read-only reviewer credential."><style>body{font-family:system-ui,-apple-system,sans-serif;max-width:780px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}h1{font-size:30px;margin:0 0 8px}.lede{color:#6b7280;font-size:18px;margin:0 0 28px}h2{font-size:20px;margin:28px 0 8px}code{background:#f3f4f6;padding:1px 6px;border-radius:4px;font-size:13.5px}pre{background:#0f172a;color:#e2e8f0;padding:14px 16px;border-radius:8px;overflow:auto;font-size:13px}a{color:#0066cc}ol,ul{padding-left:22px}li{margin:6px 0}.note{border-left:3px solid #22d3ee;background:#ecfeff;padding:10px 14px;border-radius:0 8px 8px 0;margin:16px 0}footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
|
|
8449
|
-
<h1>ThumbGate — Remote MCP Connector</h1>
|
|
8450
|
-
<p class="lede">ThumbGate is a remote Model Context Protocol (MCP) server. Add it as a connector in Claude to give an agent governed access to ThumbGate's feedback capture, lesson retrieval, context assembly, and pre-action gate tools — over HTTP, authenticated with OAuth 2.1.</p>
|
|
8451
|
-
|
|
8452
|
-
<h2>Connect URL</h2>
|
|
8453
|
-
<pre>${esc(mcpUrl)}</pre>
|
|
8454
|
-
<p>In Claude, add a custom connector and paste the URL above. Claude discovers the authorization server automatically via the protected-resource metadata at <a href="${esc(prmUrl)}">${esc(prmUrl)}</a>.</p>
|
|
8455
|
-
|
|
8456
|
-
<h2>Authorization (OAuth 2.1 + PKCE)</h2>
|
|
8457
|
-
<ol>
|
|
8458
|
-
<li>Claude reads the <code>resource</code> and <code>authorization_servers</code> from the protected-resource metadata.</li>
|
|
8459
|
-
<li>It runs the standard OAuth 2.1 authorization-code flow with PKCE (<code>S256</code> only — <code>plain</code> is rejected).</li>
|
|
8460
|
-
<li>The issued access token is audience-bound to the <code>${esc(mcpUrl)}</code> resource (RFC 8707); a token for any other audience is rejected.</li>
|
|
8461
|
-
<li>The bearer token is then sent on the <code>Authorization</code> header for every <code>tools/call</code>.</li>
|
|
8462
|
-
</ol>
|
|
8463
|
-
|
|
8464
|
-
<h2>Available tools</h2>
|
|
8465
|
-
<p>The full, machine-readable tool registry — names, input schemas, and the <code>readOnlyHint</code> / <code>destructiveHint</code> annotations — is published at <a href="${esc(cardUrl)}">${esc(cardUrl)}</a>. Tools fall into these groups:</p>
|
|
8466
|
-
<ul>
|
|
8467
|
-
<li><strong>Feedback & lessons</strong> — <code>capture_feedback</code>, <code>feedback_summary</code>, <code>search_lessons</code>, <code>retrieve_lessons</code>, <code>prevention_rules</code>.</li>
|
|
8468
|
-
<li><strong>Context engineering</strong> — <code>construct_context_pack</code>, <code>evaluate_context_pack</code>, <code>unified_context</code>, <code>recall</code>.</li>
|
|
8469
|
-
<li><strong>Pre-action gates & governance</strong> — <code>satisfy_gate</code>, <code>track_action</code>, <code>approve_protected_action</code>, <code>verify_claim</code>, <code>enforcement_matrix</code>.</li>
|
|
8470
|
-
<li><strong>Diagnostics & planning</strong> — <code>diagnose_failure</code>, <code>suggest_fix</code>, <code>security_scan</code>, and the <code>plan_*</code> advisory tools.</li>
|
|
8471
|
-
</ul>
|
|
8472
|
-
|
|
8473
|
-
<h2>Reviewer credential (read-only)</h2>
|
|
8474
|
-
<div class="note">For directory reviewers: ThumbGate issues a dedicated <strong>read-only</strong> reviewer credential. A token bound to that credential may invoke only tools annotated <code>readOnlyHint: true</code>; any write or mutating tool call is rejected. This makes the credential safe to share for review without granting the ability to mutate shared server state. Request it from the contact below.</div>
|
|
8475
|
-
|
|
8476
|
-
<h2>Source & contact</h2>
|
|
8477
|
-
<p>Open-source CLI and server: <a href="https://github.com/IgorGanapolsky/ThumbGate">github.com/IgorGanapolsky/ThumbGate</a>. Questions or reviewer-credential requests: <a href="mailto:igor.ganapolsky@gmail.com">igor.ganapolsky@gmail.com</a>.</p>
|
|
8478
|
-
|
|
8479
|
-
<footer><a href="/">ThumbGate</a> · <a href="/support">Support</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></footer>
|
|
8480
|
-
</body></html>`, {}, { headOnly: isHeadRequest });
|
|
8481
|
-
return;
|
|
8482
|
-
}
|
|
8483
|
-
|
|
8484
|
-
// Public support / contact page — required for Stripe Business → Public
|
|
8485
|
-
// details "Customer support URL" field. Single source of truth for how
|
|
8486
|
-
// customers reach us (email, GitHub issues, status page).
|
|
8487
|
-
if (isGetLikeRequest && pathname === '/support') {
|
|
8488
|
-
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Support — ThumbGate</title></head><body>
|
|
8489
|
-
<h1>Support</h1>
|
|
8490
|
-
<p><strong>ThumbGate</strong> support, billing, and contact paths.</p>
|
|
8491
|
-
<h2>Email</h2>
|
|
8492
|
-
<p>For billing questions, refunds, subscription changes, or technical issues with the hosted tier: <a href="mailto:igor.ganapolsky@gmail.com">igor.ganapolsky@gmail.com</a>. We reply within one business day.</p>
|
|
8493
|
-
<h2>GitHub Issues</h2>
|
|
8494
|
-
<p>For bugs, CLI questions, and feature requests in the open-source CLI: <a href="https://github.com/IgorGanapolsky/ThumbGate/issues">github.com/IgorGanapolsky/ThumbGate/issues</a>.</p>
|
|
8495
|
-
<h2>Status</h2>
|
|
8496
|
-
<p>Hosted-tier status: check <a href="https://thumbgate-production.up.railway.app/health">/health</a> for current health. Railway-hosted; rebuilds take 2-5 minutes.</p>
|
|
8497
|
-
<h2>Refunds</h2>
|
|
8498
|
-
<p>Pro / Team subscriptions: 7-day full refund window from first charge. One-off purchases: refund on request if we cannot deliver. Email the address above.</p>
|
|
8499
|
-
<h2>Security</h2>
|
|
8500
|
-
<p>Disclose vulnerabilities by email; please do not post to public GitHub issues. We acknowledge within 48 hours.</p>
|
|
8501
|
-
<p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></p>
|
|
8502
|
-
</body></html>`, {}, {
|
|
8503
|
-
headOnly: isHeadRequest,
|
|
8504
|
-
});
|
|
8505
|
-
return;
|
|
8506
|
-
}
|
|
8507
|
-
|
|
8508
|
-
// Public privacy policy — required for GPT Store and marketplace listings
|
|
8509
|
-
if (isGetLikeRequest && pathname === '/privacy') {
|
|
8510
|
-
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Privacy Policy — ThumbGate</title></head><body>
|
|
8511
|
-
<h1>Privacy Policy</h1>
|
|
8512
|
-
<p><strong>ThumbGate</strong> (npm: thumbgate)</p>
|
|
8513
|
-
<p>Last updated: 2026-03-11</p>
|
|
8514
|
-
<h2>Data Collection</h2>
|
|
8515
|
-
<p>The self-hosted version stores workflow data locally on your machine. Local feedback, memory entries, proof artifacts, and context packs stay in your project files unless you explicitly point the system at a hosted endpoint.</p>
|
|
8516
|
-
<p>The hosted tier (thumbgate-production.up.railway.app) stores feedback signals, memory entries, and related workflow metadata associated with your API key.</p>
|
|
8517
|
-
<p>Optional CLI telemetry is best-effort and covers install or usage metadata needed to understand adoption and failures. You can disable it with <code>THUMBGATE_NO_TELEMETRY=1</code>.</p>
|
|
8518
|
-
<h2>Data Stored</h2><ul>
|
|
8519
|
-
<li>Feedback signals (thumbs up/down) with context you provide</li>
|
|
8520
|
-
<li>Promoted memory entries</li>
|
|
8521
|
-
<li>Prevention rules generated from your feedback</li>
|
|
8522
|
-
</ul>
|
|
8523
|
-
<h2>Data Sharing</h2>
|
|
8524
|
-
<p>We do not sell customer data. Hosted data is used to operate the service and is not shared with third parties except for infrastructure providers needed to run the product.</p>
|
|
8525
|
-
<h2>Data Retention</h2>
|
|
8526
|
-
<p>Local data is retained until you delete the files. Hosted data is retained while your account or API key remains active, or until you request deletion, subject to operational or legal retention requirements.</p>
|
|
8527
|
-
<h2>Data Deletion</h2>
|
|
8528
|
-
<p>Contact igor.ganapolsky@gmail.com to request deletion of hosted data.</p>
|
|
8529
|
-
<h2>Contact</h2><p>igor.ganapolsky@gmail.com</p>
|
|
8530
|
-
<p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a></p>
|
|
8531
|
-
</body></html>`, {}, {
|
|
8532
|
-
headOnly: isHeadRequest,
|
|
8533
|
-
});
|
|
8924
|
+
// Public Remote MCP Connector Documentation.
|
|
8925
|
+
if (isGetLikeRequest && (pathname === '/docs/connectors' || pathname === '/docs/connectors/' || pathname === '/docs/connectors.html')) {
|
|
8926
|
+
try {
|
|
8927
|
+
const connectorsHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'docs', 'connectors.html'), 'utf-8');
|
|
8928
|
+
sendHtml(res, 200, connectorsHtml, {}, { headOnly: isHeadRequest });
|
|
8929
|
+
} catch {
|
|
8930
|
+
sendJson(res, 404, { error: 'Connectors documentation not found' });
|
|
8931
|
+
}
|
|
8534
8932
|
return;
|
|
8535
8933
|
}
|
|
8536
8934
|
|
|
@@ -9688,6 +10086,13 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9688
10086
|
guardrails: body.guardrails,
|
|
9689
10087
|
tags: extractTags(body.tags),
|
|
9690
10088
|
skill: body.skill,
|
|
10089
|
+
memorySource: body.memorySource && typeof body.memorySource === 'object'
|
|
10090
|
+
? {
|
|
10091
|
+
type: body.memorySource.type,
|
|
10092
|
+
identifier: body.memorySource.identifier,
|
|
10093
|
+
trust: body.memorySource.trust,
|
|
10094
|
+
}
|
|
10095
|
+
: undefined,
|
|
9691
10096
|
reviewOrigin: 'human',
|
|
9692
10097
|
});
|
|
9693
10098
|
const actionIntegration = inferActionIntegration(body, req.headers);
|
|
@@ -10026,12 +10431,23 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
10026
10431
|
} catch (err) {
|
|
10027
10432
|
throw createHttpError(400, err.message || 'Invalid namespaces');
|
|
10028
10433
|
}
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10434
|
+
let pack;
|
|
10435
|
+
try {
|
|
10436
|
+
pack = constructContextPack({
|
|
10437
|
+
query: body.query || '',
|
|
10438
|
+
maxItems: Number(body.maxItems || 8),
|
|
10439
|
+
maxChars: Number(body.maxChars || 6000),
|
|
10440
|
+
namespaces,
|
|
10441
|
+
strategy: body.strategy || null,
|
|
10442
|
+
contextEnvelope: body.contextEnvelope || null,
|
|
10443
|
+
});
|
|
10444
|
+
} catch (err) {
|
|
10445
|
+
if (err.code === 'INVALID_CONTEXT_ENVELOPE'
|
|
10446
|
+
|| err.code === 'CONTEXT_BUDGET_EXCEEDED') {
|
|
10447
|
+
throw createHttpError(400, err.message);
|
|
10448
|
+
}
|
|
10449
|
+
throw err;
|
|
10450
|
+
}
|
|
10035
10451
|
sendJson(res, 200, pack);
|
|
10036
10452
|
return;
|
|
10037
10453
|
}
|
|
@@ -10744,11 +11160,151 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
10744
11160
|
});
|
|
10745
11161
|
return;
|
|
10746
11162
|
}
|
|
10747
|
-
|
|
11163
|
+
// Attribute the override to the API surface. Without an explicit
|
|
11164
|
+
// source the record defaults to 'cli' and the audit summary cannot
|
|
11165
|
+
// tell an HTTP unlock from a local one.
|
|
11166
|
+
const entry = satisfyCondition(body.gateId, body.evidence, null, {
|
|
11167
|
+
source: 'api',
|
|
11168
|
+
actor: body.actor || 'api-client',
|
|
11169
|
+
});
|
|
10748
11170
|
sendJson(res, 200, { satisfied: true, gateId: body.gateId, ...entry });
|
|
10749
11171
|
return;
|
|
10750
11172
|
}
|
|
10751
11173
|
|
|
11174
|
+
// POST /v1/hermes/initialize — Bind a connectionId before turn start/end
|
|
11175
|
+
if (req.method === 'POST' && pathname === '/v1/hermes/initialize') {
|
|
11176
|
+
const body = await parseJsonBody(req);
|
|
11177
|
+
const { protocol } = getHostedHermes(requestDataIdentity);
|
|
11178
|
+
const result = protocol.initialize({
|
|
11179
|
+
connectionId: body.connectionId,
|
|
11180
|
+
});
|
|
11181
|
+
if (!result.ok) {
|
|
11182
|
+
sendProblem(res, {
|
|
11183
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
11184
|
+
title: 'Hermes Initialize Rejected',
|
|
11185
|
+
status: 400,
|
|
11186
|
+
detail: result.reason,
|
|
11187
|
+
...result,
|
|
11188
|
+
});
|
|
11189
|
+
return;
|
|
11190
|
+
}
|
|
11191
|
+
sendJson(res, 200, result);
|
|
11192
|
+
return;
|
|
11193
|
+
}
|
|
11194
|
+
|
|
11195
|
+
// POST /v1/hermes/turn/start — Start turn under Hermes Platform Protocol
|
|
11196
|
+
// Fail closed until POST /v1/hermes/initialize registers the connectionId.
|
|
11197
|
+
if (req.method === 'POST' && pathname === '/v1/hermes/turn/start') {
|
|
11198
|
+
const body = await parseJsonBody(req);
|
|
11199
|
+
const { protocol } = getHostedHermes(requestDataIdentity);
|
|
11200
|
+
const result = protocol.startTurn(body);
|
|
11201
|
+
if (!result.ok) {
|
|
11202
|
+
sendProblem(res, {
|
|
11203
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
11204
|
+
title: 'Hermes Turn Rejected',
|
|
11205
|
+
status: 400,
|
|
11206
|
+
detail: result.reason,
|
|
11207
|
+
...result,
|
|
11208
|
+
});
|
|
11209
|
+
return;
|
|
11210
|
+
}
|
|
11211
|
+
sendJson(res, 200, result);
|
|
11212
|
+
return;
|
|
11213
|
+
}
|
|
11214
|
+
|
|
11215
|
+
// POST /v1/hermes/turn/end — End active turn under Hermes Platform Protocol
|
|
11216
|
+
if (req.method === 'POST' && pathname === '/v1/hermes/turn/end') {
|
|
11217
|
+
const body = await parseJsonBody(req);
|
|
11218
|
+
const { protocol } = getHostedHermes(requestDataIdentity);
|
|
11219
|
+
const result = protocol.endTurn(body);
|
|
11220
|
+
if (!result.ok) {
|
|
11221
|
+
sendProblem(res, {
|
|
11222
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
11223
|
+
title: 'Hermes End Turn Rejected',
|
|
11224
|
+
status: 400,
|
|
11225
|
+
detail: result.reason,
|
|
11226
|
+
...result,
|
|
11227
|
+
});
|
|
11228
|
+
return;
|
|
11229
|
+
}
|
|
11230
|
+
sendJson(res, 200, result);
|
|
11231
|
+
return;
|
|
11232
|
+
}
|
|
11233
|
+
|
|
11234
|
+
// POST /v1/hermes/action/approve — Record human/operator approval for consequential action
|
|
11235
|
+
if (req.method === 'POST' && pathname === '/v1/hermes/action/approve') {
|
|
11236
|
+
const body = await parseJsonBody(req);
|
|
11237
|
+
const boundApproverId = resolveHermesApproverId(
|
|
11238
|
+
req,
|
|
11239
|
+
humanReviewerConfig,
|
|
11240
|
+
requestDataIdentity,
|
|
11241
|
+
);
|
|
11242
|
+
if (!boundApproverId) {
|
|
11243
|
+
sendProblem(res, {
|
|
11244
|
+
type: PROBLEM_TYPES.UNAUTHORIZED,
|
|
11245
|
+
title: 'Hermes Approval Rejected',
|
|
11246
|
+
status: 401,
|
|
11247
|
+
detail: 'Authenticated reviewer identity is required for Hermes approvals.',
|
|
11248
|
+
});
|
|
11249
|
+
return;
|
|
11250
|
+
}
|
|
11251
|
+
const { protocol } = getHostedHermes(requestDataIdentity);
|
|
11252
|
+
const result = protocol.approve({
|
|
11253
|
+
...body,
|
|
11254
|
+
approverId: boundApproverId,
|
|
11255
|
+
});
|
|
11256
|
+
if (!result.ok) {
|
|
11257
|
+
sendProblem(res, {
|
|
11258
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
11259
|
+
title: 'Hermes Approval Rejected',
|
|
11260
|
+
status: 400,
|
|
11261
|
+
detail: result.reason,
|
|
11262
|
+
...result,
|
|
11263
|
+
});
|
|
11264
|
+
return;
|
|
11265
|
+
}
|
|
11266
|
+
sendJson(res, 200, result);
|
|
11267
|
+
return;
|
|
11268
|
+
}
|
|
11269
|
+
|
|
11270
|
+
// GET /v1/hermes/sync/read — Reactive read path with cursor + offset
|
|
11271
|
+
if (req.method === 'GET' && pathname === '/v1/hermes/sync/read') {
|
|
11272
|
+
const rawOffset = parsed.searchParams.get('offset');
|
|
11273
|
+
const offset = rawOffset !== null ? Number(rawOffset) : undefined;
|
|
11274
|
+
const cursor = parsed.searchParams.get('cursor') || undefined;
|
|
11275
|
+
const threadId = parsed.searchParams.get('threadId') || undefined;
|
|
11276
|
+
const recordId = parsed.searchParams.get('recordId') || undefined;
|
|
11277
|
+
const mode = parsed.searchParams.get('mode') || undefined;
|
|
11278
|
+
// Prefer the authenticated principal over a client-supplied userId so
|
|
11279
|
+
// hosted tenants cannot probe another identity's sync shapes.
|
|
11280
|
+
const authUserId = requestDataIdentity?.accessContext?.principalId
|
|
11281
|
+
|| parsed.searchParams.get('userId')
|
|
11282
|
+
|| undefined;
|
|
11283
|
+
|
|
11284
|
+
const { syncPlane } = getHostedHermes(requestDataIdentity);
|
|
11285
|
+
const result = syncPlane.readStatus({
|
|
11286
|
+
offset,
|
|
11287
|
+
cursor,
|
|
11288
|
+
auth: authUserId ? { userId: authUserId } : null,
|
|
11289
|
+
threadId,
|
|
11290
|
+
recordId,
|
|
11291
|
+
mode,
|
|
11292
|
+
});
|
|
11293
|
+
|
|
11294
|
+
if (!result.ok) {
|
|
11295
|
+
sendProblem(res, {
|
|
11296
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
11297
|
+
title: 'Hermes Sync Read Rejected',
|
|
11298
|
+
status: 400,
|
|
11299
|
+
detail: result.reason,
|
|
11300
|
+
...result,
|
|
11301
|
+
});
|
|
11302
|
+
return;
|
|
11303
|
+
}
|
|
11304
|
+
sendJson(res, 200, result);
|
|
11305
|
+
return;
|
|
11306
|
+
}
|
|
11307
|
+
|
|
10752
11308
|
// GET /api/conversions — Conversion stats derived from the Stripe event log
|
|
10753
11309
|
if (req.method === 'GET' && pathname === '/api/conversions') {
|
|
10754
11310
|
try {
|
|
@@ -10786,7 +11342,20 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
10786
11342
|
function startServer({ port, host } = {}) {
|
|
10787
11343
|
const listenPort = Number(port ?? process.env.PORT ?? 8787);
|
|
10788
11344
|
const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
|
|
10789
|
-
|
|
11345
|
+
const feedbackPaths = getFeedbackPaths();
|
|
11346
|
+
fs.mkdirSync(feedbackPaths.FEEDBACK_DIR, { recursive: true });
|
|
11347
|
+
// Hosted volumes often start empty (Docker does not ship .claude/memory).
|
|
11348
|
+
// Seed a durable "thumbgate" corpus so authenticated deploy proof can verify retrieval.
|
|
11349
|
+
if (shouldEnsureProductionSearchCorpus(process.env)) {
|
|
11350
|
+
try {
|
|
11351
|
+
const seedResult = ensureProductionSearchCorpus({ feedbackDir: feedbackPaths.FEEDBACK_DIR });
|
|
11352
|
+
if (seedResult.wrote.memory || seedResult.wrote.feedback || seedResult.wrote.rules) {
|
|
11353
|
+
console.log('[startup] ensured production search corpus seed', JSON.stringify(seedResult.wrote));
|
|
11354
|
+
}
|
|
11355
|
+
} catch (err) {
|
|
11356
|
+
console.warn('[startup] ensure production search corpus failed:', err?.message || err);
|
|
11357
|
+
}
|
|
11358
|
+
}
|
|
10790
11359
|
const server = createApiServer();
|
|
10791
11360
|
registerGracefulShutdown(server);
|
|
10792
11361
|
return new Promise((resolve) => {
|
|
@@ -10869,6 +11438,8 @@ module.exports = {
|
|
|
10869
11438
|
sanitizeHtmlUnsafeJsonValue,
|
|
10870
11439
|
buildHostedTenantIdentity,
|
|
10871
11440
|
partitionFeedbackPaths,
|
|
11441
|
+
buildLiveDashboardCacheKey,
|
|
11442
|
+
loadCachedLiveDashboardData,
|
|
10872
11443
|
},
|
|
10873
11444
|
};
|
|
10874
11445
|
|