thumbgate 1.27.18 → 1.27.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
|
@@ -16,7 +16,7 @@ const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
|
|
|
16
16
|
* 2. Env var: THUMBGATE_DEV_BYPASS=[set via THUMBGATE_DEV_SECRET env var]
|
|
17
17
|
* Requires a specific non-obvious value (not boolean) to prevent accidental activation.
|
|
18
18
|
*/
|
|
19
|
-
function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
|
|
19
|
+
function isCreatorDev({ env = process.env, homeDir = env.HOME || env.USERPROFILE || os.homedir() } = {}) {
|
|
20
20
|
// Layer 1: env var with specific value
|
|
21
21
|
if (CREATOR_BYPASS_VALUE && String(env[CREATOR_BYPASS_ENV] || '') === CREATOR_BYPASS_VALUE) {
|
|
22
22
|
return true;
|
|
@@ -37,7 +37,7 @@ function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
|
|
|
37
37
|
* with any non-empty bypass value. No env var needed — just the config file.
|
|
38
38
|
* Used by the server to skip auth on localhost during local development.
|
|
39
39
|
*/
|
|
40
|
-
function hasDevOverride(homeDir = os.homedir()) {
|
|
40
|
+
function hasDevOverride(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
41
41
|
// Disabled during test runs to avoid interfering with auth assertions
|
|
42
42
|
if (process.env.NODE_TEST_CONTEXT || process.env.THUMBGATE_TESTING) return false;
|
|
43
43
|
try {
|
|
@@ -47,11 +47,11 @@ function hasDevOverride(homeDir = os.homedir()) {
|
|
|
47
47
|
} catch { return false; }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function getLicenseDir(homeDir = os.homedir()) {
|
|
50
|
+
function getLicenseDir(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
51
51
|
return path.join(homeDir, '.thumbgate');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function getLicensePath(homeDir = os.homedir()) {
|
|
54
|
+
function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
55
55
|
return path.join(getLicenseDir(homeDir), 'license.json');
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -39,9 +39,23 @@ function normalizeOptions(options = {}) {
|
|
|
39
39
|
...splitCsv(options.documents),
|
|
40
40
|
...splitCsv(options['document-ids']),
|
|
41
41
|
]);
|
|
42
|
+
const sourcePointers = unique([
|
|
43
|
+
...splitCsv(options['source-pointers']),
|
|
44
|
+
...splitCsv(options.pointers),
|
|
45
|
+
...splitCsv(options.sources),
|
|
46
|
+
]);
|
|
42
47
|
const candidateImages = Number.isFinite(Number(options['candidate-images']))
|
|
43
48
|
? Number(options['candidate-images'])
|
|
44
49
|
: null;
|
|
50
|
+
const extractedEntities = Number.isFinite(Number(options['extracted-entities']))
|
|
51
|
+
? Number(options['extracted-entities'])
|
|
52
|
+
: 0;
|
|
53
|
+
const extractedRelations = Number.isFinite(Number(options['extracted-relations']))
|
|
54
|
+
? Number(options['extracted-relations'])
|
|
55
|
+
: 0;
|
|
56
|
+
const promotionThreshold = Number.isFinite(Number(options['promotion-threshold']))
|
|
57
|
+
? Number(options['promotion-threshold'])
|
|
58
|
+
: 3;
|
|
45
59
|
|
|
46
60
|
return {
|
|
47
61
|
ragTool: String(options['rag-tool'] || options.tool || 'proxy-pointer-rag').trim() || 'proxy-pointer-rag',
|
|
@@ -49,10 +63,15 @@ function normalizeOptions(options = {}) {
|
|
|
49
63
|
sectionIds,
|
|
50
64
|
imagePointers,
|
|
51
65
|
documentIds,
|
|
66
|
+
sourcePointers,
|
|
52
67
|
candidateImages,
|
|
68
|
+
extractedEntities,
|
|
69
|
+
extractedRelations,
|
|
70
|
+
promotionThreshold,
|
|
53
71
|
crossDocumentPolicy: String(options['cross-doc-policy'] || options['cross-document-policy'] || '').trim().toLowerCase(),
|
|
54
72
|
visionFilter: normalizeBoolean(options['vision-filter']),
|
|
55
73
|
visualClaims: normalizeBoolean(options['visual-claims']),
|
|
74
|
+
pointerFirst: normalizeBoolean(options['pointer-first']) || normalizeBoolean(options['proxy-pointer']),
|
|
56
75
|
};
|
|
57
76
|
}
|
|
58
77
|
|
|
@@ -72,6 +91,14 @@ function gateApplicability(template, options) {
|
|
|
72
91
|
return false;
|
|
73
92
|
}
|
|
74
93
|
|
|
94
|
+
function hasExtractionSprawl(options) {
|
|
95
|
+
const extractedFacts = options.extractedEntities + options.extractedRelations;
|
|
96
|
+
if (extractedFacts === 0) return false;
|
|
97
|
+
if (options.pointerFirst) return true;
|
|
98
|
+
if (options.sourcePointers.length === 0) return true;
|
|
99
|
+
return extractedFacts > options.sourcePointers.length * Math.max(2, options.promotionThreshold);
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
function buildSignalSummary(options) {
|
|
76
103
|
const signals = [];
|
|
77
104
|
if (options.treePath || options.sectionIds.length > 0) {
|
|
@@ -110,6 +137,19 @@ function buildSignalSummary(options) {
|
|
|
110
137
|
risk: 'answers that describe image content may need a vision-model sanity check',
|
|
111
138
|
});
|
|
112
139
|
}
|
|
140
|
+
if (hasExtractionSprawl(options)) {
|
|
141
|
+
signals.push({
|
|
142
|
+
id: 'entity_relation_sprawl',
|
|
143
|
+
label: 'Entity/relation extraction sprawl',
|
|
144
|
+
values: unique([
|
|
145
|
+
`${options.extractedEntities} extracted entities`,
|
|
146
|
+
`${options.extractedRelations} extracted relations`,
|
|
147
|
+
`${options.sourcePointers.length} source pointers`,
|
|
148
|
+
`promotion threshold ${options.promotionThreshold}`,
|
|
149
|
+
]),
|
|
150
|
+
risk: 'eager graph extraction can create stale aliases, weak edges, and unauditable memory; keep source pointers first and promote relations only after repeated retrieval value',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
113
153
|
return signals;
|
|
114
154
|
}
|
|
115
155
|
|
|
@@ -139,11 +179,12 @@ function buildProxyPointerRagGuardrailsPlan(rawOptions = {}, templatesPath) {
|
|
|
139
179
|
templates: recommendedTemplates,
|
|
140
180
|
nextActions: [
|
|
141
181
|
'Preserve document hierarchy, section IDs, and image file paths during ingestion.',
|
|
182
|
+
'Store source pointers before extracting entities or relations; promote a relation only after repeated retrieval value and source verification.',
|
|
142
183
|
'Pass section-tree and image-pointer metadata into the agent before it answers with visuals.',
|
|
143
184
|
'Enable the recommended Document RAG Safety templates as pre-action gates.',
|
|
144
185
|
'Use a vision filter only for high-impact answers that make claims about visual content.',
|
|
145
186
|
],
|
|
146
|
-
exampleCommand: 'npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --
|
|
187
|
+
exampleCommand: 'npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --source-pointers=lesson/fb_123,tool/run_456 --extracted-entities=120 --extracted-relations=80 --pointer-first --json',
|
|
147
188
|
};
|
|
148
189
|
}
|
|
149
190
|
|
package/scripts/published-cli.js
CHANGED
|
@@ -37,14 +37,6 @@ function publishedCliShellCommand(pkgVersion, commandArgs = [], options = {}) {
|
|
|
37
37
|
const escapedArgs = commandArgs.map(shellQuote).join(' ');
|
|
38
38
|
const fastPath = `[ -x ${shellQuote(runtimeBin)} ] && exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`;
|
|
39
39
|
const installPath = `mkdir -p ${shellQuote(prefixDir)} && exec npm ${publishedCliArgs(pkgVersion, commandArgs, { prefixDir }).map(shellQuote).join(' ')}`;
|
|
40
|
-
if (options.preferInstalled === false) {
|
|
41
|
-
const packageSpec = `thumbgate@${pkgVersion}`;
|
|
42
|
-
return [
|
|
43
|
-
`mkdir -p ${shellQuote(prefixDir)}`,
|
|
44
|
-
`npm "install" "--prefix" ${shellQuote(prefixDir)} "--no-save" "--omit=dev" ${shellQuote(packageSpec)} >/dev/null 2>&1`,
|
|
45
|
-
`exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`,
|
|
46
|
-
].join(' && ');
|
|
47
|
-
}
|
|
48
40
|
return `${fastPath} || ${installPath}`;
|
|
49
41
|
}
|
|
50
42
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
const RUNTIME_PATTERNS = [
|
|
7
|
+
{ pattern: /^public\/.*\.(html|css|js)$/i, surface: 'browser', reason: 'public UI asset changed' },
|
|
8
|
+
{ pattern: /^src\/api\//i, surface: 'api', reason: 'API route or server behavior changed' },
|
|
9
|
+
{ pattern: /^bin\//i, surface: 'cli', reason: 'CLI entrypoint changed' },
|
|
10
|
+
{ pattern: /^scripts\/(dashboard|pro-local-dashboard|.*gate|.*scanner|.*reward|.*routing).*\.js$/i, surface: 'agent-runtime', reason: 'agent runtime or gate behavior changed' },
|
|
11
|
+
{ pattern: /^adapters\//i, surface: 'agent-adapter', reason: 'agent adapter changed' },
|
|
12
|
+
{ pattern: /^plugins\//i, surface: 'plugin', reason: 'plugin install path changed' },
|
|
13
|
+
{ pattern: /^package\.json$/i, surface: 'package', reason: 'package manifest changed' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const SKIP_PATTERNS = [
|
|
17
|
+
/^README\.md$/i,
|
|
18
|
+
/^docs\//i,
|
|
19
|
+
/^reports\//i,
|
|
20
|
+
/^proof\//i,
|
|
21
|
+
/^tests\/.*\.test\.js$/i,
|
|
22
|
+
/^\.claude\/implementation-notes\//i,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function normalizeFiles(files = []) {
|
|
26
|
+
return Array.from(new Set(files
|
|
27
|
+
.map((file) => String(file || '').trim().replace(/^\.?\//, ''))
|
|
28
|
+
.filter(Boolean)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function classifyFile(file) {
|
|
32
|
+
for (const entry of RUNTIME_PATTERNS) {
|
|
33
|
+
if (entry.pattern.test(file)) return { ...entry, file };
|
|
34
|
+
}
|
|
35
|
+
for (const pattern of SKIP_PATTERNS) {
|
|
36
|
+
if (pattern.test(file)) return { surface: 'skip', reason: 'no runtime impact', file };
|
|
37
|
+
}
|
|
38
|
+
return { surface: 'focused', reason: 'unknown runtime impact; run focused checks', file };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseChangedFilesFromDiff(diff = '') {
|
|
42
|
+
const files = [];
|
|
43
|
+
for (const line of String(diff || '').split('\n')) {
|
|
44
|
+
const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
45
|
+
if (match) files.push(match[2]);
|
|
46
|
+
}
|
|
47
|
+
return normalizeFiles(files);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function planQaScenario(input = {}) {
|
|
51
|
+
const files = normalizeFiles(input.files || parseChangedFilesFromDiff(input.diff || ''));
|
|
52
|
+
const classifications = files.map(classifyFile);
|
|
53
|
+
const surfaces = Array.from(new Set(classifications.map((entry) => entry.surface)));
|
|
54
|
+
const runtimeChanges = classifications.filter((entry) => entry.surface !== 'skip');
|
|
55
|
+
const skipOnly = files.length > 0 && runtimeChanges.length === 0;
|
|
56
|
+
|
|
57
|
+
const recommendedRunner = chooseRunner(surfaces, input);
|
|
58
|
+
const userScenario = buildUserScenario(runtimeChanges, input);
|
|
59
|
+
return {
|
|
60
|
+
name: 'thumbgate-user-impact-qa-scenario',
|
|
61
|
+
status: skipOnly ? 'skip' : 'actionable',
|
|
62
|
+
files,
|
|
63
|
+
classifications,
|
|
64
|
+
recommendedRunner,
|
|
65
|
+
userScenario,
|
|
66
|
+
commands: buildCommands(recommendedRunner, runtimeChanges),
|
|
67
|
+
regressionPolicy: skipOnly
|
|
68
|
+
? 'skip durable QA; no runtime-impact files changed'
|
|
69
|
+
: 'if the QA agent finds a deterministic failure, convert it into a focused regression test before opening a fix PR',
|
|
70
|
+
transientFailurePolicy: 'doctor the browser/computer-use runner once, retry once, then label as infrastructure-flaky instead of product-regression',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function chooseRunner(surfaces, input = {}) {
|
|
75
|
+
if (input.forceComputerUse || surfaces.includes('plugin') || surfaces.includes('agent-adapter')) return 'computer-use-qa';
|
|
76
|
+
if (surfaces.includes('browser') || surfaces.includes('api')) return 'browser-qa';
|
|
77
|
+
if (surfaces.includes('cli') || surfaces.includes('agent-runtime') || surfaces.includes('package')) return 'focused-node-qa';
|
|
78
|
+
if (surfaces.every((surface) => surface === 'skip')) return 'skip';
|
|
79
|
+
return 'focused-node-qa';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildUserScenario(runtimeChanges, input = {}) {
|
|
83
|
+
if (runtimeChanges.length === 0) return 'No user-impact scenario required; changed files are docs, tests, reports, or proof artifacts only.';
|
|
84
|
+
const surfaces = Array.from(new Set(runtimeChanges.map((entry) => entry.surface)));
|
|
85
|
+
if (surfaces.includes('browser') || surfaces.includes('api')) {
|
|
86
|
+
return 'Open the affected page as a user, perform the primary CTA or dashboard action, verify visible state changes, then check the related API response.';
|
|
87
|
+
}
|
|
88
|
+
if (surfaces.includes('plugin') || surfaces.includes('agent-adapter')) {
|
|
89
|
+
return 'Install or reload the affected agent integration, run one thumbs-up and one thumbs-down capture, then verify the next risky action is gated.';
|
|
90
|
+
}
|
|
91
|
+
if (surfaces.includes('cli')) {
|
|
92
|
+
return 'Run the changed CLI command with --help and one realistic command path, then verify exit code, JSON output, and no stale command copy.';
|
|
93
|
+
}
|
|
94
|
+
return input.scenario || 'Run the focused test for the changed runtime surface, then verify the behavior with one realistic operator workflow.';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildCommands(runner, runtimeChanges) {
|
|
98
|
+
if (runner === 'skip') return [];
|
|
99
|
+
const commands = ['npm test -- --test-concurrency=1'];
|
|
100
|
+
if (runner === 'browser-qa') commands.push('npx playwright test tests/e2e --project=chromium');
|
|
101
|
+
if (runner === 'computer-use-qa') commands.push('node scripts/qa-scenario-planner.js --doctor-runner');
|
|
102
|
+
if (runtimeChanges.some((entry) => entry.surface === 'package')) commands.push('npm pack --dry-run');
|
|
103
|
+
return commands;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
107
|
+
const args = {};
|
|
108
|
+
for (const arg of argv) {
|
|
109
|
+
if (arg === '--json') args.json = true;
|
|
110
|
+
else if (arg === '--doctor-runner') args.doctorRunner = true;
|
|
111
|
+
else if (arg.startsWith('--files=')) args.files = arg.slice('--files='.length).split(',');
|
|
112
|
+
else if (arg.startsWith('--diff-file=')) args.diff = fs.readFileSync(arg.slice('--diff-file='.length), 'utf8');
|
|
113
|
+
else if (arg.startsWith('--scenario=')) args.scenario = arg.slice('--scenario='.length);
|
|
114
|
+
}
|
|
115
|
+
return args;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (require.main === module) {
|
|
119
|
+
const args = parseArgs();
|
|
120
|
+
if (args.doctorRunner) {
|
|
121
|
+
console.log('QA runner doctor: verify browser/computer-use target, screenshot capture, and network reachability before blaming product code.');
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
const report = planQaScenario(args);
|
|
125
|
+
if (args.json) console.log(JSON.stringify(report, null, 2));
|
|
126
|
+
else {
|
|
127
|
+
console.log(`${report.status.toUpperCase()}: ${report.userScenario}`);
|
|
128
|
+
for (const command of report.commands) console.log(`- ${command}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
classifyFile,
|
|
134
|
+
parseChangedFilesFromDiff,
|
|
135
|
+
planQaScenario,
|
|
136
|
+
};
|
package/scripts/rate-limiter.js
CHANGED
|
@@ -6,42 +6,78 @@ const path = require('path');
|
|
|
6
6
|
const {
|
|
7
7
|
PRO_MONTHLY_PAYMENT_LINK,
|
|
8
8
|
PRO_PRICE_LABEL,
|
|
9
|
-
|
|
9
|
+
ENTERPRISE_PRICE_LABEL,
|
|
10
10
|
} = require('./commercial-offer');
|
|
11
11
|
|
|
12
12
|
const USAGE_FILE = path.join(process.env.HOME || '/tmp', '.thumbgate', 'usage-limits.json');
|
|
13
13
|
|
|
14
14
|
// ──────────────────────────────────────────────────────────
|
|
15
|
-
// Free tier:
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// Free tier: tight enough to create upgrade pressure after
|
|
16
|
+
// real usage. Captures and rules are capped so heavy users
|
|
17
|
+
// hit the wall within the first week, not the first quarter.
|
|
18
18
|
// ──────────────────────────────────────────────────────────
|
|
19
19
|
const FREE_TIER_LIMITS = {
|
|
20
|
-
capture_feedback: { daily:
|
|
21
|
-
prevention_rules: { daily:
|
|
20
|
+
capture_feedback: { daily: 2, lifetime: 10, label: 'feedback captures (2/day, 10 total on free)' },
|
|
21
|
+
prevention_rules: { daily: 2, lifetime: 3, label: 'prevention rules generated (2/day on free)' },
|
|
22
22
|
recall: { daily: 0, lifetime: 0, label: 'recall queries (Pro only)' },
|
|
23
23
|
search_lessons: { daily: 0, lifetime: 0, label: 'lesson searches (Pro only)' },
|
|
24
24
|
search_thumbgate: { daily: 0, lifetime: 0, label: 'ThumbGate searches (Pro only)' },
|
|
25
25
|
commerce_recall: { daily: 0, lifetime: 0, label: 'commerce recalls (Pro only)' },
|
|
26
26
|
export_dpo: { daily: 0, lifetime: 0, label: 'DPO exports (Pro only)' },
|
|
27
27
|
export_databricks: { daily: 0, lifetime: 0, label: 'Databricks exports (Pro only)' },
|
|
28
|
-
construct_context_pack: { daily:
|
|
28
|
+
construct_context_pack: { daily: 3, lifetime: Infinity, label: 'context packs (3/day on free)' },
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const FREE_TIER_MAX_GATES =
|
|
31
|
+
const FREE_TIER_MAX_GATES = 3; // 3 active prevention rules on free; Pro is unlimited
|
|
32
|
+
const FREE_TIER_DAILY_BLOCKS = 2; // 3 gate blocks/day on free; after limit, deny → warn + upgrade CTA
|
|
32
33
|
|
|
33
|
-
const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n
|
|
34
|
+
const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
|
|
34
35
|
|
|
35
36
|
const PAYWALL_MESSAGES = {
|
|
36
|
-
|
|
37
|
+
capture_feedback: 'Free tier: 2 captures/day (10 total). Your feedback is stored locally — upgrade to capture unlimited.',
|
|
38
|
+
prevention_rules: 'Free tier includes 3 active prevention rules and 2 rule generations/day. Upgrade to Pro for unlimited rules.',
|
|
37
39
|
recall: 'Recall is a Pro feature. Your past feedback is stored locally — upgrade to search and reuse it.',
|
|
38
40
|
search_lessons: 'Lesson search is a Pro feature. Upgrade to find patterns in your agent\'s mistakes.',
|
|
41
|
+
construct_context_pack: 'Free tier: 3 context packs/day. Upgrade to Pro for unlimited.',
|
|
39
42
|
default: 'This feature requires Pro. Start Pro — card required; billed today.',
|
|
40
43
|
};
|
|
41
44
|
|
|
45
|
+
const TRIAL_DAYS = 7;
|
|
46
|
+
|
|
47
|
+
function getInstallAgeDays() {
|
|
48
|
+
try {
|
|
49
|
+
const { INSTALL_ID_PATH } = require('./cli-telemetry');
|
|
50
|
+
if (!fs.existsSync(INSTALL_ID_PATH)) return null;
|
|
51
|
+
// Use mtimeMs — birthtimeMs is unreliable on Linux (ext4 doesn't backdate creation time).
|
|
52
|
+
// The install-id file is written once at install, so mtime == creation time in practice.
|
|
53
|
+
const stat = fs.statSync(INSTALL_ID_PATH);
|
|
54
|
+
const created = stat.mtimeMs || stat.birthtimeMs;
|
|
55
|
+
if (!Number.isFinite(created) || created <= 0) return null;
|
|
56
|
+
return (Date.now() - created) / (1000 * 60 * 60 * 24);
|
|
57
|
+
} catch (_) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isInTrialPeriod() {
|
|
63
|
+
if (process.env.CI || process.env.GITHUB_ACTIONS) return false;
|
|
64
|
+
if (process.env.THUMBGATE_NO_TRIAL === '1') return false;
|
|
65
|
+
const age = getInstallAgeDays();
|
|
66
|
+
if (age === null) return false;
|
|
67
|
+
if (age < 0.0007) return false; // <1 minute old — just-created install, not a real trial
|
|
68
|
+
return age < TRIAL_DAYS;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function trialDaysRemaining() {
|
|
72
|
+
const age = getInstallAgeDays();
|
|
73
|
+
if (age === null) return 0;
|
|
74
|
+
return Math.max(0, Math.ceil(TRIAL_DAYS - age));
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
function isProTier(authContext) {
|
|
43
78
|
if (authContext && authContext.tier === 'pro') return true;
|
|
44
|
-
if (process.env.THUMBGATE_API_KEY
|
|
79
|
+
if (process.env.THUMBGATE_API_KEY) return true;
|
|
80
|
+
if (process.env.THUMBGATE_NO_RATE_LIMIT === '1') return true;
|
|
45
81
|
// Creator/dogfooding bypass: when the owner has the dev secret + bypass
|
|
46
82
|
// configured (env or ~/.config/thumbgate/dev.json), treat the install as Pro
|
|
47
83
|
// so marketing nudges and rate limits stop firing on the maintainer's own
|
|
@@ -57,6 +93,9 @@ function isProTier(authContext) {
|
|
|
57
93
|
const { isProLicensed } = require('./license');
|
|
58
94
|
if (isProLicensed()) return true;
|
|
59
95
|
} catch (_) {}
|
|
96
|
+
// 7-day reverse trial: new installs get full Pro access, then hit a clear
|
|
97
|
+
// hosted-sync/unlimited-rules pay moment while the product is still fresh.
|
|
98
|
+
if (isInTrialPeriod()) return true;
|
|
60
99
|
return false;
|
|
61
100
|
}
|
|
62
101
|
|
|
@@ -114,12 +153,18 @@ function checkLimit(action, authContext) {
|
|
|
114
153
|
const dailyCurrent = usage.counts[action] || 0;
|
|
115
154
|
const lifetimeCurrent = usage.lifetime[action] || 0;
|
|
116
155
|
|
|
156
|
+
const isCapture = action === 'capture_feedback';
|
|
157
|
+
const monthlyLink = isCapture
|
|
158
|
+
? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
|
|
159
|
+
: PRO_MONTHLY_PAYMENT_LINK;
|
|
160
|
+
const upgradeMessage = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${monthlyLink}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
|
|
161
|
+
|
|
117
162
|
// Check lifetime limit first (the hard wall)
|
|
118
163
|
if (lifetimeLimit !== Infinity && lifetimeCurrent >= lifetimeLimit) {
|
|
119
164
|
const paywallMsg = PAYWALL_MESSAGES[action] || PAYWALL_MESSAGES.default;
|
|
120
165
|
return {
|
|
121
166
|
allowed: false,
|
|
122
|
-
message: `${paywallMsg}\n\n${
|
|
167
|
+
message: `${paywallMsg}\n\n${upgradeMessage}`,
|
|
123
168
|
used: lifetimeCurrent,
|
|
124
169
|
limit: lifetimeLimit,
|
|
125
170
|
limitType: 'lifetime',
|
|
@@ -128,9 +173,10 @@ function checkLimit(action, authContext) {
|
|
|
128
173
|
|
|
129
174
|
// Check daily limit
|
|
130
175
|
if (dailyLimit !== Infinity && dailyCurrent >= dailyLimit) {
|
|
176
|
+
const paywallMsg = PAYWALL_MESSAGES[action] || PAYWALL_MESSAGES.default;
|
|
131
177
|
return {
|
|
132
178
|
allowed: false,
|
|
133
|
-
message: `Daily limit reached. ${
|
|
179
|
+
message: `Daily limit reached. ${paywallMsg}\n\n${upgradeMessage}`,
|
|
134
180
|
used: dailyCurrent,
|
|
135
181
|
limit: dailyLimit,
|
|
136
182
|
limitType: 'daily',
|
|
@@ -179,12 +225,17 @@ function getUsage(action, authContext) {
|
|
|
179
225
|
module.exports = {
|
|
180
226
|
checkLimit,
|
|
181
227
|
getUsage,
|
|
228
|
+
getInstallAgeDays,
|
|
182
229
|
isProTier,
|
|
230
|
+
isInTrialPeriod,
|
|
231
|
+
trialDaysRemaining,
|
|
183
232
|
loadUsage,
|
|
184
233
|
saveUsage,
|
|
185
234
|
todayKey,
|
|
186
235
|
FREE_TIER_LIMITS,
|
|
187
236
|
FREE_TIER_MAX_GATES,
|
|
237
|
+
FREE_TIER_DAILY_BLOCKS,
|
|
238
|
+
TRIAL_DAYS,
|
|
188
239
|
UPGRADE_MESSAGE,
|
|
189
240
|
PAYWALL_MESSAGES,
|
|
190
241
|
USAGE_FILE,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// repeat-metric — first-class "repeat-attempts blocked before execution" metric
|
|
5
|
+
//
|
|
6
|
+
// This module exposes data ThumbGate already collects in gate-stats state. It
|
|
7
|
+
// does NOT write to disk; it is a pure function over gates-engine.loadStats().
|
|
8
|
+
//
|
|
9
|
+
// The headline number is stats.recurringBlocks — incremented by recordStat()
|
|
10
|
+
// in gates-engine.js every time the same gate blocks/warns the same sanitized
|
|
11
|
+
// action fingerprint within one session bucket. That is "a pre-action gate fire
|
|
12
|
+
// that stopped a tool call the agent had already been blocked on", rather than
|
|
13
|
+
// merely "the same noisy gate fired again."
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const gatesEngine = require('./gates-engine');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Derive a per-gate { firstBlocks, repeatBlocks } split from the raw stats.
|
|
20
|
+
*
|
|
21
|
+
* Modern stats record, per session bucket, which sanitized action fingerprints
|
|
22
|
+
* each gate fired on:
|
|
23
|
+
* stats.sessionFiredActions[sessionKey][gateId][fingerprint] === true
|
|
24
|
+
*
|
|
25
|
+
* firstBlocks is the count of distinct first action fingerprints. Legacy stats
|
|
26
|
+
* without fingerprints fall back to the old per-session-gate split.
|
|
27
|
+
*
|
|
28
|
+
* total block+warn events come from stats.byGate[id] (blocked + warned), which
|
|
29
|
+
* recordStat() also maintains. repeatBlocks is clamped to >= 0 to stay robust
|
|
30
|
+
* against partially-written / legacy state.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} stats raw object returned by gates-engine.loadStats()
|
|
33
|
+
* @returns {Object<string,{firstBlocks:number, repeatBlocks:number}>}
|
|
34
|
+
*/
|
|
35
|
+
function computeByGateSplit(stats) {
|
|
36
|
+
const byGate = {};
|
|
37
|
+
const sessionFiredActions = (stats && stats.sessionFiredActions) || {};
|
|
38
|
+
const sessionFiredGates = (stats && stats.sessionFiredGates) || {};
|
|
39
|
+
const rawByGate = (stats && stats.byGate) || {};
|
|
40
|
+
|
|
41
|
+
// Count distinct action fingerprints each gate fired on => firstBlocks.
|
|
42
|
+
const firstBlocksByGate = {};
|
|
43
|
+
const gatesWithActionStats = new Set();
|
|
44
|
+
for (const sessionKey of Object.keys(sessionFiredActions)) {
|
|
45
|
+
const fired = sessionFiredActions[sessionKey] || {};
|
|
46
|
+
for (const gateId of Object.keys(fired)) {
|
|
47
|
+
const fingerprints = fired[gateId] || {};
|
|
48
|
+
const count = Object.values(fingerprints).filter(Boolean).length;
|
|
49
|
+
if (count > 0) {
|
|
50
|
+
gatesWithActionStats.add(gateId);
|
|
51
|
+
firstBlocksByGate[gateId] = (firstBlocksByGate[gateId] || 0) + count;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Legacy fallback: old stats only tracked gate fired per session bucket.
|
|
57
|
+
for (const sessionKey of Object.keys(sessionFiredGates)) {
|
|
58
|
+
const fired = sessionFiredGates[sessionKey] || {};
|
|
59
|
+
for (const gateId of Object.keys(fired)) {
|
|
60
|
+
if (fired[gateId] && !gatesWithActionStats.has(gateId)) {
|
|
61
|
+
firstBlocksByGate[gateId] = (firstBlocksByGate[gateId] || 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Union of every gate id we know about from either source.
|
|
67
|
+
const gateIds = new Set([
|
|
68
|
+
...Object.keys(rawByGate),
|
|
69
|
+
...Object.keys(firstBlocksByGate),
|
|
70
|
+
...Object.keys(sessionFiredActions).flatMap((sessionKey) => Object.keys(sessionFiredActions[sessionKey] || {})),
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
for (const gateId of gateIds) {
|
|
74
|
+
const gateStat = rawByGate[gateId] || {};
|
|
75
|
+
const totalFires = (gateStat.blocked || 0) + (gateStat.warned || 0);
|
|
76
|
+
const firstBlocks = firstBlocksByGate[gateId] || 0;
|
|
77
|
+
// Repeat fires are total fires beyond the first fire per session bucket.
|
|
78
|
+
const repeatBlocks = Math.max(0, totalFires - firstBlocks);
|
|
79
|
+
byGate[gateId] = { firstBlocks, repeatBlocks };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return byGate;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compute the repeat-attempts-blocked-before-execution metric.
|
|
87
|
+
*
|
|
88
|
+
* Pure read of gates-engine.loadStats(); no disk writes.
|
|
89
|
+
*
|
|
90
|
+
* @returns {{
|
|
91
|
+
* repeatBlocksBeforeExecution: number,
|
|
92
|
+
* recurringBlocks: number,
|
|
93
|
+
* totalBlocked: number,
|
|
94
|
+
* byGate: Object<string,{firstBlocks:number, repeatBlocks:number}>
|
|
95
|
+
* }}
|
|
96
|
+
*/
|
|
97
|
+
function computeRepeatMetric() {
|
|
98
|
+
let stats;
|
|
99
|
+
try {
|
|
100
|
+
stats = gatesEngine.loadStats() || {};
|
|
101
|
+
} catch (_) {
|
|
102
|
+
stats = {};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const recurringBlocks = Number(stats.recurringBlocks || 0);
|
|
106
|
+
const totalBlocked = Number(stats.blocked || 0);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
// Headline: a pre-action block that stopped a tool call the agent had
|
|
110
|
+
// already been blocked on this session.
|
|
111
|
+
repeatBlocksBeforeExecution: recurringBlocks,
|
|
112
|
+
recurringBlocks,
|
|
113
|
+
totalBlocked,
|
|
114
|
+
byGate: computeByGateSplit(stats),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Add a `repeat` sub-key to a gate-stats object WITHOUT mutating the original.
|
|
120
|
+
*
|
|
121
|
+
* Takes the object returned by gate-stats.calculateStats() or
|
|
122
|
+
* dashboard.computeGateStats() and returns a shallow copy with the repeat
|
|
123
|
+
* metric attached. The caller's file does not need to import any internals.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} gateStatsObject
|
|
126
|
+
* @returns {object} copy of gateStatsObject with `.repeat`
|
|
127
|
+
*/
|
|
128
|
+
function mergeRepeatMetricIntoGateStats(gateStatsObject) {
|
|
129
|
+
const base = gateStatsObject && typeof gateStatsObject === 'object' ? gateStatsObject : {};
|
|
130
|
+
return Object.assign({}, base, { repeat: computeRepeatMetric() });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
computeRepeatMetric,
|
|
135
|
+
mergeRepeatMetricIntoGateStats,
|
|
136
|
+
computeByGateSplit,
|
|
137
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FIXTURE_TOKENS = {
|
|
4
|
+
awsAccessKeyId: '__TG_FIXTURE_AWS_ACCESS_KEY_ID__',
|
|
5
|
+
githubPat: '__TG_FIXTURE_GITHUB_PAT__',
|
|
6
|
+
openAiLegacyKey: '__TG_FIXTURE_OPENAI_LEGACY_KEY__',
|
|
7
|
+
openAiProjectKey: '__TG_FIXTURE_OPENAI_PROJECT_KEY__',
|
|
8
|
+
rsaPrivateKeyHeader: '__TG_FIXTURE_RSA_PRIVATE_KEY_HEADER__',
|
|
9
|
+
ecPrivateKeyHeader: '__TG_FIXTURE_EC_PRIVATE_KEY_HEADER__',
|
|
10
|
+
privateKeyHeader: '__TG_FIXTURE_PRIVATE_KEY_HEADER__',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function buildAwsAccessKeyId() {
|
|
14
|
+
return ['AKIA', 'IOSFODNN7EXAMPLE'].join('');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildGitHubPat() {
|
|
18
|
+
return ['gh', 'p_', 'x'.repeat(36)].join('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildOpenAiLegacyKey() {
|
|
22
|
+
return ['sk', '-', 'abcdefghijklmnopqrstuvwxyz01234567890'].join('');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildOpenAiProjectKey() {
|
|
26
|
+
return ['sk', '-proj-', 'abcdefghijklmnopqrstuvwxyz01234567890'].join('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildPemHeader(prefix = '') {
|
|
30
|
+
return ['-----BEGIN ', prefix, 'PRIVATE KEY-----'].join('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fixtureReplacements() {
|
|
34
|
+
return [
|
|
35
|
+
[FIXTURE_TOKENS.awsAccessKeyId, buildAwsAccessKeyId()],
|
|
36
|
+
[FIXTURE_TOKENS.githubPat, buildGitHubPat()],
|
|
37
|
+
[FIXTURE_TOKENS.openAiLegacyKey, buildOpenAiLegacyKey()],
|
|
38
|
+
[FIXTURE_TOKENS.openAiProjectKey, buildOpenAiProjectKey()],
|
|
39
|
+
[FIXTURE_TOKENS.rsaPrivateKeyHeader, buildPemHeader('RSA ')],
|
|
40
|
+
[FIXTURE_TOKENS.ecPrivateKeyHeader, buildPemHeader('EC ')],
|
|
41
|
+
[FIXTURE_TOKENS.privateKeyHeader, buildPemHeader('')],
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function expandFixturePlaceholders(value) {
|
|
46
|
+
let expanded = String(value || '');
|
|
47
|
+
for (const [token, replacement] of fixtureReplacements()) {
|
|
48
|
+
expanded = expanded.split(token).join(replacement);
|
|
49
|
+
}
|
|
50
|
+
return expanded;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
FIXTURE_TOKENS,
|
|
55
|
+
buildAwsAccessKeyId,
|
|
56
|
+
buildGitHubPat,
|
|
57
|
+
buildOpenAiLegacyKey,
|
|
58
|
+
buildOpenAiProjectKey,
|
|
59
|
+
buildPemHeader,
|
|
60
|
+
expandFixturePlaceholders,
|
|
61
|
+
};
|