@everworker/oneringai 0.1.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/LICENSE +21 -0
- package/README.md +1228 -0
- package/dist/IProvider-BP49c93d.d.cts +22 -0
- package/dist/IProvider-BP49c93d.d.ts +22 -0
- package/dist/ImageModel-B-uH3JEz.d.cts +763 -0
- package/dist/ImageModel-C7EyUfU0.d.ts +763 -0
- package/dist/capabilities/agents/index.cjs +408 -0
- package/dist/capabilities/agents/index.cjs.map +1 -0
- package/dist/capabilities/agents/index.d.cts +3 -0
- package/dist/capabilities/agents/index.d.ts +3 -0
- package/dist/capabilities/agents/index.js +405 -0
- package/dist/capabilities/agents/index.js.map +1 -0
- package/dist/capabilities/images/index.cjs +3583 -0
- package/dist/capabilities/images/index.cjs.map +1 -0
- package/dist/capabilities/images/index.d.cts +2 -0
- package/dist/capabilities/images/index.d.ts +2 -0
- package/dist/capabilities/images/index.js +3556 -0
- package/dist/capabilities/images/index.js.map +1 -0
- package/dist/index-BmOYeqU7.d.ts +1338 -0
- package/dist/index-DCzFlLoN.d.cts +1338 -0
- package/dist/index.cjs +49257 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +12263 -0
- package/dist/index.d.ts +12263 -0
- package/dist/index.js +48953 -0
- package/dist/index.js.map +1 -0
- package/package.json +162 -0
|
@@ -0,0 +1,3583 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var jose = require('jose');
|
|
5
|
+
var fs2 = require('fs');
|
|
6
|
+
var eventemitter3 = require('eventemitter3');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var OpenAI = require('openai');
|
|
9
|
+
var genai = require('@google/genai');
|
|
10
|
+
|
|
11
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
function _interopNamespace(e) {
|
|
14
|
+
if (e && e.__esModule) return e;
|
|
15
|
+
var n = Object.create(null);
|
|
16
|
+
if (e) {
|
|
17
|
+
Object.keys(e).forEach(function (k) {
|
|
18
|
+
if (k !== 'default') {
|
|
19
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
20
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
get: function () { return e[k]; }
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
n.default = e;
|
|
28
|
+
return Object.freeze(n);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
32
|
+
var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
|
|
33
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
34
|
+
var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
|
|
35
|
+
|
|
36
|
+
// src/connectors/oauth/utils/encryption.ts
|
|
37
|
+
var ALGORITHM = "aes-256-gcm";
|
|
38
|
+
var IV_LENGTH = 16;
|
|
39
|
+
var SALT_LENGTH = 64;
|
|
40
|
+
var TAG_LENGTH = 16;
|
|
41
|
+
var KEY_LENGTH = 32;
|
|
42
|
+
function encrypt(text, password) {
|
|
43
|
+
const salt = crypto__namespace.randomBytes(SALT_LENGTH);
|
|
44
|
+
const key = crypto__namespace.pbkdf2Sync(password, salt, 1e5, KEY_LENGTH, "sha512");
|
|
45
|
+
const iv = crypto__namespace.randomBytes(IV_LENGTH);
|
|
46
|
+
const cipher = crypto__namespace.createCipheriv(ALGORITHM, key, iv);
|
|
47
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
48
|
+
encrypted += cipher.final("hex");
|
|
49
|
+
const tag = cipher.getAuthTag();
|
|
50
|
+
const result = Buffer.concat([salt, iv, tag, Buffer.from(encrypted, "hex")]);
|
|
51
|
+
return result.toString("base64");
|
|
52
|
+
}
|
|
53
|
+
function decrypt(encryptedData, password) {
|
|
54
|
+
const buffer = Buffer.from(encryptedData, "base64");
|
|
55
|
+
const salt = buffer.subarray(0, SALT_LENGTH);
|
|
56
|
+
const iv = buffer.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
|
57
|
+
const tag = buffer.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
|
|
58
|
+
const encrypted = buffer.subarray(SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
|
|
59
|
+
const key = crypto__namespace.pbkdf2Sync(password, salt, 1e5, KEY_LENGTH, "sha512");
|
|
60
|
+
const decipher = crypto__namespace.createDecipheriv(ALGORITHM, key, iv);
|
|
61
|
+
decipher.setAuthTag(tag);
|
|
62
|
+
let decrypted = decipher.update(encrypted);
|
|
63
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
64
|
+
return decrypted.toString("utf8");
|
|
65
|
+
}
|
|
66
|
+
function getEncryptionKey() {
|
|
67
|
+
if (process.env.OAUTH_ENCRYPTION_KEY) {
|
|
68
|
+
return process.env.OAUTH_ENCRYPTION_KEY;
|
|
69
|
+
}
|
|
70
|
+
if (!global.__oauthEncryptionKey) {
|
|
71
|
+
global.__oauthEncryptionKey = crypto__namespace.randomBytes(32).toString("hex");
|
|
72
|
+
console.warn(
|
|
73
|
+
"WARNING: Using auto-generated encryption key. Tokens will not persist across restarts. Set OAUTH_ENCRYPTION_KEY environment variable for production!"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return global.__oauthEncryptionKey;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/connectors/oauth/infrastructure/storage/MemoryStorage.ts
|
|
80
|
+
var MemoryStorage = class {
|
|
81
|
+
tokens = /* @__PURE__ */ new Map();
|
|
82
|
+
// Stores encrypted tokens
|
|
83
|
+
async storeToken(key, token) {
|
|
84
|
+
const encryptionKey = getEncryptionKey();
|
|
85
|
+
const plaintext = JSON.stringify(token);
|
|
86
|
+
const encrypted = encrypt(plaintext, encryptionKey);
|
|
87
|
+
this.tokens.set(key, encrypted);
|
|
88
|
+
}
|
|
89
|
+
async getToken(key) {
|
|
90
|
+
const encrypted = this.tokens.get(key);
|
|
91
|
+
if (!encrypted) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const encryptionKey = getEncryptionKey();
|
|
96
|
+
const decrypted = decrypt(encrypted, encryptionKey);
|
|
97
|
+
return JSON.parse(decrypted);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("Failed to decrypt token from memory:", error);
|
|
100
|
+
this.tokens.delete(key);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async deleteToken(key) {
|
|
105
|
+
this.tokens.delete(key);
|
|
106
|
+
}
|
|
107
|
+
async hasToken(key) {
|
|
108
|
+
return this.tokens.has(key);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Clear all tokens (useful for testing)
|
|
112
|
+
*/
|
|
113
|
+
clearAll() {
|
|
114
|
+
this.tokens.clear();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get number of stored tokens
|
|
118
|
+
*/
|
|
119
|
+
size() {
|
|
120
|
+
return this.tokens.size;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/connectors/oauth/domain/TokenStore.ts
|
|
125
|
+
var TokenStore = class {
|
|
126
|
+
storage;
|
|
127
|
+
baseStorageKey;
|
|
128
|
+
constructor(storageKey = "default", storage) {
|
|
129
|
+
this.baseStorageKey = storageKey;
|
|
130
|
+
this.storage = storage || new MemoryStorage();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get user-scoped storage key
|
|
134
|
+
* For multi-user support, keys are scoped per user: "provider:userId"
|
|
135
|
+
* For single-user (backward compatible), userId is omitted or "default"
|
|
136
|
+
*
|
|
137
|
+
* @param userId - User identifier (optional, defaults to single-user mode)
|
|
138
|
+
* @returns Storage key scoped to user
|
|
139
|
+
*/
|
|
140
|
+
getScopedKey(userId) {
|
|
141
|
+
if (!userId || userId === "default") {
|
|
142
|
+
return this.baseStorageKey;
|
|
143
|
+
}
|
|
144
|
+
return `${this.baseStorageKey}:${userId}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Store token (encrypted by storage layer)
|
|
148
|
+
* @param tokenResponse - Token response from OAuth provider
|
|
149
|
+
* @param userId - Optional user identifier for multi-user support
|
|
150
|
+
*/
|
|
151
|
+
async storeToken(tokenResponse, userId) {
|
|
152
|
+
if (!tokenResponse.access_token) {
|
|
153
|
+
throw new Error("OAuth response missing required access_token field");
|
|
154
|
+
}
|
|
155
|
+
if (typeof tokenResponse.access_token !== "string") {
|
|
156
|
+
throw new Error("access_token must be a string");
|
|
157
|
+
}
|
|
158
|
+
if (tokenResponse.expires_in !== void 0 && tokenResponse.expires_in < 0) {
|
|
159
|
+
throw new Error("expires_in must be positive");
|
|
160
|
+
}
|
|
161
|
+
const token = {
|
|
162
|
+
access_token: tokenResponse.access_token,
|
|
163
|
+
refresh_token: tokenResponse.refresh_token,
|
|
164
|
+
expires_in: tokenResponse.expires_in || 3600,
|
|
165
|
+
token_type: tokenResponse.token_type || "Bearer",
|
|
166
|
+
scope: tokenResponse.scope,
|
|
167
|
+
obtained_at: Date.now()
|
|
168
|
+
};
|
|
169
|
+
const key = this.getScopedKey(userId);
|
|
170
|
+
await this.storage.storeToken(key, token);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get access token
|
|
174
|
+
* @param userId - Optional user identifier for multi-user support
|
|
175
|
+
*/
|
|
176
|
+
async getAccessToken(userId) {
|
|
177
|
+
const key = this.getScopedKey(userId);
|
|
178
|
+
const token = await this.storage.getToken(key);
|
|
179
|
+
if (!token) {
|
|
180
|
+
throw new Error(`No token stored for ${userId ? `user: ${userId}` : "default user"}`);
|
|
181
|
+
}
|
|
182
|
+
return token.access_token;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get refresh token
|
|
186
|
+
* @param userId - Optional user identifier for multi-user support
|
|
187
|
+
*/
|
|
188
|
+
async getRefreshToken(userId) {
|
|
189
|
+
const key = this.getScopedKey(userId);
|
|
190
|
+
const token = await this.storage.getToken(key);
|
|
191
|
+
if (!token?.refresh_token) {
|
|
192
|
+
throw new Error(`No refresh token available for ${userId ? `user: ${userId}` : "default user"}`);
|
|
193
|
+
}
|
|
194
|
+
return token.refresh_token;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if has refresh token
|
|
198
|
+
* @param userId - Optional user identifier for multi-user support
|
|
199
|
+
*/
|
|
200
|
+
async hasRefreshToken(userId) {
|
|
201
|
+
const key = this.getScopedKey(userId);
|
|
202
|
+
const token = await this.storage.getToken(key);
|
|
203
|
+
return !!token?.refresh_token;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if token is valid (not expired)
|
|
207
|
+
*
|
|
208
|
+
* @param bufferSeconds - Refresh this many seconds before expiry (default: 300 = 5 min)
|
|
209
|
+
* @param userId - Optional user identifier for multi-user support
|
|
210
|
+
*/
|
|
211
|
+
async isValid(bufferSeconds = 300, userId) {
|
|
212
|
+
const key = this.getScopedKey(userId);
|
|
213
|
+
const token = await this.storage.getToken(key);
|
|
214
|
+
if (!token) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
const expiresAt = token.obtained_at + token.expires_in * 1e3;
|
|
218
|
+
const bufferMs = bufferSeconds * 1e3;
|
|
219
|
+
return Date.now() < expiresAt - bufferMs;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Clear stored token
|
|
223
|
+
* @param userId - Optional user identifier for multi-user support
|
|
224
|
+
*/
|
|
225
|
+
async clear(userId) {
|
|
226
|
+
const key = this.getScopedKey(userId);
|
|
227
|
+
await this.storage.deleteToken(key);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get full token info
|
|
231
|
+
* @param userId - Optional user identifier for multi-user support
|
|
232
|
+
*/
|
|
233
|
+
async getTokenInfo(userId) {
|
|
234
|
+
const key = this.getScopedKey(userId);
|
|
235
|
+
return this.storage.getToken(key);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
function generatePKCE() {
|
|
239
|
+
const codeVerifier = base64URLEncode(crypto__namespace.randomBytes(32));
|
|
240
|
+
const hash = crypto__namespace.createHash("sha256").update(codeVerifier).digest();
|
|
241
|
+
const codeChallenge = base64URLEncode(hash);
|
|
242
|
+
return {
|
|
243
|
+
codeVerifier,
|
|
244
|
+
codeChallenge
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function base64URLEncode(buffer) {
|
|
248
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
249
|
+
}
|
|
250
|
+
function generateState() {
|
|
251
|
+
return crypto__namespace.randomBytes(16).toString("hex");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/connectors/oauth/flows/AuthCodePKCE.ts
|
|
255
|
+
var AuthCodePKCEFlow = class {
|
|
256
|
+
constructor(config) {
|
|
257
|
+
this.config = config;
|
|
258
|
+
const storageKey = config.storageKey || `auth_code:${config.clientId}`;
|
|
259
|
+
this.tokenStore = new TokenStore(storageKey, config.storage);
|
|
260
|
+
}
|
|
261
|
+
tokenStore;
|
|
262
|
+
// Store PKCE data per user with timestamps for cleanup
|
|
263
|
+
codeVerifiers = /* @__PURE__ */ new Map();
|
|
264
|
+
states = /* @__PURE__ */ new Map();
|
|
265
|
+
// Store refresh locks per user to prevent concurrent refresh
|
|
266
|
+
refreshLocks = /* @__PURE__ */ new Map();
|
|
267
|
+
// PKCE data TTL: 15 minutes (auth flows should complete within this time)
|
|
268
|
+
PKCE_TTL = 15 * 60 * 1e3;
|
|
269
|
+
/**
|
|
270
|
+
* Generate authorization URL for user to visit
|
|
271
|
+
* Opens browser or redirects user to this URL
|
|
272
|
+
*
|
|
273
|
+
* @param userId - User identifier for multi-user support (optional)
|
|
274
|
+
*/
|
|
275
|
+
async getAuthorizationUrl(userId) {
|
|
276
|
+
if (!this.config.authorizationUrl) {
|
|
277
|
+
throw new Error("authorizationUrl is required for authorization_code flow");
|
|
278
|
+
}
|
|
279
|
+
if (!this.config.redirectUri) {
|
|
280
|
+
throw new Error("redirectUri is required for authorization_code flow");
|
|
281
|
+
}
|
|
282
|
+
this.cleanupExpiredPKCE();
|
|
283
|
+
const userKey = userId || "default";
|
|
284
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
285
|
+
this.codeVerifiers.set(userKey, { verifier: codeVerifier, timestamp: Date.now() });
|
|
286
|
+
const state = generateState();
|
|
287
|
+
this.states.set(userKey, { state, timestamp: Date.now() });
|
|
288
|
+
const params = new URLSearchParams({
|
|
289
|
+
response_type: "code",
|
|
290
|
+
client_id: this.config.clientId,
|
|
291
|
+
redirect_uri: this.config.redirectUri,
|
|
292
|
+
state
|
|
293
|
+
});
|
|
294
|
+
if (this.config.scope) {
|
|
295
|
+
params.append("scope", this.config.scope);
|
|
296
|
+
}
|
|
297
|
+
if (this.config.usePKCE !== false) {
|
|
298
|
+
params.append("code_challenge", codeChallenge);
|
|
299
|
+
params.append("code_challenge_method", "S256");
|
|
300
|
+
}
|
|
301
|
+
const stateWithUser = userId ? `${state}::${userId}` : state;
|
|
302
|
+
params.set("state", stateWithUser);
|
|
303
|
+
return `${this.config.authorizationUrl}?${params.toString()}`;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Exchange authorization code for access token
|
|
307
|
+
*
|
|
308
|
+
* @param code - Authorization code from callback
|
|
309
|
+
* @param state - State parameter from callback (for CSRF verification, may include userId)
|
|
310
|
+
* @param userId - User identifier (optional, can be extracted from state)
|
|
311
|
+
*/
|
|
312
|
+
async exchangeCode(code, state, userId) {
|
|
313
|
+
let actualState = state;
|
|
314
|
+
let actualUserId = userId;
|
|
315
|
+
if (state.includes("::")) {
|
|
316
|
+
const parts = state.split("::");
|
|
317
|
+
actualState = parts[0];
|
|
318
|
+
actualUserId = parts[1];
|
|
319
|
+
}
|
|
320
|
+
const userKey = actualUserId || "default";
|
|
321
|
+
const stateData = this.states.get(userKey);
|
|
322
|
+
if (!stateData) {
|
|
323
|
+
throw new Error(`No PKCE state found for user ${actualUserId}. Authorization flow may have expired (15 min TTL).`);
|
|
324
|
+
}
|
|
325
|
+
const expectedState = stateData.state;
|
|
326
|
+
if (actualState !== expectedState) {
|
|
327
|
+
throw new Error(`State mismatch for user ${actualUserId} - possible CSRF attack. Expected: ${expectedState}, Got: ${actualState}`);
|
|
328
|
+
}
|
|
329
|
+
if (!this.config.redirectUri) {
|
|
330
|
+
throw new Error("redirectUri is required");
|
|
331
|
+
}
|
|
332
|
+
const params = new URLSearchParams({
|
|
333
|
+
grant_type: "authorization_code",
|
|
334
|
+
code,
|
|
335
|
+
redirect_uri: this.config.redirectUri,
|
|
336
|
+
client_id: this.config.clientId
|
|
337
|
+
});
|
|
338
|
+
if (this.config.clientSecret) {
|
|
339
|
+
params.append("client_secret", this.config.clientSecret);
|
|
340
|
+
}
|
|
341
|
+
const verifierData = this.codeVerifiers.get(userKey);
|
|
342
|
+
if (this.config.usePKCE !== false && verifierData) {
|
|
343
|
+
params.append("code_verifier", verifierData.verifier);
|
|
344
|
+
}
|
|
345
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: {
|
|
348
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
349
|
+
},
|
|
350
|
+
body: params
|
|
351
|
+
});
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
const error = await response.text();
|
|
354
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${error}`);
|
|
355
|
+
}
|
|
356
|
+
const data = await response.json();
|
|
357
|
+
await this.tokenStore.storeToken(data, actualUserId);
|
|
358
|
+
this.codeVerifiers.delete(userKey);
|
|
359
|
+
this.states.delete(userKey);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Get valid token (auto-refreshes if needed)
|
|
363
|
+
* @param userId - User identifier for multi-user support
|
|
364
|
+
*/
|
|
365
|
+
async getToken(userId) {
|
|
366
|
+
const key = userId || "default";
|
|
367
|
+
if (this.refreshLocks.has(key)) {
|
|
368
|
+
return this.refreshLocks.get(key);
|
|
369
|
+
}
|
|
370
|
+
if (await this.tokenStore.isValid(this.config.refreshBeforeExpiry, userId)) {
|
|
371
|
+
return this.tokenStore.getAccessToken(userId);
|
|
372
|
+
}
|
|
373
|
+
if (await this.tokenStore.hasRefreshToken(userId)) {
|
|
374
|
+
const refreshPromise = this.refreshToken(userId);
|
|
375
|
+
this.refreshLocks.set(key, refreshPromise);
|
|
376
|
+
try {
|
|
377
|
+
return await refreshPromise;
|
|
378
|
+
} finally {
|
|
379
|
+
this.refreshLocks.delete(key);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
throw new Error(`No valid token available for ${userId ? `user: ${userId}` : "default user"}. User needs to authorize (call startAuthFlow).`);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Refresh access token using refresh token
|
|
386
|
+
* @param userId - User identifier for multi-user support
|
|
387
|
+
*/
|
|
388
|
+
async refreshToken(userId) {
|
|
389
|
+
const refreshToken = await this.tokenStore.getRefreshToken(userId);
|
|
390
|
+
const params = new URLSearchParams({
|
|
391
|
+
grant_type: "refresh_token",
|
|
392
|
+
refresh_token: refreshToken,
|
|
393
|
+
client_id: this.config.clientId
|
|
394
|
+
});
|
|
395
|
+
if (this.config.clientSecret) {
|
|
396
|
+
params.append("client_secret", this.config.clientSecret);
|
|
397
|
+
}
|
|
398
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: {
|
|
401
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
402
|
+
},
|
|
403
|
+
body: params
|
|
404
|
+
});
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const error = await response.text();
|
|
407
|
+
throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${error}`);
|
|
408
|
+
}
|
|
409
|
+
const data = await response.json();
|
|
410
|
+
await this.tokenStore.storeToken(data, userId);
|
|
411
|
+
return data.access_token;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Check if token is valid
|
|
415
|
+
* @param userId - User identifier for multi-user support
|
|
416
|
+
*/
|
|
417
|
+
async isTokenValid(userId) {
|
|
418
|
+
return this.tokenStore.isValid(this.config.refreshBeforeExpiry, userId);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Revoke token (if supported by provider)
|
|
422
|
+
* @param revocationUrl - Optional revocation endpoint
|
|
423
|
+
* @param userId - User identifier for multi-user support
|
|
424
|
+
*/
|
|
425
|
+
async revokeToken(revocationUrl, userId) {
|
|
426
|
+
if (!revocationUrl) {
|
|
427
|
+
await this.tokenStore.clear(userId);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const token = await this.tokenStore.getAccessToken(userId);
|
|
432
|
+
await fetch(revocationUrl, {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers: {
|
|
435
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
436
|
+
},
|
|
437
|
+
body: new URLSearchParams({
|
|
438
|
+
token,
|
|
439
|
+
client_id: this.config.clientId
|
|
440
|
+
})
|
|
441
|
+
});
|
|
442
|
+
} finally {
|
|
443
|
+
await this.tokenStore.clear(userId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Clean up expired PKCE data to prevent memory leaks
|
|
448
|
+
* Removes verifiers and states older than PKCE_TTL (15 minutes)
|
|
449
|
+
*/
|
|
450
|
+
cleanupExpiredPKCE() {
|
|
451
|
+
const now = Date.now();
|
|
452
|
+
for (const [key, data] of this.codeVerifiers) {
|
|
453
|
+
if (now - data.timestamp > this.PKCE_TTL) {
|
|
454
|
+
this.codeVerifiers.delete(key);
|
|
455
|
+
this.states.delete(key);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/connectors/oauth/flows/ClientCredentials.ts
|
|
462
|
+
var ClientCredentialsFlow = class {
|
|
463
|
+
constructor(config) {
|
|
464
|
+
this.config = config;
|
|
465
|
+
const storageKey = config.storageKey || `client_credentials:${config.clientId}`;
|
|
466
|
+
this.tokenStore = new TokenStore(storageKey, config.storage);
|
|
467
|
+
}
|
|
468
|
+
tokenStore;
|
|
469
|
+
/**
|
|
470
|
+
* Get token using client credentials
|
|
471
|
+
*/
|
|
472
|
+
async getToken() {
|
|
473
|
+
if (await this.tokenStore.isValid(this.config.refreshBeforeExpiry)) {
|
|
474
|
+
return this.tokenStore.getAccessToken();
|
|
475
|
+
}
|
|
476
|
+
return this.requestToken();
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Request a new token from the authorization server
|
|
480
|
+
*/
|
|
481
|
+
async requestToken() {
|
|
482
|
+
const auth = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString(
|
|
483
|
+
"base64"
|
|
484
|
+
);
|
|
485
|
+
const params = new URLSearchParams({
|
|
486
|
+
grant_type: "client_credentials"
|
|
487
|
+
});
|
|
488
|
+
if (this.config.scope) {
|
|
489
|
+
params.append("scope", this.config.scope);
|
|
490
|
+
}
|
|
491
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
492
|
+
method: "POST",
|
|
493
|
+
headers: {
|
|
494
|
+
Authorization: `Basic ${auth}`,
|
|
495
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
496
|
+
},
|
|
497
|
+
body: params
|
|
498
|
+
});
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
const error = await response.text();
|
|
501
|
+
throw new Error(`Token request failed: ${response.status} ${response.statusText} - ${error}`);
|
|
502
|
+
}
|
|
503
|
+
const data = await response.json();
|
|
504
|
+
await this.tokenStore.storeToken(data);
|
|
505
|
+
return data.access_token;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Refresh token (client credentials don't use refresh tokens)
|
|
509
|
+
* Just requests a new token
|
|
510
|
+
*/
|
|
511
|
+
async refreshToken() {
|
|
512
|
+
await this.tokenStore.clear();
|
|
513
|
+
return this.requestToken();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Check if token is valid
|
|
517
|
+
*/
|
|
518
|
+
async isTokenValid() {
|
|
519
|
+
return this.tokenStore.isValid(this.config.refreshBeforeExpiry);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
var JWTBearerFlow = class {
|
|
523
|
+
constructor(config) {
|
|
524
|
+
this.config = config;
|
|
525
|
+
const storageKey = config.storageKey || `jwt_bearer:${config.clientId}`;
|
|
526
|
+
this.tokenStore = new TokenStore(storageKey, config.storage);
|
|
527
|
+
if (config.privateKey) {
|
|
528
|
+
this.privateKey = config.privateKey;
|
|
529
|
+
} else if (config.privateKeyPath) {
|
|
530
|
+
try {
|
|
531
|
+
this.privateKey = fs2__namespace.readFileSync(config.privateKeyPath, "utf8");
|
|
532
|
+
} catch (error) {
|
|
533
|
+
throw new Error(`Failed to read private key from ${config.privateKeyPath}: ${error.message}`);
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
throw new Error("JWT Bearer flow requires privateKey or privateKeyPath");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
tokenStore;
|
|
540
|
+
privateKey;
|
|
541
|
+
/**
|
|
542
|
+
* Generate signed JWT assertion
|
|
543
|
+
*/
|
|
544
|
+
async generateJWT() {
|
|
545
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
546
|
+
const alg = this.config.tokenSigningAlg || "RS256";
|
|
547
|
+
const key = await jose.importPKCS8(this.privateKey, alg);
|
|
548
|
+
const jwt = await new jose.SignJWT({
|
|
549
|
+
scope: this.config.scope || ""
|
|
550
|
+
}).setProtectedHeader({ alg }).setIssuer(this.config.clientId).setSubject(this.config.clientId).setAudience(this.config.audience || this.config.tokenUrl).setIssuedAt(now).setExpirationTime(now + 3600).sign(key);
|
|
551
|
+
return jwt;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Get token using JWT Bearer assertion
|
|
555
|
+
*/
|
|
556
|
+
async getToken() {
|
|
557
|
+
if (await this.tokenStore.isValid(this.config.refreshBeforeExpiry)) {
|
|
558
|
+
return this.tokenStore.getAccessToken();
|
|
559
|
+
}
|
|
560
|
+
return this.requestToken();
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Request token using JWT assertion
|
|
564
|
+
*/
|
|
565
|
+
async requestToken() {
|
|
566
|
+
const assertion = await this.generateJWT();
|
|
567
|
+
const params = new URLSearchParams({
|
|
568
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
569
|
+
assertion
|
|
570
|
+
});
|
|
571
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
572
|
+
method: "POST",
|
|
573
|
+
headers: {
|
|
574
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
575
|
+
},
|
|
576
|
+
body: params
|
|
577
|
+
});
|
|
578
|
+
if (!response.ok) {
|
|
579
|
+
const error = await response.text();
|
|
580
|
+
throw new Error(`JWT Bearer token request failed: ${response.status} ${response.statusText} - ${error}`);
|
|
581
|
+
}
|
|
582
|
+
const data = await response.json();
|
|
583
|
+
await this.tokenStore.storeToken(data);
|
|
584
|
+
return data.access_token;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Refresh token (generate new JWT and request new token)
|
|
588
|
+
*/
|
|
589
|
+
async refreshToken() {
|
|
590
|
+
await this.tokenStore.clear();
|
|
591
|
+
return this.requestToken();
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Check if token is valid
|
|
595
|
+
*/
|
|
596
|
+
async isTokenValid() {
|
|
597
|
+
return this.tokenStore.isValid(this.config.refreshBeforeExpiry);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/connectors/oauth/flows/StaticToken.ts
|
|
602
|
+
var StaticTokenFlow = class {
|
|
603
|
+
token;
|
|
604
|
+
constructor(config) {
|
|
605
|
+
if (!config.staticToken) {
|
|
606
|
+
throw new Error("Static token flow requires staticToken in config");
|
|
607
|
+
}
|
|
608
|
+
this.token = config.staticToken;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Get token (always returns the static token)
|
|
612
|
+
*/
|
|
613
|
+
async getToken() {
|
|
614
|
+
return this.token;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Refresh token (no-op for static tokens)
|
|
618
|
+
*/
|
|
619
|
+
async refreshToken() {
|
|
620
|
+
return this.token;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Token is always valid for static tokens
|
|
624
|
+
*/
|
|
625
|
+
async isTokenValid() {
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Update the static token
|
|
630
|
+
*/
|
|
631
|
+
updateToken(newToken) {
|
|
632
|
+
this.token = newToken;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// src/connectors/oauth/OAuthManager.ts
|
|
637
|
+
var OAuthManager = class {
|
|
638
|
+
flow;
|
|
639
|
+
constructor(config) {
|
|
640
|
+
this.validateConfig(config);
|
|
641
|
+
switch (config.flow) {
|
|
642
|
+
case "authorization_code":
|
|
643
|
+
this.flow = new AuthCodePKCEFlow(config);
|
|
644
|
+
break;
|
|
645
|
+
case "client_credentials":
|
|
646
|
+
this.flow = new ClientCredentialsFlow(config);
|
|
647
|
+
break;
|
|
648
|
+
case "jwt_bearer":
|
|
649
|
+
this.flow = new JWTBearerFlow(config);
|
|
650
|
+
break;
|
|
651
|
+
case "static_token":
|
|
652
|
+
this.flow = new StaticTokenFlow(config);
|
|
653
|
+
break;
|
|
654
|
+
default:
|
|
655
|
+
throw new Error(`Unknown OAuth flow: ${config.flow}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Get valid access token
|
|
660
|
+
* Automatically refreshes if expired
|
|
661
|
+
*
|
|
662
|
+
* @param userId - User identifier for multi-user support (optional)
|
|
663
|
+
*/
|
|
664
|
+
async getToken(userId) {
|
|
665
|
+
return this.flow.getToken(userId);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Force refresh the token
|
|
669
|
+
*
|
|
670
|
+
* @param userId - User identifier for multi-user support (optional)
|
|
671
|
+
*/
|
|
672
|
+
async refreshToken(userId) {
|
|
673
|
+
return this.flow.refreshToken(userId);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Check if current token is valid
|
|
677
|
+
*
|
|
678
|
+
* @param userId - User identifier for multi-user support (optional)
|
|
679
|
+
*/
|
|
680
|
+
async isTokenValid(userId) {
|
|
681
|
+
return this.flow.isTokenValid(userId);
|
|
682
|
+
}
|
|
683
|
+
// ==================== Authorization Code Flow Methods ====================
|
|
684
|
+
/**
|
|
685
|
+
* Start authorization flow (Authorization Code only)
|
|
686
|
+
* Returns URL for user to visit
|
|
687
|
+
*
|
|
688
|
+
* @param userId - User identifier for multi-user support (optional)
|
|
689
|
+
* @returns Authorization URL for the user to visit
|
|
690
|
+
*/
|
|
691
|
+
async startAuthFlow(userId) {
|
|
692
|
+
if (!(this.flow instanceof AuthCodePKCEFlow)) {
|
|
693
|
+
throw new Error("startAuthFlow() is only available for authorization_code flow");
|
|
694
|
+
}
|
|
695
|
+
return this.flow.getAuthorizationUrl(userId);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Handle OAuth callback (Authorization Code only)
|
|
699
|
+
* Call this with the callback URL after user authorizes
|
|
700
|
+
*
|
|
701
|
+
* @param callbackUrl - Full callback URL with code and state parameters
|
|
702
|
+
* @param userId - Optional user identifier (can be extracted from state if embedded)
|
|
703
|
+
*/
|
|
704
|
+
async handleCallback(callbackUrl, userId) {
|
|
705
|
+
if (!(this.flow instanceof AuthCodePKCEFlow)) {
|
|
706
|
+
throw new Error("handleCallback() is only available for authorization_code flow");
|
|
707
|
+
}
|
|
708
|
+
const url = new URL(callbackUrl);
|
|
709
|
+
const code = url.searchParams.get("code");
|
|
710
|
+
const state = url.searchParams.get("state");
|
|
711
|
+
if (!code) {
|
|
712
|
+
throw new Error("Missing authorization code in callback URL");
|
|
713
|
+
}
|
|
714
|
+
if (!state) {
|
|
715
|
+
throw new Error("Missing state parameter in callback URL");
|
|
716
|
+
}
|
|
717
|
+
await this.flow.exchangeCode(code, state, userId);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Revoke token (if supported by provider)
|
|
721
|
+
*
|
|
722
|
+
* @param revocationUrl - Optional revocation endpoint URL
|
|
723
|
+
* @param userId - User identifier for multi-user support (optional)
|
|
724
|
+
*/
|
|
725
|
+
async revokeToken(revocationUrl, userId) {
|
|
726
|
+
if (this.flow instanceof AuthCodePKCEFlow) {
|
|
727
|
+
await this.flow.revokeToken(revocationUrl, userId);
|
|
728
|
+
} else {
|
|
729
|
+
throw new Error("Token revocation not implemented for this flow");
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// ==================== Validation ====================
|
|
733
|
+
validateConfig(config) {
|
|
734
|
+
if (!config.flow) {
|
|
735
|
+
throw new Error("OAuth flow is required (authorization_code, client_credentials, jwt_bearer, or static_token)");
|
|
736
|
+
}
|
|
737
|
+
if (config.flow !== "static_token") {
|
|
738
|
+
if (!config.tokenUrl) {
|
|
739
|
+
throw new Error("tokenUrl is required");
|
|
740
|
+
}
|
|
741
|
+
if (!config.clientId) {
|
|
742
|
+
throw new Error("clientId is required");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
switch (config.flow) {
|
|
746
|
+
case "authorization_code":
|
|
747
|
+
if (!config.authorizationUrl) {
|
|
748
|
+
throw new Error("authorizationUrl is required for authorization_code flow");
|
|
749
|
+
}
|
|
750
|
+
if (!config.redirectUri) {
|
|
751
|
+
throw new Error("redirectUri is required for authorization_code flow");
|
|
752
|
+
}
|
|
753
|
+
break;
|
|
754
|
+
case "client_credentials":
|
|
755
|
+
if (!config.clientSecret) {
|
|
756
|
+
throw new Error("clientSecret is required for client_credentials flow");
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
case "jwt_bearer":
|
|
760
|
+
if (!config.privateKey && !config.privateKeyPath) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
"privateKey or privateKeyPath is required for jwt_bearer flow"
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
case "static_token":
|
|
767
|
+
if (!config.staticToken) {
|
|
768
|
+
throw new Error("staticToken is required for static_token flow");
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
if (config.storage && !process.env.OAUTH_ENCRYPTION_KEY) {
|
|
773
|
+
console.warn(
|
|
774
|
+
"WARNING: Using persistent storage without OAUTH_ENCRYPTION_KEY environment variable. Tokens will be encrypted with auto-generated key that changes on restart!"
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
var DEFAULT_CIRCUIT_BREAKER_CONFIG = {
|
|
780
|
+
failureThreshold: 5,
|
|
781
|
+
successThreshold: 2,
|
|
782
|
+
resetTimeoutMs: 3e4,
|
|
783
|
+
// 30 seconds
|
|
784
|
+
windowMs: 6e4,
|
|
785
|
+
// 1 minute
|
|
786
|
+
isRetryable: () => true
|
|
787
|
+
// All errors count by default
|
|
788
|
+
};
|
|
789
|
+
var CircuitOpenError = class extends Error {
|
|
790
|
+
constructor(breakerName, nextRetryTime, failureCount, lastError) {
|
|
791
|
+
const retryInSeconds = Math.ceil((nextRetryTime - Date.now()) / 1e3);
|
|
792
|
+
super(
|
|
793
|
+
`Circuit breaker '${breakerName}' is OPEN. Retry in ${retryInSeconds}s. (${failureCount} recent failures, last: ${lastError})`
|
|
794
|
+
);
|
|
795
|
+
this.breakerName = breakerName;
|
|
796
|
+
this.nextRetryTime = nextRetryTime;
|
|
797
|
+
this.failureCount = failureCount;
|
|
798
|
+
this.lastError = lastError;
|
|
799
|
+
this.name = "CircuitOpenError";
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
var CircuitBreaker = class extends eventemitter3.EventEmitter {
|
|
803
|
+
constructor(name, config = {}) {
|
|
804
|
+
super();
|
|
805
|
+
this.name = name;
|
|
806
|
+
this.config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
|
|
807
|
+
this.lastStateChange = Date.now();
|
|
808
|
+
}
|
|
809
|
+
state = "closed";
|
|
810
|
+
config;
|
|
811
|
+
// Failure tracking
|
|
812
|
+
failures = [];
|
|
813
|
+
lastError = "";
|
|
814
|
+
// Success tracking
|
|
815
|
+
consecutiveSuccesses = 0;
|
|
816
|
+
// Timing
|
|
817
|
+
openedAt;
|
|
818
|
+
lastStateChange;
|
|
819
|
+
// Metrics
|
|
820
|
+
totalRequests = 0;
|
|
821
|
+
successCount = 0;
|
|
822
|
+
failureCount = 0;
|
|
823
|
+
rejectedCount = 0;
|
|
824
|
+
lastFailureTime;
|
|
825
|
+
lastSuccessTime;
|
|
826
|
+
/**
|
|
827
|
+
* Execute function with circuit breaker protection
|
|
828
|
+
*/
|
|
829
|
+
async execute(fn) {
|
|
830
|
+
this.totalRequests++;
|
|
831
|
+
const now = Date.now();
|
|
832
|
+
switch (this.state) {
|
|
833
|
+
case "open":
|
|
834
|
+
if (this.openedAt && now - this.openedAt >= this.config.resetTimeoutMs) {
|
|
835
|
+
this.transitionTo("half-open");
|
|
836
|
+
} else {
|
|
837
|
+
this.rejectedCount++;
|
|
838
|
+
const nextRetry = (this.openedAt || now) + this.config.resetTimeoutMs;
|
|
839
|
+
throw new CircuitOpenError(this.name, nextRetry, this.failures.length, this.lastError);
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const result = await fn();
|
|
845
|
+
this.recordSuccess();
|
|
846
|
+
return result;
|
|
847
|
+
} catch (error) {
|
|
848
|
+
this.recordFailure(error);
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Record successful execution
|
|
854
|
+
*/
|
|
855
|
+
recordSuccess() {
|
|
856
|
+
this.successCount++;
|
|
857
|
+
this.lastSuccessTime = Date.now();
|
|
858
|
+
this.consecutiveSuccesses++;
|
|
859
|
+
if (this.state === "half-open") {
|
|
860
|
+
if (this.consecutiveSuccesses >= this.config.successThreshold) {
|
|
861
|
+
this.transitionTo("closed");
|
|
862
|
+
}
|
|
863
|
+
} else if (this.state === "closed") {
|
|
864
|
+
this.pruneOldFailures();
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Record failed execution
|
|
869
|
+
*/
|
|
870
|
+
recordFailure(error) {
|
|
871
|
+
if (this.config.isRetryable && !this.config.isRetryable(error)) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
this.failureCount++;
|
|
875
|
+
this.lastFailureTime = Date.now();
|
|
876
|
+
this.lastError = error.message;
|
|
877
|
+
this.consecutiveSuccesses = 0;
|
|
878
|
+
this.failures.push({
|
|
879
|
+
timestamp: Date.now(),
|
|
880
|
+
error: error.message
|
|
881
|
+
});
|
|
882
|
+
this.pruneOldFailures();
|
|
883
|
+
if (this.state === "half-open") {
|
|
884
|
+
this.transitionTo("open");
|
|
885
|
+
} else if (this.state === "closed") {
|
|
886
|
+
if (this.failures.length >= this.config.failureThreshold) {
|
|
887
|
+
this.transitionTo("open");
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Transition to new state
|
|
893
|
+
*/
|
|
894
|
+
transitionTo(newState) {
|
|
895
|
+
this.state = newState;
|
|
896
|
+
this.lastStateChange = Date.now();
|
|
897
|
+
switch (newState) {
|
|
898
|
+
case "open":
|
|
899
|
+
this.openedAt = Date.now();
|
|
900
|
+
this.emit("opened", {
|
|
901
|
+
name: this.name,
|
|
902
|
+
failureCount: this.failures.length,
|
|
903
|
+
lastError: this.lastError,
|
|
904
|
+
nextRetryTime: this.openedAt + this.config.resetTimeoutMs
|
|
905
|
+
});
|
|
906
|
+
break;
|
|
907
|
+
case "half-open":
|
|
908
|
+
this.emit("half-open", {
|
|
909
|
+
name: this.name,
|
|
910
|
+
timestamp: Date.now()
|
|
911
|
+
});
|
|
912
|
+
break;
|
|
913
|
+
case "closed":
|
|
914
|
+
this.failures = [];
|
|
915
|
+
this.consecutiveSuccesses = 0;
|
|
916
|
+
this.openedAt = void 0;
|
|
917
|
+
this.emit("closed", {
|
|
918
|
+
name: this.name,
|
|
919
|
+
successCount: this.consecutiveSuccesses,
|
|
920
|
+
timestamp: Date.now()
|
|
921
|
+
});
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Remove failures outside the time window
|
|
927
|
+
*/
|
|
928
|
+
pruneOldFailures() {
|
|
929
|
+
const now = Date.now();
|
|
930
|
+
const cutoff = now - this.config.windowMs;
|
|
931
|
+
this.failures = this.failures.filter((f) => f.timestamp > cutoff);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Get current state
|
|
935
|
+
*/
|
|
936
|
+
getState() {
|
|
937
|
+
return this.state;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Get current metrics
|
|
941
|
+
*/
|
|
942
|
+
getMetrics() {
|
|
943
|
+
this.pruneOldFailures();
|
|
944
|
+
const total = this.successCount + this.failureCount;
|
|
945
|
+
const failureRate = total > 0 ? this.failureCount / total : 0;
|
|
946
|
+
const successRate = total > 0 ? this.successCount / total : 0;
|
|
947
|
+
return {
|
|
948
|
+
name: this.name,
|
|
949
|
+
state: this.state,
|
|
950
|
+
totalRequests: this.totalRequests,
|
|
951
|
+
successCount: this.successCount,
|
|
952
|
+
failureCount: this.failureCount,
|
|
953
|
+
rejectedCount: this.rejectedCount,
|
|
954
|
+
recentFailures: this.failures.length,
|
|
955
|
+
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
956
|
+
lastFailureTime: this.lastFailureTime,
|
|
957
|
+
lastSuccessTime: this.lastSuccessTime,
|
|
958
|
+
lastStateChange: this.lastStateChange,
|
|
959
|
+
nextRetryTime: this.openedAt ? this.openedAt + this.config.resetTimeoutMs : void 0,
|
|
960
|
+
failureRate,
|
|
961
|
+
successRate
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Manually reset circuit breaker (force close)
|
|
966
|
+
*/
|
|
967
|
+
reset() {
|
|
968
|
+
this.transitionTo("closed");
|
|
969
|
+
this.totalRequests = 0;
|
|
970
|
+
this.successCount = 0;
|
|
971
|
+
this.failureCount = 0;
|
|
972
|
+
this.rejectedCount = 0;
|
|
973
|
+
this.lastFailureTime = void 0;
|
|
974
|
+
this.lastSuccessTime = void 0;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Check if circuit is allowing requests
|
|
978
|
+
*/
|
|
979
|
+
isOpen() {
|
|
980
|
+
if (this.state === "open" && this.openedAt) {
|
|
981
|
+
const now = Date.now();
|
|
982
|
+
if (now - this.openedAt >= this.config.resetTimeoutMs) {
|
|
983
|
+
this.transitionTo("half-open");
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Get configuration
|
|
992
|
+
*/
|
|
993
|
+
getConfig() {
|
|
994
|
+
return { ...this.config };
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// src/infrastructure/resilience/BackoffStrategy.ts
|
|
999
|
+
var DEFAULT_BACKOFF_CONFIG = {
|
|
1000
|
+
strategy: "exponential",
|
|
1001
|
+
initialDelayMs: 1e3,
|
|
1002
|
+
// 1 second
|
|
1003
|
+
maxDelayMs: 3e4,
|
|
1004
|
+
// 30 seconds
|
|
1005
|
+
multiplier: 2,
|
|
1006
|
+
jitter: true,
|
|
1007
|
+
jitterFactor: 0.1
|
|
1008
|
+
};
|
|
1009
|
+
function calculateBackoff(attempt, config = DEFAULT_BACKOFF_CONFIG) {
|
|
1010
|
+
let delay;
|
|
1011
|
+
switch (config.strategy) {
|
|
1012
|
+
case "exponential":
|
|
1013
|
+
delay = config.initialDelayMs * Math.pow(config.multiplier || 2, attempt - 1);
|
|
1014
|
+
break;
|
|
1015
|
+
case "linear":
|
|
1016
|
+
delay = config.initialDelayMs + (config.incrementMs || 1e3) * (attempt - 1);
|
|
1017
|
+
break;
|
|
1018
|
+
case "constant":
|
|
1019
|
+
delay = config.initialDelayMs;
|
|
1020
|
+
break;
|
|
1021
|
+
default:
|
|
1022
|
+
delay = config.initialDelayMs;
|
|
1023
|
+
}
|
|
1024
|
+
delay = Math.min(delay, config.maxDelayMs);
|
|
1025
|
+
if (config.jitter) {
|
|
1026
|
+
delay = addJitter(delay, config.jitterFactor || 0.1);
|
|
1027
|
+
}
|
|
1028
|
+
return Math.floor(delay);
|
|
1029
|
+
}
|
|
1030
|
+
function addJitter(delay, factor = 0.1) {
|
|
1031
|
+
const jitterRange = delay * factor;
|
|
1032
|
+
const jitter = (Math.random() * 2 - 1) * jitterRange;
|
|
1033
|
+
return delay + jitter;
|
|
1034
|
+
}
|
|
1035
|
+
var LOG_LEVEL_VALUES = {
|
|
1036
|
+
trace: 10,
|
|
1037
|
+
debug: 20,
|
|
1038
|
+
info: 30,
|
|
1039
|
+
warn: 40,
|
|
1040
|
+
error: 50,
|
|
1041
|
+
silent: 100
|
|
1042
|
+
};
|
|
1043
|
+
function safeStringify(obj, indent) {
|
|
1044
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1045
|
+
const replacer = (_key, value) => {
|
|
1046
|
+
if (value === null || value === void 0) {
|
|
1047
|
+
return value;
|
|
1048
|
+
}
|
|
1049
|
+
if (typeof value !== "object") {
|
|
1050
|
+
if (typeof value === "function") {
|
|
1051
|
+
return "[Function]";
|
|
1052
|
+
}
|
|
1053
|
+
if (typeof value === "bigint") {
|
|
1054
|
+
return value.toString();
|
|
1055
|
+
}
|
|
1056
|
+
return value;
|
|
1057
|
+
}
|
|
1058
|
+
const objValue = value;
|
|
1059
|
+
const constructor = objValue.constructor?.name || "";
|
|
1060
|
+
if (constructor === "Timeout" || constructor === "TimersList" || constructor === "Socket" || constructor === "Server" || constructor === "IncomingMessage" || constructor === "ServerResponse" || constructor === "WriteStream" || constructor === "ReadStream" || constructor === "EventEmitter") {
|
|
1061
|
+
return `[${constructor}]`;
|
|
1062
|
+
}
|
|
1063
|
+
if (seen.has(objValue)) {
|
|
1064
|
+
return "[Circular]";
|
|
1065
|
+
}
|
|
1066
|
+
if (objValue instanceof Error) {
|
|
1067
|
+
return {
|
|
1068
|
+
name: objValue.name,
|
|
1069
|
+
message: objValue.message,
|
|
1070
|
+
stack: objValue.stack
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
if (objValue instanceof Date) {
|
|
1074
|
+
return objValue.toISOString();
|
|
1075
|
+
}
|
|
1076
|
+
if (objValue instanceof Map) {
|
|
1077
|
+
return Object.fromEntries(objValue);
|
|
1078
|
+
}
|
|
1079
|
+
if (objValue instanceof Set) {
|
|
1080
|
+
return Array.from(objValue);
|
|
1081
|
+
}
|
|
1082
|
+
if (Buffer.isBuffer(objValue)) {
|
|
1083
|
+
return `[Buffer(${objValue.length})]`;
|
|
1084
|
+
}
|
|
1085
|
+
seen.add(objValue);
|
|
1086
|
+
return value;
|
|
1087
|
+
};
|
|
1088
|
+
try {
|
|
1089
|
+
return JSON.stringify(obj, replacer, indent);
|
|
1090
|
+
} catch {
|
|
1091
|
+
return "[Unserializable]";
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
var FrameworkLogger = class _FrameworkLogger {
|
|
1095
|
+
config;
|
|
1096
|
+
context;
|
|
1097
|
+
levelValue;
|
|
1098
|
+
fileStream;
|
|
1099
|
+
constructor(config = {}) {
|
|
1100
|
+
this.config = {
|
|
1101
|
+
level: config.level || process.env.LOG_LEVEL || "info",
|
|
1102
|
+
pretty: config.pretty ?? (process.env.LOG_PRETTY === "true" || process.env.NODE_ENV === "development"),
|
|
1103
|
+
destination: config.destination || "console",
|
|
1104
|
+
context: config.context || {},
|
|
1105
|
+
filePath: config.filePath || process.env.LOG_FILE
|
|
1106
|
+
};
|
|
1107
|
+
this.context = this.config.context || {};
|
|
1108
|
+
this.levelValue = LOG_LEVEL_VALUES[this.config.level || "info"];
|
|
1109
|
+
if (this.config.filePath) {
|
|
1110
|
+
this.initFileStream(this.config.filePath);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Initialize file stream for logging
|
|
1115
|
+
*/
|
|
1116
|
+
initFileStream(filePath) {
|
|
1117
|
+
try {
|
|
1118
|
+
const dir = path__namespace.dirname(filePath);
|
|
1119
|
+
if (!fs2__namespace.existsSync(dir)) {
|
|
1120
|
+
fs2__namespace.mkdirSync(dir, { recursive: true });
|
|
1121
|
+
}
|
|
1122
|
+
this.fileStream = fs2__namespace.createWriteStream(filePath, {
|
|
1123
|
+
flags: "a",
|
|
1124
|
+
// append mode
|
|
1125
|
+
encoding: "utf8"
|
|
1126
|
+
});
|
|
1127
|
+
this.fileStream.on("error", (err) => {
|
|
1128
|
+
console.error(`[Logger] File stream error: ${err.message}`);
|
|
1129
|
+
this.fileStream = void 0;
|
|
1130
|
+
});
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
console.error(`[Logger] Failed to initialize log file: ${err instanceof Error ? err.message : err}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Create child logger with additional context
|
|
1137
|
+
*/
|
|
1138
|
+
child(context) {
|
|
1139
|
+
return new _FrameworkLogger({
|
|
1140
|
+
...this.config,
|
|
1141
|
+
context: { ...this.context, ...context }
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Trace log
|
|
1146
|
+
*/
|
|
1147
|
+
trace(obj, msg) {
|
|
1148
|
+
this.log("trace", obj, msg);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Debug log
|
|
1152
|
+
*/
|
|
1153
|
+
debug(obj, msg) {
|
|
1154
|
+
this.log("debug", obj, msg);
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Info log
|
|
1158
|
+
*/
|
|
1159
|
+
info(obj, msg) {
|
|
1160
|
+
this.log("info", obj, msg);
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Warn log
|
|
1164
|
+
*/
|
|
1165
|
+
warn(obj, msg) {
|
|
1166
|
+
this.log("warn", obj, msg);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Error log
|
|
1170
|
+
*/
|
|
1171
|
+
error(obj, msg) {
|
|
1172
|
+
this.log("error", obj, msg);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Internal log method
|
|
1176
|
+
*/
|
|
1177
|
+
log(level, obj, msg) {
|
|
1178
|
+
if (LOG_LEVEL_VALUES[level] < this.levelValue) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
let data;
|
|
1182
|
+
let message;
|
|
1183
|
+
if (typeof obj === "string") {
|
|
1184
|
+
message = obj;
|
|
1185
|
+
data = {};
|
|
1186
|
+
} else {
|
|
1187
|
+
message = msg || "";
|
|
1188
|
+
data = obj;
|
|
1189
|
+
}
|
|
1190
|
+
const entry = {
|
|
1191
|
+
level,
|
|
1192
|
+
time: Date.now(),
|
|
1193
|
+
...this.context,
|
|
1194
|
+
...data,
|
|
1195
|
+
msg: message
|
|
1196
|
+
};
|
|
1197
|
+
this.output(entry);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Output log entry
|
|
1201
|
+
*/
|
|
1202
|
+
output(entry) {
|
|
1203
|
+
if (this.config.pretty) {
|
|
1204
|
+
this.prettyPrint(entry);
|
|
1205
|
+
} else {
|
|
1206
|
+
this.jsonPrint(entry);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Pretty print for development
|
|
1211
|
+
*/
|
|
1212
|
+
prettyPrint(entry) {
|
|
1213
|
+
const levelColors = {
|
|
1214
|
+
trace: "\x1B[90m",
|
|
1215
|
+
// Gray
|
|
1216
|
+
debug: "\x1B[36m",
|
|
1217
|
+
// Cyan
|
|
1218
|
+
info: "\x1B[32m",
|
|
1219
|
+
// Green
|
|
1220
|
+
warn: "\x1B[33m",
|
|
1221
|
+
// Yellow
|
|
1222
|
+
error: "\x1B[31m",
|
|
1223
|
+
// Red
|
|
1224
|
+
silent: ""
|
|
1225
|
+
};
|
|
1226
|
+
const reset = "\x1B[0m";
|
|
1227
|
+
const color = this.fileStream ? "" : levelColors[entry.level] || "";
|
|
1228
|
+
const time = new Date(entry.time).toISOString().substring(11, 23);
|
|
1229
|
+
const levelStr = entry.level.toUpperCase().padEnd(5);
|
|
1230
|
+
const contextParts = [];
|
|
1231
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
1232
|
+
if (key !== "level" && key !== "time" && key !== "msg") {
|
|
1233
|
+
contextParts.push(`${key}=${safeStringify(value)}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
const context = contextParts.length > 0 ? ` ${contextParts.join(" ")}` : "";
|
|
1237
|
+
const output = `${color}[${time}] ${levelStr}${reset} ${entry.msg}${context}`;
|
|
1238
|
+
if (this.fileStream) {
|
|
1239
|
+
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1240
|
+
this.fileStream.write(cleanOutput + "\n");
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
switch (entry.level) {
|
|
1244
|
+
case "error":
|
|
1245
|
+
case "warn":
|
|
1246
|
+
console.error(output);
|
|
1247
|
+
break;
|
|
1248
|
+
default:
|
|
1249
|
+
console.log(output);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* JSON print for production
|
|
1254
|
+
*/
|
|
1255
|
+
jsonPrint(entry) {
|
|
1256
|
+
const json = safeStringify(entry);
|
|
1257
|
+
if (this.fileStream) {
|
|
1258
|
+
this.fileStream.write(json + "\n");
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
switch (this.config.destination) {
|
|
1262
|
+
case "stderr":
|
|
1263
|
+
console.error(json);
|
|
1264
|
+
break;
|
|
1265
|
+
default:
|
|
1266
|
+
console.log(json);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Update configuration
|
|
1271
|
+
*/
|
|
1272
|
+
updateConfig(config) {
|
|
1273
|
+
this.config = { ...this.config, ...config };
|
|
1274
|
+
if (config.level) {
|
|
1275
|
+
this.levelValue = LOG_LEVEL_VALUES[config.level];
|
|
1276
|
+
}
|
|
1277
|
+
if (config.context) {
|
|
1278
|
+
this.context = { ...this.context, ...config.context };
|
|
1279
|
+
}
|
|
1280
|
+
if (config.filePath !== void 0) {
|
|
1281
|
+
this.closeFileStream();
|
|
1282
|
+
if (config.filePath) {
|
|
1283
|
+
this.initFileStream(config.filePath);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Close file stream
|
|
1289
|
+
*/
|
|
1290
|
+
closeFileStream() {
|
|
1291
|
+
if (this.fileStream) {
|
|
1292
|
+
this.fileStream.end();
|
|
1293
|
+
this.fileStream = void 0;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Cleanup resources (call before process exit)
|
|
1298
|
+
*/
|
|
1299
|
+
close() {
|
|
1300
|
+
this.closeFileStream();
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Get current log level
|
|
1304
|
+
*/
|
|
1305
|
+
getLevel() {
|
|
1306
|
+
return this.config.level || "info";
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Check if level is enabled
|
|
1310
|
+
*/
|
|
1311
|
+
isLevelEnabled(level) {
|
|
1312
|
+
return LOG_LEVEL_VALUES[level] >= this.levelValue;
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
var logger = new FrameworkLogger({
|
|
1316
|
+
level: process.env.LOG_LEVEL || "info",
|
|
1317
|
+
pretty: process.env.LOG_PRETTY === "true" || process.env.NODE_ENV === "development",
|
|
1318
|
+
filePath: process.env.LOG_FILE
|
|
1319
|
+
});
|
|
1320
|
+
process.on("exit", () => {
|
|
1321
|
+
logger.close();
|
|
1322
|
+
});
|
|
1323
|
+
process.on("SIGINT", () => {
|
|
1324
|
+
logger.close();
|
|
1325
|
+
process.exit(0);
|
|
1326
|
+
});
|
|
1327
|
+
process.on("SIGTERM", () => {
|
|
1328
|
+
logger.close();
|
|
1329
|
+
process.exit(0);
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
// src/infrastructure/observability/Metrics.ts
|
|
1333
|
+
var NoOpMetrics = class {
|
|
1334
|
+
increment() {
|
|
1335
|
+
}
|
|
1336
|
+
gauge() {
|
|
1337
|
+
}
|
|
1338
|
+
timing() {
|
|
1339
|
+
}
|
|
1340
|
+
histogram() {
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
var ConsoleMetrics = class {
|
|
1344
|
+
prefix;
|
|
1345
|
+
constructor(prefix = "oneringai") {
|
|
1346
|
+
this.prefix = prefix;
|
|
1347
|
+
}
|
|
1348
|
+
increment(metric, value = 1, tags) {
|
|
1349
|
+
this.log("COUNTER", metric, value, tags);
|
|
1350
|
+
}
|
|
1351
|
+
gauge(metric, value, tags) {
|
|
1352
|
+
this.log("GAUGE", metric, value, tags);
|
|
1353
|
+
}
|
|
1354
|
+
timing(metric, duration, tags) {
|
|
1355
|
+
this.log("TIMING", metric, `${duration}ms`, tags);
|
|
1356
|
+
}
|
|
1357
|
+
histogram(metric, value, tags) {
|
|
1358
|
+
this.log("HISTOGRAM", metric, value, tags);
|
|
1359
|
+
}
|
|
1360
|
+
log(type, metric, value, tags) {
|
|
1361
|
+
const fullMetric = `${this.prefix}.${metric}`;
|
|
1362
|
+
const tagsStr = tags ? ` ${JSON.stringify(tags)}` : "";
|
|
1363
|
+
console.log(`[METRIC:${type}] ${fullMetric}=${value}${tagsStr}`);
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
var InMemoryMetrics = class {
|
|
1367
|
+
counters = /* @__PURE__ */ new Map();
|
|
1368
|
+
gauges = /* @__PURE__ */ new Map();
|
|
1369
|
+
timings = /* @__PURE__ */ new Map();
|
|
1370
|
+
histograms = /* @__PURE__ */ new Map();
|
|
1371
|
+
increment(metric, value = 1, tags) {
|
|
1372
|
+
const key = this.makeKey(metric, tags);
|
|
1373
|
+
this.counters.set(key, (this.counters.get(key) || 0) + value);
|
|
1374
|
+
}
|
|
1375
|
+
gauge(metric, value, tags) {
|
|
1376
|
+
const key = this.makeKey(metric, tags);
|
|
1377
|
+
this.gauges.set(key, value);
|
|
1378
|
+
}
|
|
1379
|
+
timing(metric, duration, tags) {
|
|
1380
|
+
const key = this.makeKey(metric, tags);
|
|
1381
|
+
const timings = this.timings.get(key) || [];
|
|
1382
|
+
timings.push(duration);
|
|
1383
|
+
this.timings.set(key, timings);
|
|
1384
|
+
}
|
|
1385
|
+
histogram(metric, value, tags) {
|
|
1386
|
+
const key = this.makeKey(metric, tags);
|
|
1387
|
+
const values = this.histograms.get(key) || [];
|
|
1388
|
+
values.push(value);
|
|
1389
|
+
this.histograms.set(key, values);
|
|
1390
|
+
}
|
|
1391
|
+
makeKey(metric, tags) {
|
|
1392
|
+
if (!tags) return metric;
|
|
1393
|
+
const tagStr = Object.entries(tags).map(([k, v]) => `${k}:${v}`).sort().join(",");
|
|
1394
|
+
return `${metric}{${tagStr}}`;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Get all metrics (for testing)
|
|
1398
|
+
*/
|
|
1399
|
+
getMetrics() {
|
|
1400
|
+
return {
|
|
1401
|
+
counters: new Map(this.counters),
|
|
1402
|
+
gauges: new Map(this.gauges),
|
|
1403
|
+
timings: new Map(this.timings),
|
|
1404
|
+
histograms: new Map(this.histograms)
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Clear all metrics
|
|
1409
|
+
*/
|
|
1410
|
+
clear() {
|
|
1411
|
+
this.counters.clear();
|
|
1412
|
+
this.gauges.clear();
|
|
1413
|
+
this.timings.clear();
|
|
1414
|
+
this.histograms.clear();
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Get summary statistics for timings
|
|
1418
|
+
*/
|
|
1419
|
+
getTimingStats(metric, tags) {
|
|
1420
|
+
const key = this.makeKey(metric, tags);
|
|
1421
|
+
const timings = this.timings.get(key);
|
|
1422
|
+
if (!timings || timings.length === 0) {
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
const sorted = [...timings].sort((a, b) => a - b);
|
|
1426
|
+
const count = sorted.length;
|
|
1427
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
1428
|
+
return {
|
|
1429
|
+
count,
|
|
1430
|
+
min: sorted[0] ?? 0,
|
|
1431
|
+
max: sorted[count - 1] ?? 0,
|
|
1432
|
+
mean: sum / count,
|
|
1433
|
+
p50: sorted[Math.floor(count * 0.5)] ?? 0,
|
|
1434
|
+
p95: sorted[Math.floor(count * 0.95)] ?? 0,
|
|
1435
|
+
p99: sorted[Math.floor(count * 0.99)] ?? 0
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
function createMetricsCollector(type, prefix) {
|
|
1440
|
+
const collectorType = process.env.METRICS_COLLECTOR || "noop";
|
|
1441
|
+
switch (collectorType) {
|
|
1442
|
+
case "console":
|
|
1443
|
+
return new ConsoleMetrics(prefix);
|
|
1444
|
+
case "inmemory":
|
|
1445
|
+
return new InMemoryMetrics();
|
|
1446
|
+
default:
|
|
1447
|
+
return new NoOpMetrics();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
var metrics = createMetricsCollector(
|
|
1451
|
+
void 0,
|
|
1452
|
+
process.env.METRICS_PREFIX || "oneringai"
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
// src/core/Connector.ts
|
|
1456
|
+
var DEFAULT_CONNECTOR_TIMEOUT = 3e4;
|
|
1457
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
1458
|
+
var DEFAULT_RETRYABLE_STATUSES = [429, 500, 502, 503, 504];
|
|
1459
|
+
var DEFAULT_BASE_DELAY_MS = 1e3;
|
|
1460
|
+
var DEFAULT_MAX_DELAY_MS = 3e4;
|
|
1461
|
+
var Connector = class _Connector {
|
|
1462
|
+
// ============ Static Registry ============
|
|
1463
|
+
static registry = /* @__PURE__ */ new Map();
|
|
1464
|
+
static defaultStorage = new MemoryStorage();
|
|
1465
|
+
/**
|
|
1466
|
+
* Create and register a new connector
|
|
1467
|
+
* @param config - Must include `name` field
|
|
1468
|
+
*/
|
|
1469
|
+
static create(config) {
|
|
1470
|
+
if (!config.name || config.name.trim().length === 0) {
|
|
1471
|
+
throw new Error("Connector name is required");
|
|
1472
|
+
}
|
|
1473
|
+
if (_Connector.registry.has(config.name)) {
|
|
1474
|
+
throw new Error(`Connector '${config.name}' already exists. Use Connector.get() or choose a different name.`);
|
|
1475
|
+
}
|
|
1476
|
+
const connector = new _Connector(config);
|
|
1477
|
+
_Connector.registry.set(config.name, connector);
|
|
1478
|
+
return connector;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Get a connector by name
|
|
1482
|
+
*/
|
|
1483
|
+
static get(name) {
|
|
1484
|
+
const connector = _Connector.registry.get(name);
|
|
1485
|
+
if (!connector) {
|
|
1486
|
+
const available = _Connector.list().join(", ") || "none";
|
|
1487
|
+
throw new Error(`Connector '${name}' not found. Available: ${available}`);
|
|
1488
|
+
}
|
|
1489
|
+
return connector;
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Check if a connector exists
|
|
1493
|
+
*/
|
|
1494
|
+
static has(name) {
|
|
1495
|
+
return _Connector.registry.has(name);
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* List all registered connector names
|
|
1499
|
+
*/
|
|
1500
|
+
static list() {
|
|
1501
|
+
return Array.from(_Connector.registry.keys());
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Remove a connector
|
|
1505
|
+
*/
|
|
1506
|
+
static remove(name) {
|
|
1507
|
+
const connector = _Connector.registry.get(name);
|
|
1508
|
+
if (connector) {
|
|
1509
|
+
connector.dispose();
|
|
1510
|
+
}
|
|
1511
|
+
return _Connector.registry.delete(name);
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Clear all connectors (useful for testing)
|
|
1515
|
+
*/
|
|
1516
|
+
static clear() {
|
|
1517
|
+
for (const connector of _Connector.registry.values()) {
|
|
1518
|
+
connector.dispose();
|
|
1519
|
+
}
|
|
1520
|
+
_Connector.registry.clear();
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Set default token storage for OAuth connectors
|
|
1524
|
+
*/
|
|
1525
|
+
static setDefaultStorage(storage) {
|
|
1526
|
+
_Connector.defaultStorage = storage;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Get all registered connectors
|
|
1530
|
+
*/
|
|
1531
|
+
static listAll() {
|
|
1532
|
+
return Array.from(_Connector.registry.values());
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Get number of registered connectors
|
|
1536
|
+
*/
|
|
1537
|
+
static size() {
|
|
1538
|
+
return _Connector.registry.size;
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Get connector descriptions formatted for tool parameters
|
|
1542
|
+
* Useful for generating dynamic tool descriptions
|
|
1543
|
+
*/
|
|
1544
|
+
static getDescriptionsForTools() {
|
|
1545
|
+
const connectors = _Connector.listAll();
|
|
1546
|
+
if (connectors.length === 0) {
|
|
1547
|
+
return "No connectors registered yet.";
|
|
1548
|
+
}
|
|
1549
|
+
return connectors.map((c) => ` - "${c.name}": ${c.displayName} - ${c.config.description || "No description"}`).join("\n");
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Get connector info (for tools and documentation)
|
|
1553
|
+
*/
|
|
1554
|
+
static getInfo() {
|
|
1555
|
+
const info = {};
|
|
1556
|
+
for (const connector of _Connector.registry.values()) {
|
|
1557
|
+
info[connector.name] = {
|
|
1558
|
+
displayName: connector.displayName,
|
|
1559
|
+
description: connector.config.description || "",
|
|
1560
|
+
baseURL: connector.baseURL
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
return info;
|
|
1564
|
+
}
|
|
1565
|
+
// ============ Instance ============
|
|
1566
|
+
name;
|
|
1567
|
+
vendor;
|
|
1568
|
+
config;
|
|
1569
|
+
oauthManager;
|
|
1570
|
+
circuitBreaker;
|
|
1571
|
+
disposed = false;
|
|
1572
|
+
// Metrics
|
|
1573
|
+
requestCount = 0;
|
|
1574
|
+
successCount = 0;
|
|
1575
|
+
failureCount = 0;
|
|
1576
|
+
totalLatencyMs = 0;
|
|
1577
|
+
constructor(config) {
|
|
1578
|
+
this.name = config.name;
|
|
1579
|
+
this.vendor = config.vendor;
|
|
1580
|
+
this.config = config;
|
|
1581
|
+
if (config.auth.type === "oauth") {
|
|
1582
|
+
this.initOAuthManager(config.auth);
|
|
1583
|
+
} else if (config.auth.type === "jwt") {
|
|
1584
|
+
this.initJWTManager(config.auth);
|
|
1585
|
+
}
|
|
1586
|
+
this.initCircuitBreaker();
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Initialize circuit breaker with config or defaults
|
|
1590
|
+
*/
|
|
1591
|
+
initCircuitBreaker() {
|
|
1592
|
+
const cbConfig = this.config.circuitBreaker;
|
|
1593
|
+
const enabled = cbConfig?.enabled ?? true;
|
|
1594
|
+
if (enabled) {
|
|
1595
|
+
this.circuitBreaker = new CircuitBreaker(`connector:${this.name}`, {
|
|
1596
|
+
failureThreshold: cbConfig?.failureThreshold ?? 5,
|
|
1597
|
+
successThreshold: cbConfig?.successThreshold ?? 2,
|
|
1598
|
+
resetTimeoutMs: cbConfig?.resetTimeoutMs ?? 3e4,
|
|
1599
|
+
windowMs: 6e4,
|
|
1600
|
+
// 1 minute window
|
|
1601
|
+
isRetryable: (error) => {
|
|
1602
|
+
if (error.message.includes("HTTP 4") && !error.message.includes("HTTP 429")) {
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
return true;
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
this.circuitBreaker.on("opened", ({ name, failureCount, lastError }) => {
|
|
1609
|
+
logger.warn(`Circuit breaker opened for ${name}: ${failureCount} failures, last error: ${lastError}`);
|
|
1610
|
+
metrics.increment("connector.circuit_breaker.opened", 1, { connector: this.name });
|
|
1611
|
+
});
|
|
1612
|
+
this.circuitBreaker.on("closed", ({ name }) => {
|
|
1613
|
+
logger.info(`Circuit breaker closed for ${name}`);
|
|
1614
|
+
metrics.increment("connector.circuit_breaker.closed", 1, { connector: this.name });
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Human-readable display name
|
|
1620
|
+
*/
|
|
1621
|
+
get displayName() {
|
|
1622
|
+
return this.config.displayName || this.name;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* API base URL for this connector
|
|
1626
|
+
*/
|
|
1627
|
+
get baseURL() {
|
|
1628
|
+
return this.config.baseURL || "";
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Get the API key (for api_key auth type)
|
|
1632
|
+
*/
|
|
1633
|
+
getApiKey() {
|
|
1634
|
+
if (this.config.auth.type !== "api_key") {
|
|
1635
|
+
throw new Error(`Connector '${this.name}' does not use API key auth. Type: ${this.config.auth.type}`);
|
|
1636
|
+
}
|
|
1637
|
+
return this.config.auth.apiKey;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Get the current access token (for OAuth, JWT, or API key)
|
|
1641
|
+
* Handles automatic refresh if needed
|
|
1642
|
+
*/
|
|
1643
|
+
async getToken(userId) {
|
|
1644
|
+
if (this.config.auth.type === "api_key") {
|
|
1645
|
+
return this.config.auth.apiKey;
|
|
1646
|
+
}
|
|
1647
|
+
if (!this.oauthManager) {
|
|
1648
|
+
throw new Error(`OAuth manager not initialized for connector '${this.name}'`);
|
|
1649
|
+
}
|
|
1650
|
+
return this.oauthManager.getToken(userId);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Start OAuth authorization flow
|
|
1654
|
+
* Returns the URL to redirect the user to
|
|
1655
|
+
*/
|
|
1656
|
+
async startAuth(userId) {
|
|
1657
|
+
if (!this.oauthManager) {
|
|
1658
|
+
throw new Error(`Connector '${this.name}' is not an OAuth connector`);
|
|
1659
|
+
}
|
|
1660
|
+
return this.oauthManager.startAuthFlow(userId);
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Handle OAuth callback
|
|
1664
|
+
* Call this after user is redirected back from OAuth provider
|
|
1665
|
+
*/
|
|
1666
|
+
async handleCallback(callbackUrl, userId) {
|
|
1667
|
+
if (!this.oauthManager) {
|
|
1668
|
+
throw new Error(`Connector '${this.name}' is not an OAuth connector`);
|
|
1669
|
+
}
|
|
1670
|
+
await this.oauthManager.handleCallback(callbackUrl, userId);
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Check if the connector has a valid token
|
|
1674
|
+
*/
|
|
1675
|
+
async hasValidToken(userId) {
|
|
1676
|
+
try {
|
|
1677
|
+
if (this.config.auth.type === "api_key") {
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
if (this.oauthManager) {
|
|
1681
|
+
const token = await this.oauthManager.getToken(userId);
|
|
1682
|
+
return !!token;
|
|
1683
|
+
}
|
|
1684
|
+
return false;
|
|
1685
|
+
} catch {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Get vendor-specific options from config
|
|
1691
|
+
*/
|
|
1692
|
+
getOptions() {
|
|
1693
|
+
return this.config.options ?? {};
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Get the service type (explicit or undefined)
|
|
1697
|
+
*/
|
|
1698
|
+
get serviceType() {
|
|
1699
|
+
return this.config.serviceType;
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Get connector metrics
|
|
1703
|
+
*/
|
|
1704
|
+
getMetrics() {
|
|
1705
|
+
return {
|
|
1706
|
+
requestCount: this.requestCount,
|
|
1707
|
+
successCount: this.successCount,
|
|
1708
|
+
failureCount: this.failureCount,
|
|
1709
|
+
avgLatencyMs: this.requestCount > 0 ? this.totalLatencyMs / this.requestCount : 0,
|
|
1710
|
+
circuitBreakerState: this.circuitBreaker?.getState()
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Reset circuit breaker (force close)
|
|
1715
|
+
*/
|
|
1716
|
+
resetCircuitBreaker() {
|
|
1717
|
+
this.circuitBreaker?.reset();
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Make an authenticated fetch request using this connector
|
|
1721
|
+
* This is the foundation for all vendor-dependent tools
|
|
1722
|
+
*
|
|
1723
|
+
* Features:
|
|
1724
|
+
* - Timeout with AbortController
|
|
1725
|
+
* - Circuit breaker protection
|
|
1726
|
+
* - Retry with exponential backoff
|
|
1727
|
+
* - Request/response logging
|
|
1728
|
+
*
|
|
1729
|
+
* @param endpoint - API endpoint (relative to baseURL) or full URL
|
|
1730
|
+
* @param options - Fetch options with connector-specific settings
|
|
1731
|
+
* @param userId - Optional user ID for multi-user OAuth
|
|
1732
|
+
* @returns Fetch Response
|
|
1733
|
+
*/
|
|
1734
|
+
async fetch(endpoint, options, userId) {
|
|
1735
|
+
if (this.disposed) {
|
|
1736
|
+
throw new Error(`Connector '${this.name}' has been disposed`);
|
|
1737
|
+
}
|
|
1738
|
+
const startTime = Date.now();
|
|
1739
|
+
this.requestCount++;
|
|
1740
|
+
const url = endpoint.startsWith("http") ? endpoint : `${this.baseURL}${endpoint}`;
|
|
1741
|
+
const timeout = options?.timeout ?? this.config.timeout ?? DEFAULT_CONNECTOR_TIMEOUT;
|
|
1742
|
+
if (this.config.logging?.enabled) {
|
|
1743
|
+
this.logRequest(url, options);
|
|
1744
|
+
}
|
|
1745
|
+
const doFetch = async () => {
|
|
1746
|
+
const token = await this.getToken(userId);
|
|
1747
|
+
const auth = this.config.auth;
|
|
1748
|
+
let headerName = "Authorization";
|
|
1749
|
+
let headerValue = `Bearer ${token}`;
|
|
1750
|
+
if (auth.type === "api_key") {
|
|
1751
|
+
headerName = auth.headerName || "Authorization";
|
|
1752
|
+
const prefix = auth.headerPrefix ?? "Bearer";
|
|
1753
|
+
headerValue = prefix ? `${prefix} ${token}` : token;
|
|
1754
|
+
}
|
|
1755
|
+
const controller = new AbortController();
|
|
1756
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1757
|
+
try {
|
|
1758
|
+
const response = await fetch(url, {
|
|
1759
|
+
...options,
|
|
1760
|
+
signal: controller.signal,
|
|
1761
|
+
headers: {
|
|
1762
|
+
...options?.headers,
|
|
1763
|
+
[headerName]: headerValue
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
return response;
|
|
1767
|
+
} finally {
|
|
1768
|
+
clearTimeout(timeoutId);
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
const doFetchWithRetry = async () => {
|
|
1772
|
+
const retryConfig = this.config.retry;
|
|
1773
|
+
const maxRetries = retryConfig?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1774
|
+
const retryableStatuses = retryConfig?.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES;
|
|
1775
|
+
const baseDelayMs = retryConfig?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
1776
|
+
const maxDelayMs = retryConfig?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
1777
|
+
const backoffConfig = {
|
|
1778
|
+
strategy: "exponential",
|
|
1779
|
+
initialDelayMs: baseDelayMs,
|
|
1780
|
+
maxDelayMs,
|
|
1781
|
+
multiplier: 2,
|
|
1782
|
+
jitter: true,
|
|
1783
|
+
jitterFactor: 0.1
|
|
1784
|
+
};
|
|
1785
|
+
let lastError;
|
|
1786
|
+
let lastResponse;
|
|
1787
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
1788
|
+
try {
|
|
1789
|
+
const response = await doFetch();
|
|
1790
|
+
if (!response.ok && retryableStatuses.includes(response.status) && attempt <= maxRetries) {
|
|
1791
|
+
lastResponse = response;
|
|
1792
|
+
const delay = calculateBackoff(attempt, backoffConfig);
|
|
1793
|
+
if (this.config.logging?.enabled) {
|
|
1794
|
+
logger.debug(`Connector ${this.name}: Retry ${attempt}/${maxRetries} after ${delay}ms (status ${response.status})`);
|
|
1795
|
+
}
|
|
1796
|
+
await this.sleep(delay);
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
return response;
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
lastError = error;
|
|
1802
|
+
if (lastError.name === "AbortError") {
|
|
1803
|
+
throw new Error(`Request timeout after ${timeout}ms: ${url}`);
|
|
1804
|
+
}
|
|
1805
|
+
if (attempt <= maxRetries && !options?.skipRetry) {
|
|
1806
|
+
const delay = calculateBackoff(attempt, backoffConfig);
|
|
1807
|
+
if (this.config.logging?.enabled) {
|
|
1808
|
+
logger.debug(`Connector ${this.name}: Retry ${attempt}/${maxRetries} after ${delay}ms (error: ${lastError.message})`);
|
|
1809
|
+
}
|
|
1810
|
+
await this.sleep(delay);
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
throw lastError;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (lastResponse) {
|
|
1817
|
+
return lastResponse;
|
|
1818
|
+
}
|
|
1819
|
+
throw lastError ?? new Error("Unknown error during fetch");
|
|
1820
|
+
};
|
|
1821
|
+
try {
|
|
1822
|
+
let response;
|
|
1823
|
+
if (this.circuitBreaker && !options?.skipCircuitBreaker) {
|
|
1824
|
+
response = await this.circuitBreaker.execute(doFetchWithRetry);
|
|
1825
|
+
} else {
|
|
1826
|
+
response = await doFetchWithRetry();
|
|
1827
|
+
}
|
|
1828
|
+
const latency = Date.now() - startTime;
|
|
1829
|
+
this.successCount++;
|
|
1830
|
+
this.totalLatencyMs += latency;
|
|
1831
|
+
metrics.timing("connector.latency", latency, { connector: this.name });
|
|
1832
|
+
metrics.increment("connector.success", 1, { connector: this.name });
|
|
1833
|
+
if (this.config.logging?.enabled) {
|
|
1834
|
+
this.logResponse(url, response, latency);
|
|
1835
|
+
}
|
|
1836
|
+
return response;
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
const latency = Date.now() - startTime;
|
|
1839
|
+
this.failureCount++;
|
|
1840
|
+
this.totalLatencyMs += latency;
|
|
1841
|
+
metrics.increment("connector.failure", 1, { connector: this.name, error: error.name });
|
|
1842
|
+
if (this.config.logging?.enabled) {
|
|
1843
|
+
logger.error(
|
|
1844
|
+
{ connector: this.name, url, latency, error: error.message },
|
|
1845
|
+
`Connector ${this.name} fetch failed: ${error.message}`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
throw error;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Make an authenticated fetch request and parse JSON response
|
|
1853
|
+
* Throws on non-OK responses
|
|
1854
|
+
*
|
|
1855
|
+
* @param endpoint - API endpoint (relative to baseURL) or full URL
|
|
1856
|
+
* @param options - Fetch options with connector-specific settings
|
|
1857
|
+
* @param userId - Optional user ID for multi-user OAuth
|
|
1858
|
+
* @returns Parsed JSON response
|
|
1859
|
+
*/
|
|
1860
|
+
async fetchJSON(endpoint, options, userId) {
|
|
1861
|
+
const response = await this.fetch(endpoint, options, userId);
|
|
1862
|
+
const text = await response.text();
|
|
1863
|
+
let data;
|
|
1864
|
+
try {
|
|
1865
|
+
data = JSON.parse(text);
|
|
1866
|
+
} catch {
|
|
1867
|
+
if (!response.ok) {
|
|
1868
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1869
|
+
}
|
|
1870
|
+
throw new Error(`Invalid JSON response: ${text.slice(0, 100)}`);
|
|
1871
|
+
}
|
|
1872
|
+
if (!response.ok) {
|
|
1873
|
+
const errorMsg = typeof data === "object" && data !== null ? JSON.stringify(data) : text;
|
|
1874
|
+
throw new Error(`HTTP ${response.status}: ${errorMsg}`);
|
|
1875
|
+
}
|
|
1876
|
+
return data;
|
|
1877
|
+
}
|
|
1878
|
+
// ============ Private Helpers ============
|
|
1879
|
+
sleep(ms) {
|
|
1880
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1881
|
+
}
|
|
1882
|
+
logRequest(url, options) {
|
|
1883
|
+
const logData = {
|
|
1884
|
+
connector: this.name,
|
|
1885
|
+
method: options?.method ?? "GET",
|
|
1886
|
+
url
|
|
1887
|
+
};
|
|
1888
|
+
if (this.config.logging?.logHeaders && options?.headers) {
|
|
1889
|
+
const headers = { ...options.headers };
|
|
1890
|
+
if (headers["Authorization"]) {
|
|
1891
|
+
headers["Authorization"] = "[REDACTED]";
|
|
1892
|
+
}
|
|
1893
|
+
if (headers["authorization"]) {
|
|
1894
|
+
headers["authorization"] = "[REDACTED]";
|
|
1895
|
+
}
|
|
1896
|
+
logData.headers = headers;
|
|
1897
|
+
}
|
|
1898
|
+
if (this.config.logging?.logBody && options?.body) {
|
|
1899
|
+
logData.body = typeof options.body === "string" ? options.body.slice(0, 1e3) : "[non-string body]";
|
|
1900
|
+
}
|
|
1901
|
+
logger.debug(logData, `Connector ${this.name} request`);
|
|
1902
|
+
}
|
|
1903
|
+
logResponse(url, response, latency) {
|
|
1904
|
+
logger.debug(
|
|
1905
|
+
{ connector: this.name, url, status: response.status, latency },
|
|
1906
|
+
`Connector ${this.name} response`
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Dispose of resources
|
|
1911
|
+
*/
|
|
1912
|
+
dispose() {
|
|
1913
|
+
if (this.disposed) return;
|
|
1914
|
+
this.disposed = true;
|
|
1915
|
+
this.oauthManager = void 0;
|
|
1916
|
+
this.circuitBreaker = void 0;
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Check if connector is disposed
|
|
1920
|
+
*/
|
|
1921
|
+
isDisposed() {
|
|
1922
|
+
return this.disposed;
|
|
1923
|
+
}
|
|
1924
|
+
// ============ Private ============
|
|
1925
|
+
initOAuthManager(auth) {
|
|
1926
|
+
const oauthConfig = {
|
|
1927
|
+
flow: auth.flow,
|
|
1928
|
+
clientId: auth.clientId,
|
|
1929
|
+
clientSecret: auth.clientSecret,
|
|
1930
|
+
tokenUrl: auth.tokenUrl,
|
|
1931
|
+
authorizationUrl: auth.authorizationUrl,
|
|
1932
|
+
redirectUri: auth.redirectUri,
|
|
1933
|
+
scope: auth.scope,
|
|
1934
|
+
usePKCE: auth.usePKCE,
|
|
1935
|
+
privateKey: auth.privateKey,
|
|
1936
|
+
privateKeyPath: auth.privateKeyPath,
|
|
1937
|
+
audience: auth.audience,
|
|
1938
|
+
refreshBeforeExpiry: auth.refreshBeforeExpiry,
|
|
1939
|
+
storage: _Connector.defaultStorage,
|
|
1940
|
+
storageKey: auth.storageKey ?? this.name
|
|
1941
|
+
};
|
|
1942
|
+
this.oauthManager = new OAuthManager(oauthConfig);
|
|
1943
|
+
}
|
|
1944
|
+
initJWTManager(auth) {
|
|
1945
|
+
this.oauthManager = new OAuthManager({
|
|
1946
|
+
flow: "jwt_bearer",
|
|
1947
|
+
clientId: auth.clientId,
|
|
1948
|
+
tokenUrl: auth.tokenUrl,
|
|
1949
|
+
privateKey: auth.privateKey,
|
|
1950
|
+
privateKeyPath: auth.privateKeyPath,
|
|
1951
|
+
scope: auth.scope,
|
|
1952
|
+
audience: auth.audience,
|
|
1953
|
+
storage: _Connector.defaultStorage,
|
|
1954
|
+
storageKey: this.name
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1959
|
+
// src/core/Vendor.ts
|
|
1960
|
+
var Vendor = {
|
|
1961
|
+
OpenAI: "openai",
|
|
1962
|
+
Google: "google",
|
|
1963
|
+
Grok: "grok"};
|
|
1964
|
+
|
|
1965
|
+
// src/domain/errors/AIErrors.ts
|
|
1966
|
+
var AIError = class _AIError extends Error {
|
|
1967
|
+
constructor(message, code, statusCode, originalError) {
|
|
1968
|
+
super(message);
|
|
1969
|
+
this.code = code;
|
|
1970
|
+
this.statusCode = statusCode;
|
|
1971
|
+
this.originalError = originalError;
|
|
1972
|
+
this.name = "AIError";
|
|
1973
|
+
Object.setPrototypeOf(this, _AIError.prototype);
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
var ProviderAuthError = class _ProviderAuthError extends AIError {
|
|
1977
|
+
constructor(providerName, message = "Authentication failed") {
|
|
1978
|
+
super(
|
|
1979
|
+
`${providerName}: ${message}`,
|
|
1980
|
+
"PROVIDER_AUTH_ERROR",
|
|
1981
|
+
401
|
|
1982
|
+
);
|
|
1983
|
+
this.name = "ProviderAuthError";
|
|
1984
|
+
Object.setPrototypeOf(this, _ProviderAuthError.prototype);
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
var ProviderRateLimitError = class _ProviderRateLimitError extends AIError {
|
|
1988
|
+
constructor(providerName, retryAfter) {
|
|
1989
|
+
super(
|
|
1990
|
+
`${providerName}: Rate limit exceeded${retryAfter ? `. Retry after ${retryAfter}ms` : ""}`,
|
|
1991
|
+
"PROVIDER_RATE_LIMIT",
|
|
1992
|
+
429
|
|
1993
|
+
);
|
|
1994
|
+
this.retryAfter = retryAfter;
|
|
1995
|
+
this.name = "ProviderRateLimitError";
|
|
1996
|
+
Object.setPrototypeOf(this, _ProviderRateLimitError.prototype);
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
var InvalidConfigError = class _InvalidConfigError extends AIError {
|
|
2000
|
+
constructor(message) {
|
|
2001
|
+
super(message, "INVALID_CONFIG", 400);
|
|
2002
|
+
this.name = "InvalidConfigError";
|
|
2003
|
+
Object.setPrototypeOf(this, _InvalidConfigError.prototype);
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
var ProviderError = class _ProviderError extends AIError {
|
|
2007
|
+
constructor(providerName, message, statusCode, originalError) {
|
|
2008
|
+
super(
|
|
2009
|
+
`${providerName}: ${message}`,
|
|
2010
|
+
"PROVIDER_ERROR",
|
|
2011
|
+
statusCode,
|
|
2012
|
+
originalError
|
|
2013
|
+
);
|
|
2014
|
+
this.providerName = providerName;
|
|
2015
|
+
this.name = "ProviderError";
|
|
2016
|
+
Object.setPrototypeOf(this, _ProviderError.prototype);
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
// src/infrastructure/providers/base/BaseProvider.ts
|
|
2021
|
+
var BaseProvider = class {
|
|
2022
|
+
constructor(config) {
|
|
2023
|
+
this.config = config;
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Validate provider configuration
|
|
2027
|
+
* Returns validation result with details
|
|
2028
|
+
*/
|
|
2029
|
+
async validateConfig() {
|
|
2030
|
+
const validation = this.validateApiKey();
|
|
2031
|
+
return validation.isValid;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Validate API key format and presence
|
|
2035
|
+
* Can be overridden by providers with specific key formats
|
|
2036
|
+
*/
|
|
2037
|
+
validateApiKey() {
|
|
2038
|
+
const apiKey = this.config.apiKey;
|
|
2039
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
2040
|
+
return { isValid: false };
|
|
2041
|
+
}
|
|
2042
|
+
const placeholders = [
|
|
2043
|
+
"your-api-key",
|
|
2044
|
+
"YOUR_API_KEY",
|
|
2045
|
+
"sk-xxx",
|
|
2046
|
+
"api-key-here",
|
|
2047
|
+
"REPLACE_ME",
|
|
2048
|
+
"<your-key>"
|
|
2049
|
+
];
|
|
2050
|
+
if (placeholders.some((p) => apiKey.includes(p))) {
|
|
2051
|
+
return {
|
|
2052
|
+
isValid: false,
|
|
2053
|
+
warning: `API key appears to be a placeholder value`
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
return this.validateProviderSpecificKeyFormat(apiKey);
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Override this method in provider implementations for specific key format validation
|
|
2060
|
+
*/
|
|
2061
|
+
validateProviderSpecificKeyFormat(_apiKey) {
|
|
2062
|
+
return { isValid: true };
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Validate config and throw if invalid
|
|
2066
|
+
*/
|
|
2067
|
+
assertValidConfig() {
|
|
2068
|
+
const validation = this.validateApiKey();
|
|
2069
|
+
if (!validation.isValid) {
|
|
2070
|
+
throw new InvalidConfigError(
|
|
2071
|
+
`Invalid API key for provider '${this.name}'${validation.warning ? `: ${validation.warning}` : ""}`
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Get API key from config
|
|
2077
|
+
*/
|
|
2078
|
+
getApiKey() {
|
|
2079
|
+
return this.config.apiKey;
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Get base URL if configured
|
|
2083
|
+
*/
|
|
2084
|
+
getBaseURL() {
|
|
2085
|
+
return this.config.baseURL;
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Get timeout configuration
|
|
2089
|
+
*/
|
|
2090
|
+
getTimeout() {
|
|
2091
|
+
return this.config.timeout || 6e4;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Get max retries configuration
|
|
2095
|
+
*/
|
|
2096
|
+
getMaxRetries() {
|
|
2097
|
+
return this.config.maxRetries || 3;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
// src/infrastructure/providers/base/BaseMediaProvider.ts
|
|
2102
|
+
var BaseMediaProvider = class extends BaseProvider {
|
|
2103
|
+
circuitBreaker;
|
|
2104
|
+
logger;
|
|
2105
|
+
_isObservabilityInitialized = false;
|
|
2106
|
+
constructor(config) {
|
|
2107
|
+
super(config);
|
|
2108
|
+
this.logger = logger.child({
|
|
2109
|
+
component: "MediaProvider",
|
|
2110
|
+
provider: "unknown"
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Auto-initialize observability on first use (lazy initialization)
|
|
2115
|
+
* This is called automatically by executeWithCircuitBreaker()
|
|
2116
|
+
* @internal
|
|
2117
|
+
*/
|
|
2118
|
+
ensureObservabilityInitialized() {
|
|
2119
|
+
if (this._isObservabilityInitialized) {
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
const providerName = this.name || "unknown";
|
|
2123
|
+
const cbConfig = this.config.circuitBreaker || DEFAULT_CIRCUIT_BREAKER_CONFIG;
|
|
2124
|
+
this.circuitBreaker = new CircuitBreaker(
|
|
2125
|
+
`media-provider:${providerName}`,
|
|
2126
|
+
cbConfig
|
|
2127
|
+
);
|
|
2128
|
+
this.logger = logger.child({
|
|
2129
|
+
component: "MediaProvider",
|
|
2130
|
+
provider: providerName
|
|
2131
|
+
});
|
|
2132
|
+
this.circuitBreaker.on("opened", (data) => {
|
|
2133
|
+
this.logger.warn(data, "Circuit breaker opened");
|
|
2134
|
+
metrics.increment("circuit_breaker.opened", 1, {
|
|
2135
|
+
breaker: data.name,
|
|
2136
|
+
provider: providerName
|
|
2137
|
+
});
|
|
2138
|
+
});
|
|
2139
|
+
this.circuitBreaker.on("closed", (data) => {
|
|
2140
|
+
this.logger.info(data, "Circuit breaker closed");
|
|
2141
|
+
metrics.increment("circuit_breaker.closed", 1, {
|
|
2142
|
+
breaker: data.name,
|
|
2143
|
+
provider: providerName
|
|
2144
|
+
});
|
|
2145
|
+
});
|
|
2146
|
+
this._isObservabilityInitialized = true;
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Execute operation with circuit breaker protection
|
|
2150
|
+
* Automatically records metrics and handles errors
|
|
2151
|
+
*
|
|
2152
|
+
* @param operation - The async operation to execute
|
|
2153
|
+
* @param operationName - Name of the operation for metrics (e.g., 'image.generate', 'audio.synthesize')
|
|
2154
|
+
* @param metadata - Additional metadata to log/record
|
|
2155
|
+
*/
|
|
2156
|
+
async executeWithCircuitBreaker(operation, operationName, metadata) {
|
|
2157
|
+
this.ensureObservabilityInitialized();
|
|
2158
|
+
const startTime = Date.now();
|
|
2159
|
+
const metricLabels = {
|
|
2160
|
+
provider: this.name,
|
|
2161
|
+
operation: operationName,
|
|
2162
|
+
...metadata
|
|
2163
|
+
};
|
|
2164
|
+
try {
|
|
2165
|
+
const result = await this.circuitBreaker.execute(operation);
|
|
2166
|
+
const duration = Date.now() - startTime;
|
|
2167
|
+
metrics.histogram(`${operationName}.duration`, duration, metricLabels);
|
|
2168
|
+
metrics.increment(`${operationName}.success`, 1, metricLabels);
|
|
2169
|
+
this.logger.debug(
|
|
2170
|
+
{ operation: operationName, duration, ...metadata },
|
|
2171
|
+
"Operation completed successfully"
|
|
2172
|
+
);
|
|
2173
|
+
return result;
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
const duration = Date.now() - startTime;
|
|
2176
|
+
metrics.increment(`${operationName}.error`, 1, {
|
|
2177
|
+
...metricLabels,
|
|
2178
|
+
error: error instanceof Error ? error.name : "unknown"
|
|
2179
|
+
});
|
|
2180
|
+
this.logger.error(
|
|
2181
|
+
{
|
|
2182
|
+
operation: operationName,
|
|
2183
|
+
duration,
|
|
2184
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2185
|
+
...metadata
|
|
2186
|
+
},
|
|
2187
|
+
"Operation failed"
|
|
2188
|
+
);
|
|
2189
|
+
throw error;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Log operation start with context
|
|
2194
|
+
* Useful for logging before async operations
|
|
2195
|
+
*/
|
|
2196
|
+
logOperationStart(operation, context) {
|
|
2197
|
+
this.ensureObservabilityInitialized();
|
|
2198
|
+
this.logger.info({ operation, ...context }, `${operation} started`);
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Log operation completion with context
|
|
2202
|
+
*/
|
|
2203
|
+
logOperationComplete(operation, context) {
|
|
2204
|
+
this.ensureObservabilityInitialized();
|
|
2205
|
+
this.logger.info({ operation, ...context }, `${operation} completed`);
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
|
|
2209
|
+
// src/infrastructure/providers/openai/OpenAIImageProvider.ts
|
|
2210
|
+
var OpenAIImageProvider = class extends BaseMediaProvider {
|
|
2211
|
+
name = "openai-image";
|
|
2212
|
+
vendor = "openai";
|
|
2213
|
+
capabilities = {
|
|
2214
|
+
text: false,
|
|
2215
|
+
images: true,
|
|
2216
|
+
videos: false,
|
|
2217
|
+
audio: false,
|
|
2218
|
+
features: {
|
|
2219
|
+
imageGeneration: true,
|
|
2220
|
+
imageEditing: true
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
client;
|
|
2224
|
+
constructor(config) {
|
|
2225
|
+
super({ apiKey: config.auth.apiKey, ...config });
|
|
2226
|
+
this.client = new OpenAI__default.default({
|
|
2227
|
+
apiKey: config.auth.apiKey,
|
|
2228
|
+
baseURL: config.baseURL,
|
|
2229
|
+
organization: config.organization,
|
|
2230
|
+
timeout: config.timeout,
|
|
2231
|
+
maxRetries: config.maxRetries
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Generate images from a text prompt
|
|
2236
|
+
*/
|
|
2237
|
+
async generateImage(options) {
|
|
2238
|
+
return this.executeWithCircuitBreaker(
|
|
2239
|
+
async () => {
|
|
2240
|
+
try {
|
|
2241
|
+
this.logOperationStart("image.generate", {
|
|
2242
|
+
model: options.model,
|
|
2243
|
+
size: options.size,
|
|
2244
|
+
quality: options.quality,
|
|
2245
|
+
n: options.n
|
|
2246
|
+
});
|
|
2247
|
+
const isGptImage = options.model === "gpt-image-1";
|
|
2248
|
+
const params = {
|
|
2249
|
+
model: options.model,
|
|
2250
|
+
prompt: options.prompt,
|
|
2251
|
+
size: options.size,
|
|
2252
|
+
quality: options.quality,
|
|
2253
|
+
style: options.style,
|
|
2254
|
+
n: options.n || 1
|
|
2255
|
+
};
|
|
2256
|
+
if (!isGptImage) {
|
|
2257
|
+
params.response_format = options.response_format || "b64_json";
|
|
2258
|
+
}
|
|
2259
|
+
const response = await this.client.images.generate(params);
|
|
2260
|
+
const data = response.data || [];
|
|
2261
|
+
this.logOperationComplete("image.generate", {
|
|
2262
|
+
model: options.model,
|
|
2263
|
+
imagesGenerated: data.length
|
|
2264
|
+
});
|
|
2265
|
+
return {
|
|
2266
|
+
created: response.created,
|
|
2267
|
+
data: data.map((img) => ({
|
|
2268
|
+
url: img.url,
|
|
2269
|
+
b64_json: img.b64_json,
|
|
2270
|
+
revised_prompt: img.revised_prompt
|
|
2271
|
+
}))
|
|
2272
|
+
};
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
this.handleError(error);
|
|
2275
|
+
throw error;
|
|
2276
|
+
}
|
|
2277
|
+
},
|
|
2278
|
+
"image.generate",
|
|
2279
|
+
{ model: options.model }
|
|
2280
|
+
);
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Edit an existing image with a prompt
|
|
2284
|
+
* Supported by: gpt-image-1, dall-e-2
|
|
2285
|
+
*/
|
|
2286
|
+
async editImage(options) {
|
|
2287
|
+
return this.executeWithCircuitBreaker(
|
|
2288
|
+
async () => {
|
|
2289
|
+
try {
|
|
2290
|
+
this.logOperationStart("image.edit", {
|
|
2291
|
+
model: options.model,
|
|
2292
|
+
size: options.size,
|
|
2293
|
+
n: options.n
|
|
2294
|
+
});
|
|
2295
|
+
const image = this.prepareImageInput(options.image);
|
|
2296
|
+
const mask = options.mask ? this.prepareImageInput(options.mask) : void 0;
|
|
2297
|
+
const isGptImage = options.model === "gpt-image-1";
|
|
2298
|
+
const params = {
|
|
2299
|
+
model: options.model,
|
|
2300
|
+
image,
|
|
2301
|
+
prompt: options.prompt,
|
|
2302
|
+
mask,
|
|
2303
|
+
size: options.size,
|
|
2304
|
+
n: options.n || 1
|
|
2305
|
+
};
|
|
2306
|
+
if (!isGptImage) {
|
|
2307
|
+
params.response_format = options.response_format || "b64_json";
|
|
2308
|
+
}
|
|
2309
|
+
const response = await this.client.images.edit(params);
|
|
2310
|
+
const data = response.data || [];
|
|
2311
|
+
this.logOperationComplete("image.edit", {
|
|
2312
|
+
model: options.model,
|
|
2313
|
+
imagesGenerated: data.length
|
|
2314
|
+
});
|
|
2315
|
+
return {
|
|
2316
|
+
created: response.created,
|
|
2317
|
+
data: data.map((img) => ({
|
|
2318
|
+
url: img.url,
|
|
2319
|
+
b64_json: img.b64_json,
|
|
2320
|
+
revised_prompt: img.revised_prompt
|
|
2321
|
+
}))
|
|
2322
|
+
};
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
this.handleError(error);
|
|
2325
|
+
throw error;
|
|
2326
|
+
}
|
|
2327
|
+
},
|
|
2328
|
+
"image.edit",
|
|
2329
|
+
{ model: options.model }
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Create variations of an existing image
|
|
2334
|
+
* Supported by: dall-e-2 only
|
|
2335
|
+
*/
|
|
2336
|
+
async createVariation(options) {
|
|
2337
|
+
return this.executeWithCircuitBreaker(
|
|
2338
|
+
async () => {
|
|
2339
|
+
try {
|
|
2340
|
+
this.logOperationStart("image.variation", {
|
|
2341
|
+
model: options.model,
|
|
2342
|
+
size: options.size,
|
|
2343
|
+
n: options.n
|
|
2344
|
+
});
|
|
2345
|
+
const image = this.prepareImageInput(options.image);
|
|
2346
|
+
const response = await this.client.images.createVariation({
|
|
2347
|
+
model: options.model,
|
|
2348
|
+
image,
|
|
2349
|
+
size: options.size,
|
|
2350
|
+
n: options.n || 1,
|
|
2351
|
+
response_format: options.response_format || "b64_json"
|
|
2352
|
+
});
|
|
2353
|
+
const data = response.data || [];
|
|
2354
|
+
this.logOperationComplete("image.variation", {
|
|
2355
|
+
model: options.model,
|
|
2356
|
+
imagesGenerated: data.length
|
|
2357
|
+
});
|
|
2358
|
+
return {
|
|
2359
|
+
created: response.created,
|
|
2360
|
+
data: data.map((img) => ({
|
|
2361
|
+
url: img.url,
|
|
2362
|
+
b64_json: img.b64_json,
|
|
2363
|
+
revised_prompt: img.revised_prompt
|
|
2364
|
+
}))
|
|
2365
|
+
};
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
this.handleError(error);
|
|
2368
|
+
throw error;
|
|
2369
|
+
}
|
|
2370
|
+
},
|
|
2371
|
+
"image.variation",
|
|
2372
|
+
{ model: options.model }
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* List available image models
|
|
2377
|
+
*/
|
|
2378
|
+
async listModels() {
|
|
2379
|
+
return ["gpt-image-1", "dall-e-3", "dall-e-2"];
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Prepare image input (Buffer or file path) for OpenAI API
|
|
2383
|
+
*/
|
|
2384
|
+
prepareImageInput(image) {
|
|
2385
|
+
if (Buffer.isBuffer(image)) {
|
|
2386
|
+
return new File([new Uint8Array(image)], "image.png", { type: "image/png" });
|
|
2387
|
+
}
|
|
2388
|
+
return fs2__namespace.createReadStream(image);
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Handle OpenAI API errors
|
|
2392
|
+
*/
|
|
2393
|
+
handleError(error) {
|
|
2394
|
+
const message = error.message || "Unknown OpenAI API error";
|
|
2395
|
+
const status = error.status;
|
|
2396
|
+
if (status === 401) {
|
|
2397
|
+
throw new ProviderAuthError("openai", "Invalid API key");
|
|
2398
|
+
}
|
|
2399
|
+
if (status === 429) {
|
|
2400
|
+
throw new ProviderRateLimitError("openai", message);
|
|
2401
|
+
}
|
|
2402
|
+
if (status === 400) {
|
|
2403
|
+
if (message.includes("safety system")) {
|
|
2404
|
+
throw new ProviderError("openai", `Content policy violation: ${message}`);
|
|
2405
|
+
}
|
|
2406
|
+
throw new ProviderError("openai", `Bad request: ${message}`);
|
|
2407
|
+
}
|
|
2408
|
+
throw new ProviderError("openai", message);
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
var GoogleImageProvider = class extends BaseMediaProvider {
|
|
2412
|
+
name = "google-image";
|
|
2413
|
+
vendor = "google";
|
|
2414
|
+
capabilities = {
|
|
2415
|
+
text: false,
|
|
2416
|
+
images: true,
|
|
2417
|
+
videos: false,
|
|
2418
|
+
audio: false,
|
|
2419
|
+
features: {
|
|
2420
|
+
imageGeneration: true,
|
|
2421
|
+
imageEditing: true
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
client;
|
|
2425
|
+
constructor(config) {
|
|
2426
|
+
super(config);
|
|
2427
|
+
this.client = new genai.GoogleGenAI({
|
|
2428
|
+
apiKey: config.apiKey
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Generate images from a text prompt using Google Imagen
|
|
2433
|
+
*/
|
|
2434
|
+
async generateImage(options) {
|
|
2435
|
+
return this.executeWithCircuitBreaker(
|
|
2436
|
+
async () => {
|
|
2437
|
+
try {
|
|
2438
|
+
this.logOperationStart("image.generate", {
|
|
2439
|
+
model: options.model,
|
|
2440
|
+
n: options.n
|
|
2441
|
+
});
|
|
2442
|
+
const googleOptions = options;
|
|
2443
|
+
const response = await this.client.models.generateImages({
|
|
2444
|
+
model: options.model,
|
|
2445
|
+
prompt: options.prompt,
|
|
2446
|
+
config: {
|
|
2447
|
+
numberOfImages: options.n || 1,
|
|
2448
|
+
negativePrompt: googleOptions.negativePrompt,
|
|
2449
|
+
aspectRatio: googleOptions.aspectRatio,
|
|
2450
|
+
seed: googleOptions.seed,
|
|
2451
|
+
outputMimeType: googleOptions.outputMimeType,
|
|
2452
|
+
includeRaiReason: googleOptions.includeRaiReason
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
const images = response.generatedImages || [];
|
|
2456
|
+
this.logOperationComplete("image.generate", {
|
|
2457
|
+
model: options.model,
|
|
2458
|
+
imagesGenerated: images.length
|
|
2459
|
+
});
|
|
2460
|
+
return {
|
|
2461
|
+
created: Math.floor(Date.now() / 1e3),
|
|
2462
|
+
data: images.map((img) => ({
|
|
2463
|
+
b64_json: img.image?.imageBytes
|
|
2464
|
+
// Google doesn't provide URLs, only base64
|
|
2465
|
+
}))
|
|
2466
|
+
};
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
this.handleError(error);
|
|
2469
|
+
throw error;
|
|
2470
|
+
}
|
|
2471
|
+
},
|
|
2472
|
+
"image.generate",
|
|
2473
|
+
{ model: options.model }
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Edit an existing image using Imagen capability model
|
|
2478
|
+
* Uses imagen-3.0-capability-001
|
|
2479
|
+
*/
|
|
2480
|
+
async editImage(options) {
|
|
2481
|
+
return this.executeWithCircuitBreaker(
|
|
2482
|
+
async () => {
|
|
2483
|
+
try {
|
|
2484
|
+
this.logOperationStart("image.edit", {
|
|
2485
|
+
model: options.model,
|
|
2486
|
+
n: options.n
|
|
2487
|
+
});
|
|
2488
|
+
const referenceImage = await this.prepareReferenceImage(options.image);
|
|
2489
|
+
const response = await this.client.models.editImage({
|
|
2490
|
+
model: options.model || "imagen-3.0-capability-001",
|
|
2491
|
+
prompt: options.prompt,
|
|
2492
|
+
referenceImages: [referenceImage],
|
|
2493
|
+
config: {
|
|
2494
|
+
numberOfImages: options.n || 1
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
const images = response.generatedImages || [];
|
|
2498
|
+
this.logOperationComplete("image.edit", {
|
|
2499
|
+
model: options.model,
|
|
2500
|
+
imagesGenerated: images.length
|
|
2501
|
+
});
|
|
2502
|
+
return {
|
|
2503
|
+
created: Math.floor(Date.now() / 1e3),
|
|
2504
|
+
data: images.map((img) => ({
|
|
2505
|
+
b64_json: img.image?.imageBytes
|
|
2506
|
+
}))
|
|
2507
|
+
};
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
this.handleError(error);
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2512
|
+
},
|
|
2513
|
+
"image.edit",
|
|
2514
|
+
{ model: options.model }
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* List available image models
|
|
2519
|
+
*/
|
|
2520
|
+
async listModels() {
|
|
2521
|
+
return [
|
|
2522
|
+
"imagen-4.0-generate-001",
|
|
2523
|
+
"imagen-4.0-ultra-generate-001",
|
|
2524
|
+
"imagen-4.0-fast-generate-001"
|
|
2525
|
+
];
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* Prepare a reference image for Google's editImage API
|
|
2529
|
+
*/
|
|
2530
|
+
async prepareReferenceImage(image) {
|
|
2531
|
+
let imageBytes;
|
|
2532
|
+
if (Buffer.isBuffer(image)) {
|
|
2533
|
+
imageBytes = image.toString("base64");
|
|
2534
|
+
} else {
|
|
2535
|
+
const fs5 = await import('fs');
|
|
2536
|
+
const buffer = fs5.readFileSync(image);
|
|
2537
|
+
imageBytes = buffer.toString("base64");
|
|
2538
|
+
}
|
|
2539
|
+
return {
|
|
2540
|
+
referenceImage: {
|
|
2541
|
+
image: {
|
|
2542
|
+
imageBytes
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
referenceType: "REFERENCE_TYPE_SUBJECT"
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Handle Google API errors
|
|
2550
|
+
*/
|
|
2551
|
+
handleError(error) {
|
|
2552
|
+
const message = error.message || "Unknown Google API error";
|
|
2553
|
+
const status = error.status || error.code;
|
|
2554
|
+
if (status === 401 || message.includes("API key not valid")) {
|
|
2555
|
+
throw new ProviderAuthError("google", "Invalid API key");
|
|
2556
|
+
}
|
|
2557
|
+
if (status === 429 || message.includes("Resource exhausted")) {
|
|
2558
|
+
throw new ProviderRateLimitError("google", message);
|
|
2559
|
+
}
|
|
2560
|
+
if (status === 400) {
|
|
2561
|
+
if (message.includes("SAFETY") || message.includes("blocked") || message.includes("Responsible AI")) {
|
|
2562
|
+
throw new ProviderError("google", `Content policy violation: ${message}`);
|
|
2563
|
+
}
|
|
2564
|
+
throw new ProviderError("google", `Bad request: ${message}`);
|
|
2565
|
+
}
|
|
2566
|
+
throw new ProviderError("google", message);
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
var GROK_API_BASE_URL = "https://api.x.ai/v1";
|
|
2570
|
+
var GrokImageProvider = class extends BaseMediaProvider {
|
|
2571
|
+
name = "grok-image";
|
|
2572
|
+
vendor = "grok";
|
|
2573
|
+
capabilities = {
|
|
2574
|
+
text: false,
|
|
2575
|
+
images: true,
|
|
2576
|
+
videos: false,
|
|
2577
|
+
audio: false,
|
|
2578
|
+
features: {
|
|
2579
|
+
imageGeneration: true,
|
|
2580
|
+
imageEditing: true
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
client;
|
|
2584
|
+
constructor(config) {
|
|
2585
|
+
super({ apiKey: config.auth.apiKey, ...config });
|
|
2586
|
+
this.client = new OpenAI__default.default({
|
|
2587
|
+
apiKey: config.auth.apiKey,
|
|
2588
|
+
baseURL: config.baseURL || GROK_API_BASE_URL,
|
|
2589
|
+
timeout: config.timeout,
|
|
2590
|
+
maxRetries: config.maxRetries
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Generate images from a text prompt
|
|
2595
|
+
*/
|
|
2596
|
+
async generateImage(options) {
|
|
2597
|
+
return this.executeWithCircuitBreaker(
|
|
2598
|
+
async () => {
|
|
2599
|
+
try {
|
|
2600
|
+
this.logOperationStart("image.generate", {
|
|
2601
|
+
model: options.model,
|
|
2602
|
+
size: options.size,
|
|
2603
|
+
quality: options.quality,
|
|
2604
|
+
n: options.n
|
|
2605
|
+
});
|
|
2606
|
+
const params = {
|
|
2607
|
+
model: options.model || "grok-imagine-image",
|
|
2608
|
+
prompt: options.prompt,
|
|
2609
|
+
n: options.n || 1,
|
|
2610
|
+
response_format: options.response_format || "b64_json"
|
|
2611
|
+
};
|
|
2612
|
+
if (options.aspectRatio) {
|
|
2613
|
+
params.aspect_ratio = options.aspectRatio;
|
|
2614
|
+
}
|
|
2615
|
+
const response = await this.client.images.generate(params);
|
|
2616
|
+
const data = response.data || [];
|
|
2617
|
+
this.logOperationComplete("image.generate", {
|
|
2618
|
+
model: options.model,
|
|
2619
|
+
imagesGenerated: data.length
|
|
2620
|
+
});
|
|
2621
|
+
return {
|
|
2622
|
+
created: response.created,
|
|
2623
|
+
data: data.map((img) => ({
|
|
2624
|
+
url: img.url,
|
|
2625
|
+
b64_json: img.b64_json,
|
|
2626
|
+
revised_prompt: img.revised_prompt
|
|
2627
|
+
}))
|
|
2628
|
+
};
|
|
2629
|
+
} catch (error) {
|
|
2630
|
+
this.handleError(error);
|
|
2631
|
+
throw error;
|
|
2632
|
+
}
|
|
2633
|
+
},
|
|
2634
|
+
"image.generate",
|
|
2635
|
+
{ model: options.model }
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Edit an existing image with a prompt
|
|
2640
|
+
*/
|
|
2641
|
+
async editImage(options) {
|
|
2642
|
+
return this.executeWithCircuitBreaker(
|
|
2643
|
+
async () => {
|
|
2644
|
+
try {
|
|
2645
|
+
this.logOperationStart("image.edit", {
|
|
2646
|
+
model: options.model,
|
|
2647
|
+
size: options.size,
|
|
2648
|
+
n: options.n
|
|
2649
|
+
});
|
|
2650
|
+
const image = this.prepareImageInput(options.image);
|
|
2651
|
+
const mask = options.mask ? this.prepareImageInput(options.mask) : void 0;
|
|
2652
|
+
const params = {
|
|
2653
|
+
model: options.model || "grok-imagine-image",
|
|
2654
|
+
image,
|
|
2655
|
+
prompt: options.prompt,
|
|
2656
|
+
mask,
|
|
2657
|
+
size: options.size,
|
|
2658
|
+
n: options.n || 1,
|
|
2659
|
+
response_format: options.response_format || "b64_json"
|
|
2660
|
+
};
|
|
2661
|
+
const response = await this.client.images.edit(params);
|
|
2662
|
+
const data = response.data || [];
|
|
2663
|
+
this.logOperationComplete("image.edit", {
|
|
2664
|
+
model: options.model,
|
|
2665
|
+
imagesGenerated: data.length
|
|
2666
|
+
});
|
|
2667
|
+
return {
|
|
2668
|
+
created: response.created,
|
|
2669
|
+
data: data.map((img) => ({
|
|
2670
|
+
url: img.url,
|
|
2671
|
+
b64_json: img.b64_json,
|
|
2672
|
+
revised_prompt: img.revised_prompt
|
|
2673
|
+
}))
|
|
2674
|
+
};
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
this.handleError(error);
|
|
2677
|
+
throw error;
|
|
2678
|
+
}
|
|
2679
|
+
},
|
|
2680
|
+
"image.edit",
|
|
2681
|
+
{ model: options.model }
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* List available image models
|
|
2686
|
+
*/
|
|
2687
|
+
async listModels() {
|
|
2688
|
+
return ["grok-imagine-image"];
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Prepare image input (Buffer or file path) for API
|
|
2692
|
+
*/
|
|
2693
|
+
prepareImageInput(image) {
|
|
2694
|
+
if (Buffer.isBuffer(image)) {
|
|
2695
|
+
return new File([new Uint8Array(image)], "image.png", { type: "image/png" });
|
|
2696
|
+
}
|
|
2697
|
+
return fs2__namespace.createReadStream(image);
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Handle API errors
|
|
2701
|
+
*/
|
|
2702
|
+
handleError(error) {
|
|
2703
|
+
const message = error.message || "Unknown Grok API error";
|
|
2704
|
+
const status = error.status;
|
|
2705
|
+
if (status === 401) {
|
|
2706
|
+
throw new ProviderAuthError("grok", "Invalid API key");
|
|
2707
|
+
}
|
|
2708
|
+
if (status === 429) {
|
|
2709
|
+
throw new ProviderRateLimitError("grok", message);
|
|
2710
|
+
}
|
|
2711
|
+
if (status === 400) {
|
|
2712
|
+
if (message.includes("safety") || message.includes("policy")) {
|
|
2713
|
+
throw new ProviderError("grok", `Content policy violation: ${message}`);
|
|
2714
|
+
}
|
|
2715
|
+
throw new ProviderError("grok", `Bad request: ${message}`);
|
|
2716
|
+
}
|
|
2717
|
+
throw new ProviderError("grok", message);
|
|
2718
|
+
}
|
|
2719
|
+
};
|
|
2720
|
+
|
|
2721
|
+
// src/core/createImageProvider.ts
|
|
2722
|
+
function createImageProvider(connector) {
|
|
2723
|
+
const vendor = connector.vendor;
|
|
2724
|
+
switch (vendor) {
|
|
2725
|
+
case Vendor.OpenAI:
|
|
2726
|
+
return new OpenAIImageProvider(extractOpenAIConfig(connector));
|
|
2727
|
+
case Vendor.Google:
|
|
2728
|
+
return new GoogleImageProvider(extractGoogleConfig(connector));
|
|
2729
|
+
case Vendor.Grok:
|
|
2730
|
+
return new GrokImageProvider(extractGrokConfig(connector));
|
|
2731
|
+
default:
|
|
2732
|
+
throw new Error(
|
|
2733
|
+
`No Image provider available for vendor: ${vendor}. Supported vendors: ${Vendor.OpenAI}, ${Vendor.Google}, ${Vendor.Grok}`
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
function extractOpenAIConfig(connector) {
|
|
2738
|
+
const auth = connector.config.auth;
|
|
2739
|
+
if (auth.type !== "api_key") {
|
|
2740
|
+
throw new Error("OpenAI requires API key authentication");
|
|
2741
|
+
}
|
|
2742
|
+
const options = connector.getOptions();
|
|
2743
|
+
return {
|
|
2744
|
+
auth: {
|
|
2745
|
+
type: "api_key",
|
|
2746
|
+
apiKey: auth.apiKey
|
|
2747
|
+
},
|
|
2748
|
+
baseURL: connector.baseURL,
|
|
2749
|
+
organization: options.organization,
|
|
2750
|
+
timeout: options.timeout,
|
|
2751
|
+
maxRetries: options.maxRetries
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
function extractGoogleConfig(connector) {
|
|
2755
|
+
const auth = connector.config.auth;
|
|
2756
|
+
if (auth.type !== "api_key") {
|
|
2757
|
+
throw new Error("Google requires API key authentication");
|
|
2758
|
+
}
|
|
2759
|
+
return {
|
|
2760
|
+
apiKey: auth.apiKey
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
function extractGrokConfig(connector) {
|
|
2764
|
+
const auth = connector.config.auth;
|
|
2765
|
+
if (auth.type !== "api_key") {
|
|
2766
|
+
throw new Error("Grok requires API key authentication");
|
|
2767
|
+
}
|
|
2768
|
+
const options = connector.getOptions();
|
|
2769
|
+
return {
|
|
2770
|
+
auth: {
|
|
2771
|
+
type: "api_key",
|
|
2772
|
+
apiKey: auth.apiKey
|
|
2773
|
+
},
|
|
2774
|
+
baseURL: connector.baseURL,
|
|
2775
|
+
timeout: options.timeout,
|
|
2776
|
+
maxRetries: options.maxRetries
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// src/domain/entities/RegistryUtils.ts
|
|
2781
|
+
function createRegistryHelpers(registry) {
|
|
2782
|
+
return {
|
|
2783
|
+
/**
|
|
2784
|
+
* Get model information by name
|
|
2785
|
+
*/
|
|
2786
|
+
getInfo: (modelName) => {
|
|
2787
|
+
return registry[modelName];
|
|
2788
|
+
},
|
|
2789
|
+
/**
|
|
2790
|
+
* Get all active models for a specific vendor
|
|
2791
|
+
*/
|
|
2792
|
+
getByVendor: (vendor) => {
|
|
2793
|
+
return Object.values(registry).filter(
|
|
2794
|
+
(model) => model.provider === vendor && model.isActive
|
|
2795
|
+
);
|
|
2796
|
+
},
|
|
2797
|
+
/**
|
|
2798
|
+
* Get all currently active models (across all vendors)
|
|
2799
|
+
*/
|
|
2800
|
+
getActive: () => {
|
|
2801
|
+
return Object.values(registry).filter((model) => model.isActive);
|
|
2802
|
+
},
|
|
2803
|
+
/**
|
|
2804
|
+
* Get all models (including inactive/deprecated)
|
|
2805
|
+
*/
|
|
2806
|
+
getAll: () => {
|
|
2807
|
+
return Object.values(registry);
|
|
2808
|
+
},
|
|
2809
|
+
/**
|
|
2810
|
+
* Check if model exists in registry
|
|
2811
|
+
*/
|
|
2812
|
+
has: (modelName) => {
|
|
2813
|
+
return modelName in registry;
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// src/domain/entities/ImageModel.ts
|
|
2819
|
+
var IMAGE_MODELS = {
|
|
2820
|
+
[Vendor.OpenAI]: {
|
|
2821
|
+
/** GPT-Image-1: Latest OpenAI image model with best quality */
|
|
2822
|
+
GPT_IMAGE_1: "gpt-image-1",
|
|
2823
|
+
/** DALL-E 3: High quality image generation */
|
|
2824
|
+
DALL_E_3: "dall-e-3",
|
|
2825
|
+
/** DALL-E 2: Fast, supports editing and variations */
|
|
2826
|
+
DALL_E_2: "dall-e-2"
|
|
2827
|
+
},
|
|
2828
|
+
[Vendor.Google]: {
|
|
2829
|
+
/** Imagen 4.0: Latest Google image generation model */
|
|
2830
|
+
IMAGEN_4_GENERATE: "imagen-4.0-generate-001",
|
|
2831
|
+
/** Imagen 4.0 Ultra: Highest quality */
|
|
2832
|
+
IMAGEN_4_ULTRA: "imagen-4.0-ultra-generate-001",
|
|
2833
|
+
/** Imagen 4.0 Fast: Optimized for speed */
|
|
2834
|
+
IMAGEN_4_FAST: "imagen-4.0-fast-generate-001"
|
|
2835
|
+
},
|
|
2836
|
+
[Vendor.Grok]: {
|
|
2837
|
+
/** Grok Imagine Image: xAI image generation with editing support */
|
|
2838
|
+
GROK_IMAGINE_IMAGE: "grok-imagine-image",
|
|
2839
|
+
/** Grok 2 Image: xAI image generation (text-only input) */
|
|
2840
|
+
GROK_2_IMAGE_1212: "grok-2-image-1212"
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
var IMAGE_MODEL_REGISTRY = {
|
|
2844
|
+
// ======================== OpenAI ========================
|
|
2845
|
+
"gpt-image-1": {
|
|
2846
|
+
name: "gpt-image-1",
|
|
2847
|
+
displayName: "GPT-Image-1",
|
|
2848
|
+
provider: Vendor.OpenAI,
|
|
2849
|
+
description: "OpenAI latest image generation model with best quality and features",
|
|
2850
|
+
isActive: true,
|
|
2851
|
+
releaseDate: "2025-04-01",
|
|
2852
|
+
sources: {
|
|
2853
|
+
documentation: "https://platform.openai.com/docs/guides/images",
|
|
2854
|
+
pricing: "https://openai.com/pricing",
|
|
2855
|
+
lastVerified: "2026-01-25"
|
|
2856
|
+
},
|
|
2857
|
+
capabilities: {
|
|
2858
|
+
sizes: ["1024x1024", "1024x1536", "1536x1024", "auto"],
|
|
2859
|
+
maxImagesPerRequest: 10,
|
|
2860
|
+
outputFormats: ["png", "webp", "jpeg"],
|
|
2861
|
+
features: {
|
|
2862
|
+
generation: true,
|
|
2863
|
+
editing: true,
|
|
2864
|
+
variations: false,
|
|
2865
|
+
styleControl: false,
|
|
2866
|
+
qualityControl: true,
|
|
2867
|
+
transparency: true,
|
|
2868
|
+
promptRevision: false
|
|
2869
|
+
},
|
|
2870
|
+
limits: { maxPromptLength: 32e3 },
|
|
2871
|
+
vendorOptions: {
|
|
2872
|
+
quality: {
|
|
2873
|
+
type: "enum",
|
|
2874
|
+
label: "Quality",
|
|
2875
|
+
description: "Image quality level",
|
|
2876
|
+
enum: ["auto", "low", "medium", "high"],
|
|
2877
|
+
default: "auto",
|
|
2878
|
+
controlType: "select"
|
|
2879
|
+
},
|
|
2880
|
+
background: {
|
|
2881
|
+
type: "enum",
|
|
2882
|
+
label: "Background",
|
|
2883
|
+
description: "Background transparency",
|
|
2884
|
+
enum: ["auto", "transparent", "opaque"],
|
|
2885
|
+
default: "auto",
|
|
2886
|
+
controlType: "select"
|
|
2887
|
+
},
|
|
2888
|
+
output_format: {
|
|
2889
|
+
type: "enum",
|
|
2890
|
+
label: "Output Format",
|
|
2891
|
+
description: "Image file format",
|
|
2892
|
+
enum: ["png", "jpeg", "webp"],
|
|
2893
|
+
default: "png",
|
|
2894
|
+
controlType: "select"
|
|
2895
|
+
},
|
|
2896
|
+
output_compression: {
|
|
2897
|
+
type: "number",
|
|
2898
|
+
label: "Compression",
|
|
2899
|
+
description: "Compression level for JPEG/WebP (0-100)",
|
|
2900
|
+
min: 0,
|
|
2901
|
+
max: 100,
|
|
2902
|
+
default: 75,
|
|
2903
|
+
controlType: "slider"
|
|
2904
|
+
},
|
|
2905
|
+
moderation: {
|
|
2906
|
+
type: "enum",
|
|
2907
|
+
label: "Moderation",
|
|
2908
|
+
description: "Content moderation strictness",
|
|
2909
|
+
enum: ["auto", "low"],
|
|
2910
|
+
default: "auto",
|
|
2911
|
+
controlType: "radio"
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
},
|
|
2915
|
+
pricing: {
|
|
2916
|
+
perImageStandard: 0.011,
|
|
2917
|
+
perImageHD: 0.042,
|
|
2918
|
+
currency: "USD"
|
|
2919
|
+
}
|
|
2920
|
+
},
|
|
2921
|
+
"dall-e-3": {
|
|
2922
|
+
name: "dall-e-3",
|
|
2923
|
+
displayName: "DALL-E 3",
|
|
2924
|
+
provider: Vendor.OpenAI,
|
|
2925
|
+
description: "High quality image generation with prompt revision",
|
|
2926
|
+
isActive: true,
|
|
2927
|
+
releaseDate: "2023-11-06",
|
|
2928
|
+
deprecationDate: "2026-05-12",
|
|
2929
|
+
sources: {
|
|
2930
|
+
documentation: "https://platform.openai.com/docs/guides/images",
|
|
2931
|
+
pricing: "https://openai.com/pricing",
|
|
2932
|
+
lastVerified: "2026-01-25"
|
|
2933
|
+
},
|
|
2934
|
+
capabilities: {
|
|
2935
|
+
sizes: ["1024x1024", "1024x1792", "1792x1024"],
|
|
2936
|
+
maxImagesPerRequest: 1,
|
|
2937
|
+
outputFormats: ["png", "url"],
|
|
2938
|
+
features: {
|
|
2939
|
+
generation: true,
|
|
2940
|
+
editing: false,
|
|
2941
|
+
variations: false,
|
|
2942
|
+
styleControl: true,
|
|
2943
|
+
qualityControl: true,
|
|
2944
|
+
transparency: false,
|
|
2945
|
+
promptRevision: true
|
|
2946
|
+
},
|
|
2947
|
+
limits: { maxPromptLength: 4e3 },
|
|
2948
|
+
vendorOptions: {
|
|
2949
|
+
quality: {
|
|
2950
|
+
type: "enum",
|
|
2951
|
+
label: "Quality",
|
|
2952
|
+
description: "Image quality: standard or HD",
|
|
2953
|
+
enum: ["standard", "hd"],
|
|
2954
|
+
default: "standard",
|
|
2955
|
+
controlType: "radio"
|
|
2956
|
+
},
|
|
2957
|
+
style: {
|
|
2958
|
+
type: "enum",
|
|
2959
|
+
label: "Style",
|
|
2960
|
+
description: "Image style: vivid (hyper-real) or natural",
|
|
2961
|
+
enum: ["vivid", "natural"],
|
|
2962
|
+
default: "vivid",
|
|
2963
|
+
controlType: "radio"
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
},
|
|
2967
|
+
pricing: {
|
|
2968
|
+
perImageStandard: 0.04,
|
|
2969
|
+
perImageHD: 0.08,
|
|
2970
|
+
currency: "USD"
|
|
2971
|
+
}
|
|
2972
|
+
},
|
|
2973
|
+
"dall-e-2": {
|
|
2974
|
+
name: "dall-e-2",
|
|
2975
|
+
displayName: "DALL-E 2",
|
|
2976
|
+
provider: Vendor.OpenAI,
|
|
2977
|
+
description: "Fast image generation with editing and variation support",
|
|
2978
|
+
isActive: true,
|
|
2979
|
+
releaseDate: "2022-11-03",
|
|
2980
|
+
deprecationDate: "2026-05-12",
|
|
2981
|
+
sources: {
|
|
2982
|
+
documentation: "https://platform.openai.com/docs/guides/images",
|
|
2983
|
+
pricing: "https://openai.com/pricing",
|
|
2984
|
+
lastVerified: "2026-01-25"
|
|
2985
|
+
},
|
|
2986
|
+
capabilities: {
|
|
2987
|
+
sizes: ["256x256", "512x512", "1024x1024"],
|
|
2988
|
+
maxImagesPerRequest: 10,
|
|
2989
|
+
outputFormats: ["png", "url"],
|
|
2990
|
+
features: {
|
|
2991
|
+
generation: true,
|
|
2992
|
+
editing: true,
|
|
2993
|
+
variations: true,
|
|
2994
|
+
styleControl: false,
|
|
2995
|
+
qualityControl: false,
|
|
2996
|
+
transparency: false,
|
|
2997
|
+
promptRevision: false
|
|
2998
|
+
},
|
|
2999
|
+
limits: { maxPromptLength: 1e3 },
|
|
3000
|
+
vendorOptions: {}
|
|
3001
|
+
},
|
|
3002
|
+
pricing: {
|
|
3003
|
+
perImage: 0.02,
|
|
3004
|
+
currency: "USD"
|
|
3005
|
+
}
|
|
3006
|
+
},
|
|
3007
|
+
// ======================== Google ========================
|
|
3008
|
+
"imagen-4.0-generate-001": {
|
|
3009
|
+
name: "imagen-4.0-generate-001",
|
|
3010
|
+
displayName: "Imagen 4.0 Generate",
|
|
3011
|
+
provider: Vendor.Google,
|
|
3012
|
+
description: "Google Imagen 4.0 - standard quality image generation",
|
|
3013
|
+
isActive: true,
|
|
3014
|
+
releaseDate: "2025-06-01",
|
|
3015
|
+
sources: {
|
|
3016
|
+
documentation: "https://ai.google.dev/gemini-api/docs/imagen",
|
|
3017
|
+
pricing: "https://ai.google.dev/pricing",
|
|
3018
|
+
lastVerified: "2026-01-25"
|
|
3019
|
+
},
|
|
3020
|
+
capabilities: {
|
|
3021
|
+
sizes: ["1024x1024"],
|
|
3022
|
+
aspectRatios: ["1:1", "3:4", "4:3", "9:16", "16:9"],
|
|
3023
|
+
maxImagesPerRequest: 4,
|
|
3024
|
+
outputFormats: ["png", "jpeg"],
|
|
3025
|
+
features: {
|
|
3026
|
+
generation: true,
|
|
3027
|
+
editing: false,
|
|
3028
|
+
variations: false,
|
|
3029
|
+
styleControl: false,
|
|
3030
|
+
qualityControl: false,
|
|
3031
|
+
transparency: false,
|
|
3032
|
+
promptRevision: false
|
|
3033
|
+
},
|
|
3034
|
+
limits: { maxPromptLength: 480 },
|
|
3035
|
+
vendorOptions: {
|
|
3036
|
+
aspectRatio: {
|
|
3037
|
+
type: "enum",
|
|
3038
|
+
label: "Aspect Ratio",
|
|
3039
|
+
description: "Output image proportions",
|
|
3040
|
+
enum: ["1:1", "3:4", "4:3", "16:9", "9:16"],
|
|
3041
|
+
default: "1:1",
|
|
3042
|
+
controlType: "select"
|
|
3043
|
+
},
|
|
3044
|
+
sampleImageSize: {
|
|
3045
|
+
type: "enum",
|
|
3046
|
+
label: "Resolution",
|
|
3047
|
+
description: "Output image resolution",
|
|
3048
|
+
enum: ["1K", "2K"],
|
|
3049
|
+
default: "1K",
|
|
3050
|
+
controlType: "radio"
|
|
3051
|
+
},
|
|
3052
|
+
outputMimeType: {
|
|
3053
|
+
type: "enum",
|
|
3054
|
+
label: "Output Format",
|
|
3055
|
+
description: "Image file format",
|
|
3056
|
+
enum: ["image/png", "image/jpeg"],
|
|
3057
|
+
default: "image/png",
|
|
3058
|
+
controlType: "select"
|
|
3059
|
+
},
|
|
3060
|
+
negativePrompt: {
|
|
3061
|
+
type: "string",
|
|
3062
|
+
label: "Negative Prompt",
|
|
3063
|
+
description: "Elements to avoid in the generated image",
|
|
3064
|
+
controlType: "textarea"
|
|
3065
|
+
},
|
|
3066
|
+
personGeneration: {
|
|
3067
|
+
type: "enum",
|
|
3068
|
+
label: "Person Generation",
|
|
3069
|
+
description: "Controls whether people can appear in images",
|
|
3070
|
+
enum: ["dont_allow", "allow_adult", "allow_all"],
|
|
3071
|
+
default: "allow_adult",
|
|
3072
|
+
controlType: "select"
|
|
3073
|
+
},
|
|
3074
|
+
safetyFilterLevel: {
|
|
3075
|
+
type: "enum",
|
|
3076
|
+
label: "Safety Filter",
|
|
3077
|
+
description: "Content safety filtering threshold",
|
|
3078
|
+
enum: ["block_none", "block_only_high", "block_medium_and_above", "block_low_and_above"],
|
|
3079
|
+
default: "block_medium_and_above",
|
|
3080
|
+
controlType: "select"
|
|
3081
|
+
},
|
|
3082
|
+
enhancePrompt: {
|
|
3083
|
+
type: "boolean",
|
|
3084
|
+
label: "Enhance Prompt",
|
|
3085
|
+
description: "Use LLM-based prompt rewriting for better quality",
|
|
3086
|
+
default: true,
|
|
3087
|
+
controlType: "checkbox"
|
|
3088
|
+
},
|
|
3089
|
+
seed: {
|
|
3090
|
+
type: "number",
|
|
3091
|
+
label: "Seed",
|
|
3092
|
+
description: "Random seed for reproducible generation (1-2147483647)",
|
|
3093
|
+
min: 1,
|
|
3094
|
+
max: 2147483647,
|
|
3095
|
+
controlType: "text"
|
|
3096
|
+
},
|
|
3097
|
+
addWatermark: {
|
|
3098
|
+
type: "boolean",
|
|
3099
|
+
label: "Add Watermark",
|
|
3100
|
+
description: "Add invisible SynthID watermark",
|
|
3101
|
+
default: true,
|
|
3102
|
+
controlType: "checkbox"
|
|
3103
|
+
},
|
|
3104
|
+
language: {
|
|
3105
|
+
type: "enum",
|
|
3106
|
+
label: "Prompt Language",
|
|
3107
|
+
description: "Language of the input prompt",
|
|
3108
|
+
enum: ["auto", "en", "zh", "zh-CN", "zh-TW", "hi", "ja", "ko", "pt", "es"],
|
|
3109
|
+
default: "en",
|
|
3110
|
+
controlType: "select"
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
},
|
|
3114
|
+
pricing: {
|
|
3115
|
+
perImage: 0.04,
|
|
3116
|
+
currency: "USD"
|
|
3117
|
+
}
|
|
3118
|
+
},
|
|
3119
|
+
"imagen-4.0-ultra-generate-001": {
|
|
3120
|
+
name: "imagen-4.0-ultra-generate-001",
|
|
3121
|
+
displayName: "Imagen 4.0 Ultra",
|
|
3122
|
+
provider: Vendor.Google,
|
|
3123
|
+
description: "Google Imagen 4.0 Ultra - highest quality image generation",
|
|
3124
|
+
isActive: true,
|
|
3125
|
+
releaseDate: "2025-06-01",
|
|
3126
|
+
sources: {
|
|
3127
|
+
documentation: "https://ai.google.dev/gemini-api/docs/imagen",
|
|
3128
|
+
pricing: "https://ai.google.dev/pricing",
|
|
3129
|
+
lastVerified: "2026-01-25"
|
|
3130
|
+
},
|
|
3131
|
+
capabilities: {
|
|
3132
|
+
sizes: ["1024x1024"],
|
|
3133
|
+
aspectRatios: ["1:1", "3:4", "4:3", "9:16", "16:9"],
|
|
3134
|
+
maxImagesPerRequest: 4,
|
|
3135
|
+
outputFormats: ["png", "jpeg"],
|
|
3136
|
+
features: {
|
|
3137
|
+
generation: true,
|
|
3138
|
+
editing: false,
|
|
3139
|
+
variations: false,
|
|
3140
|
+
styleControl: false,
|
|
3141
|
+
qualityControl: true,
|
|
3142
|
+
transparency: false,
|
|
3143
|
+
promptRevision: false
|
|
3144
|
+
},
|
|
3145
|
+
limits: { maxPromptLength: 480 },
|
|
3146
|
+
vendorOptions: {
|
|
3147
|
+
aspectRatio: {
|
|
3148
|
+
type: "enum",
|
|
3149
|
+
label: "Aspect Ratio",
|
|
3150
|
+
description: "Output image proportions",
|
|
3151
|
+
enum: ["1:1", "3:4", "4:3", "16:9", "9:16"],
|
|
3152
|
+
default: "1:1",
|
|
3153
|
+
controlType: "select"
|
|
3154
|
+
},
|
|
3155
|
+
sampleImageSize: {
|
|
3156
|
+
type: "enum",
|
|
3157
|
+
label: "Resolution",
|
|
3158
|
+
description: "Output image resolution",
|
|
3159
|
+
enum: ["1K", "2K"],
|
|
3160
|
+
default: "1K",
|
|
3161
|
+
controlType: "radio"
|
|
3162
|
+
},
|
|
3163
|
+
outputMimeType: {
|
|
3164
|
+
type: "enum",
|
|
3165
|
+
label: "Output Format",
|
|
3166
|
+
description: "Image file format",
|
|
3167
|
+
enum: ["image/png", "image/jpeg"],
|
|
3168
|
+
default: "image/png",
|
|
3169
|
+
controlType: "select"
|
|
3170
|
+
},
|
|
3171
|
+
negativePrompt: {
|
|
3172
|
+
type: "string",
|
|
3173
|
+
label: "Negative Prompt",
|
|
3174
|
+
description: "Elements to avoid in the generated image",
|
|
3175
|
+
controlType: "textarea"
|
|
3176
|
+
},
|
|
3177
|
+
personGeneration: {
|
|
3178
|
+
type: "enum",
|
|
3179
|
+
label: "Person Generation",
|
|
3180
|
+
description: "Controls whether people can appear in images",
|
|
3181
|
+
enum: ["dont_allow", "allow_adult", "allow_all"],
|
|
3182
|
+
default: "allow_adult",
|
|
3183
|
+
controlType: "select"
|
|
3184
|
+
},
|
|
3185
|
+
safetyFilterLevel: {
|
|
3186
|
+
type: "enum",
|
|
3187
|
+
label: "Safety Filter",
|
|
3188
|
+
description: "Content safety filtering threshold",
|
|
3189
|
+
enum: ["block_none", "block_only_high", "block_medium_and_above", "block_low_and_above"],
|
|
3190
|
+
default: "block_medium_and_above",
|
|
3191
|
+
controlType: "select"
|
|
3192
|
+
},
|
|
3193
|
+
enhancePrompt: {
|
|
3194
|
+
type: "boolean",
|
|
3195
|
+
label: "Enhance Prompt",
|
|
3196
|
+
description: "Use LLM-based prompt rewriting for better quality",
|
|
3197
|
+
default: true,
|
|
3198
|
+
controlType: "checkbox"
|
|
3199
|
+
},
|
|
3200
|
+
seed: {
|
|
3201
|
+
type: "number",
|
|
3202
|
+
label: "Seed",
|
|
3203
|
+
description: "Random seed for reproducible generation (1-2147483647)",
|
|
3204
|
+
min: 1,
|
|
3205
|
+
max: 2147483647,
|
|
3206
|
+
controlType: "text"
|
|
3207
|
+
},
|
|
3208
|
+
addWatermark: {
|
|
3209
|
+
type: "boolean",
|
|
3210
|
+
label: "Add Watermark",
|
|
3211
|
+
description: "Add invisible SynthID watermark",
|
|
3212
|
+
default: true,
|
|
3213
|
+
controlType: "checkbox"
|
|
3214
|
+
},
|
|
3215
|
+
language: {
|
|
3216
|
+
type: "enum",
|
|
3217
|
+
label: "Prompt Language",
|
|
3218
|
+
description: "Language of the input prompt",
|
|
3219
|
+
enum: ["auto", "en", "zh", "zh-CN", "zh-TW", "hi", "ja", "ko", "pt", "es"],
|
|
3220
|
+
default: "en",
|
|
3221
|
+
controlType: "select"
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
},
|
|
3225
|
+
pricing: {
|
|
3226
|
+
perImage: 0.08,
|
|
3227
|
+
currency: "USD"
|
|
3228
|
+
}
|
|
3229
|
+
},
|
|
3230
|
+
"imagen-4.0-fast-generate-001": {
|
|
3231
|
+
name: "imagen-4.0-fast-generate-001",
|
|
3232
|
+
displayName: "Imagen 4.0 Fast",
|
|
3233
|
+
provider: Vendor.Google,
|
|
3234
|
+
description: "Google Imagen 4.0 Fast - optimized for speed",
|
|
3235
|
+
isActive: true,
|
|
3236
|
+
releaseDate: "2025-06-01",
|
|
3237
|
+
sources: {
|
|
3238
|
+
documentation: "https://ai.google.dev/gemini-api/docs/imagen",
|
|
3239
|
+
pricing: "https://ai.google.dev/pricing",
|
|
3240
|
+
lastVerified: "2026-01-25"
|
|
3241
|
+
},
|
|
3242
|
+
capabilities: {
|
|
3243
|
+
sizes: ["1024x1024"],
|
|
3244
|
+
aspectRatios: ["1:1", "3:4", "4:3", "9:16", "16:9"],
|
|
3245
|
+
maxImagesPerRequest: 4,
|
|
3246
|
+
outputFormats: ["png", "jpeg"],
|
|
3247
|
+
features: {
|
|
3248
|
+
generation: true,
|
|
3249
|
+
editing: false,
|
|
3250
|
+
variations: false,
|
|
3251
|
+
styleControl: false,
|
|
3252
|
+
qualityControl: false,
|
|
3253
|
+
transparency: false,
|
|
3254
|
+
promptRevision: false
|
|
3255
|
+
},
|
|
3256
|
+
limits: { maxPromptLength: 480 },
|
|
3257
|
+
vendorOptions: {
|
|
3258
|
+
aspectRatio: {
|
|
3259
|
+
type: "enum",
|
|
3260
|
+
label: "Aspect Ratio",
|
|
3261
|
+
description: "Output image proportions",
|
|
3262
|
+
enum: ["1:1", "3:4", "4:3", "16:9", "9:16"],
|
|
3263
|
+
default: "1:1",
|
|
3264
|
+
controlType: "select"
|
|
3265
|
+
},
|
|
3266
|
+
sampleImageSize: {
|
|
3267
|
+
type: "enum",
|
|
3268
|
+
label: "Resolution",
|
|
3269
|
+
description: "Output image resolution",
|
|
3270
|
+
enum: ["1K", "2K"],
|
|
3271
|
+
default: "1K",
|
|
3272
|
+
controlType: "radio"
|
|
3273
|
+
},
|
|
3274
|
+
outputMimeType: {
|
|
3275
|
+
type: "enum",
|
|
3276
|
+
label: "Output Format",
|
|
3277
|
+
description: "Image file format",
|
|
3278
|
+
enum: ["image/png", "image/jpeg"],
|
|
3279
|
+
default: "image/png",
|
|
3280
|
+
controlType: "select"
|
|
3281
|
+
},
|
|
3282
|
+
negativePrompt: {
|
|
3283
|
+
type: "string",
|
|
3284
|
+
label: "Negative Prompt",
|
|
3285
|
+
description: "Elements to avoid in the generated image",
|
|
3286
|
+
controlType: "textarea"
|
|
3287
|
+
},
|
|
3288
|
+
personGeneration: {
|
|
3289
|
+
type: "enum",
|
|
3290
|
+
label: "Person Generation",
|
|
3291
|
+
description: "Controls whether people can appear in images",
|
|
3292
|
+
enum: ["dont_allow", "allow_adult", "allow_all"],
|
|
3293
|
+
default: "allow_adult",
|
|
3294
|
+
controlType: "select"
|
|
3295
|
+
},
|
|
3296
|
+
safetyFilterLevel: {
|
|
3297
|
+
type: "enum",
|
|
3298
|
+
label: "Safety Filter",
|
|
3299
|
+
description: "Content safety filtering threshold",
|
|
3300
|
+
enum: ["block_none", "block_only_high", "block_medium_and_above", "block_low_and_above"],
|
|
3301
|
+
default: "block_medium_and_above",
|
|
3302
|
+
controlType: "select"
|
|
3303
|
+
},
|
|
3304
|
+
enhancePrompt: {
|
|
3305
|
+
type: "boolean",
|
|
3306
|
+
label: "Enhance Prompt",
|
|
3307
|
+
description: "Use LLM-based prompt rewriting for better quality",
|
|
3308
|
+
default: true,
|
|
3309
|
+
controlType: "checkbox"
|
|
3310
|
+
},
|
|
3311
|
+
seed: {
|
|
3312
|
+
type: "number",
|
|
3313
|
+
label: "Seed",
|
|
3314
|
+
description: "Random seed for reproducible generation (1-2147483647)",
|
|
3315
|
+
min: 1,
|
|
3316
|
+
max: 2147483647,
|
|
3317
|
+
controlType: "text"
|
|
3318
|
+
},
|
|
3319
|
+
addWatermark: {
|
|
3320
|
+
type: "boolean",
|
|
3321
|
+
label: "Add Watermark",
|
|
3322
|
+
description: "Add invisible SynthID watermark",
|
|
3323
|
+
default: true,
|
|
3324
|
+
controlType: "checkbox"
|
|
3325
|
+
},
|
|
3326
|
+
language: {
|
|
3327
|
+
type: "enum",
|
|
3328
|
+
label: "Prompt Language",
|
|
3329
|
+
description: "Language of the input prompt",
|
|
3330
|
+
enum: ["auto", "en", "zh", "zh-CN", "zh-TW", "hi", "ja", "ko", "pt", "es"],
|
|
3331
|
+
default: "en",
|
|
3332
|
+
controlType: "select"
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
},
|
|
3336
|
+
pricing: {
|
|
3337
|
+
perImage: 0.02,
|
|
3338
|
+
currency: "USD"
|
|
3339
|
+
}
|
|
3340
|
+
},
|
|
3341
|
+
// ======================== xAI Grok ========================
|
|
3342
|
+
"grok-imagine-image": {
|
|
3343
|
+
name: "grok-imagine-image",
|
|
3344
|
+
displayName: "Grok Imagine Image",
|
|
3345
|
+
provider: Vendor.Grok,
|
|
3346
|
+
description: "xAI Grok Imagine image generation with aspect ratio control and editing support",
|
|
3347
|
+
isActive: true,
|
|
3348
|
+
releaseDate: "2025-01-01",
|
|
3349
|
+
sources: {
|
|
3350
|
+
documentation: "https://docs.x.ai/docs/guides/image-generation",
|
|
3351
|
+
pricing: "https://docs.x.ai/docs/models",
|
|
3352
|
+
lastVerified: "2026-02-01"
|
|
3353
|
+
},
|
|
3354
|
+
capabilities: {
|
|
3355
|
+
sizes: ["1024x1024"],
|
|
3356
|
+
aspectRatios: ["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3"],
|
|
3357
|
+
maxImagesPerRequest: 10,
|
|
3358
|
+
outputFormats: ["png", "jpeg"],
|
|
3359
|
+
features: {
|
|
3360
|
+
generation: true,
|
|
3361
|
+
editing: true,
|
|
3362
|
+
variations: false,
|
|
3363
|
+
styleControl: false,
|
|
3364
|
+
qualityControl: false,
|
|
3365
|
+
// quality not supported by xAI API
|
|
3366
|
+
transparency: false,
|
|
3367
|
+
promptRevision: true
|
|
3368
|
+
},
|
|
3369
|
+
limits: { maxPromptLength: 4096 },
|
|
3370
|
+
vendorOptions: {
|
|
3371
|
+
n: {
|
|
3372
|
+
type: "number",
|
|
3373
|
+
label: "Number of Images",
|
|
3374
|
+
description: "Number of images to generate (1-10)",
|
|
3375
|
+
min: 1,
|
|
3376
|
+
max: 10,
|
|
3377
|
+
default: 1,
|
|
3378
|
+
controlType: "slider"
|
|
3379
|
+
},
|
|
3380
|
+
response_format: {
|
|
3381
|
+
type: "enum",
|
|
3382
|
+
label: "Response Format",
|
|
3383
|
+
description: "Format of the returned image",
|
|
3384
|
+
enum: ["url", "b64_json"],
|
|
3385
|
+
default: "url",
|
|
3386
|
+
controlType: "radio"
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
},
|
|
3390
|
+
pricing: {
|
|
3391
|
+
perImage: 0.02,
|
|
3392
|
+
currency: "USD"
|
|
3393
|
+
}
|
|
3394
|
+
},
|
|
3395
|
+
"grok-2-image-1212": {
|
|
3396
|
+
name: "grok-2-image-1212",
|
|
3397
|
+
displayName: "Grok 2 Image",
|
|
3398
|
+
provider: Vendor.Grok,
|
|
3399
|
+
description: "xAI Grok 2 image generation (text-only input, no editing)",
|
|
3400
|
+
isActive: true,
|
|
3401
|
+
releaseDate: "2024-12-12",
|
|
3402
|
+
sources: {
|
|
3403
|
+
documentation: "https://docs.x.ai/docs/guides/image-generation",
|
|
3404
|
+
pricing: "https://docs.x.ai/docs/models",
|
|
3405
|
+
lastVerified: "2026-02-01"
|
|
3406
|
+
},
|
|
3407
|
+
capabilities: {
|
|
3408
|
+
sizes: ["1024x1024"],
|
|
3409
|
+
aspectRatios: ["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3"],
|
|
3410
|
+
maxImagesPerRequest: 10,
|
|
3411
|
+
outputFormats: ["png", "jpeg"],
|
|
3412
|
+
features: {
|
|
3413
|
+
generation: true,
|
|
3414
|
+
editing: false,
|
|
3415
|
+
variations: false,
|
|
3416
|
+
styleControl: false,
|
|
3417
|
+
qualityControl: false,
|
|
3418
|
+
// quality not supported by xAI API
|
|
3419
|
+
transparency: false,
|
|
3420
|
+
promptRevision: false
|
|
3421
|
+
},
|
|
3422
|
+
limits: { maxPromptLength: 4096 },
|
|
3423
|
+
vendorOptions: {
|
|
3424
|
+
n: {
|
|
3425
|
+
type: "number",
|
|
3426
|
+
label: "Number of Images",
|
|
3427
|
+
description: "Number of images to generate (1-10)",
|
|
3428
|
+
min: 1,
|
|
3429
|
+
max: 10,
|
|
3430
|
+
default: 1,
|
|
3431
|
+
controlType: "slider"
|
|
3432
|
+
},
|
|
3433
|
+
response_format: {
|
|
3434
|
+
type: "enum",
|
|
3435
|
+
label: "Response Format",
|
|
3436
|
+
description: "Format of the returned image",
|
|
3437
|
+
enum: ["url", "b64_json"],
|
|
3438
|
+
default: "url",
|
|
3439
|
+
controlType: "radio"
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
},
|
|
3443
|
+
pricing: {
|
|
3444
|
+
perImage: 0.07,
|
|
3445
|
+
currency: "USD"
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
};
|
|
3449
|
+
var helpers = createRegistryHelpers(IMAGE_MODEL_REGISTRY);
|
|
3450
|
+
var getImageModelInfo = helpers.getInfo;
|
|
3451
|
+
|
|
3452
|
+
// src/capabilities/images/ImageGeneration.ts
|
|
3453
|
+
var ImageGeneration = class _ImageGeneration {
|
|
3454
|
+
provider;
|
|
3455
|
+
connector;
|
|
3456
|
+
defaultModel;
|
|
3457
|
+
constructor(connector) {
|
|
3458
|
+
this.connector = connector;
|
|
3459
|
+
this.provider = createImageProvider(connector);
|
|
3460
|
+
this.defaultModel = this.getDefaultModel();
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Create an ImageGeneration instance
|
|
3464
|
+
*/
|
|
3465
|
+
static create(options) {
|
|
3466
|
+
const connector = typeof options.connector === "string" ? Connector.get(options.connector) : options.connector;
|
|
3467
|
+
if (!connector) {
|
|
3468
|
+
throw new Error(`Connector not found: ${options.connector}`);
|
|
3469
|
+
}
|
|
3470
|
+
return new _ImageGeneration(connector);
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Generate images from a text prompt
|
|
3474
|
+
*/
|
|
3475
|
+
async generate(options) {
|
|
3476
|
+
const fullOptions = {
|
|
3477
|
+
model: options.model || this.defaultModel,
|
|
3478
|
+
prompt: options.prompt,
|
|
3479
|
+
size: options.size,
|
|
3480
|
+
quality: options.quality,
|
|
3481
|
+
style: options.style,
|
|
3482
|
+
n: options.n,
|
|
3483
|
+
response_format: options.response_format || "b64_json"
|
|
3484
|
+
};
|
|
3485
|
+
return this.provider.generateImage(fullOptions);
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* Edit an existing image
|
|
3489
|
+
* Note: Not all models/vendors support this
|
|
3490
|
+
*/
|
|
3491
|
+
async edit(options) {
|
|
3492
|
+
if (!this.provider.editImage) {
|
|
3493
|
+
throw new Error(`Image editing not supported by ${this.provider.name}`);
|
|
3494
|
+
}
|
|
3495
|
+
const fullOptions = {
|
|
3496
|
+
...options,
|
|
3497
|
+
model: options.model || this.getEditModel()
|
|
3498
|
+
};
|
|
3499
|
+
return this.provider.editImage(fullOptions);
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Create variations of an existing image
|
|
3503
|
+
* Note: Only DALL-E 2 supports this
|
|
3504
|
+
*/
|
|
3505
|
+
async createVariation(options) {
|
|
3506
|
+
if (!this.provider.createVariation) {
|
|
3507
|
+
throw new Error(`Image variations not supported by ${this.provider.name}`);
|
|
3508
|
+
}
|
|
3509
|
+
const fullOptions = {
|
|
3510
|
+
...options,
|
|
3511
|
+
model: options.model || "dall-e-2"
|
|
3512
|
+
// Only DALL-E 2 supports variations
|
|
3513
|
+
};
|
|
3514
|
+
return this.provider.createVariation(fullOptions);
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* List available models for this provider
|
|
3518
|
+
*/
|
|
3519
|
+
async listModels() {
|
|
3520
|
+
if (this.provider.listModels) {
|
|
3521
|
+
return this.provider.listModels();
|
|
3522
|
+
}
|
|
3523
|
+
const vendor = this.connector.vendor;
|
|
3524
|
+
if (vendor && IMAGE_MODELS[vendor]) {
|
|
3525
|
+
return Object.values(IMAGE_MODELS[vendor]);
|
|
3526
|
+
}
|
|
3527
|
+
return [];
|
|
3528
|
+
}
|
|
3529
|
+
/**
|
|
3530
|
+
* Get information about a specific model
|
|
3531
|
+
*/
|
|
3532
|
+
getModelInfo(modelName) {
|
|
3533
|
+
return getImageModelInfo(modelName);
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3536
|
+
* Get the underlying provider
|
|
3537
|
+
*/
|
|
3538
|
+
getProvider() {
|
|
3539
|
+
return this.provider;
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* Get the current connector
|
|
3543
|
+
*/
|
|
3544
|
+
getConnector() {
|
|
3545
|
+
return this.connector;
|
|
3546
|
+
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Get the default model for this vendor
|
|
3549
|
+
*/
|
|
3550
|
+
getDefaultModel() {
|
|
3551
|
+
const vendor = this.connector.vendor;
|
|
3552
|
+
switch (vendor) {
|
|
3553
|
+
case Vendor.OpenAI:
|
|
3554
|
+
return IMAGE_MODELS[Vendor.OpenAI].DALL_E_3;
|
|
3555
|
+
case Vendor.Google:
|
|
3556
|
+
return IMAGE_MODELS[Vendor.Google].IMAGEN_4_GENERATE;
|
|
3557
|
+
case Vendor.Grok:
|
|
3558
|
+
return IMAGE_MODELS[Vendor.Grok].GROK_IMAGINE_IMAGE;
|
|
3559
|
+
default:
|
|
3560
|
+
throw new Error(`No default image model for vendor: ${vendor}`);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Get the default edit model for this vendor
|
|
3565
|
+
*/
|
|
3566
|
+
getEditModel() {
|
|
3567
|
+
const vendor = this.connector.vendor;
|
|
3568
|
+
switch (vendor) {
|
|
3569
|
+
case Vendor.OpenAI:
|
|
3570
|
+
return IMAGE_MODELS[Vendor.OpenAI].GPT_IMAGE_1;
|
|
3571
|
+
case Vendor.Google:
|
|
3572
|
+
return IMAGE_MODELS[Vendor.Google].IMAGEN_4_GENERATE;
|
|
3573
|
+
case Vendor.Grok:
|
|
3574
|
+
return IMAGE_MODELS[Vendor.Grok].GROK_IMAGINE_IMAGE;
|
|
3575
|
+
default:
|
|
3576
|
+
throw new Error(`No edit model for vendor: ${vendor}`);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
|
|
3581
|
+
exports.ImageGeneration = ImageGeneration;
|
|
3582
|
+
//# sourceMappingURL=index.cjs.map
|
|
3583
|
+
//# sourceMappingURL=index.cjs.map
|