@nordsym/apiclaw 1.5.13 → 1.5.15
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/dist/bin.js +1 -1
- package/dist/cli/commands/mcp-install.js +44 -49
- package/dist/cli/commands/mcp-install.js.map +1 -1
- package/dist/cli/index.js +7 -0
- package/dist/convex/adminActivate.js +46 -0
- package/dist/convex/adminStats.js +41 -0
- package/dist/convex/agents.js +498 -0
- package/dist/convex/analytics.js +165 -0
- package/dist/convex/billing.js +654 -0
- package/dist/convex/capabilities.js +144 -0
- package/dist/convex/chains.js +1041 -0
- package/dist/convex/credits.js +185 -0
- package/dist/convex/crons.js +16 -0
- package/dist/convex/directCall.js +626 -0
- package/dist/convex/earnProgress.js +648 -0
- package/dist/convex/email.js +299 -0
- package/dist/convex/feedback.js +226 -0
- package/dist/convex/http.js +909 -0
- package/dist/convex/logs.js +486 -0
- package/dist/convex/mou.js +81 -0
- package/dist/convex/providerKeys.js +256 -0
- package/dist/convex/providers.js +755 -0
- package/dist/convex/purchases.js +156 -0
- package/dist/convex/ratelimit.js +90 -0
- package/dist/convex/schema.js +709 -0
- package/dist/convex/searchLogs.js +128 -0
- package/dist/convex/spendAlerts.js +379 -0
- package/dist/convex/stripeActions.js +410 -0
- package/dist/convex/teams.js +214 -0
- package/dist/convex/telemetry.js +73 -0
- package/dist/convex/usage.js +228 -0
- package/dist/convex/waitlist.js +48 -0
- package/dist/convex/webhooks.js +409 -0
- package/dist/convex/workspaces.js +879 -0
- package/dist/src/analytics.js +129 -0
- package/dist/src/bin.js +17 -0
- package/dist/src/capability-router.js +240 -0
- package/dist/src/chainExecutor.js +451 -0
- package/dist/src/chainResolver.js +518 -0
- package/dist/src/cli/commands/doctor.js +324 -0
- package/dist/src/cli/commands/mcp-install.js +255 -0
- package/dist/src/cli/commands/restore.js +259 -0
- package/dist/src/cli/commands/setup.js +205 -0
- package/dist/src/cli/commands/uninstall.js +188 -0
- package/dist/src/cli/index.js +111 -0
- package/dist/src/cli.js +302 -0
- package/dist/src/confirmation.js +240 -0
- package/dist/src/credentials.js +357 -0
- package/dist/src/credits.js +260 -0
- package/dist/src/crypto.js +66 -0
- package/dist/src/discovery.js +504 -0
- package/dist/src/enterprise/env.js +123 -0
- package/dist/src/enterprise/script-generator.js +460 -0
- package/dist/src/execute-dynamic.js +473 -0
- package/dist/src/execute.js +1727 -0
- package/dist/src/index.js +2062 -0
- package/dist/src/metered.js +80 -0
- package/dist/src/open-apis.js +276 -0
- package/dist/src/proxy.js +28 -0
- package/dist/src/session.js +86 -0
- package/dist/src/stripe.js +407 -0
- package/dist/src/telemetry.js +49 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/backup.js +181 -0
- package/dist/src/utils/config.js +220 -0
- package/dist/src/utils/os.js +105 -0
- package/dist/src/utils/paths.js +159 -0
- package/package.json +1 -1
- package/src/bin.ts +1 -1
- package/src/cli/index.ts +8 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Real credential providers for APIClaw Connected tier
|
|
2
|
+
// Reads from environment variables or ~/.secrets/
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
// Load env file helper
|
|
7
|
+
function loadEnvFile(filename) {
|
|
8
|
+
const paths = [
|
|
9
|
+
join(homedir(), '.secrets', filename),
|
|
10
|
+
join(process.cwd(), filename),
|
|
11
|
+
join(process.cwd(), '.env.local'),
|
|
12
|
+
];
|
|
13
|
+
for (const path of paths) {
|
|
14
|
+
if (existsSync(path)) {
|
|
15
|
+
const content = readFileSync(path, 'utf-8');
|
|
16
|
+
const vars = {};
|
|
17
|
+
for (const line of content.split('\n')) {
|
|
18
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
19
|
+
if (match) {
|
|
20
|
+
vars[match[1].trim()] = match[2].trim().replace(/^["']|["']$/g, '');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return vars;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
// Provider credential getters
|
|
29
|
+
const providers = {
|
|
30
|
+
'46elks': {
|
|
31
|
+
type: 'basic',
|
|
32
|
+
get() {
|
|
33
|
+
const env = loadEnvFile('46elks.env');
|
|
34
|
+
const user = env.ELKS_API_USER || process.env.ELKS_API_USER;
|
|
35
|
+
const pass = env.ELKS_API_PASSWORD || process.env.ELKS_API_PASSWORD;
|
|
36
|
+
if (!user || !pass)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
type: 'basic',
|
|
40
|
+
username: user,
|
|
41
|
+
password: pass,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
twilio: {
|
|
46
|
+
type: 'basic',
|
|
47
|
+
get() {
|
|
48
|
+
const env = loadEnvFile('twilio.env');
|
|
49
|
+
const sid = env.TWILIO_ACCOUNT_SID || process.env.TWILIO_ACCOUNT_SID;
|
|
50
|
+
const token = env.TWILIO_AUTH_TOKEN || process.env.TWILIO_AUTH_TOKEN;
|
|
51
|
+
if (!sid || !token)
|
|
52
|
+
return null;
|
|
53
|
+
return {
|
|
54
|
+
type: 'basic',
|
|
55
|
+
username: sid,
|
|
56
|
+
password: token,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
// Real credential providers
|
|
61
|
+
resend: {
|
|
62
|
+
type: 'api_key',
|
|
63
|
+
get() {
|
|
64
|
+
const env = loadEnvFile('resend.env');
|
|
65
|
+
const key = env.RESEND_API_KEY || process.env.RESEND_API_KEY;
|
|
66
|
+
if (key) {
|
|
67
|
+
return { type: 'api_key', api_key: key };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
brave_search: {
|
|
73
|
+
type: 'api_key',
|
|
74
|
+
get() {
|
|
75
|
+
const env = loadEnvFile('brave.env');
|
|
76
|
+
const key = env.BRAVE_API_KEY || process.env.BRAVE_API_KEY;
|
|
77
|
+
if (key) {
|
|
78
|
+
return { type: 'api_key', api_key: key };
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
openrouter: {
|
|
84
|
+
type: 'bearer',
|
|
85
|
+
get() {
|
|
86
|
+
const env = loadEnvFile('openrouter.env');
|
|
87
|
+
const key = env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY;
|
|
88
|
+
if (key) {
|
|
89
|
+
return { type: 'bearer', api_key: key };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
elevenlabs: {
|
|
95
|
+
type: 'api_key',
|
|
96
|
+
get() {
|
|
97
|
+
const env = loadEnvFile('elevenlabs.env');
|
|
98
|
+
const key = env.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY;
|
|
99
|
+
if (key) {
|
|
100
|
+
return { type: 'api_key', api_key: key };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
replicate: {
|
|
106
|
+
type: 'bearer',
|
|
107
|
+
get() {
|
|
108
|
+
const env = loadEnvFile('replicate.env');
|
|
109
|
+
const key = env.REPLICATE_API_TOKEN || process.env.REPLICATE_API_TOKEN;
|
|
110
|
+
if (key) {
|
|
111
|
+
return { type: 'bearer', api_key: key };
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
firecrawl: {
|
|
117
|
+
type: 'bearer',
|
|
118
|
+
get() {
|
|
119
|
+
const env = loadEnvFile('firecrawl.env');
|
|
120
|
+
const key = env.FIRECRAWL_API_KEY || process.env.FIRECRAWL_API_KEY;
|
|
121
|
+
if (key) {
|
|
122
|
+
return { type: 'bearer', api_key: key };
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
github: {
|
|
128
|
+
type: 'bearer',
|
|
129
|
+
get() {
|
|
130
|
+
const env = loadEnvFile('github.env');
|
|
131
|
+
const key = env.GITHUB_TOKEN || process.env.GITHUB_TOKEN;
|
|
132
|
+
if (key) {
|
|
133
|
+
return { type: 'bearer', token: key };
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
e2b: {
|
|
139
|
+
type: 'api_key',
|
|
140
|
+
get() {
|
|
141
|
+
const env = loadEnvFile('e2b.env');
|
|
142
|
+
const key = env.E2B_API_KEY || process.env.E2B_API_KEY;
|
|
143
|
+
if (key) {
|
|
144
|
+
return { type: 'api_key', api_key: key };
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
apilayer: {
|
|
150
|
+
type: 'api_key',
|
|
151
|
+
get() {
|
|
152
|
+
const env = loadEnvFile('apilayer.env');
|
|
153
|
+
// Return all keys — handler picks the right one per action
|
|
154
|
+
const keys = {};
|
|
155
|
+
for (const [k, v] of Object.entries(env)) {
|
|
156
|
+
if (k.startsWith('APILAYER_') && v) {
|
|
157
|
+
keys[k] = v;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (Object.keys(keys).length === 0)
|
|
161
|
+
return null;
|
|
162
|
+
return { type: 'api_key', api_key: keys.APILAYER_EXCHANGERATE_KEY || '', ...keys };
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
groq: {
|
|
166
|
+
type: 'bearer',
|
|
167
|
+
get() {
|
|
168
|
+
const env = loadEnvFile('groq.env');
|
|
169
|
+
const key = env.GROQ_API_KEY || process.env.GROQ_API_KEY;
|
|
170
|
+
if (key) {
|
|
171
|
+
return { type: 'bearer', api_key: key };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
deepgram: {
|
|
177
|
+
type: 'bearer',
|
|
178
|
+
get() {
|
|
179
|
+
const env = loadEnvFile('deepgram.env');
|
|
180
|
+
const key = env.DEEPGRAM_API_KEY || process.env.DEEPGRAM_API_KEY;
|
|
181
|
+
if (key) {
|
|
182
|
+
return { type: 'bearer', api_key: key };
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
mistral: {
|
|
188
|
+
type: 'api_key',
|
|
189
|
+
get() {
|
|
190
|
+
const env = loadEnvFile('mistral.env');
|
|
191
|
+
const key = env.MISTRAL_API_KEY || process.env.MISTRAL_API_KEY;
|
|
192
|
+
if (key) {
|
|
193
|
+
return { type: 'api_key', api_key: key };
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
cohere: {
|
|
199
|
+
type: 'api_key',
|
|
200
|
+
get() {
|
|
201
|
+
const env = loadEnvFile('cohere.env');
|
|
202
|
+
const key = env.COHERE_API_KEY || process.env.COHERE_API_KEY;
|
|
203
|
+
if (key) {
|
|
204
|
+
return { type: 'api_key', api_key: key };
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
serper: {
|
|
210
|
+
type: 'api_key',
|
|
211
|
+
get() {
|
|
212
|
+
const env = loadEnvFile('serpapi.env');
|
|
213
|
+
const key = env.SERPAPI_API_KEY || process.env.SERPER_API_KEY || process.env.SERPAPI_API_KEY;
|
|
214
|
+
if (key) {
|
|
215
|
+
return { type: 'api_key', api_key: key };
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
stability: {
|
|
221
|
+
type: 'api_key',
|
|
222
|
+
get() {
|
|
223
|
+
const env = loadEnvFile('stability.env');
|
|
224
|
+
const key = env.STABILITY_API_KEY || process.env.STABILITY_API_KEY;
|
|
225
|
+
if (key) {
|
|
226
|
+
return { type: 'api_key', api_key: key };
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
together: {
|
|
232
|
+
type: 'bearer',
|
|
233
|
+
get() {
|
|
234
|
+
const env = loadEnvFile('together.env');
|
|
235
|
+
const key = env.TOGETHER_API_KEY || process.env.TOGETHER_API_KEY;
|
|
236
|
+
if (key) {
|
|
237
|
+
return { type: 'bearer', api_key: key };
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
assemblyai: {
|
|
243
|
+
type: 'api_key',
|
|
244
|
+
get() {
|
|
245
|
+
const env = loadEnvFile('assemblyai.env');
|
|
246
|
+
const key = env.ASSEMBLYAI_API_KEY || process.env.ASSEMBLYAI_API_KEY;
|
|
247
|
+
if (key) {
|
|
248
|
+
return { type: 'api_key', api_key: key };
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Get real credentials for a provider
|
|
256
|
+
* Returns null if provider not supported
|
|
257
|
+
*/
|
|
258
|
+
export function getCredentials(providerId) {
|
|
259
|
+
const provider = providers[providerId];
|
|
260
|
+
if (!provider)
|
|
261
|
+
return null;
|
|
262
|
+
return provider.get();
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check if a provider has real (non-demo) credentials available
|
|
266
|
+
*/
|
|
267
|
+
export function hasRealCredentials(providerId) {
|
|
268
|
+
if (providerId === '46elks') {
|
|
269
|
+
const env = loadEnvFile('46elks.env');
|
|
270
|
+
return !!(env.ELKS_API_USER || process.env.ELKS_API_USER);
|
|
271
|
+
}
|
|
272
|
+
if (providerId === 'twilio') {
|
|
273
|
+
const env = loadEnvFile('twilio.env');
|
|
274
|
+
return !!(env.TWILIO_ACCOUNT_SID || process.env.TWILIO_ACCOUNT_SID);
|
|
275
|
+
}
|
|
276
|
+
if (providerId === 'resend') {
|
|
277
|
+
const env = loadEnvFile('resend.env');
|
|
278
|
+
return !!(env.RESEND_API_KEY || process.env.RESEND_API_KEY);
|
|
279
|
+
}
|
|
280
|
+
if (providerId === 'brave_search') {
|
|
281
|
+
const env = loadEnvFile('brave.env');
|
|
282
|
+
return !!(env.BRAVE_API_KEY || process.env.BRAVE_API_KEY);
|
|
283
|
+
}
|
|
284
|
+
if (providerId === 'openrouter') {
|
|
285
|
+
const env = loadEnvFile('openrouter.env');
|
|
286
|
+
return !!(env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY);
|
|
287
|
+
}
|
|
288
|
+
if (providerId === 'elevenlabs') {
|
|
289
|
+
const env = loadEnvFile('elevenlabs.env');
|
|
290
|
+
return !!(env.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY);
|
|
291
|
+
}
|
|
292
|
+
if (providerId === 'replicate') {
|
|
293
|
+
const env = loadEnvFile('replicate.env');
|
|
294
|
+
return !!(env.REPLICATE_API_TOKEN || process.env.REPLICATE_API_TOKEN);
|
|
295
|
+
}
|
|
296
|
+
if (providerId === 'e2b') {
|
|
297
|
+
const env = loadEnvFile('e2b.env');
|
|
298
|
+
return !!(env.E2B_API_KEY || process.env.E2B_API_KEY);
|
|
299
|
+
}
|
|
300
|
+
if (providerId === 'firecrawl') {
|
|
301
|
+
const env = loadEnvFile('firecrawl.env');
|
|
302
|
+
return !!(env.FIRECRAWL_API_KEY || process.env.FIRECRAWL_API_KEY);
|
|
303
|
+
}
|
|
304
|
+
if (providerId === 'github') {
|
|
305
|
+
const env = loadEnvFile('github.env');
|
|
306
|
+
return !!(env.GITHUB_TOKEN || process.env.GITHUB_TOKEN);
|
|
307
|
+
}
|
|
308
|
+
if (providerId === 'apilayer') {
|
|
309
|
+
const env = loadEnvFile('apilayer.env');
|
|
310
|
+
return !!(env.APILAYER_EXCHANGERATE_KEY || process.env.APILAYER_EXCHANGERATE_KEY);
|
|
311
|
+
}
|
|
312
|
+
if (providerId === 'groq') {
|
|
313
|
+
const env = loadEnvFile('groq.env');
|
|
314
|
+
return !!(env.GROQ_API_KEY || process.env.GROQ_API_KEY);
|
|
315
|
+
}
|
|
316
|
+
if (providerId === 'deepgram') {
|
|
317
|
+
const env = loadEnvFile('deepgram.env');
|
|
318
|
+
return !!(env.DEEPGRAM_API_KEY || process.env.DEEPGRAM_API_KEY);
|
|
319
|
+
}
|
|
320
|
+
if (providerId === 'mistral') {
|
|
321
|
+
const env = loadEnvFile('mistral.env');
|
|
322
|
+
return !!(env.MISTRAL_API_KEY || process.env.MISTRAL_API_KEY);
|
|
323
|
+
}
|
|
324
|
+
if (providerId === 'cohere') {
|
|
325
|
+
const env = loadEnvFile('cohere.env');
|
|
326
|
+
return !!(env.COHERE_API_KEY || process.env.COHERE_API_KEY);
|
|
327
|
+
}
|
|
328
|
+
if (providerId === 'serper') {
|
|
329
|
+
const env = loadEnvFile('serpapi.env');
|
|
330
|
+
return !!(env.SERPAPI_API_KEY || process.env.SERPER_API_KEY || process.env.SERPAPI_API_KEY);
|
|
331
|
+
}
|
|
332
|
+
if (providerId === 'stability') {
|
|
333
|
+
const env = loadEnvFile('stability.env');
|
|
334
|
+
return !!(env.STABILITY_API_KEY || process.env.STABILITY_API_KEY);
|
|
335
|
+
}
|
|
336
|
+
if (providerId === 'together') {
|
|
337
|
+
const env = loadEnvFile('together.env');
|
|
338
|
+
return !!(env.TOGETHER_API_KEY || process.env.TOGETHER_API_KEY);
|
|
339
|
+
}
|
|
340
|
+
if (providerId === 'assemblyai') {
|
|
341
|
+
const env = loadEnvFile('assemblyai.env');
|
|
342
|
+
return !!(env.ASSEMBLYAI_API_KEY || process.env.ASSEMBLYAI_API_KEY);
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* List all supported providers
|
|
348
|
+
*/
|
|
349
|
+
export function listProviders() {
|
|
350
|
+
return Object.keys(providers);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get credential type for a provider
|
|
354
|
+
*/
|
|
355
|
+
export function getCredentialType(providerId) {
|
|
356
|
+
return providers[providerId]?.type || null;
|
|
357
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Credit system for APIClaw
|
|
2
|
+
// Supports both in-memory (dev) and Convex (production) backends
|
|
3
|
+
import { getCredentials, hasRealCredentials } from './credentials.js';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
// Configuration
|
|
6
|
+
const BACKEND = process.env.APICLAW_BACKEND === 'convex' ? 'convex' : 'memory';
|
|
7
|
+
const CONVEX_URL = process.env.CONVEX_URL || '';
|
|
8
|
+
const CONVEX_DEPLOY_KEY = process.env.CONVEX_DEPLOY_KEY || '';
|
|
9
|
+
// In-memory stores (for local development)
|
|
10
|
+
const agentCreditsStore = new Map();
|
|
11
|
+
const purchasesStore = new Map();
|
|
12
|
+
const usageStore = new Map();
|
|
13
|
+
// Provider that have real credentials available
|
|
14
|
+
const REAL_CREDENTIAL_PROVIDERS = ['46elks', 'twilio'];
|
|
15
|
+
// Credits per dollar by provider
|
|
16
|
+
const CREDITS_PER_DOLLAR = {
|
|
17
|
+
'46elks': 30, // ~30 SMS per dollar
|
|
18
|
+
'twilio': 25, // ~25 SMS per dollar
|
|
19
|
+
'resend': 1000, // ~1000 emails per dollar
|
|
20
|
+
'brave_search': 200, // ~200 searches per dollar
|
|
21
|
+
'openrouter': 100, // ~100k tokens per dollar (varies by model)
|
|
22
|
+
'elevenlabs': 3333 // ~3333 characters per dollar
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Calculate credits based on provider pricing
|
|
26
|
+
*/
|
|
27
|
+
function calculateCredits(providerId, amountUsd) {
|
|
28
|
+
return Math.floor(amountUsd * (CREDITS_PER_DOLLAR[providerId] || 100));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate credentials for a provider
|
|
32
|
+
* Returns real credentials for supported providers, mock for others
|
|
33
|
+
*/
|
|
34
|
+
function generateCredentials(providerId) {
|
|
35
|
+
// Try to get real credentials first
|
|
36
|
+
const realCreds = getCredentials(providerId);
|
|
37
|
+
if (realCreds && hasRealCredentials(providerId)) {
|
|
38
|
+
console.log(`[APIClaw] Using REAL credentials for ${providerId}`);
|
|
39
|
+
return realCreds;
|
|
40
|
+
}
|
|
41
|
+
// Fall back to mock credentials
|
|
42
|
+
console.log(`[APIClaw] Using MOCK credentials for ${providerId}`);
|
|
43
|
+
return realCreds || {
|
|
44
|
+
type: 'api_key',
|
|
45
|
+
api_key: `mock_${providerId}_${randomUUID().slice(0, 8)}`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// In-Memory Backend (for development)
|
|
50
|
+
// =============================================================================
|
|
51
|
+
/**
|
|
52
|
+
* Get or create agent credits account
|
|
53
|
+
*/
|
|
54
|
+
export function getAgentCredits(agentId) {
|
|
55
|
+
if (!agentCreditsStore.has(agentId)) {
|
|
56
|
+
agentCreditsStore.set(agentId, {
|
|
57
|
+
agent_id: agentId,
|
|
58
|
+
balance_usd: 0,
|
|
59
|
+
currency: 'USD',
|
|
60
|
+
created_at: new Date().toISOString(),
|
|
61
|
+
updated_at: new Date().toISOString()
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return agentCreditsStore.get(agentId);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Add credits to an agent's account
|
|
68
|
+
*/
|
|
69
|
+
export function addCredits(agentId, amountUsd) {
|
|
70
|
+
const credits = getAgentCredits(agentId);
|
|
71
|
+
credits.balance_usd += amountUsd;
|
|
72
|
+
credits.updated_at = new Date().toISOString();
|
|
73
|
+
return credits;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Purchase API access
|
|
77
|
+
* Returns real credentials for 46elks and Twilio
|
|
78
|
+
*/
|
|
79
|
+
export function purchaseAPIAccess(agentId, providerId, amountUsd) {
|
|
80
|
+
const credits = getAgentCredits(agentId);
|
|
81
|
+
// Check balance
|
|
82
|
+
if (credits.balance_usd < amountUsd) {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
error: `Insufficient balance. Have $${credits.balance_usd.toFixed(2)}, need $${amountUsd.toFixed(2)}`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Check if provider is supported
|
|
89
|
+
const providerCredentials = getCredentials(providerId);
|
|
90
|
+
if (!providerCredentials) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: `Unknown provider: ${providerId}. Supported: ${Object.keys(CREDITS_PER_DOLLAR).join(', ')}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Deduct credits
|
|
97
|
+
credits.balance_usd -= amountUsd;
|
|
98
|
+
credits.updated_at = new Date().toISOString();
|
|
99
|
+
// Generate credentials (real for 46elks/twilio, mock for others)
|
|
100
|
+
const credentials = generateCredentials(providerId);
|
|
101
|
+
// Create purchase record
|
|
102
|
+
const purchaseId = `pur_${randomUUID().slice(0, 12)}`;
|
|
103
|
+
const purchase = {
|
|
104
|
+
id: purchaseId,
|
|
105
|
+
agent_id: agentId,
|
|
106
|
+
provider_id: providerId,
|
|
107
|
+
amount_usd: amountUsd,
|
|
108
|
+
credits_purchased: calculateCredits(providerId, amountUsd),
|
|
109
|
+
status: 'active',
|
|
110
|
+
credentials,
|
|
111
|
+
created_at: new Date().toISOString()
|
|
112
|
+
};
|
|
113
|
+
purchasesStore.set(purchaseId, purchase);
|
|
114
|
+
// Initialize usage tracking
|
|
115
|
+
usageStore.set(purchaseId, {
|
|
116
|
+
purchase_id: purchaseId,
|
|
117
|
+
provider_id: providerId,
|
|
118
|
+
units_used: 0,
|
|
119
|
+
units_remaining: purchase.credits_purchased,
|
|
120
|
+
cost_incurred_usd: 0,
|
|
121
|
+
last_used_at: new Date().toISOString()
|
|
122
|
+
});
|
|
123
|
+
// Flag if using real credentials
|
|
124
|
+
const isRealCredentials = hasRealCredentials(providerId);
|
|
125
|
+
console.log(`[APIClaw] Purchase complete: ${providerId} ($${amountUsd}) - Real credentials: ${isRealCredentials}`);
|
|
126
|
+
return { success: true, purchase };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get all purchases for an agent
|
|
130
|
+
*/
|
|
131
|
+
export function getAgentPurchases(agentId) {
|
|
132
|
+
return Array.from(purchasesStore.values()).filter(p => p.agent_id === agentId);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get usage for a purchase
|
|
136
|
+
*/
|
|
137
|
+
export function getUsage(purchaseId) {
|
|
138
|
+
return usageStore.get(purchaseId) || null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Record usage (call this when API is used)
|
|
142
|
+
*/
|
|
143
|
+
export function recordUsage(purchaseId, unitsUsed, costUsd) {
|
|
144
|
+
const record = usageStore.get(purchaseId);
|
|
145
|
+
if (!record)
|
|
146
|
+
return null;
|
|
147
|
+
record.units_used += unitsUsed;
|
|
148
|
+
record.units_remaining = Math.max(0, record.units_remaining - unitsUsed);
|
|
149
|
+
record.cost_incurred_usd += costUsd;
|
|
150
|
+
record.last_used_at = new Date().toISOString();
|
|
151
|
+
// Update purchase status if depleted
|
|
152
|
+
if (record.units_remaining === 0) {
|
|
153
|
+
const purchase = purchasesStore.get(purchaseId);
|
|
154
|
+
if (purchase)
|
|
155
|
+
purchase.status = 'exhausted';
|
|
156
|
+
}
|
|
157
|
+
return record;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get full balance summary for an agent
|
|
161
|
+
*/
|
|
162
|
+
export function getBalanceSummary(agentId) {
|
|
163
|
+
const credits = getAgentCredits(agentId);
|
|
164
|
+
const agentPurchases = getAgentPurchases(agentId);
|
|
165
|
+
const activePurchases = agentPurchases.filter(p => p.status === 'active');
|
|
166
|
+
const totalSpent = agentPurchases.reduce((sum, p) => sum + p.amount_usd, 0);
|
|
167
|
+
// List providers with real credentials
|
|
168
|
+
const realCredentialProviders = REAL_CREDENTIAL_PROVIDERS.filter(p => hasRealCredentials(p));
|
|
169
|
+
return {
|
|
170
|
+
credits,
|
|
171
|
+
active_purchases: activePurchases,
|
|
172
|
+
total_spent_usd: totalSpent,
|
|
173
|
+
real_credentials_available: realCredentialProviders
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check which providers have real credentials
|
|
178
|
+
*/
|
|
179
|
+
export function getProvidersWithRealCredentials() {
|
|
180
|
+
return REAL_CREDENTIAL_PROVIDERS.filter(p => hasRealCredentials(p));
|
|
181
|
+
}
|
|
182
|
+
async function convexQuery(path, args) {
|
|
183
|
+
if (!CONVEX_URL) {
|
|
184
|
+
return { error: 'Convex URL not configured' };
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const response = await fetch(`${CONVEX_URL}/api/query`, {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: {
|
|
190
|
+
'Content-Type': 'application/json',
|
|
191
|
+
...(CONVEX_DEPLOY_KEY ? { 'Authorization': `Convex ${CONVEX_DEPLOY_KEY}` } : {}),
|
|
192
|
+
},
|
|
193
|
+
body: JSON.stringify({ path, args }),
|
|
194
|
+
});
|
|
195
|
+
const result = await response.json();
|
|
196
|
+
return { data: result };
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return { error: error instanceof Error ? error.message : 'Convex query failed' };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function convexMutation(path, args) {
|
|
203
|
+
if (!CONVEX_URL) {
|
|
204
|
+
return { error: 'Convex URL not configured' };
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Content-Type': 'application/json',
|
|
211
|
+
...(CONVEX_DEPLOY_KEY ? { 'Authorization': `Convex ${CONVEX_DEPLOY_KEY}` } : {}),
|
|
212
|
+
},
|
|
213
|
+
body: JSON.stringify({ path, args }),
|
|
214
|
+
});
|
|
215
|
+
const result = await response.json();
|
|
216
|
+
return { data: result };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return { error: error instanceof Error ? error.message : 'Convex mutation failed' };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Convex-backed versions (async)
|
|
223
|
+
export async function getAgentCreditsAsync(agentId) {
|
|
224
|
+
if (BACKEND === 'memory') {
|
|
225
|
+
return getAgentCredits(agentId);
|
|
226
|
+
}
|
|
227
|
+
const result = await convexQuery('credits:getAgentCredits', { agentId });
|
|
228
|
+
return result.data || null;
|
|
229
|
+
}
|
|
230
|
+
export async function addCreditsAsync(agentId, amountUsd) {
|
|
231
|
+
if (BACKEND === 'memory') {
|
|
232
|
+
return addCredits(agentId, amountUsd);
|
|
233
|
+
}
|
|
234
|
+
const result = await convexMutation('credits:addCredits', { agentId, amountUsd });
|
|
235
|
+
return result.data || null;
|
|
236
|
+
}
|
|
237
|
+
export async function purchaseAPIAccessAsync(agentId, providerId, amountUsd) {
|
|
238
|
+
if (BACKEND === 'memory') {
|
|
239
|
+
return purchaseAPIAccess(agentId, providerId, amountUsd);
|
|
240
|
+
}
|
|
241
|
+
// Generate credentials server-side
|
|
242
|
+
const credentials = generateCredentials(providerId);
|
|
243
|
+
const result = await convexMutation('purchases:purchaseAccess', {
|
|
244
|
+
agentId,
|
|
245
|
+
providerId,
|
|
246
|
+
amountUsd,
|
|
247
|
+
credentials,
|
|
248
|
+
});
|
|
249
|
+
if (result.error) {
|
|
250
|
+
return { success: false, error: result.error };
|
|
251
|
+
}
|
|
252
|
+
return { success: true, purchase: result.data };
|
|
253
|
+
}
|
|
254
|
+
// Export backend info
|
|
255
|
+
export function getBackendInfo() {
|
|
256
|
+
return {
|
|
257
|
+
type: BACKEND,
|
|
258
|
+
convexUrl: BACKEND === 'convex' ? CONVEX_URL : null,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
|
2
|
+
const ENCRYPTION_KEY = process.env.APICLAW_KEY_ENCRYPTION_SECRET;
|
|
3
|
+
// Validate key exists and is correct length
|
|
4
|
+
function getKey() {
|
|
5
|
+
if (!ENCRYPTION_KEY) {
|
|
6
|
+
throw new Error('APICLAW_KEY_ENCRYPTION_SECRET not set');
|
|
7
|
+
}
|
|
8
|
+
// Key should be 32 bytes for AES-256
|
|
9
|
+
const key = Buffer.from(ENCRYPTION_KEY, 'hex');
|
|
10
|
+
if (key.length !== 32) {
|
|
11
|
+
throw new Error('APICLAW_KEY_ENCRYPTION_SECRET must be 64 hex chars (32 bytes)');
|
|
12
|
+
}
|
|
13
|
+
return key;
|
|
14
|
+
}
|
|
15
|
+
export function encryptKey(plainKey) {
|
|
16
|
+
const key = getKey();
|
|
17
|
+
const iv = randomBytes(16);
|
|
18
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
19
|
+
const encrypted = Buffer.concat([cipher.update(plainKey, 'utf8'), cipher.final()]);
|
|
20
|
+
const tag = cipher.getAuthTag();
|
|
21
|
+
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`;
|
|
22
|
+
}
|
|
23
|
+
export function decryptKey(encryptedKey) {
|
|
24
|
+
const key = getKey();
|
|
25
|
+
const [ivHex, tagHex, dataHex] = encryptedKey.split(':');
|
|
26
|
+
if (!ivHex || !tagHex || !dataHex) {
|
|
27
|
+
throw new Error('Invalid encrypted key format');
|
|
28
|
+
}
|
|
29
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivHex, 'hex'));
|
|
30
|
+
decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
|
|
31
|
+
const decrypted = Buffer.concat([
|
|
32
|
+
decipher.update(Buffer.from(dataHex, 'hex')),
|
|
33
|
+
decipher.final()
|
|
34
|
+
]);
|
|
35
|
+
return decrypted.toString('utf8');
|
|
36
|
+
}
|
|
37
|
+
// SSRF Prevention
|
|
38
|
+
export function validateBaseUrl(url) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = new URL(url);
|
|
41
|
+
if (parsed.protocol !== 'https:') {
|
|
42
|
+
return { valid: false, error: 'URL must use HTTPS' };
|
|
43
|
+
}
|
|
44
|
+
const host = parsed.hostname.toLowerCase();
|
|
45
|
+
const blockedPatterns = [
|
|
46
|
+
/^127\./,
|
|
47
|
+
/^10\./,
|
|
48
|
+
/^192\.168\./,
|
|
49
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
50
|
+
/^localhost$/,
|
|
51
|
+
/^0\.0\.0\.0$/,
|
|
52
|
+
/^::1$/,
|
|
53
|
+
/\.local$/,
|
|
54
|
+
/\.internal$/,
|
|
55
|
+
];
|
|
56
|
+
for (const pattern of blockedPatterns) {
|
|
57
|
+
if (pattern.test(host)) {
|
|
58
|
+
return { valid: false, error: 'Internal/private URLs not allowed' };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { valid: true };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { valid: false, error: 'Invalid URL format' };
|
|
65
|
+
}
|
|
66
|
+
}
|