thumbgate 1.34.3 → 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 +373 -5
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +653 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +21 -2
- 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 +10 -1
- package/config/model-candidates.json +382 -24
- 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 +401 -147
- 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/auto-promote-gates.js +178 -27
- 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-progress.js +111 -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-limits.js +27 -0
- package/scripts/dashboard.js +279 -9
- 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/gate-stats.js +2 -2
- package/scripts/gates-engine.js +859 -58
- 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 +802 -185
- 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,
|
|
@@ -195,6 +246,10 @@ const {
|
|
|
195
246
|
writeDashboardReviewState,
|
|
196
247
|
collectAllFeedbackEntries,
|
|
197
248
|
} = require('../../scripts/dashboard');
|
|
249
|
+
const {
|
|
250
|
+
isDashboardDataLimitError,
|
|
251
|
+
formatDashboardLimitDetail,
|
|
252
|
+
} = require('../../scripts/dashboard-limits');
|
|
198
253
|
const {
|
|
199
254
|
collectAggregateLogEntries,
|
|
200
255
|
computeAggregateFeedbackStats,
|
|
@@ -214,6 +269,7 @@ const {
|
|
|
214
269
|
} = require('../../scripts/thumbgate-search');
|
|
215
270
|
const {
|
|
216
271
|
appendTelemetryPing,
|
|
272
|
+
headerHasPrivacyOptOut,
|
|
217
273
|
} = require('../../scripts/telemetry-analytics');
|
|
218
274
|
const {
|
|
219
275
|
buildProductIssueTitle,
|
|
@@ -274,10 +330,13 @@ const EVAL_SCORECARD_PAGE_PATH = path.resolve(__dirname, '../../public/eval-scor
|
|
|
274
330
|
const EVALUATIONS_PAGE_PATH = path.resolve(__dirname, '../../public/evaluations.html');
|
|
275
331
|
const CASE_STUDIES_PAGE_PATH = path.resolve(__dirname, '../../public/case-studies.html');
|
|
276
332
|
const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
|
|
333
|
+
const YT_PAGE_PATH = path.resolve(__dirname, '../../public/yt.html');
|
|
277
334
|
const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
|
|
278
335
|
const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
|
|
279
336
|
const DIAGNOSTIC_PAGE_PATH = path.resolve(__dirname, '../../public/diagnostic.html');
|
|
337
|
+
const FOUNDERS_PAGE_PATH = path.resolve(__dirname, '../../public/founders.html');
|
|
280
338
|
const PARTNER_INTAKE_PAGE_PATH = path.resolve(__dirname, '../../public/partner-intake.html');
|
|
339
|
+
const PETER_PAGE_PATH = path.resolve(__dirname, '../../public/peter.html');
|
|
281
340
|
const INSTALL_PAGE_PATH = path.resolve(__dirname, '../../public/install.html');
|
|
282
341
|
const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
283
342
|
const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
|
|
@@ -292,6 +351,7 @@ const COMPARE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(COMPARE_DIR);
|
|
|
292
351
|
const USE_CASE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(USE_CASES_DIR);
|
|
293
352
|
const BLOG_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(BLOG_DIR);
|
|
294
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');
|
|
295
355
|
const STATIC_MIME_BY_EXT = Object.freeze({
|
|
296
356
|
'.png': 'image/png',
|
|
297
357
|
'.jpg': 'image/jpeg',
|
|
@@ -640,6 +700,30 @@ const TRACKED_LINK_TARGETS = Object.freeze({
|
|
|
640
700
|
utm_campaign: 'github_repo',
|
|
641
701
|
},
|
|
642
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
|
+
},
|
|
643
727
|
});
|
|
644
728
|
|
|
645
729
|
// ---------------------------------------------------------------------------
|
|
@@ -883,7 +967,7 @@ function hashDataIdentity(prefix, value) {
|
|
|
883
967
|
}
|
|
884
968
|
|
|
885
969
|
function buildHostedTenantIdentity(validation = {}) {
|
|
886
|
-
if (
|
|
970
|
+
if (validation?.valid !== true || !validation.customerId) return null;
|
|
887
971
|
const tenantSeed = validation.customerId;
|
|
888
972
|
const principalSeed = validation.installId || tenantSeed;
|
|
889
973
|
const tenantId = hashDataIdentity('tenant', tenantSeed);
|
|
@@ -1820,11 +1904,20 @@ function buildWorkflowIntakeQueue(parsed, workflowSprintIntake, feedbackDir, opt
|
|
|
1820
1904
|
}
|
|
1821
1905
|
|
|
1822
1906
|
function sendInvalidAnalyticsWindowProblem(res, title, err) {
|
|
1907
|
+
if (isDashboardDataLimitError(err)) {
|
|
1908
|
+
sendProblem(res, {
|
|
1909
|
+
type: PROBLEM_TYPES.INTERNAL,
|
|
1910
|
+
title: 'Dashboard data too large',
|
|
1911
|
+
status: 503,
|
|
1912
|
+
detail: formatDashboardLimitDetail(err, { phase: 'assembly' }),
|
|
1913
|
+
});
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1823
1916
|
sendProblem(res, {
|
|
1824
1917
|
type: PROBLEM_TYPES.INVALID_REQUEST,
|
|
1825
1918
|
title,
|
|
1826
1919
|
status: 400,
|
|
1827
|
-
detail: err?.message
|
|
1920
|
+
detail: err?.message || 'Invalid analytics window request.',
|
|
1828
1921
|
});
|
|
1829
1922
|
}
|
|
1830
1923
|
|
|
@@ -1851,19 +1944,103 @@ function resolveBillingSummaryOptionsOrRespondProblem(res, parsed, invalidTitle)
|
|
|
1851
1944
|
|
|
1852
1945
|
async function buildLiveDashboardData(parsed, feedbackDir) {
|
|
1853
1946
|
const summaryOptions = resolveBillingSummaryOptions(parsed);
|
|
1854
|
-
|
|
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
|
+
}
|
|
1855
1965
|
const data = generateDashboard(feedbackDir, {
|
|
1856
1966
|
analyticsWindow: summaryOptions,
|
|
1857
1967
|
billingSummary,
|
|
1858
|
-
billingSource
|
|
1968
|
+
billingSource,
|
|
1859
1969
|
authContext: { tier: 'pro' },
|
|
1860
1970
|
});
|
|
1861
1971
|
return { summaryOptions, data };
|
|
1862
1972
|
}
|
|
1863
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
|
+
|
|
1864
2041
|
async function loadLiveDashboardDataOrRespondProblem(res, parsed, feedbackDir, invalidTitle) {
|
|
1865
2042
|
try {
|
|
1866
|
-
return await
|
|
2043
|
+
return await loadCachedLiveDashboardData(parsed, feedbackDir);
|
|
1867
2044
|
} catch (err) {
|
|
1868
2045
|
sendInvalidAnalyticsWindowProblem(res, invalidTitle, err);
|
|
1869
2046
|
return null;
|
|
@@ -1980,14 +2157,22 @@ function readRecentFeedbackEntries(feedbackDir, signal, windowMs, limit = 5, opt
|
|
|
1980
2157
|
try {
|
|
1981
2158
|
if (!feedbackDir) return [];
|
|
1982
2159
|
const rows = shouldAggregateFeedback()
|
|
1983
|
-
? collectAggregateLogEntries('feedback-log.jsonl', {
|
|
2160
|
+
? collectAggregateLogEntries('feedback-log.jsonl', {
|
|
2161
|
+
feedbackDir,
|
|
2162
|
+
maxLines: 20_000,
|
|
2163
|
+
maxBytes: 4 * 1024 * 1024,
|
|
2164
|
+
}).entries
|
|
1984
2165
|
: (() => {
|
|
1985
2166
|
const fsLocal = require('node:fs');
|
|
1986
2167
|
const pathLocal = require('node:path');
|
|
1987
2168
|
const logPath = pathLocal.join(feedbackDir, 'feedback-log.jsonl');
|
|
1988
2169
|
if (!fsLocal.existsSync(logPath)) return [];
|
|
1989
2170
|
const { readJsonl } = require('../../scripts/fs-utils');
|
|
1990
|
-
return readJsonl(logPath
|
|
2171
|
+
return readJsonl(logPath, {
|
|
2172
|
+
maxLines: 20_000,
|
|
2173
|
+
maxBytes: 4 * 1024 * 1024,
|
|
2174
|
+
tail: true,
|
|
2175
|
+
}) || [];
|
|
1991
2176
|
})();
|
|
1992
2177
|
const cutoff = windowMs ? Date.now() - windowMs : 0;
|
|
1993
2178
|
const filtered = rows
|
|
@@ -2775,32 +2960,37 @@ function getTrackedLinkTarget(slug) {
|
|
|
2775
2960
|
: null;
|
|
2776
2961
|
}
|
|
2777
2962
|
|
|
2778
|
-
function
|
|
2779
|
-
const params = parsed.searchParams;
|
|
2963
|
+
function applyTrackedLinkDefaults(destinationUrl, target) {
|
|
2780
2964
|
for (const [key, value] of Object.entries(target.defaults || {})) {
|
|
2781
2965
|
if (!destinationUrl.searchParams.has(key)) {
|
|
2782
2966
|
appendQueryParam(destinationUrl, key, value);
|
|
2783
2967
|
}
|
|
2784
2968
|
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
function applyTrackedLinkQueryKeys(destinationUrl, params) {
|
|
2785
2972
|
for (const key of TRACKED_LINK_QUERY_KEYS) {
|
|
2786
2973
|
const value = params.get(key);
|
|
2787
|
-
if (value
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
if (target.allowCustomerEmail) {
|
|
2795
|
-
const customerEmail = normalizeCheckoutCustomerEmail(params.get('customer_email'));
|
|
2796
|
-
if (customerEmail) {
|
|
2797
|
-
appendQueryParam(
|
|
2798
|
-
destinationUrl,
|
|
2799
|
-
target.prefillStripeEmail ? 'prefilled_email' : 'customer_email',
|
|
2800
|
-
customerEmail
|
|
2801
|
-
);
|
|
2802
|
-
}
|
|
2974
|
+
if (!value?.trim()) continue;
|
|
2975
|
+
const normalizedValue = key === 'utm_campaign'
|
|
2976
|
+
? normalizeCampaignId(value)
|
|
2977
|
+
: value.trim();
|
|
2978
|
+
destinationUrl.searchParams.set(key, normalizedValue);
|
|
2803
2979
|
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
function applyTrackedLinkCustomerEmail(destinationUrl, params, target) {
|
|
2983
|
+
if (!target.allowCustomerEmail) return;
|
|
2984
|
+
const customerEmail = normalizeCheckoutCustomerEmail(params.get('customer_email'));
|
|
2985
|
+
if (!customerEmail) return;
|
|
2986
|
+
appendQueryParam(
|
|
2987
|
+
destinationUrl,
|
|
2988
|
+
target.prefillStripeEmail ? 'prefilled_email' : 'customer_email',
|
|
2989
|
+
customerEmail,
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
function applyTrackedLinkCtaDefaults(destinationUrl, target) {
|
|
2804
2994
|
if (!destinationUrl.searchParams.has('cta_id')) {
|
|
2805
2995
|
appendQueryParam(destinationUrl, 'cta_id', target.ctaId);
|
|
2806
2996
|
}
|
|
@@ -2812,6 +3002,14 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
|
2812
3002
|
}
|
|
2813
3003
|
}
|
|
2814
3004
|
|
|
3005
|
+
function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
3006
|
+
const params = parsed.searchParams;
|
|
3007
|
+
applyTrackedLinkDefaults(destinationUrl, target);
|
|
3008
|
+
applyTrackedLinkQueryKeys(destinationUrl, params);
|
|
3009
|
+
applyTrackedLinkCustomerEmail(destinationUrl, params, target);
|
|
3010
|
+
applyTrackedLinkCtaDefaults(destinationUrl, target);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
2815
3013
|
function buildTrackedLinkDestination(target, hostedConfig, parsed) {
|
|
2816
3014
|
const configuredHref = target.configUrlKey ? hostedConfig[target.configUrlKey] : null;
|
|
2817
3015
|
const href = target.href || configuredHref || target.fallbackHref;
|
|
@@ -3006,7 +3204,21 @@ function resolveCheckoutOfferSummary(metadata = {}) {
|
|
|
3006
3204
|
|
|
3007
3205
|
function sendJson(res, statusCode, payload, extraHeaders = {}, options = {}) {
|
|
3008
3206
|
const { headOnly = false } = options;
|
|
3009
|
-
|
|
3207
|
+
let body;
|
|
3208
|
+
try {
|
|
3209
|
+
body = JSON.stringify(payload);
|
|
3210
|
+
} catch (err) {
|
|
3211
|
+
if (isDashboardDataLimitError(err)) {
|
|
3212
|
+
sendProblem(res, {
|
|
3213
|
+
type: PROBLEM_TYPES.INTERNAL,
|
|
3214
|
+
title: 'Dashboard response too large',
|
|
3215
|
+
status: 503,
|
|
3216
|
+
detail: formatDashboardLimitDetail(err, { phase: 'stringify' }),
|
|
3217
|
+
});
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
throw err;
|
|
3221
|
+
}
|
|
3010
3222
|
res.writeHead(statusCode, {
|
|
3011
3223
|
'Content-Type': 'application/json; charset=utf-8',
|
|
3012
3224
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -3060,8 +3272,8 @@ function sendPublicBillingPreflight(res) {
|
|
|
3060
3272
|
|
|
3061
3273
|
function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
|
|
3062
3274
|
try {
|
|
3063
|
-
appendTelemetryPing(feedbackDir, payload, headers);
|
|
3064
|
-
return
|
|
3275
|
+
const written = appendTelemetryPing(feedbackDir, payload, headers);
|
|
3276
|
+
return Boolean(written);
|
|
3065
3277
|
} catch (err) {
|
|
3066
3278
|
try {
|
|
3067
3279
|
appendDiagnosticRecord({
|
|
@@ -3248,7 +3460,8 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
3248
3460
|
const googleSiteVerificationMeta = runtimeConfig.googleSiteVerification
|
|
3249
3461
|
? ` <meta name="google-site-verification" content="${escapeHtmlAttribute(runtimeConfig.googleSiteVerification)}" />`
|
|
3250
3462
|
: '';
|
|
3251
|
-
const
|
|
3463
|
+
const privacyOptOut = Boolean(pageContext.privacyOptOut);
|
|
3464
|
+
const gaBootstrap = (!privacyOptOut && runtimeConfig.gaMeasurementId)
|
|
3252
3465
|
? [
|
|
3253
3466
|
` <script async src="https://www.googletagmanager.com/gtag/js?id=${runtimeConfig.gaMeasurementId}"></script>`,
|
|
3254
3467
|
' <script>',
|
|
@@ -3259,7 +3472,7 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
3259
3472
|
' </script>',
|
|
3260
3473
|
].join('\n')
|
|
3261
3474
|
: '';
|
|
3262
|
-
|
|
3475
|
+
let rendered = fillTemplate(template, {
|
|
3263
3476
|
'__PACKAGE_VERSION__': pkg.version,
|
|
3264
3477
|
'__APP_ORIGIN__': runtimeConfig.appOrigin,
|
|
3265
3478
|
'__CHECKOUT_ENDPOINT__': runtimeConfig.checkoutEndpoint,
|
|
@@ -3288,7 +3501,34 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
3288
3501
|
'__GTM_PLAN_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/GO_TO_MARKET_REVENUE_WEDGE_2026-03.md',
|
|
3289
3502
|
'__GITHUB_URL__': 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
3290
3503
|
'__POSTHOG_API_KEY__': runtimeConfig.posthogApiKey || '',
|
|
3291
|
-
})
|
|
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;
|
|
3292
3532
|
}
|
|
3293
3533
|
|
|
3294
3534
|
function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
|
|
@@ -3303,10 +3543,18 @@ function loadDiagnosticPageHtml(runtimeConfig, pageContext = {}) {
|
|
|
3303
3543
|
return loadPublicMarketingTemplateHtml(DIAGNOSTIC_PAGE_PATH, runtimeConfig, pageContext);
|
|
3304
3544
|
}
|
|
3305
3545
|
|
|
3546
|
+
function loadFoundersPageHtml(runtimeConfig, pageContext = {}) {
|
|
3547
|
+
return loadPublicMarketingTemplateHtml(FOUNDERS_PAGE_PATH, runtimeConfig, pageContext);
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3306
3550
|
function loadPartnerIntakePageHtml(runtimeConfig, pageContext = {}) {
|
|
3307
3551
|
return loadPublicMarketingTemplateHtml(PARTNER_INTAKE_PAGE_PATH, runtimeConfig, pageContext);
|
|
3308
3552
|
}
|
|
3309
3553
|
|
|
3554
|
+
function loadPeterPageHtml(runtimeConfig, pageContext = {}) {
|
|
3555
|
+
return loadPublicMarketingTemplateHtml(PETER_PAGE_PATH, runtimeConfig, pageContext);
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3310
3558
|
function loadInstallPageHtml(runtimeConfig, pageContext = {}) {
|
|
3311
3559
|
return loadPublicMarketingTemplateHtml(INSTALL_PAGE_PATH, runtimeConfig, pageContext);
|
|
3312
3560
|
}
|
|
@@ -3945,8 +4193,20 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3945
4193
|
{ path: '/', changefreq: 'weekly', priority: '1.0' },
|
|
3946
4194
|
{ path: '/pro', changefreq: 'weekly', priority: '0.9' },
|
|
3947
4195
|
{ path: '/diagnostic', changefreq: 'weekly', priority: '0.9' },
|
|
4196
|
+
{ path: '/founders', changefreq: 'weekly', priority: '0.95' },
|
|
4197
|
+
{ path: '/peter', changefreq: 'weekly', priority: '0.9' },
|
|
3948
4198
|
{ path: '/workflow-hardening-sprint', changefreq: 'weekly', priority: '0.9' },
|
|
3949
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' },
|
|
3950
4210
|
{ path: '/agent-manager', changefreq: 'weekly', priority: '0.9' },
|
|
3951
4211
|
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
3952
4212
|
{ path: '/chatgpt-app', changefreq: 'weekly', priority: '0.85' },
|
|
@@ -3960,6 +4220,7 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3960
4220
|
{ path: '/evaluations', changefreq: 'weekly', priority: '0.85' },
|
|
3961
4221
|
{ path: '/case-studies', changefreq: 'weekly', priority: '0.9' },
|
|
3962
4222
|
{ path: '/numbers', changefreq: 'weekly', priority: '0.8' },
|
|
4223
|
+
{ path: '/yt', changefreq: 'weekly', priority: '0.8' },
|
|
3963
4224
|
{ path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
|
|
3964
4225
|
{ path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
|
|
3965
4226
|
{ path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
|
|
@@ -3969,10 +4230,14 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3969
4230
|
{ path: '/learn/agentic-os-team-governance', changefreq: 'weekly', priority: '0.85' },
|
|
3970
4231
|
{ path: '/learn/cost-aware-agent-gate-routing', changefreq: 'weekly', priority: '0.85' },
|
|
3971
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' },
|
|
3972
4236
|
{ path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
|
|
3973
4237
|
{ path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },
|
|
3974
4238
|
{ path: '/compare/anthropic-containment', changefreq: 'weekly', priority: '0.85' },
|
|
3975
4239
|
{ path: '/compare/oak-and-sparrow-gatekeeper', changefreq: 'weekly', priority: '0.85' },
|
|
4240
|
+
{ path: '/compare/cloudflare-writeguard', changefreq: 'weekly', priority: '0.85' },
|
|
3976
4241
|
{ path: '/compare/arcjet', changefreq: 'weekly', priority: '0.85' },
|
|
3977
4242
|
{ path: '/compare/anthropic-claude-for-legal', changefreq: 'weekly', priority: '0.9' },
|
|
3978
4243
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
@@ -4141,6 +4406,7 @@ function servePublicMarketingPage({
|
|
|
4141
4406
|
serverUtmCampaign: landingAttribution.utmCampaign,
|
|
4142
4407
|
serverUtmContent: landingAttribution.utmContent,
|
|
4143
4408
|
requestHost,
|
|
4409
|
+
privacyOptOut: headerHasPrivacyOptOut(req.headers),
|
|
4144
4410
|
});
|
|
4145
4411
|
|
|
4146
4412
|
sendHtml(
|
|
@@ -5131,6 +5397,7 @@ function authenticateHumanReviewer(req, reviewerConfig) {
|
|
|
5131
5397
|
return {
|
|
5132
5398
|
id: reviewerConfig.id,
|
|
5133
5399
|
kind: 'human',
|
|
5400
|
+
role: 'admin',
|
|
5134
5401
|
};
|
|
5135
5402
|
}
|
|
5136
5403
|
|
|
@@ -5169,6 +5436,23 @@ function isBillingSummaryAuthorized(req, expectedAdminKey, expectedOperatorKey)
|
|
|
5169
5436
|
return false;
|
|
5170
5437
|
}
|
|
5171
5438
|
|
|
5439
|
+
/** Read-only paths the operator key may access (CLI dashboard / CFO / north-star). */
|
|
5440
|
+
const OPERATOR_READONLY_GET_PATHS = new Set([
|
|
5441
|
+
'/v1/billing/summary',
|
|
5442
|
+
'/v1/dashboard',
|
|
5443
|
+
'/v1/dashboard/render-spec',
|
|
5444
|
+
'/v1/dashboard/ai-inventory',
|
|
5445
|
+
'/v1/dashboard/review-state',
|
|
5446
|
+
'/v1/intake/workflow-sprint/queue',
|
|
5447
|
+
'/v1/task-outcomes/monitor',
|
|
5448
|
+
]);
|
|
5449
|
+
|
|
5450
|
+
function isOperatorReadonlyGet(req, expectedOperatorKey, pathname) {
|
|
5451
|
+
if (!expectedOperatorKey || req.method !== 'GET') return false;
|
|
5452
|
+
if (!OPERATOR_READONLY_GET_PATHS.has(pathname)) return false;
|
|
5453
|
+
return safeKeyEqual(extractApiKey(req), expectedOperatorKey);
|
|
5454
|
+
}
|
|
5455
|
+
|
|
5172
5456
|
function extractTags(input) {
|
|
5173
5457
|
if (Array.isArray(input)) return input;
|
|
5174
5458
|
if (typeof input === 'string') {
|
|
@@ -5412,13 +5696,22 @@ function createApiServer() {
|
|
|
5412
5696
|
? mcpOauth.tokenAudienceValid(oauthSession, resourceUrl)
|
|
5413
5697
|
: rawKeyValid;
|
|
5414
5698
|
if (!authed) {
|
|
5699
|
+
const deniedName = msg.params?.name || 'unknown';
|
|
5700
|
+
const deniedTool = MCP_TOOLS.find((t) => t.name === deniedName);
|
|
5415
5701
|
recordToolCall({
|
|
5416
|
-
toolName:
|
|
5702
|
+
toolName: deniedName,
|
|
5417
5703
|
serverName: 'mcp-http',
|
|
5418
5704
|
latencyMs: 0,
|
|
5419
5705
|
success: false,
|
|
5706
|
+
outcome: 'blocked',
|
|
5707
|
+
blocked: true,
|
|
5420
5708
|
agentId: 'unauthenticated',
|
|
5421
|
-
|
|
5709
|
+
writeRiskTier: deniedTool?.annotations?.writeRiskTier || null,
|
|
5710
|
+
metadata: {
|
|
5711
|
+
denied: true,
|
|
5712
|
+
deniedReason: 'authentication_required',
|
|
5713
|
+
writeRiskTier: deniedTool?.annotations?.writeRiskTier || null,
|
|
5714
|
+
},
|
|
5422
5715
|
});
|
|
5423
5716
|
res.writeHead(401, {
|
|
5424
5717
|
'Content-Type': 'application/json',
|
|
@@ -5447,8 +5740,15 @@ function createApiServer() {
|
|
|
5447
5740
|
serverName: 'mcp-http',
|
|
5448
5741
|
latencyMs: 0,
|
|
5449
5742
|
success: false,
|
|
5743
|
+
outcome: 'blocked',
|
|
5744
|
+
blocked: true,
|
|
5450
5745
|
agentId: 'reviewer',
|
|
5451
|
-
|
|
5746
|
+
writeRiskTier: tool?.annotations?.writeRiskTier || null,
|
|
5747
|
+
metadata: {
|
|
5748
|
+
denied: true,
|
|
5749
|
+
deniedReason: 'reviewer_read_only',
|
|
5750
|
+
writeRiskTier: tool?.annotations?.writeRiskTier || null,
|
|
5751
|
+
},
|
|
5452
5752
|
});
|
|
5453
5753
|
sendJson(res, 200, {
|
|
5454
5754
|
jsonrpc: '2.0', id: msg.id,
|
|
@@ -5460,18 +5760,25 @@ function createApiServer() {
|
|
|
5460
5760
|
if (oauthSession) {
|
|
5461
5761
|
const name = msg.params && msg.params.name;
|
|
5462
5762
|
const tool = MCP_TOOLS.find((candidate) => candidate.name === name);
|
|
5463
|
-
|
|
5763
|
+
// Hierarchy: mcp:write implies read/gates/feedback (WorkOS MCP Auth style).
|
|
5764
|
+
const requiredScope = mcpOauth.requiredScopeForTool(tool || {});
|
|
5464
5765
|
if (!mcpOauth.scopeAllows(oauthSession, requiredScope)) {
|
|
5465
5766
|
recordToolCall({
|
|
5466
5767
|
toolName: name || 'unknown',
|
|
5467
5768
|
serverName: 'mcp-http',
|
|
5468
5769
|
latencyMs: 0,
|
|
5469
5770
|
success: false,
|
|
5771
|
+
outcome: 'blocked',
|
|
5772
|
+
blocked: true,
|
|
5470
5773
|
agentId: oauthSession.clientId || 'oauth-client',
|
|
5774
|
+
clientId: oauthSession.clientId || null,
|
|
5775
|
+
sessionId: oauthSession.sessionId || oauthSession.id || null,
|
|
5776
|
+
writeRiskTier: tool?.annotations?.writeRiskTier || null,
|
|
5471
5777
|
metadata: {
|
|
5472
5778
|
denied: true,
|
|
5473
5779
|
deniedReason: 'insufficient_scope',
|
|
5474
5780
|
requiredScope,
|
|
5781
|
+
writeRiskTier: tool?.annotations?.writeRiskTier || null,
|
|
5475
5782
|
},
|
|
5476
5783
|
});
|
|
5477
5784
|
sendJson(res, 200, {
|
|
@@ -5486,7 +5793,14 @@ function createApiServer() {
|
|
|
5486
5793
|
const { callTool } = require('../../adapters/mcp/server-stdio');
|
|
5487
5794
|
const name = msg.params?.name;
|
|
5488
5795
|
const args = (msg.params && msg.params.arguments) || {};
|
|
5489
|
-
|
|
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
|
+
});
|
|
5490
5804
|
sendJson(res, 200, {
|
|
5491
5805
|
jsonrpc: '2.0', id: msg.id,
|
|
5492
5806
|
result,
|
|
@@ -5569,6 +5883,27 @@ function createApiServer() {
|
|
|
5569
5883
|
return;
|
|
5570
5884
|
}
|
|
5571
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
|
+
|
|
5572
5907
|
|
|
5573
5908
|
// User feedback → GitHub Issues
|
|
5574
5909
|
if (req.method === 'POST' && pathname === '/api/feedback/submit') {
|
|
@@ -6336,6 +6671,57 @@ async function addContext(){
|
|
|
6336
6671
|
return;
|
|
6337
6672
|
}
|
|
6338
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
|
+
|
|
6339
6725
|
// Natural marketing URLs for the Workflow Hardening Diagnostic/Sprint.
|
|
6340
6726
|
// High-intent outreach refers to "the diagnostic", "the sprint", or
|
|
6341
6727
|
// "workflow hardening"; serve a focused buyer page instead of sending
|
|
@@ -6482,6 +6868,35 @@ async function addContext(){
|
|
|
6482
6868
|
return;
|
|
6483
6869
|
}
|
|
6484
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
|
+
|
|
6485
6900
|
if (isGetLikeRequest && (pathname === '/agent-manager' || pathname === '/agent-manager.html')) {
|
|
6486
6901
|
// ICP landing page for the role Anthropic named (Agent Manager —
|
|
6487
6902
|
// hybrid PM/engineer DRI who owns CLAUDE.md hierarchy, plugin
|
|
@@ -8306,35 +8721,168 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8306
8721
|
// Public terms of service — required by Stripe / Stripe Checkout for the
|
|
8307
8722
|
// "Terms of service URL" field in Business → Public details. Mirrors the
|
|
8308
8723
|
// /privacy + /support pages: thin HTML, no external deps, no DB hit.
|
|
8309
|
-
if (isGetLikeRequest && pathname === '/terms') {
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
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
|
+
}
|
|
8731
|
+
return;
|
|
8732
|
+
}
|
|
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 });
|
|
8335
8807
|
return;
|
|
8336
8808
|
}
|
|
8337
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
|
+
|
|
8338
8886
|
// Public case studies — dogfood proof narratives (static HTML; no fabricated logos).
|
|
8339
8887
|
if (isGetLikeRequest && (pathname === '/case-studies' || pathname === '/case-studies.html')) {
|
|
8340
8888
|
try {
|
|
@@ -8353,14 +8901,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8353
8901
|
return;
|
|
8354
8902
|
}
|
|
8355
8903
|
|
|
8356
|
-
|
|
8357
|
-
// Public canonical pricing page. The audit flagged "pricing schizophrenia":
|
|
8358
|
-
// sales/pricing.json said $49 / $299, COMMERCIAL_TRUTH.md said $19 / $149,
|
|
8359
|
-
// and there was no buyer-facing surface to reconcile the two. This is now
|
|
8360
|
-
// the single source of truth for what ThumbGate sells, in priority order:
|
|
8361
|
-
// Sprint (proof-pack, sales-led) → Pro (self-serve recurring) → Team
|
|
8362
|
-
// (after qualification). Every paid CTA across the site should funnel
|
|
8363
|
-
// here OR directly into Stripe checkout — never a different price.
|
|
8904
|
+
// Public canonical pricing page.
|
|
8364
8905
|
if (isGetLikeRequest && (pathname === '/pricing' || pathname === '/pricing.html')) {
|
|
8365
8906
|
try {
|
|
8366
8907
|
servePublicMarketingPage({
|
|
@@ -8380,100 +8921,14 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8380
8921
|
return;
|
|
8381
8922
|
}
|
|
8382
8923
|
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
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>
|
|
8392
|
-
<h1>ThumbGate — Remote MCP Connector</h1>
|
|
8393
|
-
<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>
|
|
8394
|
-
|
|
8395
|
-
<h2>Connect URL</h2>
|
|
8396
|
-
<pre>${esc(mcpUrl)}</pre>
|
|
8397
|
-
<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>
|
|
8398
|
-
|
|
8399
|
-
<h2>Authorization (OAuth 2.1 + PKCE)</h2>
|
|
8400
|
-
<ol>
|
|
8401
|
-
<li>Claude reads the <code>resource</code> and <code>authorization_servers</code> from the protected-resource metadata.</li>
|
|
8402
|
-
<li>It runs the standard OAuth 2.1 authorization-code flow with PKCE (<code>S256</code> only — <code>plain</code> is rejected).</li>
|
|
8403
|
-
<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>
|
|
8404
|
-
<li>The bearer token is then sent on the <code>Authorization</code> header for every <code>tools/call</code>.</li>
|
|
8405
|
-
</ol>
|
|
8406
|
-
|
|
8407
|
-
<h2>Available tools</h2>
|
|
8408
|
-
<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>
|
|
8409
|
-
<ul>
|
|
8410
|
-
<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>
|
|
8411
|
-
<li><strong>Context engineering</strong> — <code>construct_context_pack</code>, <code>evaluate_context_pack</code>, <code>unified_context</code>, <code>recall</code>.</li>
|
|
8412
|
-
<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>
|
|
8413
|
-
<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>
|
|
8414
|
-
</ul>
|
|
8415
|
-
|
|
8416
|
-
<h2>Reviewer credential (read-only)</h2>
|
|
8417
|
-
<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>
|
|
8418
|
-
|
|
8419
|
-
<h2>Source & contact</h2>
|
|
8420
|
-
<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>
|
|
8421
|
-
|
|
8422
|
-
<footer><a href="/">ThumbGate</a> · <a href="/support">Support</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></footer>
|
|
8423
|
-
</body></html>`, {}, { headOnly: isHeadRequest });
|
|
8424
|
-
return;
|
|
8425
|
-
}
|
|
8426
|
-
|
|
8427
|
-
// Public support / contact page — required for Stripe Business → Public
|
|
8428
|
-
// details "Customer support URL" field. Single source of truth for how
|
|
8429
|
-
// customers reach us (email, GitHub issues, status page).
|
|
8430
|
-
if (isGetLikeRequest && pathname === '/support') {
|
|
8431
|
-
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Support — ThumbGate</title></head><body>
|
|
8432
|
-
<h1>Support</h1>
|
|
8433
|
-
<p><strong>ThumbGate</strong> support, billing, and contact paths.</p>
|
|
8434
|
-
<h2>Email</h2>
|
|
8435
|
-
<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>
|
|
8436
|
-
<h2>GitHub Issues</h2>
|
|
8437
|
-
<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>
|
|
8438
|
-
<h2>Status</h2>
|
|
8439
|
-
<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>
|
|
8440
|
-
<h2>Refunds</h2>
|
|
8441
|
-
<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>
|
|
8442
|
-
<h2>Security</h2>
|
|
8443
|
-
<p>Disclose vulnerabilities by email; please do not post to public GitHub issues. We acknowledge within 48 hours.</p>
|
|
8444
|
-
<p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></p>
|
|
8445
|
-
</body></html>`, {}, {
|
|
8446
|
-
headOnly: isHeadRequest,
|
|
8447
|
-
});
|
|
8448
|
-
return;
|
|
8449
|
-
}
|
|
8450
|
-
|
|
8451
|
-
// Public privacy policy — required for GPT Store and marketplace listings
|
|
8452
|
-
if (isGetLikeRequest && pathname === '/privacy') {
|
|
8453
|
-
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Privacy Policy — ThumbGate</title></head><body>
|
|
8454
|
-
<h1>Privacy Policy</h1>
|
|
8455
|
-
<p><strong>ThumbGate</strong> (npm: thumbgate)</p>
|
|
8456
|
-
<p>Last updated: 2026-03-11</p>
|
|
8457
|
-
<h2>Data Collection</h2>
|
|
8458
|
-
<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>
|
|
8459
|
-
<p>The hosted tier (thumbgate-production.up.railway.app) stores feedback signals, memory entries, and related workflow metadata associated with your API key.</p>
|
|
8460
|
-
<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>
|
|
8461
|
-
<h2>Data Stored</h2><ul>
|
|
8462
|
-
<li>Feedback signals (thumbs up/down) with context you provide</li>
|
|
8463
|
-
<li>Promoted memory entries</li>
|
|
8464
|
-
<li>Prevention rules generated from your feedback</li>
|
|
8465
|
-
</ul>
|
|
8466
|
-
<h2>Data Sharing</h2>
|
|
8467
|
-
<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>
|
|
8468
|
-
<h2>Data Retention</h2>
|
|
8469
|
-
<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>
|
|
8470
|
-
<h2>Data Deletion</h2>
|
|
8471
|
-
<p>Contact igor.ganapolsky@gmail.com to request deletion of hosted data.</p>
|
|
8472
|
-
<h2>Contact</h2><p>igor.ganapolsky@gmail.com</p>
|
|
8473
|
-
<p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a></p>
|
|
8474
|
-
</body></html>`, {}, {
|
|
8475
|
-
headOnly: isHeadRequest,
|
|
8476
|
-
});
|
|
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
|
+
}
|
|
8477
8932
|
return;
|
|
8478
8933
|
}
|
|
8479
8934
|
|
|
@@ -8736,19 +9191,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8736
9191
|
return;
|
|
8737
9192
|
}
|
|
8738
9193
|
|
|
8739
|
-
// Operator key
|
|
8740
|
-
|
|
8741
|
-
const _reqToken = extractApiKey(req);
|
|
8742
|
-
const isOperatorReadRequest = Boolean(expectedOperatorKey)
|
|
8743
|
-
&& safeKeyEqual(_reqToken, expectedOperatorKey)
|
|
8744
|
-
&& req.method === 'GET'
|
|
8745
|
-
&& [
|
|
8746
|
-
'/v1/billing/summary',
|
|
8747
|
-
'/v1/intake/workflow-sprint/queue',
|
|
8748
|
-
'/v1/task-outcomes/monitor',
|
|
8749
|
-
].includes(pathname);
|
|
8750
|
-
|
|
8751
|
-
if (!isOperatorReadRequest && !isAuthorized(req, expectedApiKey)) {
|
|
9194
|
+
// Operator key: read-only operational endpoints only (see OPERATOR_READONLY_GET_PATHS).
|
|
9195
|
+
if (!isOperatorReadonlyGet(req, expectedOperatorKey, pathname) && !isAuthorized(req, expectedApiKey)) {
|
|
8752
9196
|
sendProblem(res, {
|
|
8753
9197
|
type: PROBLEM_TYPES.UNAUTHORIZED,
|
|
8754
9198
|
title: 'Unauthorized',
|
|
@@ -9642,6 +10086,13 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9642
10086
|
guardrails: body.guardrails,
|
|
9643
10087
|
tags: extractTags(body.tags),
|
|
9644
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,
|
|
9645
10096
|
reviewOrigin: 'human',
|
|
9646
10097
|
});
|
|
9647
10098
|
const actionIntegration = inferActionIntegration(body, req.headers);
|
|
@@ -9980,12 +10431,23 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9980
10431
|
} catch (err) {
|
|
9981
10432
|
throw createHttpError(400, err.message || 'Invalid namespaces');
|
|
9982
10433
|
}
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
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
|
+
}
|
|
9989
10451
|
sendJson(res, 200, pack);
|
|
9990
10452
|
return;
|
|
9991
10453
|
}
|
|
@@ -10698,11 +11160,151 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
10698
11160
|
});
|
|
10699
11161
|
return;
|
|
10700
11162
|
}
|
|
10701
|
-
|
|
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
|
+
});
|
|
10702
11170
|
sendJson(res, 200, { satisfied: true, gateId: body.gateId, ...entry });
|
|
10703
11171
|
return;
|
|
10704
11172
|
}
|
|
10705
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
|
+
|
|
10706
11308
|
// GET /api/conversions — Conversion stats derived from the Stripe event log
|
|
10707
11309
|
if (req.method === 'GET' && pathname === '/api/conversions') {
|
|
10708
11310
|
try {
|
|
@@ -10740,7 +11342,20 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
10740
11342
|
function startServer({ port, host } = {}) {
|
|
10741
11343
|
const listenPort = Number(port ?? process.env.PORT ?? 8787);
|
|
10742
11344
|
const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
|
|
10743
|
-
|
|
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
|
+
}
|
|
10744
11359
|
const server = createApiServer();
|
|
10745
11360
|
registerGracefulShutdown(server);
|
|
10746
11361
|
return new Promise((resolve) => {
|
|
@@ -10823,6 +11438,8 @@ module.exports = {
|
|
|
10823
11438
|
sanitizeHtmlUnsafeJsonValue,
|
|
10824
11439
|
buildHostedTenantIdentity,
|
|
10825
11440
|
partitionFeedbackPaths,
|
|
11441
|
+
buildLiveDashboardCacheKey,
|
|
11442
|
+
loadCachedLiveDashboardData,
|
|
10826
11443
|
},
|
|
10827
11444
|
};
|
|
10828
11445
|
|