total-recall-brain 3.18.2 → 3.18.3
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/frontend/dist/assets/index-CGqvdwHT.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/config.ts +16 -2
- package/frontend/src/api/keys.ts +111 -0
- package/frontend/src/api/notifications.ts +54 -6
- package/frontend/src/api/update.ts +12 -1
- package/frontend/src/components/BrainSelector.spec.tsx +71 -47
- package/frontend/src/components/BrainSelector.tsx +112 -35
- package/frontend/src/components/MobilePairing.spec.tsx +44 -14
- package/frontend/src/components/MobilePairing.tsx +229 -22
- package/frontend/src/pages/NotificationsPage.tsx +6 -3
- package/frontend/src/pages/RulesPage.spec.tsx +46 -15
- package/frontend/src/pages/RulesPage.tsx +41 -9
- package/frontend/src/pages/SecretsPage.tsx +316 -3
- package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
- package/frontend/src/pages/SettingsPage.tsx +206 -85
- package/package.json +1 -1
- package/src/cli/agent-dir.mjs +2 -1
- package/src/cli/secret.mjs +180 -3
- package/src/core/daemon-loop.mjs +31 -5
- package/src/core/notifications.mjs +204 -34
- package/src/core/package-auto-update.mjs +99 -3
- package/src/core/pairing.mjs +168 -0
- package/src/core/provider-account-sync.mjs +1145 -0
- package/src/core/provider-catalog.mjs +11 -4
- package/src/core/research.mjs +98 -24
- package/src/core/scheduler.mjs +31 -9
- package/src/core/schema.mjs +23 -0
- package/src/core/secret-integration-research.mjs +325 -0
- package/src/core/secrets-store.mjs +278 -14
- package/src/core/secrets-sync.mjs +1 -1
- package/src/core/source-adapters.mjs +247 -22
- package/src/core/task-executors.mjs +161 -14
- package/src/core/webhook-handlers.mjs +123 -14
- package/src/server/auth.mjs +6 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +24 -0
- package/src/server/routes/config.mjs +64 -23
- package/src/server/routes/mesh.mjs +2 -1
- package/src/server/routes/notifications.mjs +103 -0
- package/src/server/routes/rules.mjs +75 -20
- package/src/server/routes/secrets.mjs +110 -4
- package/src/server/routes/system.mjs +20 -1
- package/src/server/routes/update.mjs +11 -4
- package/frontend/dist/assets/index-D5xRw5uq.js +0 -5750
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live provider account / usage / subscription sync for vault secrets.
|
|
3
|
+
*
|
|
4
|
+
* Probes real vendor APIs where they expose key/account endpoints.
|
|
5
|
+
* Secrets that look like API credentials MUST end up tracked (ok) or ERROR.
|
|
6
|
+
* Operator may set tracking_exempt + monthly_cost_usd when a vendor has no API.
|
|
7
|
+
*
|
|
8
|
+
* Tracking statuses:
|
|
9
|
+
* ok — account and/or usage/subscription cost signal retrieved from provider API
|
|
10
|
+
* error — invalid key, no probe, or probe cannot return cost/subscription (strict)
|
|
11
|
+
* exempt — operator waived (self-hosted / non-billable / manual monthly_cost_usd)
|
|
12
|
+
*
|
|
13
|
+
* Reality (2026):
|
|
14
|
+
* - OpenRouter: GET /api/v1/key → usage + credit limits (full) ✅
|
|
15
|
+
* - ElevenLabs: GET /v1/user/subscription → credits + tier (full) ✅
|
|
16
|
+
* - GitHub: /user + /rate_limit (account + quota; $ needs billing APIs)
|
|
17
|
+
* - DigitalOcean: /v2/account (account; balance needs billing scope)
|
|
18
|
+
* - OpenAI/Anthropic: project keys validate only; $ needs Admin API keys
|
|
19
|
+
* - Brave/Serper/Tavily/Exa: validity only — no balance on search key → ERROR
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { throttledFetch } from './throttled-fetch.mjs';
|
|
23
|
+
import { getSecret, listSecretsMeta, updateSecretMeta, recordUsage } from './secrets-store.mjs';
|
|
24
|
+
import { logger } from './logger.mjs';
|
|
25
|
+
import { providerForKeyName } from './provider-catalog.mjs';
|
|
26
|
+
|
|
27
|
+
const TIMEOUT_MS = 12_000;
|
|
28
|
+
|
|
29
|
+
/** Providers that are never billable cloud spend (self-hosted / free bots / local). */
|
|
30
|
+
const SELF_HOSTED_OR_FREE = new Set([
|
|
31
|
+
'mailcow',
|
|
32
|
+
'headscale',
|
|
33
|
+
'telegram',
|
|
34
|
+
'total-recall',
|
|
35
|
+
'redis',
|
|
36
|
+
'telephony',
|
|
37
|
+
'mail',
|
|
38
|
+
'asterisk',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** Key-name patterns that are internal passwords / config, not billable vendor API credentials. */
|
|
42
|
+
function looksLikeInternalPassword(key) {
|
|
43
|
+
const k = String(key || '').toUpperCase();
|
|
44
|
+
if (/^(TR_|TOTAL_RECALL_)/.test(k) && /PASSWORD|SECRET|TOKEN/.test(k)) return true;
|
|
45
|
+
if (/_PASSWORD$|_SIP_SECRET$|_ARI_PASSWORD$|_MASTER_PASSWORD$|_DOVECOT_/.test(k)) return true;
|
|
46
|
+
if (/^(ADMIN_API_SECRET|ADMIN_API_TOKEN|JWT_SECRET|SESSION_SECRET|ENCRYPTION_KEY|BETTER_AUTH_SECRET)$/.test(k)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (/SSH_KEY|SSH_PRIVATE|PRIVATE_KEY$|_CLIENT_SECRET$|APP_PRIVATE_KEY/.test(k)) return true;
|
|
50
|
+
if (/_WEBHOOK_SECRET$|_SIGNING_SECRET$|COOKIE_SECRET|NEXTAUTH_SECRET/.test(k)) return true;
|
|
51
|
+
if (/_LOGIN_EMAIL$|_EMAIL$/.test(k) && !/API/.test(k)) return true;
|
|
52
|
+
if (/SSO_SECRET|RECOVERY_CODE|TEST_USER_/.test(k)) return true;
|
|
53
|
+
// Project URLs and public/anon client keys are not billing credentials
|
|
54
|
+
if (/_URL$|_ANON_KEY$|PUBLISHABLE_KEY|NEXT_PUBLIC_|VITE_.*_ANON/.test(k)) return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function baseResult(key, provider) {
|
|
59
|
+
return {
|
|
60
|
+
key,
|
|
61
|
+
provider: provider || null,
|
|
62
|
+
tracking_status: 'error',
|
|
63
|
+
account_api: false,
|
|
64
|
+
usage_api: false,
|
|
65
|
+
subscription_api: false,
|
|
66
|
+
key_valid: null,
|
|
67
|
+
account: null,
|
|
68
|
+
usage: null,
|
|
69
|
+
subscription: null,
|
|
70
|
+
docs_url: null,
|
|
71
|
+
pricing_url: null,
|
|
72
|
+
console_url: null,
|
|
73
|
+
error: null,
|
|
74
|
+
probe: null,
|
|
75
|
+
synced_at: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function fetchJson(url, headers = {}, timeoutMs = TIMEOUT_MS) {
|
|
80
|
+
const res = await throttledFetch(
|
|
81
|
+
url,
|
|
82
|
+
{ headers: { Accept: 'application/json', ...headers } },
|
|
83
|
+
timeoutMs,
|
|
84
|
+
);
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
let body = null;
|
|
87
|
+
try {
|
|
88
|
+
body = text ? JSON.parse(text) : null;
|
|
89
|
+
} catch {
|
|
90
|
+
body = { raw: text?.slice(0, 200) };
|
|
91
|
+
}
|
|
92
|
+
return { res, body };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** OpenRouter — full credit/usage on the API key itself. */
|
|
96
|
+
async function probeOpenRouter(value) {
|
|
97
|
+
const out = baseResult('OPENROUTER', 'openrouter');
|
|
98
|
+
out.probe = 'GET https://openrouter.ai/api/v1/key';
|
|
99
|
+
out.docs_url = 'https://openrouter.ai/docs';
|
|
100
|
+
out.pricing_url = 'https://openrouter.ai/models';
|
|
101
|
+
out.console_url = 'https://openrouter.ai/keys';
|
|
102
|
+
out.account_api = true;
|
|
103
|
+
out.usage_api = true;
|
|
104
|
+
out.subscription_api = true;
|
|
105
|
+
const { res, body } = await fetchJson('https://openrouter.ai/api/v1/key', {
|
|
106
|
+
Authorization: `Bearer ${value}`,
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
out.key_valid = false;
|
|
110
|
+
out.error = `OpenRouter key probe failed HTTP ${res.status}`;
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
const d = body?.data || body || {};
|
|
114
|
+
out.key_valid = true;
|
|
115
|
+
out.tracking_status = 'ok';
|
|
116
|
+
out.account = {
|
|
117
|
+
label: d.label || null,
|
|
118
|
+
is_free_tier: d.is_free_tier ?? null,
|
|
119
|
+
};
|
|
120
|
+
out.usage = {
|
|
121
|
+
credits_used_all_time: d.usage ?? null,
|
|
122
|
+
credits_used_daily: d.usage_daily ?? null,
|
|
123
|
+
credits_used_weekly: d.usage_weekly ?? null,
|
|
124
|
+
credits_used_monthly: d.usage_monthly ?? null,
|
|
125
|
+
};
|
|
126
|
+
out.subscription = {
|
|
127
|
+
credit_limit: d.limit ?? null,
|
|
128
|
+
credit_limit_remaining: d.limit_remaining ?? null,
|
|
129
|
+
limit_reset: d.limit_reset ?? null,
|
|
130
|
+
};
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** ElevenLabs — full subscription + character credits on the same key. */
|
|
135
|
+
async function probeElevenLabs(value) {
|
|
136
|
+
const out = baseResult('ELEVENLABS', 'elevenlabs');
|
|
137
|
+
out.probe = 'GET https://api.elevenlabs.io/v1/user/subscription';
|
|
138
|
+
out.docs_url = 'https://elevenlabs.io/docs/api-reference/user/subscription/get';
|
|
139
|
+
out.pricing_url = 'https://elevenlabs.io/pricing';
|
|
140
|
+
out.console_url = 'https://elevenlabs.io/app/settings/api-keys';
|
|
141
|
+
out.account_api = true;
|
|
142
|
+
out.usage_api = true;
|
|
143
|
+
out.subscription_api = true;
|
|
144
|
+
const { res, body } = await fetchJson('https://api.elevenlabs.io/v1/user/subscription', {
|
|
145
|
+
'xi-api-key': value,
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
out.key_valid = false;
|
|
149
|
+
out.error = `ElevenLabs subscription probe failed HTTP ${res.status}`;
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
out.key_valid = true;
|
|
153
|
+
out.tracking_status = 'ok';
|
|
154
|
+
out.account = {
|
|
155
|
+
tier: body?.tier || body?.subscription?.tier || null,
|
|
156
|
+
status: body?.status || null,
|
|
157
|
+
};
|
|
158
|
+
out.usage = {
|
|
159
|
+
character_count: body?.character_count ?? null,
|
|
160
|
+
character_limit: body?.character_limit ?? null,
|
|
161
|
+
next_character_count_reset_unix: body?.next_character_count_reset_unix ?? null,
|
|
162
|
+
};
|
|
163
|
+
out.subscription = {
|
|
164
|
+
tier: body?.tier || null,
|
|
165
|
+
status: body?.status || null,
|
|
166
|
+
billing_period: body?.billing_period || null,
|
|
167
|
+
currency: body?.currency || null,
|
|
168
|
+
next_invoice: body?.next_invoice || null,
|
|
169
|
+
can_extend_character_limit: body?.can_extend_character_limit ?? null,
|
|
170
|
+
};
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** GitHub PAT — account + rate limit (not dollar billing). */
|
|
175
|
+
async function probeGithub(value) {
|
|
176
|
+
const out = baseResult('GITHUB', 'github');
|
|
177
|
+
out.probe = 'GET https://api.github.com/user + /rate_limit';
|
|
178
|
+
out.docs_url = 'https://docs.github.com/en/rest';
|
|
179
|
+
out.console_url = 'https://github.com/settings/tokens';
|
|
180
|
+
out.account_api = true;
|
|
181
|
+
out.usage_api = true;
|
|
182
|
+
out.subscription_api = false;
|
|
183
|
+
const headers = {
|
|
184
|
+
Authorization: `Bearer ${value}`,
|
|
185
|
+
'User-Agent': 'TotalRecall-ProviderSync',
|
|
186
|
+
Accept: 'application/vnd.github+json',
|
|
187
|
+
};
|
|
188
|
+
const user = await fetchJson('https://api.github.com/user', headers);
|
|
189
|
+
if (!user.res.ok) {
|
|
190
|
+
out.key_valid = false;
|
|
191
|
+
out.error = `GitHub user probe failed HTTP ${user.res.status}`;
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
const rl = await fetchJson('https://api.github.com/rate_limit', headers);
|
|
195
|
+
out.key_valid = true;
|
|
196
|
+
const planName = String(user.body?.plan?.name || 'free').toLowerCase();
|
|
197
|
+
// Personal free/pro PATs: no cloud $ API; treat as tracked at $0 (Actions/Packages $ needs separate billing)
|
|
198
|
+
const freeish = ['free', 'pro', 'team'].includes(planName) || !user.body?.plan;
|
|
199
|
+
out.account = {
|
|
200
|
+
login: user.body?.login || null,
|
|
201
|
+
id: user.body?.id || null,
|
|
202
|
+
plan: user.body?.plan?.name || null,
|
|
203
|
+
type: user.body?.type || null,
|
|
204
|
+
};
|
|
205
|
+
out.usage = {
|
|
206
|
+
rate_limit: rl.body?.resources?.core || rl.body?.rate || null,
|
|
207
|
+
};
|
|
208
|
+
out.subscription = {
|
|
209
|
+
plan: user.body?.plan || null,
|
|
210
|
+
monthly_cost_usd: freeish ? 0 : null,
|
|
211
|
+
note: freeish
|
|
212
|
+
? 'GitHub account + rate limits tracked; plan has no PAT-level $ spend API (use $0 or set monthly_cost_usd for org billing).'
|
|
213
|
+
: 'GitHub enterprise/org billing not on this PAT.',
|
|
214
|
+
};
|
|
215
|
+
if (freeish) {
|
|
216
|
+
out.tracking_status = 'ok';
|
|
217
|
+
out.subscription_api = true;
|
|
218
|
+
out.error = null;
|
|
219
|
+
} else {
|
|
220
|
+
out.tracking_status = 'partial';
|
|
221
|
+
out.error =
|
|
222
|
+
'GitHub dollar spend is not available on this PAT. Set tracking_exempt + monthly_cost_usd for org billing.';
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** DigitalOcean — account identity (+ optional balance if scope allows). */
|
|
228
|
+
async function probeDigitalOcean(value) {
|
|
229
|
+
const out = baseResult('DIGITALOCEAN', 'digitalocean');
|
|
230
|
+
out.probe = 'GET https://api.digitalocean.com/v2/account';
|
|
231
|
+
out.docs_url = 'https://docs.digitalocean.com/reference/api/';
|
|
232
|
+
out.console_url = 'https://cloud.digitalocean.com/account/api/tokens';
|
|
233
|
+
out.account_api = true;
|
|
234
|
+
out.usage_api = false;
|
|
235
|
+
out.subscription_api = true;
|
|
236
|
+
const { res, body } = await fetchJson('https://api.digitalocean.com/v2/account', {
|
|
237
|
+
Authorization: `Bearer ${value}`,
|
|
238
|
+
});
|
|
239
|
+
if (!res.ok) {
|
|
240
|
+
out.key_valid = false;
|
|
241
|
+
out.error = `DigitalOcean account probe failed HTTP ${res.status}`;
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
const acct = body?.account || body || {};
|
|
245
|
+
out.key_valid = true;
|
|
246
|
+
out.account = {
|
|
247
|
+
email: acct.email || null,
|
|
248
|
+
uuid: acct.uuid || null,
|
|
249
|
+
status: acct.status || null,
|
|
250
|
+
team: acct.team || null,
|
|
251
|
+
};
|
|
252
|
+
out.subscription = {
|
|
253
|
+
status: acct.status || null,
|
|
254
|
+
droplet_limit: acct.droplet_limit ?? null,
|
|
255
|
+
email_verified: acct.email_verified ?? null,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Try customer balance (requires billing:read)
|
|
259
|
+
try {
|
|
260
|
+
const bal = await fetchJson('https://api.digitalocean.com/v2/customers/my/balance', {
|
|
261
|
+
Authorization: `Bearer ${value}`,
|
|
262
|
+
});
|
|
263
|
+
if (bal.res.ok && bal.body) {
|
|
264
|
+
out.usage_api = true;
|
|
265
|
+
out.usage = {
|
|
266
|
+
month_to_date_balance: bal.body.month_to_date_balance ?? null,
|
|
267
|
+
account_balance: bal.body.account_balance ?? null,
|
|
268
|
+
month_to_date_usage: bal.body.month_to_date_usage ?? null,
|
|
269
|
+
generated_at: bal.body.generated_at ?? null,
|
|
270
|
+
};
|
|
271
|
+
out.tracking_status = 'ok';
|
|
272
|
+
out.error = null;
|
|
273
|
+
out.probe += ' + GET /v2/customers/my/balance';
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
/* ignore */
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
out.tracking_status = 'partial';
|
|
281
|
+
out.error =
|
|
282
|
+
'DigitalOcean account tracked; balance needs billing:read scope (GET /v2/customers/my/balance). Re-issue token with billing or set tracking_exempt + monthly_cost_usd.';
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** OpenAI — validate key; try org costs when Admin key. */
|
|
287
|
+
async function probeOpenAI(value) {
|
|
288
|
+
const out = baseResult('OPENAI', 'openai');
|
|
289
|
+
out.probe = 'GET https://api.openai.com/v1/models';
|
|
290
|
+
out.docs_url = 'https://platform.openai.com/docs/api-reference';
|
|
291
|
+
out.pricing_url = 'https://openai.com/api/pricing/';
|
|
292
|
+
out.console_url = 'https://platform.openai.com/api-keys';
|
|
293
|
+
out.account_api = false;
|
|
294
|
+
out.usage_api = false;
|
|
295
|
+
out.subscription_api = false;
|
|
296
|
+
const { res, body } = await fetchJson('https://api.openai.com/v1/models', {
|
|
297
|
+
Authorization: `Bearer ${value}`,
|
|
298
|
+
});
|
|
299
|
+
if (!res.ok) {
|
|
300
|
+
out.key_valid = false;
|
|
301
|
+
out.error = `OpenAI key invalid or blocked HTTP ${res.status}`;
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
out.key_valid = true;
|
|
305
|
+
out.account = { models_visible: Array.isArray(body?.data) ? body.data.length : null };
|
|
306
|
+
|
|
307
|
+
// Admin / org costs (requires organization admin API key)
|
|
308
|
+
const end = new Date();
|
|
309
|
+
const start = new Date(end.getTime() - 30 * 86400000);
|
|
310
|
+
const start_time = Math.floor(start.getTime() / 1000);
|
|
311
|
+
const end_time = Math.floor(end.getTime() / 1000);
|
|
312
|
+
try {
|
|
313
|
+
const costs = await fetchJson(
|
|
314
|
+
`https://api.openai.com/v1/organization/costs?start_time=${start_time}&end_time=${end_time}&bucket_width=1d&limit=30`,
|
|
315
|
+
{ Authorization: `Bearer ${value}` },
|
|
316
|
+
);
|
|
317
|
+
if (costs.res.ok && costs.body) {
|
|
318
|
+
out.account_api = true;
|
|
319
|
+
out.usage_api = true;
|
|
320
|
+
out.subscription_api = true;
|
|
321
|
+
out.tracking_status = 'ok';
|
|
322
|
+
out.probe += ' + GET /v1/organization/costs';
|
|
323
|
+
out.usage = { organization_costs: costs.body };
|
|
324
|
+
out.error = null;
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
/* ignore */
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
out.tracking_status = 'error';
|
|
332
|
+
out.error =
|
|
333
|
+
'OpenAI usage/cost requires Organization Admin API key (GET /v1/organization/costs). Standard project keys only validate. Store an admin key or set tracking_exempt + monthly_cost_usd.';
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Anthropic — validate; try Usage & Cost Admin API. */
|
|
338
|
+
async function probeAnthropic(value) {
|
|
339
|
+
const out = baseResult('ANTHROPIC', 'anthropic');
|
|
340
|
+
out.probe = 'GET https://api.anthropic.com/v1/models';
|
|
341
|
+
out.docs_url = 'https://docs.anthropic.com/en/api/getting-started';
|
|
342
|
+
out.pricing_url = 'https://www.anthropic.com/pricing';
|
|
343
|
+
out.console_url = 'https://console.anthropic.com/settings/keys';
|
|
344
|
+
out.account_api = false;
|
|
345
|
+
out.usage_api = false;
|
|
346
|
+
out.subscription_api = false;
|
|
347
|
+
const headers = {
|
|
348
|
+
'x-api-key': value,
|
|
349
|
+
'anthropic-version': '2023-06-01',
|
|
350
|
+
};
|
|
351
|
+
const { res } = await fetchJson('https://api.anthropic.com/v1/models', headers);
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
// Admin keys may not work on /models — still try cost endpoints
|
|
354
|
+
out.key_valid = res.status !== 401 && res.status !== 403 ? null : false;
|
|
355
|
+
} else {
|
|
356
|
+
out.key_valid = true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const end = new Date();
|
|
360
|
+
const start = new Date(end.getTime() - 30 * 86400000);
|
|
361
|
+
const qs = `starting_at=${start.toISOString().slice(0, 10)}&ending_at=${end.toISOString().slice(0, 10)}`;
|
|
362
|
+
try {
|
|
363
|
+
const cost = await fetchJson(`https://api.anthropic.com/v1/organizations/cost_report?${qs}`, {
|
|
364
|
+
...headers,
|
|
365
|
+
'anthropic-version': '2023-06-01',
|
|
366
|
+
});
|
|
367
|
+
if (cost.res.ok && cost.body) {
|
|
368
|
+
out.key_valid = true;
|
|
369
|
+
out.account_api = true;
|
|
370
|
+
out.usage_api = true;
|
|
371
|
+
out.subscription_api = true;
|
|
372
|
+
out.tracking_status = 'ok';
|
|
373
|
+
out.probe = 'GET /v1/organizations/cost_report';
|
|
374
|
+
out.usage = { cost_report: cost.body };
|
|
375
|
+
out.error = null;
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
/* ignore */
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (out.key_valid === false) {
|
|
383
|
+
out.error = `Anthropic key invalid HTTP ${res.status}`;
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
out.key_valid = out.key_valid ?? true;
|
|
387
|
+
out.tracking_status = 'error';
|
|
388
|
+
out.error =
|
|
389
|
+
'Anthropic Usage & Cost API requires an Admin API key (GET /v1/organizations/cost_report). Messages API keys only validate. Add admin key or set tracking_exempt + monthly_cost_usd.';
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Google Generative Language — list models validates key. */
|
|
394
|
+
async function probeGoogle(value) {
|
|
395
|
+
const out = baseResult('GOOGLE', 'google');
|
|
396
|
+
out.probe = 'GET generativelanguage.googleapis.com/v1beta/models';
|
|
397
|
+
out.docs_url = 'https://ai.google.dev/gemini-api/docs';
|
|
398
|
+
out.pricing_url = 'https://ai.google.dev/pricing';
|
|
399
|
+
out.console_url = 'https://aistudio.google.com/apikey';
|
|
400
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(value)}`;
|
|
401
|
+
const { res, body } = await fetchJson(url);
|
|
402
|
+
if (!res.ok) {
|
|
403
|
+
out.key_valid = false;
|
|
404
|
+
out.error = `Google/Gemini key invalid HTTP ${res.status}`;
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
out.key_valid = true;
|
|
408
|
+
out.account = { models_visible: Array.isArray(body?.models) ? body.models.length : null };
|
|
409
|
+
out.tracking_status = 'error';
|
|
410
|
+
out.error =
|
|
411
|
+
'Google AI Studio keys have no public account/usage balance endpoint on the same key. Use Cloud Billing APIs with GCP credentials or set tracking_exempt + monthly_cost_usd.';
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** xAI — OpenAI-compatible models list; no public balance on key. */
|
|
416
|
+
async function probeXai(value) {
|
|
417
|
+
const out = baseResult('XAI', 'xai');
|
|
418
|
+
out.probe = 'GET https://api.x.ai/v1/models';
|
|
419
|
+
out.docs_url = 'https://docs.x.ai/docs';
|
|
420
|
+
out.console_url = 'https://console.x.ai';
|
|
421
|
+
const { res, body } = await fetchJson('https://api.x.ai/v1/models', {
|
|
422
|
+
Authorization: `Bearer ${value}`,
|
|
423
|
+
});
|
|
424
|
+
if (!res.ok) {
|
|
425
|
+
out.key_valid = false;
|
|
426
|
+
out.error = `xAI key invalid HTTP ${res.status}`;
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
out.key_valid = true;
|
|
430
|
+
out.account = { models_visible: Array.isArray(body?.data) ? body.data.length : null };
|
|
431
|
+
out.tracking_status = 'error';
|
|
432
|
+
out.error =
|
|
433
|
+
'xAI has no public account/balance endpoint on the API key (console-only credits). Set tracking_exempt + monthly_cost_usd or record usage events.';
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Hugging Face — whoami-v2 validates token. */
|
|
438
|
+
async function probeHuggingFace(value) {
|
|
439
|
+
const out = baseResult('HUGGINGFACE', 'huggingface');
|
|
440
|
+
out.probe = 'GET https://huggingface.co/api/whoami-v2';
|
|
441
|
+
out.docs_url = 'https://huggingface.co/docs/api-inference';
|
|
442
|
+
out.console_url = 'https://huggingface.co/settings/tokens';
|
|
443
|
+
out.account_api = true;
|
|
444
|
+
const { res, body } = await fetchJson('https://huggingface.co/api/whoami-v2', {
|
|
445
|
+
Authorization: `Bearer ${value}`,
|
|
446
|
+
});
|
|
447
|
+
if (!res.ok) {
|
|
448
|
+
out.key_valid = false;
|
|
449
|
+
out.error = `Hugging Face token invalid HTTP ${res.status}`;
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
out.key_valid = true;
|
|
453
|
+
out.account = {
|
|
454
|
+
name: body?.name || body?.fullname || null,
|
|
455
|
+
type: body?.type || null,
|
|
456
|
+
email: body?.email || null,
|
|
457
|
+
};
|
|
458
|
+
out.tracking_status = 'partial';
|
|
459
|
+
out.error =
|
|
460
|
+
'Hugging Face whoami tracks account identity; billed inference spend is not on this endpoint. Set tracking_exempt + monthly_cost_usd for Pro/Inference spend.';
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Telegram bot — free; getMe = tracked ok at $0. */
|
|
465
|
+
async function probeTelegram(value) {
|
|
466
|
+
const out = baseResult('TELEGRAM', 'telegram');
|
|
467
|
+
out.probe = 'GET api.telegram.org/bot…/getMe';
|
|
468
|
+
out.docs_url = 'https://core.telegram.org/bots/api';
|
|
469
|
+
out.console_url = 'https://t.me/BotFather';
|
|
470
|
+
out.account_api = true;
|
|
471
|
+
out.usage_api = true;
|
|
472
|
+
out.subscription_api = true;
|
|
473
|
+
const token = String(value || '').trim();
|
|
474
|
+
const { res, body } = await fetchJson(`https://api.telegram.org/bot${token}/getMe`);
|
|
475
|
+
if (!res.ok || !body?.ok) {
|
|
476
|
+
out.key_valid = false;
|
|
477
|
+
out.error = `Telegram bot token invalid HTTP ${res.status}`;
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
out.key_valid = true;
|
|
481
|
+
out.tracking_status = 'ok';
|
|
482
|
+
out.account = body.result || null;
|
|
483
|
+
out.subscription = { monthly_cost_usd: 0, tier: 'free', note: 'Telegram Bot API is free' };
|
|
484
|
+
out.usage = { cost_usd: 0 };
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Brave Search — no account API on search token; validity probe only. */
|
|
489
|
+
async function probeBrave(value) {
|
|
490
|
+
const out = baseResult('BRAVE', 'brave');
|
|
491
|
+
out.probe = 'GET api.search.brave.com/res/v1/web/search?q=ping';
|
|
492
|
+
out.docs_url = 'https://api-dashboard.search.brave.com/app/documentation';
|
|
493
|
+
out.pricing_url = 'https://brave.com/search/api/';
|
|
494
|
+
out.console_url = 'https://api-dashboard.search.brave.com/';
|
|
495
|
+
const { res } = await fetchJson(
|
|
496
|
+
'https://api.search.brave.com/res/v1/web/search?q=total-recall-healthcheck&count=1',
|
|
497
|
+
{ 'X-Subscription-Token': value },
|
|
498
|
+
);
|
|
499
|
+
if (!res.ok) {
|
|
500
|
+
out.key_valid = false;
|
|
501
|
+
out.error = `Brave Search key invalid HTTP ${res.status}`;
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
out.key_valid = true;
|
|
505
|
+
out.tracking_status = 'error';
|
|
506
|
+
out.error =
|
|
507
|
+
'Brave Search does not expose a balance/usage API on the search subscription token. Track spend via dashboard or set tracking_exempt + monthly_cost_usd / caps.';
|
|
508
|
+
return out;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function probeSerper(value) {
|
|
512
|
+
const out = baseResult('SERPER', 'serper');
|
|
513
|
+
out.probe = 'POST google.serper.dev/search';
|
|
514
|
+
out.docs_url = 'https://serper.dev/docs';
|
|
515
|
+
out.console_url = 'https://serper.dev/dashboard';
|
|
516
|
+
const res = await throttledFetch(
|
|
517
|
+
'https://google.serper.dev/search',
|
|
518
|
+
{
|
|
519
|
+
method: 'POST',
|
|
520
|
+
headers: { 'X-API-KEY': value, 'Content-Type': 'application/json' },
|
|
521
|
+
body: JSON.stringify({ q: 'total-recall-healthcheck', num: 1 }),
|
|
522
|
+
},
|
|
523
|
+
TIMEOUT_MS,
|
|
524
|
+
);
|
|
525
|
+
if (!res.ok) {
|
|
526
|
+
out.key_valid = false;
|
|
527
|
+
out.error = `Serper key invalid HTTP ${res.status}`;
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
out.key_valid = true;
|
|
531
|
+
out.tracking_status = 'error';
|
|
532
|
+
out.error =
|
|
533
|
+
'Serper has no documented account-balance API on the search key. Set tracking_exempt + monthly_cost_usd or add manual usage recording.';
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function probeTavily(value) {
|
|
538
|
+
const out = baseResult('TAVILY', 'tavily');
|
|
539
|
+
out.probe = 'POST api.tavily.com/search';
|
|
540
|
+
out.docs_url = 'https://docs.tavily.com/';
|
|
541
|
+
out.console_url = 'https://app.tavily.com/';
|
|
542
|
+
const res = await throttledFetch(
|
|
543
|
+
'https://api.tavily.com/search',
|
|
544
|
+
{
|
|
545
|
+
method: 'POST',
|
|
546
|
+
headers: { 'Content-Type': 'application/json' },
|
|
547
|
+
body: JSON.stringify({ api_key: value, query: 'total-recall-healthcheck', max_results: 1 }),
|
|
548
|
+
},
|
|
549
|
+
TIMEOUT_MS,
|
|
550
|
+
);
|
|
551
|
+
if (!res.ok) {
|
|
552
|
+
out.key_valid = false;
|
|
553
|
+
out.error = `Tavily key invalid HTTP ${res.status}`;
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
out.key_valid = true;
|
|
557
|
+
out.tracking_status = 'error';
|
|
558
|
+
out.error =
|
|
559
|
+
'Tavily search key has no public subscription-balance endpoint. Set tracking_exempt + monthly_cost_usd.';
|
|
560
|
+
return out;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function probeExa(value) {
|
|
564
|
+
const out = baseResult('EXA', 'exa');
|
|
565
|
+
out.probe = 'POST api.exa.ai/search';
|
|
566
|
+
out.docs_url = 'https://docs.exa.ai/';
|
|
567
|
+
out.console_url = 'https://dashboard.exa.ai/';
|
|
568
|
+
const res = await throttledFetch(
|
|
569
|
+
'https://api.exa.ai/search',
|
|
570
|
+
{
|
|
571
|
+
method: 'POST',
|
|
572
|
+
headers: {
|
|
573
|
+
'Content-Type': 'application/json',
|
|
574
|
+
'x-api-key': value,
|
|
575
|
+
},
|
|
576
|
+
body: JSON.stringify({ query: 'total-recall-healthcheck', numResults: 1 }),
|
|
577
|
+
},
|
|
578
|
+
TIMEOUT_MS,
|
|
579
|
+
);
|
|
580
|
+
if (!res.ok) {
|
|
581
|
+
out.key_valid = false;
|
|
582
|
+
out.error = `Exa key invalid HTTP ${res.status}`;
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
out.key_valid = true;
|
|
586
|
+
out.tracking_status = 'error';
|
|
587
|
+
out.error =
|
|
588
|
+
'Exa has no public account/usage balance API on the search key. Set tracking_exempt + monthly_cost_usd.';
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Stripe — balance is real $ on the secret key. */
|
|
593
|
+
async function probeStripe(value) {
|
|
594
|
+
const out = baseResult('STRIPE', 'stripe');
|
|
595
|
+
out.probe = 'GET https://api.stripe.com/v1/balance';
|
|
596
|
+
out.docs_url = 'https://docs.stripe.com/api/balance';
|
|
597
|
+
out.console_url = 'https://dashboard.stripe.com/apikeys';
|
|
598
|
+
out.account_api = true;
|
|
599
|
+
out.usage_api = true;
|
|
600
|
+
out.subscription_api = true;
|
|
601
|
+
if (/^pk_/.test(String(value || ''))) {
|
|
602
|
+
out.key_valid = true;
|
|
603
|
+
out.tracking_status = 'ok';
|
|
604
|
+
out.account_api = true;
|
|
605
|
+
out.usage = { cost_usd: 0 };
|
|
606
|
+
out.subscription = {
|
|
607
|
+
monthly_cost_usd: 0,
|
|
608
|
+
note: 'Stripe publishable key — not a secret; billing is on sk_ live secret key',
|
|
609
|
+
};
|
|
610
|
+
out.probe = 'publishable-key';
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
const { res, body } = await fetchJson('https://api.stripe.com/v1/balance', {
|
|
614
|
+
Authorization: `Bearer ${value}`,
|
|
615
|
+
});
|
|
616
|
+
if (!res.ok) {
|
|
617
|
+
out.key_valid = false;
|
|
618
|
+
out.error = `Stripe balance probe failed HTTP ${res.status}`;
|
|
619
|
+
return out;
|
|
620
|
+
}
|
|
621
|
+
out.key_valid = true;
|
|
622
|
+
out.tracking_status = 'ok';
|
|
623
|
+
out.usage = {
|
|
624
|
+
available: body?.available || null,
|
|
625
|
+
pending: body?.pending || null,
|
|
626
|
+
livemode: body?.livemode ?? null,
|
|
627
|
+
};
|
|
628
|
+
out.account = { object: body?.object || 'balance' };
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Telnyx — balance endpoint on API v2. */
|
|
633
|
+
async function probeTelnyx(value) {
|
|
634
|
+
const out = baseResult('TELNYX', 'telnyx');
|
|
635
|
+
out.probe = 'GET https://api.telnyx.com/v2/balance';
|
|
636
|
+
out.docs_url = 'https://developers.telnyx.com/api/v2/overview';
|
|
637
|
+
out.console_url = 'https://portal.telnyx.com/';
|
|
638
|
+
out.account_api = true;
|
|
639
|
+
out.usage_api = true;
|
|
640
|
+
out.subscription_api = true;
|
|
641
|
+
const { res, body } = await fetchJson('https://api.telnyx.com/v2/balance', {
|
|
642
|
+
Authorization: `Bearer ${value}`,
|
|
643
|
+
});
|
|
644
|
+
if (!res.ok) {
|
|
645
|
+
out.key_valid = false;
|
|
646
|
+
out.error = `Telnyx balance probe failed HTTP ${res.status}`;
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
const d = body?.data || body || {};
|
|
650
|
+
out.key_valid = true;
|
|
651
|
+
out.tracking_status = 'ok';
|
|
652
|
+
out.usage = {
|
|
653
|
+
balance: d.balance ?? d.credit_balance ?? null,
|
|
654
|
+
currency: d.currency || null,
|
|
655
|
+
credit_limit: d.credit_limit ?? null,
|
|
656
|
+
};
|
|
657
|
+
out.subscription = { currency: d.currency || null };
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Vercel — user + team; spend is not always on user token. */
|
|
662
|
+
async function probeVercel(value) {
|
|
663
|
+
const out = baseResult('VERCEL', 'vercel');
|
|
664
|
+
out.probe = 'GET https://api.vercel.com/v2/user';
|
|
665
|
+
out.docs_url = 'https://vercel.com/docs/rest-api';
|
|
666
|
+
out.console_url = 'https://vercel.com/account/tokens';
|
|
667
|
+
out.account_api = true;
|
|
668
|
+
const { res, body } = await fetchJson('https://api.vercel.com/v2/user', {
|
|
669
|
+
Authorization: `Bearer ${value}`,
|
|
670
|
+
});
|
|
671
|
+
if (!res.ok) {
|
|
672
|
+
out.key_valid = false;
|
|
673
|
+
out.error = `Vercel user probe failed HTTP ${res.status}`;
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
out.key_valid = true;
|
|
677
|
+
const u = body?.user || body || {};
|
|
678
|
+
out.account = {
|
|
679
|
+
id: u.id || null,
|
|
680
|
+
username: u.username || null,
|
|
681
|
+
email: u.email || null,
|
|
682
|
+
};
|
|
683
|
+
out.tracking_status = 'partial';
|
|
684
|
+
out.error =
|
|
685
|
+
'Vercel account identity tracked; dollar spend requires billing team APIs / dashboard. Set tracking_exempt + monthly_cost_usd if no billing token.';
|
|
686
|
+
return out;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** npm — whoami validates token (no $). */
|
|
690
|
+
async function probeNpm(value) {
|
|
691
|
+
const out = baseResult('NPM', 'npm');
|
|
692
|
+
out.probe = 'GET https://registry.npmjs.org/-/whoami';
|
|
693
|
+
out.docs_url = 'https://docs.npmjs.com/about-access-tokens';
|
|
694
|
+
out.console_url = 'https://www.npmjs.com/settings/~/tokens';
|
|
695
|
+
out.account_api = true;
|
|
696
|
+
const { res, body } = await fetchJson('https://registry.npmjs.org/-/whoami', {
|
|
697
|
+
Authorization: `Bearer ${value}`,
|
|
698
|
+
});
|
|
699
|
+
if (!res.ok) {
|
|
700
|
+
out.key_valid = false;
|
|
701
|
+
out.error = `npm whoami failed HTTP ${res.status}`;
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
out.key_valid = true;
|
|
705
|
+
out.account = { username: body?.username || body || null };
|
|
706
|
+
// npm publish tokens are free for public packages; treat identity as ok at $0
|
|
707
|
+
out.tracking_status = 'ok';
|
|
708
|
+
out.usage_api = true;
|
|
709
|
+
out.subscription_api = true;
|
|
710
|
+
out.subscription = { monthly_cost_usd: 0, note: 'npm access token — no usage balance API; $0 unless npm Pro' };
|
|
711
|
+
out.usage = { cost_usd: 0 };
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** SMTP2GO — validate via stats/subaccount summary. */
|
|
716
|
+
async function probeSmtp2go(value) {
|
|
717
|
+
const out = baseResult('SMTP2GO', 'smtp2go');
|
|
718
|
+
out.probe = 'POST https://api.smtp2go.com/v3/stats/email_summary';
|
|
719
|
+
out.docs_url = 'https://developers.smtp2go.com/docs/';
|
|
720
|
+
out.console_url = 'https://app.smtp2go.com/settings/api_keys/';
|
|
721
|
+
out.account_api = true;
|
|
722
|
+
out.usage_api = true;
|
|
723
|
+
out.subscription_api = true;
|
|
724
|
+
const endpoints = [
|
|
725
|
+
'https://api.smtp2go.com/v3/stats/email_summary',
|
|
726
|
+
'https://api.smtp2go.com/v3/stats/email_credits',
|
|
727
|
+
'https://api.smtp2go.com/v3/user/password_check',
|
|
728
|
+
];
|
|
729
|
+
for (const url of endpoints) {
|
|
730
|
+
try {
|
|
731
|
+
const res = await throttledFetch(
|
|
732
|
+
url,
|
|
733
|
+
{
|
|
734
|
+
method: 'POST',
|
|
735
|
+
headers: { 'Content-Type': 'application/json' },
|
|
736
|
+
body: JSON.stringify({ api_key: value }),
|
|
737
|
+
},
|
|
738
|
+
TIMEOUT_MS,
|
|
739
|
+
);
|
|
740
|
+
const text = await res.text();
|
|
741
|
+
let body = null;
|
|
742
|
+
try {
|
|
743
|
+
body = text ? JSON.parse(text) : null;
|
|
744
|
+
} catch {
|
|
745
|
+
body = { raw: text?.slice(0, 200) };
|
|
746
|
+
}
|
|
747
|
+
if (res.ok && !body?.data?.error_code) {
|
|
748
|
+
out.key_valid = true;
|
|
749
|
+
out.tracking_status = 'ok';
|
|
750
|
+
out.probe = `POST ${url}`;
|
|
751
|
+
out.usage = body?.data || body;
|
|
752
|
+
out.subscription = { note: 'SMTP2GO stats API' };
|
|
753
|
+
return out;
|
|
754
|
+
}
|
|
755
|
+
// invalid key
|
|
756
|
+
if (res.status === 401 || res.status === 403 || /invalid|auth/i.test(String(body?.data?.error || ''))) {
|
|
757
|
+
out.key_valid = false;
|
|
758
|
+
out.error = `SMTP2GO key invalid HTTP ${res.status}`;
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
/* try next */
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
out.key_valid = true;
|
|
766
|
+
out.tracking_status = 'error';
|
|
767
|
+
out.error =
|
|
768
|
+
'SMTP2GO key accepted or unknown; no credits endpoint available on this API version. Set tracking_exempt + monthly_cost_usd from plan.';
|
|
769
|
+
return out;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Deterministic probe selection from key name, value shape, and provider meta.
|
|
774
|
+
* Does NOT call AI by default (bulk sync must stay fast).
|
|
775
|
+
*/
|
|
776
|
+
export function resolveProbeName(key, meta = {}, value = '') {
|
|
777
|
+
const catalog = providerForKeyName(key);
|
|
778
|
+
const fromMeta = String(meta?.provider || catalog?.id || '').toLowerCase();
|
|
779
|
+
const blob = `${fromMeta} ${key}`.toLowerCase();
|
|
780
|
+
const val = String(value || '');
|
|
781
|
+
|
|
782
|
+
if (/openrouter/i.test(blob) || /sk-or-v1/i.test(val) || /^or-v1/i.test(val)) return 'openrouter';
|
|
783
|
+
if (/elevenlabs|eleven_labs/i.test(blob)) return 'elevenlabs';
|
|
784
|
+
// GitHub PAT only — not app private keys / client secrets
|
|
785
|
+
if (
|
|
786
|
+
!/PRIVATE_KEY|CLIENT_SECRET|WEBHOOK/i.test(key) &&
|
|
787
|
+
(/github_token|gh_token|github_pat/i.test(blob) ||
|
|
788
|
+
val.startsWith('ghp_') ||
|
|
789
|
+
val.startsWith('github_pat_') ||
|
|
790
|
+
val.startsWith('gho_'))
|
|
791
|
+
) {
|
|
792
|
+
return 'github';
|
|
793
|
+
}
|
|
794
|
+
if (/digitalocean|do_api|digital_ocean/i.test(blob)) return 'digitalocean';
|
|
795
|
+
if (/anthropic|claude|sk-ant/i.test(blob + val) || val.startsWith('sk-ant-')) return 'anthropic';
|
|
796
|
+
if (/xai|grok/i.test(blob) && !/openrouter/i.test(blob)) return 'xai';
|
|
797
|
+
if (/google|gemini|generativelanguage/i.test(blob)) return 'google';
|
|
798
|
+
if (/huggingface|hf_token|hugging_face/i.test(blob) || val.startsWith('hf_')) return 'huggingface';
|
|
799
|
+
if (/brave/i.test(blob)) return 'brave';
|
|
800
|
+
if (/serper/i.test(blob)) return 'serper';
|
|
801
|
+
if (/tavily/i.test(blob)) return 'tavily';
|
|
802
|
+
if (/\bexa\b/i.test(blob) || /exa_api/i.test(blob)) return 'exa';
|
|
803
|
+
if (/telegram/i.test(blob) || /^\d+:[A-Za-z0-9_-]+$/.test(val)) return 'telegram';
|
|
804
|
+
if (/stripe/i.test(blob) || /^sk_live_|^sk_test_/.test(val)) {
|
|
805
|
+
// publishable keys cannot call balance
|
|
806
|
+
if (/publishable|pk_live|pk_test/i.test(blob + val)) return null;
|
|
807
|
+
return 'stripe';
|
|
808
|
+
}
|
|
809
|
+
if (/telnyx/i.test(blob)) return 'telnyx';
|
|
810
|
+
if (/vercel/i.test(blob)) return 'vercel';
|
|
811
|
+
if (
|
|
812
|
+
(/npm_token|NPM_TOKEN|NPM_AUTH/i.test(key) || /^npm_/i.test(val)) &&
|
|
813
|
+
!/recovery/i.test(key)
|
|
814
|
+
) {
|
|
815
|
+
return 'npm';
|
|
816
|
+
}
|
|
817
|
+
if (/smtp2go/i.test(blob)) return 'smtp2go';
|
|
818
|
+
if (/openai/i.test(blob) || /^sk-/.test(val)) {
|
|
819
|
+
if (/or-v1|openrouter/i.test(val)) return 'openrouter';
|
|
820
|
+
return 'openai';
|
|
821
|
+
}
|
|
822
|
+
if (fromMeta && PROBES[fromMeta]) return fromMeta;
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const PROBES = {
|
|
827
|
+
openrouter: probeOpenRouter,
|
|
828
|
+
elevenlabs: probeElevenLabs,
|
|
829
|
+
github: probeGithub,
|
|
830
|
+
digitalocean: probeDigitalOcean,
|
|
831
|
+
openai: probeOpenAI,
|
|
832
|
+
anthropic: probeAnthropic,
|
|
833
|
+
google: probeGoogle,
|
|
834
|
+
xai: probeXai,
|
|
835
|
+
huggingface: probeHuggingFace,
|
|
836
|
+
brave: probeBrave,
|
|
837
|
+
serper: probeSerper,
|
|
838
|
+
tavily: probeTavily,
|
|
839
|
+
exa: probeExa,
|
|
840
|
+
telegram: probeTelegram,
|
|
841
|
+
stripe: probeStripe,
|
|
842
|
+
telnyx: probeTelnyx,
|
|
843
|
+
vercel: probeVercel,
|
|
844
|
+
npm: probeNpm,
|
|
845
|
+
smtp2go: probeSmtp2go,
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Sync one secret key: live provider probe + persist tracking meta.
|
|
850
|
+
* @param {object} opts
|
|
851
|
+
* @param {boolean} [opts.strict=true] partial (account without $) → error
|
|
852
|
+
* @param {boolean} [opts.force_exempt]
|
|
853
|
+
* @param {boolean} [opts.use_ai=false] optional AI classification for unknown keys
|
|
854
|
+
*/
|
|
855
|
+
export async function syncSecretAccount(brainDir, key, opts = {}) {
|
|
856
|
+
const strict = opts.strict !== false;
|
|
857
|
+
const got = await getSecret(brainDir, key, { action: 'use', actor: 'provider-account-sync' });
|
|
858
|
+
if (!got.found) {
|
|
859
|
+
return { key, tracking_status: 'error', error: 'secret not found' };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const metaList = await listSecretsMeta(brainDir);
|
|
863
|
+
const meta = metaList.find((k) => k.key === key) || { key };
|
|
864
|
+
const catalog = providerForKeyName(key);
|
|
865
|
+
const providerGuess = meta.provider || catalog?.id || null;
|
|
866
|
+
|
|
867
|
+
// Operator waiver
|
|
868
|
+
if (meta.tracking_exempt === true || opts.force_exempt) {
|
|
869
|
+
const exempt = baseResult(key, providerGuess);
|
|
870
|
+
exempt.tracking_status = 'exempt';
|
|
871
|
+
exempt.key_valid = true;
|
|
872
|
+
exempt.error = null;
|
|
873
|
+
exempt.docs_url = meta.api_docs_url || catalog?.docs_url || null;
|
|
874
|
+
exempt.pricing_url = meta.pricing_url || catalog?.pricing_url || null;
|
|
875
|
+
exempt.console_url = meta.console_url || catalog?.console_url || null;
|
|
876
|
+
exempt.subscription = {
|
|
877
|
+
monthly_cost_usd: meta.monthly_cost_usd ?? null,
|
|
878
|
+
subscription_tier: meta.subscription_tier ?? null,
|
|
879
|
+
note: 'Operator marked tracking_exempt',
|
|
880
|
+
};
|
|
881
|
+
await persistTracking(brainDir, key, exempt, meta);
|
|
882
|
+
return exempt;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Internal app passwords / JWT secrets — not vendor account APIs
|
|
886
|
+
if (looksLikeInternalPassword(key)) {
|
|
887
|
+
const skip = baseResult(key, providerGuess || 'internal');
|
|
888
|
+
skip.tracking_status = 'ok';
|
|
889
|
+
skip.key_valid = true;
|
|
890
|
+
skip.account_api = true;
|
|
891
|
+
skip.usage_api = true;
|
|
892
|
+
skip.subscription_api = true;
|
|
893
|
+
skip.probe = 'internal-password';
|
|
894
|
+
skip.subscription = {
|
|
895
|
+
monthly_cost_usd: 0,
|
|
896
|
+
note: 'Internal password/secret — not a billable vendor API key',
|
|
897
|
+
};
|
|
898
|
+
skip.usage = { cost_usd: 0 };
|
|
899
|
+
await persistTracking(brainDir, key, skip, { ...meta, provider: providerGuess || 'internal' });
|
|
900
|
+
return skip;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Prefer a live probe when key name maps to a billable vendor (even if meta.provider is "mail")
|
|
904
|
+
let probeName = resolveProbeName(key, meta, got.value);
|
|
905
|
+
|
|
906
|
+
// Self-hosted / free product families → auto ok at $0 unless we have a vendor probe
|
|
907
|
+
if (
|
|
908
|
+
!probeName &&
|
|
909
|
+
providerGuess &&
|
|
910
|
+
SELF_HOSTED_OR_FREE.has(String(providerGuess).toLowerCase())
|
|
911
|
+
) {
|
|
912
|
+
const skip = baseResult(key, providerGuess);
|
|
913
|
+
skip.tracking_status = 'ok';
|
|
914
|
+
skip.key_valid = true;
|
|
915
|
+
skip.account_api = true;
|
|
916
|
+
skip.usage_api = true;
|
|
917
|
+
skip.subscription_api = true;
|
|
918
|
+
skip.probe = 'self-hosted-or-free';
|
|
919
|
+
skip.docs_url = catalog?.docs_url || null;
|
|
920
|
+
skip.subscription = {
|
|
921
|
+
monthly_cost_usd: meta.monthly_cost_usd ?? 0,
|
|
922
|
+
tier: 'self-hosted',
|
|
923
|
+
note: 'Self-hosted or free product — no cloud billing API required',
|
|
924
|
+
};
|
|
925
|
+
skip.usage = { cost_usd: 0 };
|
|
926
|
+
await persistTracking(brainDir, key, skip, meta);
|
|
927
|
+
return skip;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Optional AI only when unknown and explicitly requested
|
|
931
|
+
if (!probeName && opts.use_ai) {
|
|
932
|
+
try {
|
|
933
|
+
const { inferSecretIntegrationWithAi, gatherCodeUsageContext } = await import(
|
|
934
|
+
'./secret-integration-research.mjs'
|
|
935
|
+
);
|
|
936
|
+
const ai = await inferSecretIntegrationWithAi(key, {
|
|
937
|
+
meta,
|
|
938
|
+
codeContext: gatherCodeUsageContext(key),
|
|
939
|
+
});
|
|
940
|
+
if (ai?.researchable === false) {
|
|
941
|
+
const skip = baseResult(key, ai.product_slug || providerGuess);
|
|
942
|
+
skip.tracking_status = 'exempt';
|
|
943
|
+
skip.key_valid = true;
|
|
944
|
+
skip.error = null;
|
|
945
|
+
skip.probe = 'ai-classification';
|
|
946
|
+
skip.account = { product_name: ai.product_name, kind: ai.kind };
|
|
947
|
+
skip.subscription = { note: ai.skip_reason };
|
|
948
|
+
await persistTracking(brainDir, key, skip, {
|
|
949
|
+
...meta,
|
|
950
|
+
provider: ai.product_slug || meta.provider,
|
|
951
|
+
});
|
|
952
|
+
return skip;
|
|
953
|
+
}
|
|
954
|
+
if (ai?.product_slug && PROBES[String(ai.product_slug).toLowerCase()]) {
|
|
955
|
+
probeName = String(ai.product_slug).toLowerCase();
|
|
956
|
+
}
|
|
957
|
+
} catch {
|
|
958
|
+
/* ignore AI failures */
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (!probeName || !PROBES[probeName]) {
|
|
963
|
+
const err = baseResult(key, providerGuess);
|
|
964
|
+
err.tracking_status = 'error';
|
|
965
|
+
err.error = `No live account/usage probe for this credential (provider=${providerGuess || 'unknown'}). Add a probe, store an Admin/billing key, or set tracking_exempt + monthly_cost_usd.`;
|
|
966
|
+
err.probe = 'none';
|
|
967
|
+
err.docs_url = catalog?.docs_url || meta.api_docs_url || null;
|
|
968
|
+
err.pricing_url = catalog?.pricing_url || meta.pricing_url || null;
|
|
969
|
+
err.console_url = catalog?.console_url || meta.console_url || null;
|
|
970
|
+
await persistTracking(brainDir, key, err, meta);
|
|
971
|
+
return err;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
let result;
|
|
975
|
+
try {
|
|
976
|
+
result = await PROBES[probeName](got.value);
|
|
977
|
+
} catch (e) {
|
|
978
|
+
result = baseResult(key, probeName);
|
|
979
|
+
result.error = e.message;
|
|
980
|
+
result.tracking_status = 'error';
|
|
981
|
+
}
|
|
982
|
+
result.key = key;
|
|
983
|
+
result.provider = result.provider || probeName;
|
|
984
|
+
|
|
985
|
+
// Strict: valid key but no full $ / subscription API still ERROR
|
|
986
|
+
if (result.tracking_status === 'partial' && strict) {
|
|
987
|
+
result.tracking_status = 'error';
|
|
988
|
+
result.error =
|
|
989
|
+
result.error ||
|
|
990
|
+
'Partial tracking only (account/quota without full subscription cost API). Set tracking_exempt or attach billing/admin credentials.';
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
await persistTracking(brainDir, key, result, {
|
|
994
|
+
...meta,
|
|
995
|
+
provider: result.provider || meta.provider,
|
|
996
|
+
api_docs_url: result.docs_url || meta.api_docs_url,
|
|
997
|
+
console_url: result.console_url || meta.console_url,
|
|
998
|
+
pricing_url: result.pricing_url || meta.pricing_url,
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// Mirror numeric credit spend into usage ledger when present
|
|
1002
|
+
if (result.usage?.credits_used_daily != null || result.usage?.character_count != null) {
|
|
1003
|
+
try {
|
|
1004
|
+
const cost = Number(result.usage.credits_used_daily || 0);
|
|
1005
|
+
if (cost > 0) {
|
|
1006
|
+
recordUsage(brainDir, {
|
|
1007
|
+
key_ref: key,
|
|
1008
|
+
provider: result.provider,
|
|
1009
|
+
cost_usd: cost,
|
|
1010
|
+
source: 'provider-account-sync',
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
} catch {
|
|
1014
|
+
/* ignore */
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
return result;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function persistTracking(brainDir, key, result, meta = {}) {
|
|
1022
|
+
const patch = {
|
|
1023
|
+
provider: result.provider || meta.provider || null,
|
|
1024
|
+
api_docs_url: result.docs_url || meta.api_docs_url || null,
|
|
1025
|
+
console_url: result.console_url || meta.console_url || null,
|
|
1026
|
+
pricing_url: result.pricing_url || meta.pricing_url || null,
|
|
1027
|
+
tracking_status: result.tracking_status,
|
|
1028
|
+
tracking_error: result.error || null,
|
|
1029
|
+
tracking_synced_at: result.synced_at,
|
|
1030
|
+
tracking_probe: result.probe || null,
|
|
1031
|
+
tracking_account: result.account || null,
|
|
1032
|
+
tracking_usage: result.usage || null,
|
|
1033
|
+
tracking_subscription: result.subscription || null,
|
|
1034
|
+
account_api: !!result.account_api,
|
|
1035
|
+
usage_api: !!result.usage_api,
|
|
1036
|
+
subscription_api: !!result.subscription_api,
|
|
1037
|
+
key_valid: result.key_valid,
|
|
1038
|
+
};
|
|
1039
|
+
if (result.subscription?.credit_limit != null && meta.monthly_cap_usd == null) {
|
|
1040
|
+
patch.monthly_cap_usd = result.subscription.credit_limit;
|
|
1041
|
+
}
|
|
1042
|
+
if (
|
|
1043
|
+
result.subscription?.monthly_cost_usd != null &&
|
|
1044
|
+
meta.monthly_cost_usd == null
|
|
1045
|
+
) {
|
|
1046
|
+
patch.monthly_cost_usd = result.subscription.monthly_cost_usd;
|
|
1047
|
+
}
|
|
1048
|
+
await updateSecretMeta(brainDir, key, patch);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Sync all secrets; returns report with hard errors.
|
|
1053
|
+
*/
|
|
1054
|
+
export async function syncAllSecretAccounts(brainDir, opts = {}) {
|
|
1055
|
+
const keys = await listSecretsMeta(brainDir);
|
|
1056
|
+
const results = [];
|
|
1057
|
+
const errors = [];
|
|
1058
|
+
const only = opts.key || opts.keys || null;
|
|
1059
|
+
const onlySet = only
|
|
1060
|
+
? new Set(Array.isArray(only) ? only : [only])
|
|
1061
|
+
: null;
|
|
1062
|
+
|
|
1063
|
+
for (const row of keys) {
|
|
1064
|
+
if (!row.set) continue;
|
|
1065
|
+
if (onlySet && !onlySet.has(row.key)) continue;
|
|
1066
|
+
try {
|
|
1067
|
+
const r = await syncSecretAccount(brainDir, row.key, opts);
|
|
1068
|
+
results.push(r);
|
|
1069
|
+
if (r.tracking_status === 'error') {
|
|
1070
|
+
errors.push({ key: row.key, error: r.error || 'tracking failed' });
|
|
1071
|
+
}
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
const err = { key: row.key, tracking_status: 'error', error: e.message };
|
|
1074
|
+
results.push(err);
|
|
1075
|
+
errors.push(err);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const ok = results.filter((r) => r.tracking_status === 'ok' || r.tracking_status === 'exempt')
|
|
1080
|
+
.length;
|
|
1081
|
+
const report = {
|
|
1082
|
+
total: results.length,
|
|
1083
|
+
ok,
|
|
1084
|
+
errors: errors.length,
|
|
1085
|
+
error_keys: errors,
|
|
1086
|
+
results,
|
|
1087
|
+
healthy: errors.length === 0,
|
|
1088
|
+
message:
|
|
1089
|
+
errors.length === 0
|
|
1090
|
+
? 'All set secrets are tracked or exempt'
|
|
1091
|
+
: `TRACKING ERROR: ${errors.length} secret(s) lack account/usage/subscription tracking`,
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
if (!report.healthy) {
|
|
1095
|
+
logger.error('provider-account-sync', report.message, { count: errors.length });
|
|
1096
|
+
} else {
|
|
1097
|
+
logger.info('provider-account-sync', report.message);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return report;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Catalog health: any non-exempt set key without ok tracking is an error.
|
|
1105
|
+
*/
|
|
1106
|
+
export async function getTrackingHealth(brainDir) {
|
|
1107
|
+
const keys = await listSecretsMeta(brainDir);
|
|
1108
|
+
const errors = [];
|
|
1109
|
+
const ok = [];
|
|
1110
|
+
const exempt = [];
|
|
1111
|
+
for (const k of keys) {
|
|
1112
|
+
if (!k.set) continue;
|
|
1113
|
+
const status = k.tracking_status || 'error';
|
|
1114
|
+
if (status === 'ok') ok.push(k.key);
|
|
1115
|
+
else if (status === 'exempt' || k.tracking_exempt) exempt.push(k.key);
|
|
1116
|
+
else {
|
|
1117
|
+
errors.push({
|
|
1118
|
+
key: k.key,
|
|
1119
|
+
provider: k.provider,
|
|
1120
|
+
tracking_status: status,
|
|
1121
|
+
error: k.tracking_error || 'Never synced — run secret account-sync',
|
|
1122
|
+
account_api: k.account_api,
|
|
1123
|
+
usage_api: k.usage_api,
|
|
1124
|
+
subscription_api: k.subscription_api,
|
|
1125
|
+
key_valid: k.key_valid,
|
|
1126
|
+
docs_url: k.api_docs_url,
|
|
1127
|
+
pricing_url: k.pricing_url,
|
|
1128
|
+
console_url: k.console_url,
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
healthy: errors.length === 0,
|
|
1134
|
+
ok: ok.length,
|
|
1135
|
+
exempt: exempt.length,
|
|
1136
|
+
errors: errors.length,
|
|
1137
|
+
error_keys: errors,
|
|
1138
|
+
message:
|
|
1139
|
+
errors.length === 0
|
|
1140
|
+
? 'Secret tracking healthy'
|
|
1141
|
+
: `TRACKING ERROR: ${errors.length} credential(s) without account/usage/subscription coverage`,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
export { PROBES };
|