thumbgate 1.27.19 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +63 -45
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +1 -1
- 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/bin/cli.js +52 -9
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +14 -23
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/entitlement-public-keys.json +6 -0
- package/config/gates/default.json +1 -1
- package/config/github-about.json +2 -2
- package/config/merge-quality-checks.json +3 -0
- package/package.json +233 -100
- package/public/about.html +159 -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 +12 -9
- 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 +5 -5
- package/public/compare.html +10 -10
- package/public/dashboard.html +1 -1
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +14 -14
- package/public/index.html +81 -70
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +4 -17
- package/public/numbers.html +2 -2
- package/public/pricing.html +392 -0
- package/public/pro.html +2 -2
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-reward-model.js +13 -0
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/bayes-optimal-gate.js +6 -1
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +17 -1
- package/scripts/commercial-offer.js +3 -3
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/entitlement.js +250 -0
- package/scripts/export-databricks-bundle.js +5 -0
- package/scripts/export-dpo-pairs.js +6 -0
- package/scripts/export-hf-dataset.js +5 -0
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/gates-engine.js +15 -3
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/imperative-detector.js +85 -0
- package/scripts/install-shim.js +87 -0
- package/scripts/intervention-policy.js +13 -0
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/noop-detect.js +285 -0
- 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/pr-manager.js +9 -22
- package/scripts/pro-local-dashboard.js +198 -0
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/repeat-metric.js +137 -0
- package/scripts/risk-scorer.js +6 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- 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 +2 -2
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/thompson-sampling.js +11 -2
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/src/api/server.js +81 -10
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1707
package/scripts/pr-manager.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* PR Manager — High-Throughput Merge & Blocker Diagnosis
|
|
4
|
-
*
|
|
5
|
-
* Inspired by the 2026 GitHub 'Quick Access' update. Centralizes merge status
|
|
6
|
-
* detection and triggers autonomous self-healing for common blockers.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
2
|
'use strict';
|
|
10
3
|
|
|
11
4
|
const fs = require('node:fs');
|
|
@@ -34,6 +27,8 @@ const PASSING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.passingBuckets || []).map(
|
|
|
34
27
|
const PENDING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.pendingBuckets || []).map((value) => String(value || '').toLowerCase()));
|
|
35
28
|
const FAILING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.failingBuckets || []).map((value) => String(value || '').toLowerCase()));
|
|
36
29
|
|
|
30
|
+
const SELF_REFERENTIAL_CHECKS = new Set(MERGE_QUALITY_CHECKS.selfReferentialChecks || []);
|
|
31
|
+
|
|
37
32
|
function assertSafeGhArgs(args) {
|
|
38
33
|
if (!Array.isArray(args) || args.length === 0) {
|
|
39
34
|
throw new Error('GH CLI args must be a non-empty array.');
|
|
@@ -119,9 +114,6 @@ function isMissingCurrentBranchPr(result, prNumber) {
|
|
|
119
114
|
|| /could not determine current branch:.*not on any branch/i.test(formatGhError(result));
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
/**
|
|
123
|
-
* Fetch granular PR status using GH CLI
|
|
124
|
-
*/
|
|
125
117
|
function getPrStatus(prNumber = '', runner = runGh) {
|
|
126
118
|
const normalizedPrNumber = normalizePrNumber(prNumber);
|
|
127
119
|
const args = ['pr', 'view'];
|
|
@@ -182,6 +174,9 @@ function summarizeChecks(checks = []) {
|
|
|
182
174
|
|
|
183
175
|
for (const check of checks) {
|
|
184
176
|
const name = check.name || 'unknown-check';
|
|
177
|
+
|
|
178
|
+
if (SELF_REFERENTIAL_CHECKS.has(name)) continue;
|
|
179
|
+
|
|
185
180
|
const bucket = String(check.bucket || '').toLowerCase();
|
|
186
181
|
if (bucket) {
|
|
187
182
|
if (FAILING_BUCKETS.has(bucket)) {
|
|
@@ -225,9 +220,6 @@ function sleep(ms) {
|
|
|
225
220
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
226
221
|
}
|
|
227
222
|
|
|
228
|
-
/**
|
|
229
|
-
* Diagnose and resolve blockers autonomously
|
|
230
|
-
*/
|
|
231
223
|
async function resolveBlockers(pr, runner = runGh) {
|
|
232
224
|
const title = pr.title || 'Untitled PR';
|
|
233
225
|
const mergeState = pr.mergeStateStatus || 'UNKNOWN';
|
|
@@ -241,7 +233,6 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
241
233
|
return { status: 'skipped', reason: 'draft' };
|
|
242
234
|
}
|
|
243
235
|
|
|
244
|
-
// 1. Handle Outdated Branch (BEHIND)
|
|
245
236
|
if (pr.mergeStateStatus === 'BEHIND') {
|
|
246
237
|
console.log('[PR Manager] PR is behind main. Triggering auto-update...');
|
|
247
238
|
const update = runner(['pr', 'update-branch', pr.number.toString()]);
|
|
@@ -250,13 +241,11 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
250
241
|
}
|
|
251
242
|
}
|
|
252
243
|
|
|
253
|
-
// 2. Handle Merge Conflicts (DIRTY)
|
|
254
244
|
if (pr.mergeStateStatus === 'DIRTY' || pr.mergeable === 'CONFLICTING') {
|
|
255
245
|
console.log('[PR Manager] CRITICAL: Merge conflicts detected. Manual intervention or advanced rebase required.');
|
|
256
246
|
return { status: 'blocked', reason: 'conflicts' };
|
|
257
247
|
}
|
|
258
248
|
|
|
259
|
-
// 3. Handle CI Failures
|
|
260
249
|
let checks = pr.statusCheckRollup || [];
|
|
261
250
|
let checkSource = 'statusCheckRollup';
|
|
262
251
|
|
|
@@ -282,7 +271,6 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
282
271
|
return { status: 'blocked', reason: 'ci_pending', checks: checkSummary.pending, checkSource };
|
|
283
272
|
}
|
|
284
273
|
|
|
285
|
-
// 4. Handle Review Blockers
|
|
286
274
|
if (pr.reviewDecision === 'CHANGES_REQUESTED') {
|
|
287
275
|
console.log('[PR Manager] BLOCKED: Changes requested by reviewer.');
|
|
288
276
|
return { status: 'blocked', reason: 'changes_requested' };
|
|
@@ -293,8 +281,10 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
293
281
|
return { status: 'blocked', reason: 'review_required' };
|
|
294
282
|
}
|
|
295
283
|
|
|
296
|
-
|
|
297
|
-
|
|
284
|
+
if (
|
|
285
|
+
(pr.mergeStateStatus === 'CLEAN' || pr.mergeStateStatus === 'UNSTABLE')
|
|
286
|
+
&& pr.mergeable === 'MERGEABLE'
|
|
287
|
+
) {
|
|
298
288
|
console.log('[PR Manager] SUCCESS: PR is ready for protected autonomous merge.');
|
|
299
289
|
return { status: 'ready' };
|
|
300
290
|
}
|
|
@@ -368,9 +358,6 @@ function submitTrunkMergeRequest(prNumber, runner = runGh) {
|
|
|
368
358
|
};
|
|
369
359
|
}
|
|
370
360
|
|
|
371
|
-
/**
|
|
372
|
-
* Perform autonomous merge
|
|
373
|
-
*/
|
|
374
361
|
function performMerge(prInput, runner = runGh, options = {}) {
|
|
375
362
|
const pr = (prInput && typeof prInput === 'object')
|
|
376
363
|
? prInput
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_PRO_API = 'https://thumbgate-production.up.railway.app';
|
|
9
|
+
const DEFAULT_PRO_ACTIVATION_ALERT_EMAIL = 'igor.ganapolsky@gmail.com';
|
|
8
10
|
const CREATOR_BYPASS_VALUE = process.env.THUMBGATE_DEV_SECRET || '';
|
|
9
11
|
const CREATOR_BYPASS_ENV = 'THUMBGATE_DEV_BYPASS';
|
|
10
12
|
const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
|
|
@@ -55,6 +57,195 @@ function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE ||
|
|
|
55
57
|
return path.join(getLicenseDir(homeDir), 'license.json');
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function isTruthyEnv(value) {
|
|
61
|
+
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeText(value) {
|
|
65
|
+
return String(value || '').trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function escapeHtml(value) {
|
|
69
|
+
return String(value || '')
|
|
70
|
+
.replace(/&/g, '&')
|
|
71
|
+
.replace(/</g, '<')
|
|
72
|
+
.replace(/>/g, '>')
|
|
73
|
+
.replace(/"/g, '"')
|
|
74
|
+
.replace(/'/g, ''');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fingerprintProKey(key) {
|
|
78
|
+
const normalized = normalizeText(key);
|
|
79
|
+
if (!normalized) return '';
|
|
80
|
+
return `sha256:${crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 12)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveProActivationAlertRecipient(env = process.env) {
|
|
84
|
+
return normalizeText(
|
|
85
|
+
env.THUMBGATE_PRO_ACTIVATION_ALERT_EMAIL ||
|
|
86
|
+
env.THUMBGATE_OPERATOR_ALERT_EMAIL ||
|
|
87
|
+
env.THUMBGATE_SUPPORT_EMAIL ||
|
|
88
|
+
DEFAULT_PRO_ACTIVATION_ALERT_EMAIL
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function canSendProActivationAlert({ env = process.env, sendEmailImpl } = {}) {
|
|
93
|
+
if (isTruthyEnv(env.THUMBGATE_DISABLE_PRO_ACTIVATION_ALERTS)) return false;
|
|
94
|
+
if (!resolveProActivationAlertRecipient(env)) return false;
|
|
95
|
+
if (sendEmailImpl) return true;
|
|
96
|
+
return Boolean(normalizeText(env.RESEND_API_KEY || env.THUMBGATE_RESEND_API_KEY));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderProActivationAlertBodies({
|
|
100
|
+
keyFingerprint,
|
|
101
|
+
source,
|
|
102
|
+
version,
|
|
103
|
+
activatedAt,
|
|
104
|
+
hostname,
|
|
105
|
+
platform,
|
|
106
|
+
arch,
|
|
107
|
+
nodeVersion,
|
|
108
|
+
customerId,
|
|
109
|
+
installId,
|
|
110
|
+
usageCount,
|
|
111
|
+
} = {}) {
|
|
112
|
+
const occurredAt = activatedAt || new Date().toISOString();
|
|
113
|
+
const runtimePlatform = platform || process.platform;
|
|
114
|
+
const runtimeArch = arch || process.arch;
|
|
115
|
+
const runtimeNode = nodeVersion || process.version;
|
|
116
|
+
const text = [
|
|
117
|
+
'ThumbGate Pro activation detected.',
|
|
118
|
+
'',
|
|
119
|
+
`Activated at: ${occurredAt}`,
|
|
120
|
+
`Key fingerprint: ${keyFingerprint || 'unknown'}`,
|
|
121
|
+
`Source: ${source || 'unknown'}`,
|
|
122
|
+
`Version: ${version || 'unknown'}`,
|
|
123
|
+
`Customer ID: ${customerId || 'unknown'}`,
|
|
124
|
+
`Install ID: ${installId || 'unknown'}`,
|
|
125
|
+
`Usage count: ${usageCount ?? 'unknown'}`,
|
|
126
|
+
`Host: ${hostname || 'unknown'}`,
|
|
127
|
+
`Runtime: ${runtimePlatform}/${runtimeArch} on ${runtimeNode}`,
|
|
128
|
+
'',
|
|
129
|
+
'Secret hygiene: this alert intentionally does not include the raw Pro key.',
|
|
130
|
+
].join('\n');
|
|
131
|
+
const html = `<!doctype html>
|
|
132
|
+
<html>
|
|
133
|
+
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#17212b;line-height:1.5;">
|
|
134
|
+
<h1 style="font-size:20px;margin:0 0 12px;">ThumbGate Pro activation detected</h1>
|
|
135
|
+
<table role="presentation" cellspacing="0" cellpadding="0" style="border-collapse:collapse;">
|
|
136
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Activated at</td><td style="padding:4px 0;">${escapeHtml(occurredAt)}</td></tr>
|
|
137
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Key fingerprint</td><td style="padding:4px 0;"><code>${escapeHtml(keyFingerprint || 'unknown')}</code></td></tr>
|
|
138
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Source</td><td style="padding:4px 0;">${escapeHtml(source || 'unknown')}</td></tr>
|
|
139
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Version</td><td style="padding:4px 0;">${escapeHtml(version || 'unknown')}</td></tr>
|
|
140
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Customer ID</td><td style="padding:4px 0;">${escapeHtml(customerId || 'unknown')}</td></tr>
|
|
141
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Install ID</td><td style="padding:4px 0;">${escapeHtml(installId || 'unknown')}</td></tr>
|
|
142
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Usage count</td><td style="padding:4px 0;">${escapeHtml(usageCount ?? 'unknown')}</td></tr>
|
|
143
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Host</td><td style="padding:4px 0;">${escapeHtml(hostname || 'unknown')}</td></tr>
|
|
144
|
+
<tr><td style="padding:4px 14px 4px 0;color:#64748b;">Runtime</td><td style="padding:4px 0;">${escapeHtml(runtimePlatform)}/${escapeHtml(runtimeArch)} on ${escapeHtml(runtimeNode)}</td></tr>
|
|
145
|
+
</table>
|
|
146
|
+
<p style="margin-top:16px;color:#64748b;">Secret hygiene: this alert intentionally does not include the raw Pro key.</p>
|
|
147
|
+
</body>
|
|
148
|
+
</html>`;
|
|
149
|
+
return { text, html };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function sendProActivationAlert({
|
|
153
|
+
key,
|
|
154
|
+
source = 'unknown',
|
|
155
|
+
version,
|
|
156
|
+
customerId,
|
|
157
|
+
installId,
|
|
158
|
+
usageCount,
|
|
159
|
+
env = process.env,
|
|
160
|
+
sendEmailImpl,
|
|
161
|
+
} = {}) {
|
|
162
|
+
const keyFingerprint = fingerprintProKey(key);
|
|
163
|
+
if (!keyFingerprint) return { sent: false, reason: 'missing_key' };
|
|
164
|
+
if (!canSendProActivationAlert({ env, sendEmailImpl })) {
|
|
165
|
+
return { sent: false, reason: 'activation_alert_disabled_or_unconfigured' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const to = resolveProActivationAlertRecipient(env);
|
|
169
|
+
const { html, text } = renderProActivationAlertBodies({
|
|
170
|
+
keyFingerprint,
|
|
171
|
+
source,
|
|
172
|
+
version,
|
|
173
|
+
customerId,
|
|
174
|
+
installId,
|
|
175
|
+
usageCount,
|
|
176
|
+
activatedAt: new Date().toISOString(),
|
|
177
|
+
hostname: os.hostname(),
|
|
178
|
+
platform: process.platform,
|
|
179
|
+
arch: process.arch,
|
|
180
|
+
nodeVersion: process.version,
|
|
181
|
+
});
|
|
182
|
+
const sender = sendEmailImpl || require('./mailer').sendEmail;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
return await sender({
|
|
186
|
+
to,
|
|
187
|
+
subject: 'ThumbGate Pro activated',
|
|
188
|
+
html,
|
|
189
|
+
text,
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
sent: false,
|
|
194
|
+
reason: 'exception',
|
|
195
|
+
error: error && error.message ? error.message : String(error),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function notifyHostedProActivation({
|
|
201
|
+
key,
|
|
202
|
+
source = 'cli_pro_activate',
|
|
203
|
+
version,
|
|
204
|
+
apiBaseUrl = process.env.THUMBGATE_API_BASE_URL || DEFAULT_PRO_API,
|
|
205
|
+
fetchImpl = globalThis.fetch,
|
|
206
|
+
} = {}) {
|
|
207
|
+
const normalizedKey = normalizeText(key);
|
|
208
|
+
if (!normalizedKey) return { notified: false, reason: 'missing_key' };
|
|
209
|
+
if (typeof fetchImpl !== 'function') return { notified: false, reason: 'no_fetch' };
|
|
210
|
+
|
|
211
|
+
const keyFingerprint = fingerprintProKey(normalizedKey);
|
|
212
|
+
const endpoint = new URL('/v1/billing/pro-activation', apiBaseUrl).toString();
|
|
213
|
+
try {
|
|
214
|
+
const response = await fetchImpl(endpoint, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: {
|
|
217
|
+
authorization: `Bearer ${normalizedKey}`,
|
|
218
|
+
'content-type': 'application/json',
|
|
219
|
+
},
|
|
220
|
+
body: JSON.stringify({
|
|
221
|
+
keyFingerprint,
|
|
222
|
+
source,
|
|
223
|
+
version: version || null,
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
const body = await response.json().catch(() => ({}));
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
return {
|
|
229
|
+
notified: false,
|
|
230
|
+
reason: body && body.detail ? body.detail : `http_${response.status}`,
|
|
231
|
+
status: response.status,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
notified: true,
|
|
236
|
+
status: response.status,
|
|
237
|
+
alert: body.alert || null,
|
|
238
|
+
keyFingerprint: body.keyFingerprint || keyFingerprint,
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
notified: false,
|
|
243
|
+
reason: 'exception',
|
|
244
|
+
error: error && error.message ? error.message : String(error),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
58
249
|
function readLicense({ homeDir } = {}) {
|
|
59
250
|
try {
|
|
60
251
|
return JSON.parse(fs.readFileSync(getLicensePath(homeDir), 'utf8'));
|
|
@@ -162,12 +353,19 @@ module.exports = {
|
|
|
162
353
|
CREATOR_BYPASS_VALUE,
|
|
163
354
|
CREATOR_SYNTHETIC_KEY,
|
|
164
355
|
DEFAULT_PRO_API,
|
|
356
|
+
DEFAULT_PRO_ACTIVATION_ALERT_EMAIL,
|
|
357
|
+
canSendProActivationAlert,
|
|
358
|
+
fingerprintProKey,
|
|
165
359
|
getLicenseDir,
|
|
166
360
|
getLicensePath,
|
|
167
361
|
hasDevOverride,
|
|
168
362
|
isCreatorDev,
|
|
169
363
|
readLicense,
|
|
364
|
+
renderProActivationAlertBodies,
|
|
365
|
+
notifyHostedProActivation,
|
|
366
|
+
resolveProActivationAlertRecipient,
|
|
170
367
|
saveLicense,
|
|
368
|
+
sendProActivationAlert,
|
|
171
369
|
resolveProKey,
|
|
172
370
|
validateProKey,
|
|
173
371
|
startLocalProDashboard,
|
|
@@ -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
|
+
};
|
|
@@ -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
|
+
};
|
package/scripts/risk-scorer.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { resolveFeedbackDir: resolveSharedFeedbackDir } = require('./feedback-paths');
|
|
7
|
+
const { requireLearnedModelsEntitlement } = require('./entitlement');
|
|
7
8
|
|
|
8
9
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
9
10
|
const DEFAULT_FEEDBACK_DIR = resolveSharedFeedbackDir();
|
|
@@ -268,6 +269,10 @@ function buildPatternSummary(rows) {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
function trainRiskModel(rows, options = {}) {
|
|
272
|
+
requireLearnedModelsEntitlement({
|
|
273
|
+
...(options.entitlement || {}),
|
|
274
|
+
label: 'risk-scorer AdaBoost training',
|
|
275
|
+
});
|
|
271
276
|
const registry = buildFeatureRegistry(rows, options);
|
|
272
277
|
const examples = rows.map((row) => ({
|
|
273
278
|
row,
|
|
@@ -419,6 +424,7 @@ function trainAndPersistRiskModel(feedbackDir, options = {}) {
|
|
|
419
424
|
}
|
|
420
425
|
|
|
421
426
|
function getRiskSummary(feedbackDir) {
|
|
427
|
+
requireLearnedModelsEntitlement({ label: 'risk-scorer summary' });
|
|
422
428
|
const resolvedDir = resolveFeedbackDir(feedbackDir);
|
|
423
429
|
const rows = readJSONL(sequencePathFor(resolvedDir));
|
|
424
430
|
if (rows.length === 0) return null;
|
|
@@ -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
|
+
};
|