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