characterforge-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1170 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ApiError: () => ApiError,
24
+ AppError: () => AppError,
25
+ AuthenticationError: () => AuthenticationError,
26
+ AuthorizationError: () => AuthorizationError,
27
+ CacheError: () => CacheError,
28
+ CharacterForgeClient: () => CharacterForgeClient,
29
+ CharacterForgeError: () => GenerationError,
30
+ ConfigValidationError: () => ConfigValidationError,
31
+ GenerationError: () => GenerationError,
32
+ ImageProcessingError: () => ImageProcessingError,
33
+ InsufficientCreditsError: () => InsufficientCreditsError,
34
+ Logger: () => Logger,
35
+ NativeCacheManager: () => NativeCacheManager,
36
+ NetworkError: () => NetworkError,
37
+ PaymentError: () => PaymentError,
38
+ RateLimitError: () => RateLimitError,
39
+ VERSION: () => VERSION,
40
+ ValidationError: () => ValidationError,
41
+ WebCacheManager: () => WebCacheManager,
42
+ createCacheManager: () => createCacheManager,
43
+ createCharacterForgeClient: () => createCharacterForgeClient,
44
+ getUserFriendlyMessage: () => getUserFriendlyMessage,
45
+ isAppError: () => isAppError,
46
+ isAuthenticationError: () => isAuthenticationError,
47
+ isBrowser: () => isBrowser,
48
+ isInsufficientCreditsError: () => isInsufficientCreditsError,
49
+ isNetworkError: () => isNetworkError,
50
+ isRateLimitError: () => isRateLimitError,
51
+ isReactNative: () => isReactNative,
52
+ logger: () => logger,
53
+ parseError: () => parseError,
54
+ sdkLogger: () => sdkLogger
55
+ });
56
+ module.exports = __toCommonJS(index_exports);
57
+
58
+ // src/logger.ts
59
+ var LOG_LEVELS = {
60
+ debug: 0,
61
+ info: 1,
62
+ warn: 2,
63
+ error: 3
64
+ };
65
+ var isDevelopment = (() => {
66
+ if (typeof window !== "undefined") {
67
+ return true;
68
+ }
69
+ if (typeof process !== "undefined" && process.env) {
70
+ return process.env.NODE_ENV !== "production";
71
+ }
72
+ return true;
73
+ })();
74
+ var defaultConfig = {
75
+ minLevel: isDevelopment ? "debug" : "warn",
76
+ enableConsole: true
77
+ };
78
+ var Logger = class _Logger {
79
+ constructor(config = {}) {
80
+ this.config = { ...defaultConfig, ...config };
81
+ this.context = config.context;
82
+ }
83
+ /**
84
+ * Create a child logger with additional context
85
+ */
86
+ child(context) {
87
+ return new _Logger({
88
+ ...this.config,
89
+ context: this.context ? `${this.context}:${context}` : context
90
+ });
91
+ }
92
+ /**
93
+ * Check if a log level should be output
94
+ */
95
+ shouldLog(level) {
96
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.config.minLevel];
97
+ }
98
+ /**
99
+ * Format the log entry for console output
100
+ */
101
+ formatConsoleMessage(entry) {
102
+ const parts = [
103
+ `[${entry.timestamp}]`,
104
+ `[${entry.level.toUpperCase()}]`
105
+ ];
106
+ if (entry.context) {
107
+ parts.push(`[${entry.context}]`);
108
+ }
109
+ parts.push(entry.message);
110
+ return parts.join(" ");
111
+ }
112
+ /**
113
+ * Core logging method
114
+ */
115
+ log(level, message, data, error) {
116
+ if (!this.shouldLog(level)) {
117
+ return;
118
+ }
119
+ const entry = {
120
+ level,
121
+ message,
122
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
123
+ context: this.context,
124
+ data,
125
+ error
126
+ };
127
+ if (this.config.enableConsole) {
128
+ const formattedMessage = this.formatConsoleMessage(entry);
129
+ const consoleMethod = level === "debug" ? "log" : level;
130
+ if (data && Object.keys(data).length > 0) {
131
+ console[consoleMethod](formattedMessage, data);
132
+ } else if (error) {
133
+ console[consoleMethod](formattedMessage, error);
134
+ } else {
135
+ console[consoleMethod](formattedMessage);
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Debug level logging - for development diagnostics
141
+ */
142
+ debug(message, data) {
143
+ this.log("debug", message, data);
144
+ }
145
+ /**
146
+ * Info level logging - for general information
147
+ */
148
+ info(message, data) {
149
+ this.log("info", message, data);
150
+ }
151
+ /**
152
+ * Warning level logging - for potential issues
153
+ */
154
+ warn(message, data) {
155
+ this.log("warn", message, data);
156
+ }
157
+ /**
158
+ * Error level logging - for errors and exceptions
159
+ */
160
+ error(message, error, data) {
161
+ const errorObj = error instanceof Error ? error : void 0;
162
+ this.log("error", message, data, errorObj);
163
+ }
164
+ /**
165
+ * Log with timing information
166
+ */
167
+ time(label) {
168
+ const start = Date.now();
169
+ this.debug(`Timer started: ${label}`);
170
+ return () => {
171
+ const duration = Date.now() - start;
172
+ this.debug(`Timer ended: ${label}`, { durationMs: duration });
173
+ };
174
+ }
175
+ };
176
+ var logger = new Logger();
177
+ var sdkLogger = logger.child("SDK");
178
+ var apiLogger = logger.child("API");
179
+ var cacheLogger = logger.child("Cache");
180
+
181
+ // src/cache/web.ts
182
+ var DB_NAME = "CharacterForgeDB";
183
+ var STORE_NAME = "images";
184
+ var DB_VERSION = 2;
185
+ var MAX_CACHE_SIZE = 100;
186
+ var CACHE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
187
+ var ObjectURLManager = class {
188
+ constructor() {
189
+ this.urls = /* @__PURE__ */ new Map();
190
+ }
191
+ create(key, blob) {
192
+ this.revoke(key);
193
+ const url = URL.createObjectURL(blob);
194
+ this.urls.set(key, url);
195
+ return url;
196
+ }
197
+ revoke(key) {
198
+ const url = this.urls.get(key);
199
+ if (url) {
200
+ URL.revokeObjectURL(url);
201
+ this.urls.delete(key);
202
+ }
203
+ }
204
+ revokeAll() {
205
+ this.urls.forEach((url) => URL.revokeObjectURL(url));
206
+ this.urls.clear();
207
+ }
208
+ get(key) {
209
+ return this.urls.get(key);
210
+ }
211
+ };
212
+ var WebCacheManager = class {
213
+ constructor() {
214
+ this.dbPromise = null;
215
+ this.urlManager = new ObjectURLManager();
216
+ this.cleanupIntervalId = null;
217
+ this.isSupported = typeof window !== "undefined" && !!window.indexedDB;
218
+ if (this.isSupported) {
219
+ this.dbPromise = this.openDB();
220
+ this.scheduleCleanup();
221
+ }
222
+ }
223
+ openDB() {
224
+ return new Promise((resolve, reject) => {
225
+ if (!window.indexedDB) {
226
+ reject(new Error("IndexedDB not supported"));
227
+ return;
228
+ }
229
+ const request = window.indexedDB.open(DB_NAME, DB_VERSION);
230
+ request.onerror = () => {
231
+ cacheLogger.error("Failed to open database", request.error);
232
+ reject(request.error);
233
+ };
234
+ request.onsuccess = () => resolve(request.result);
235
+ request.onupgradeneeded = (event) => {
236
+ const db = event.target.result;
237
+ if (db.objectStoreNames.contains(STORE_NAME)) {
238
+ db.deleteObjectStore(STORE_NAME);
239
+ }
240
+ db.createObjectStore(STORE_NAME);
241
+ };
242
+ });
243
+ }
244
+ async get(key) {
245
+ if (!this.isSupported || !this.dbPromise) {
246
+ return null;
247
+ }
248
+ const existingUrl = this.urlManager.get(key);
249
+ if (existingUrl) {
250
+ this.updateAccessTime(key).catch(() => {
251
+ });
252
+ return existingUrl;
253
+ }
254
+ try {
255
+ const db = await this.dbPromise;
256
+ const entry = await this.getEntry(db, key);
257
+ if (!entry) {
258
+ return null;
259
+ }
260
+ if (Date.now() - entry.createdAt > CACHE_EXPIRY_MS) {
261
+ await this.delete(key);
262
+ return null;
263
+ }
264
+ const url = this.urlManager.create(key, entry.blob);
265
+ this.updateAccessTime(key).catch(() => {
266
+ });
267
+ return url;
268
+ } catch (error) {
269
+ cacheLogger.warn("Cache retrieval failed", { error });
270
+ return null;
271
+ }
272
+ }
273
+ async set(key, data) {
274
+ if (!this.isSupported || !this.dbPromise) {
275
+ if (typeof data === "string") return data;
276
+ return URL.createObjectURL(data);
277
+ }
278
+ try {
279
+ const db = await this.dbPromise;
280
+ let blob;
281
+ if (typeof data === "string") {
282
+ const response = await fetch(data);
283
+ if (!response.ok) {
284
+ throw new Error(`Failed to fetch image: ${response.status}`);
285
+ }
286
+ blob = await response.blob();
287
+ } else {
288
+ blob = data;
289
+ }
290
+ const entry = {
291
+ blob,
292
+ createdAt: Date.now(),
293
+ accessedAt: Date.now()
294
+ };
295
+ await this.putEntry(db, key, entry);
296
+ this.enforceLimit(db).catch(() => {
297
+ });
298
+ return this.urlManager.create(key, blob);
299
+ } catch (error) {
300
+ cacheLogger.warn("Cache storage failed", { error });
301
+ if (typeof data === "string") return data;
302
+ return URL.createObjectURL(data);
303
+ }
304
+ }
305
+ async delete(key) {
306
+ this.urlManager.revoke(key);
307
+ if (!this.isSupported || !this.dbPromise) return;
308
+ try {
309
+ const db = await this.dbPromise;
310
+ await this.deleteEntry(db, key);
311
+ } catch (error) {
312
+ cacheLogger.warn("Cache deletion failed", { error });
313
+ }
314
+ }
315
+ async clear() {
316
+ this.urlManager.revokeAll();
317
+ if (!this.isSupported || !this.dbPromise) return;
318
+ try {
319
+ const db = await this.dbPromise;
320
+ await new Promise((resolve, reject) => {
321
+ const transaction = db.transaction(STORE_NAME, "readwrite");
322
+ const store = transaction.objectStore(STORE_NAME);
323
+ const request = store.clear();
324
+ request.onerror = () => reject(request.error);
325
+ request.onsuccess = () => resolve();
326
+ });
327
+ } catch (error) {
328
+ cacheLogger.warn("Cache clear failed", { error });
329
+ }
330
+ }
331
+ /**
332
+ * Destroy the cache manager and clean up resources
333
+ * Call this when you no longer need the cache instance
334
+ */
335
+ destroy() {
336
+ if (this.cleanupIntervalId !== null) {
337
+ clearInterval(this.cleanupIntervalId);
338
+ this.cleanupIntervalId = null;
339
+ }
340
+ this.urlManager.revokeAll();
341
+ }
342
+ // =============================================================================
343
+ // Private Helpers
344
+ // =============================================================================
345
+ async getEntry(db, key) {
346
+ return new Promise((resolve, reject) => {
347
+ const transaction = db.transaction(STORE_NAME, "readonly");
348
+ const store = transaction.objectStore(STORE_NAME);
349
+ const request = store.get(key);
350
+ request.onerror = () => reject(request.error);
351
+ request.onsuccess = () => resolve(request.result || null);
352
+ });
353
+ }
354
+ async putEntry(db, key, entry) {
355
+ return new Promise((resolve, reject) => {
356
+ const transaction = db.transaction(STORE_NAME, "readwrite");
357
+ const store = transaction.objectStore(STORE_NAME);
358
+ const request = store.put(entry, key);
359
+ request.onerror = () => reject(request.error);
360
+ request.onsuccess = () => resolve();
361
+ });
362
+ }
363
+ async deleteEntry(db, key) {
364
+ return new Promise((resolve, reject) => {
365
+ const transaction = db.transaction(STORE_NAME, "readwrite");
366
+ const store = transaction.objectStore(STORE_NAME);
367
+ const request = store.delete(key);
368
+ request.onerror = () => reject(request.error);
369
+ request.onsuccess = () => resolve();
370
+ });
371
+ }
372
+ async updateAccessTime(key) {
373
+ if (!this.dbPromise) return;
374
+ try {
375
+ const db = await this.dbPromise;
376
+ const entry = await this.getEntry(db, key);
377
+ if (entry) {
378
+ entry.accessedAt = Date.now();
379
+ await this.putEntry(db, key, entry);
380
+ }
381
+ } catch {
382
+ }
383
+ }
384
+ async enforceLimit(db) {
385
+ try {
386
+ const count = await this.getCount(db);
387
+ if (count > MAX_CACHE_SIZE) {
388
+ const keysToDelete = await this.getOldestKeys(db, count - MAX_CACHE_SIZE);
389
+ for (const key of keysToDelete) {
390
+ this.urlManager.revoke(key);
391
+ await this.deleteEntry(db, key);
392
+ }
393
+ }
394
+ } catch (error) {
395
+ cacheLogger.warn("Failed to enforce cache limit", { error });
396
+ }
397
+ }
398
+ async getCount(db) {
399
+ return new Promise((resolve, reject) => {
400
+ const transaction = db.transaction(STORE_NAME, "readonly");
401
+ const store = transaction.objectStore(STORE_NAME);
402
+ const request = store.count();
403
+ request.onerror = () => reject(request.error);
404
+ request.onsuccess = () => resolve(request.result);
405
+ });
406
+ }
407
+ async getOldestKeys(db, count) {
408
+ return new Promise((resolve, reject) => {
409
+ const transaction = db.transaction(STORE_NAME, "readonly");
410
+ const store = transaction.objectStore(STORE_NAME);
411
+ const entries = [];
412
+ const request = store.openCursor();
413
+ request.onerror = () => reject(request.error);
414
+ request.onsuccess = (event) => {
415
+ const cursor = event.target.result;
416
+ if (cursor) {
417
+ const entry = cursor.value;
418
+ entries.push({
419
+ key: cursor.key,
420
+ accessedAt: entry.accessedAt
421
+ });
422
+ cursor.continue();
423
+ } else {
424
+ entries.sort((a, b) => a.accessedAt - b.accessedAt);
425
+ const oldestKeys = entries.slice(0, count).map((e) => e.key);
426
+ resolve(oldestKeys);
427
+ }
428
+ };
429
+ });
430
+ }
431
+ scheduleCleanup() {
432
+ if (this.cleanupIntervalId !== null) {
433
+ clearInterval(this.cleanupIntervalId);
434
+ }
435
+ this.cleanupIntervalId = setInterval(() => this.cleanup().catch(() => {
436
+ }), 60 * 60 * 1e3);
437
+ }
438
+ async cleanup() {
439
+ if (!this.dbPromise) return;
440
+ try {
441
+ const db = await this.dbPromise;
442
+ const expiredKeys = [];
443
+ await new Promise((resolve, reject) => {
444
+ const transaction = db.transaction(STORE_NAME, "readonly");
445
+ const store = transaction.objectStore(STORE_NAME);
446
+ const request = store.openCursor();
447
+ request.onerror = () => reject(request.error);
448
+ request.onsuccess = (event) => {
449
+ const cursor = event.target.result;
450
+ if (cursor) {
451
+ const entry = cursor.value;
452
+ if (Date.now() - entry.createdAt > CACHE_EXPIRY_MS) {
453
+ expiredKeys.push(cursor.key);
454
+ }
455
+ cursor.continue();
456
+ } else {
457
+ resolve();
458
+ }
459
+ };
460
+ });
461
+ for (const key of expiredKeys) {
462
+ await this.delete(key);
463
+ }
464
+ if (expiredKeys.length > 0) {
465
+ cacheLogger.info(`Cleaned up ${expiredKeys.length} expired entries`);
466
+ }
467
+ } catch (error) {
468
+ cacheLogger.warn("Cleanup failed", { error });
469
+ }
470
+ }
471
+ };
472
+
473
+ // src/cache/native.ts
474
+ var CACHE_DIR = "character-forge-cache/";
475
+ var MAX_CACHE_SIZE2 = 100;
476
+ var CACHE_EXPIRY_MS2 = 7 * 24 * 60 * 60 * 1e3;
477
+ var METADATA_KEY = "@characterforge:cache-metadata";
478
+ function getFileSystemAdapter() {
479
+ try {
480
+ const ExpoFileSystem = require("expo-file-system");
481
+ if (ExpoFileSystem?.documentDirectory) {
482
+ return {
483
+ documentDirectory: ExpoFileSystem.documentDirectory,
484
+ downloadAsync: ExpoFileSystem.downloadAsync,
485
+ writeAsStringAsync: ExpoFileSystem.writeAsStringAsync,
486
+ readAsStringAsync: ExpoFileSystem.readAsStringAsync,
487
+ getInfoAsync: ExpoFileSystem.getInfoAsync,
488
+ makeDirectoryAsync: ExpoFileSystem.makeDirectoryAsync,
489
+ deleteAsync: ExpoFileSystem.deleteAsync,
490
+ readDirectoryAsync: ExpoFileSystem.readDirectoryAsync
491
+ };
492
+ }
493
+ } catch {
494
+ }
495
+ try {
496
+ const RNFS = require("react-native-fs");
497
+ if (RNFS?.DocumentDirectoryPath) {
498
+ return {
499
+ documentDirectory: RNFS.DocumentDirectoryPath + "/",
500
+ downloadAsync: async (url, fileUri) => {
501
+ await RNFS.downloadFile({ fromUrl: url, toFile: fileUri }).promise;
502
+ },
503
+ writeAsStringAsync: async (fileUri, contents) => {
504
+ await RNFS.writeFile(fileUri, contents, "utf8");
505
+ },
506
+ readAsStringAsync: async (fileUri) => {
507
+ return await RNFS.readFile(fileUri, "utf8");
508
+ },
509
+ getInfoAsync: async (fileUri) => {
510
+ const exists = await RNFS.exists(fileUri);
511
+ if (exists) {
512
+ const stat = await RNFS.stat(fileUri);
513
+ return { exists: true, size: stat.size };
514
+ }
515
+ return { exists: false };
516
+ },
517
+ makeDirectoryAsync: async (dirUri) => {
518
+ await RNFS.mkdir(dirUri);
519
+ },
520
+ deleteAsync: async (fileUri) => {
521
+ const exists = await RNFS.exists(fileUri);
522
+ if (exists) {
523
+ await RNFS.unlink(fileUri);
524
+ }
525
+ },
526
+ readDirectoryAsync: async (dirUri) => {
527
+ const files = await RNFS.readDir(dirUri);
528
+ return files.map((file) => file.name);
529
+ }
530
+ };
531
+ }
532
+ } catch {
533
+ }
534
+ return null;
535
+ }
536
+ function getAsyncStorage() {
537
+ try {
538
+ return require("@react-native-async-storage/async-storage").default;
539
+ } catch {
540
+ try {
541
+ return require("react-native").AsyncStorage;
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
546
+ }
547
+ var NativeCacheManager = class {
548
+ constructor() {
549
+ this.metadata = {};
550
+ this.cleanupIntervalId = null;
551
+ this.fs = getFileSystemAdapter();
552
+ this.asyncStorage = getAsyncStorage();
553
+ this.isSupported = !!(this.fs && this.asyncStorage);
554
+ if (this.isSupported && this.fs) {
555
+ this.cacheDir = this.fs.documentDirectory + CACHE_DIR;
556
+ this.initPromise = this.initialize();
557
+ } else {
558
+ this.cacheDir = "";
559
+ this.initPromise = Promise.resolve();
560
+ cacheLogger.warn(
561
+ "React Native cache not supported. Install expo-file-system or react-native-fs and @react-native-async-storage/async-storage."
562
+ );
563
+ }
564
+ }
565
+ async initialize() {
566
+ if (!this.fs) return;
567
+ try {
568
+ const dirInfo = await this.fs.getInfoAsync(this.cacheDir);
569
+ if (!dirInfo.exists) {
570
+ await this.fs.makeDirectoryAsync(this.cacheDir, { intermediates: true });
571
+ }
572
+ await this.loadMetadata();
573
+ this.scheduleCleanup();
574
+ } catch (error) {
575
+ cacheLogger.error("Failed to initialize cache", error);
576
+ }
577
+ }
578
+ async loadMetadata() {
579
+ if (!this.asyncStorage) return;
580
+ try {
581
+ const data = await this.asyncStorage.getItem(METADATA_KEY);
582
+ if (data) {
583
+ this.metadata = JSON.parse(data);
584
+ }
585
+ } catch (error) {
586
+ cacheLogger.warn("Failed to load cache metadata", { error });
587
+ this.metadata = {};
588
+ }
589
+ }
590
+ async saveMetadata() {
591
+ if (!this.asyncStorage) return;
592
+ try {
593
+ await this.asyncStorage.setItem(METADATA_KEY, JSON.stringify(this.metadata));
594
+ } catch (error) {
595
+ cacheLogger.warn("Failed to save cache metadata", { error });
596
+ }
597
+ }
598
+ async get(key) {
599
+ if (!this.isSupported || !this.fs) {
600
+ return null;
601
+ }
602
+ await this.initPromise;
603
+ const entry = this.metadata[key];
604
+ if (!entry) {
605
+ return null;
606
+ }
607
+ if (Date.now() - entry.createdAt > CACHE_EXPIRY_MS2) {
608
+ await this.delete(key);
609
+ return null;
610
+ }
611
+ const fileUri = this.cacheDir + entry.fileName;
612
+ const fileInfo = await this.fs.getInfoAsync(fileUri);
613
+ if (!fileInfo.exists) {
614
+ delete this.metadata[key];
615
+ await this.saveMetadata();
616
+ return null;
617
+ }
618
+ entry.accessedAt = Date.now();
619
+ await this.saveMetadata();
620
+ return fileUri;
621
+ }
622
+ async set(key, data) {
623
+ if (!this.isSupported || !this.fs) {
624
+ if (typeof data === "string") return data;
625
+ throw new Error("Cache not supported in this environment");
626
+ }
627
+ await this.initPromise;
628
+ if (typeof data !== "string") {
629
+ cacheLogger.warn("Blob caching not supported in React Native. Use URL strings instead.");
630
+ throw new Error("Blob caching not supported in React Native. Please provide a URL string instead.");
631
+ }
632
+ try {
633
+ const fileName = `${Date.now()}-${Math.random().toString(36).substring(7)}.png`;
634
+ const fileUri = this.cacheDir + fileName;
635
+ await this.fs.downloadAsync(data, fileUri);
636
+ this.metadata[key] = {
637
+ fileName,
638
+ createdAt: Date.now(),
639
+ accessedAt: Date.now()
640
+ };
641
+ await this.saveMetadata();
642
+ await this.enforceLimit();
643
+ return fileUri;
644
+ } catch (error) {
645
+ cacheLogger.warn("Cache storage failed", { error });
646
+ return data;
647
+ }
648
+ }
649
+ async delete(key) {
650
+ if (!this.isSupported || !this.fs) return;
651
+ await this.initPromise;
652
+ const entry = this.metadata[key];
653
+ if (!entry) return;
654
+ try {
655
+ const fileUri = this.cacheDir + entry.fileName;
656
+ await this.fs.deleteAsync(fileUri, { idempotent: true });
657
+ } catch (error) {
658
+ cacheLogger.warn("Failed to delete cache file", { error });
659
+ }
660
+ delete this.metadata[key];
661
+ await this.saveMetadata();
662
+ }
663
+ async clear() {
664
+ if (!this.isSupported || !this.fs) return;
665
+ await this.initPromise;
666
+ try {
667
+ const files = await this.fs.readDirectoryAsync(this.cacheDir);
668
+ for (const file of files) {
669
+ await this.fs.deleteAsync(this.cacheDir + file, { idempotent: true });
670
+ }
671
+ this.metadata = {};
672
+ await this.saveMetadata();
673
+ } catch (error) {
674
+ cacheLogger.warn("Cache clear failed", { error });
675
+ }
676
+ }
677
+ /**
678
+ * Destroy the cache manager and clean up resources
679
+ * Call this when you no longer need the cache instance
680
+ */
681
+ destroy() {
682
+ if (this.cleanupIntervalId !== null) {
683
+ clearInterval(this.cleanupIntervalId);
684
+ this.cleanupIntervalId = null;
685
+ }
686
+ }
687
+ // =============================================================================
688
+ // Private Helpers
689
+ // =============================================================================
690
+ async enforceLimit() {
691
+ const keys = Object.keys(this.metadata);
692
+ if (keys.length <= MAX_CACHE_SIZE2) return;
693
+ const sortedKeys = keys.sort((a, b) => {
694
+ return this.metadata[a].accessedAt - this.metadata[b].accessedAt;
695
+ });
696
+ const toDelete = sortedKeys.slice(0, keys.length - MAX_CACHE_SIZE2);
697
+ for (const key of toDelete) {
698
+ await this.delete(key);
699
+ }
700
+ }
701
+ scheduleCleanup() {
702
+ if (this.cleanupIntervalId !== null) {
703
+ clearInterval(this.cleanupIntervalId);
704
+ }
705
+ this.cleanupIntervalId = setInterval(() => this.cleanup().catch(() => {
706
+ }), 60 * 60 * 1e3);
707
+ }
708
+ async cleanup() {
709
+ if (!this.isSupported) return;
710
+ const now = Date.now();
711
+ const expiredKeys = Object.keys(this.metadata).filter(
712
+ (key) => now - this.metadata[key].createdAt > CACHE_EXPIRY_MS2
713
+ );
714
+ for (const key of expiredKeys) {
715
+ await this.delete(key);
716
+ }
717
+ if (expiredKeys.length > 0) {
718
+ cacheLogger.info(`Cleaned up ${expiredKeys.length} expired entries`);
719
+ }
720
+ }
721
+ };
722
+
723
+ // src/cache/index.ts
724
+ function isReactNative() {
725
+ return typeof navigator !== "undefined" && navigator.product === "ReactNative";
726
+ }
727
+ function isBrowser() {
728
+ return typeof window !== "undefined" && typeof window.document !== "undefined";
729
+ }
730
+ function createCacheManager() {
731
+ if (isReactNative()) {
732
+ cacheLogger.debug("Creating React Native cache manager");
733
+ return new NativeCacheManager();
734
+ }
735
+ if (isBrowser()) {
736
+ cacheLogger.debug("Creating Web cache manager");
737
+ return new WebCacheManager();
738
+ }
739
+ cacheLogger.warn("Platform not supported, using no-op cache");
740
+ return new NoOpCacheManager();
741
+ }
742
+ var NoOpCacheManager = class {
743
+ async get(_key) {
744
+ return null;
745
+ }
746
+ async set(_key, data) {
747
+ if (typeof data === "string") {
748
+ return data;
749
+ }
750
+ throw new Error("Cache not supported in this environment");
751
+ }
752
+ async clear() {
753
+ }
754
+ async delete(_key) {
755
+ }
756
+ destroy() {
757
+ }
758
+ };
759
+
760
+ // src/errors.ts
761
+ var AppError = class extends Error {
762
+ constructor(message, code = "APP_ERROR", statusCode = 500, isOperational = true) {
763
+ super(message);
764
+ this.name = this.constructor.name;
765
+ this.code = code;
766
+ this.statusCode = statusCode;
767
+ this.isOperational = isOperational;
768
+ this.timestamp = /* @__PURE__ */ new Date();
769
+ if (Error.captureStackTrace) {
770
+ Error.captureStackTrace(this, this.constructor);
771
+ }
772
+ }
773
+ toJSON() {
774
+ return {
775
+ name: this.name,
776
+ message: this.message,
777
+ code: this.code,
778
+ statusCode: this.statusCode,
779
+ timestamp: this.timestamp.toISOString()
780
+ };
781
+ }
782
+ };
783
+ var AuthenticationError = class extends AppError {
784
+ constructor(message = "Invalid or missing API key") {
785
+ super(message, "AUTH_ERROR", 401);
786
+ }
787
+ };
788
+ var AuthorizationError = class extends AppError {
789
+ constructor(message = "Not authorized to perform this action") {
790
+ super(message, "AUTHORIZATION_ERROR", 403);
791
+ }
792
+ };
793
+ var InsufficientCreditsError = class extends AppError {
794
+ constructor(required = 1, available = 0) {
795
+ super(
796
+ "Insufficient credits. Please purchase more credits to continue.",
797
+ "INSUFFICIENT_CREDITS",
798
+ 402
799
+ );
800
+ this.required = required;
801
+ this.available = available;
802
+ }
803
+ };
804
+ var PaymentError = class extends AppError {
805
+ constructor(message = "Payment processing failed") {
806
+ super(message, "PAYMENT_ERROR", 402);
807
+ }
808
+ };
809
+ var ApiError = class extends AppError {
810
+ constructor(message, statusCode = 500, endpoint, code = "API_ERROR") {
811
+ super(message, code, statusCode);
812
+ this.endpoint = endpoint;
813
+ }
814
+ };
815
+ var RateLimitError = class extends ApiError {
816
+ constructor(retryAfter) {
817
+ super("Too many requests. Please try again later.", 429, void 0, "RATE_LIMIT");
818
+ this.retryAfter = retryAfter;
819
+ }
820
+ };
821
+ var NetworkError = class extends AppError {
822
+ constructor(message = "Network error. Please check your connection.") {
823
+ super(message, "NETWORK_ERROR", 0);
824
+ }
825
+ };
826
+ var GenerationError = class extends AppError {
827
+ constructor(message = "Failed to generate character", code = "GENERATION_ERROR") {
828
+ super(message, code, 500);
829
+ }
830
+ };
831
+ var ImageProcessingError = class extends GenerationError {
832
+ constructor(message = "Failed to process image") {
833
+ super(message, "IMAGE_PROCESSING_ERROR");
834
+ }
835
+ };
836
+ var ValidationError = class extends AppError {
837
+ constructor(message, field, value, code = "VALIDATION_ERROR") {
838
+ super(message, code, 400);
839
+ this.field = field;
840
+ this.value = value;
841
+ }
842
+ };
843
+ var ConfigValidationError = class extends ValidationError {
844
+ constructor(message, field) {
845
+ super(message, field, void 0, "CONFIG_VALIDATION_ERROR");
846
+ }
847
+ };
848
+ var CacheError = class extends AppError {
849
+ constructor(message = "Cache operation failed") {
850
+ super(message, "CACHE_ERROR", 500);
851
+ }
852
+ };
853
+ function isAppError(error) {
854
+ return error instanceof AppError;
855
+ }
856
+ function isAuthenticationError(error) {
857
+ return error instanceof AuthenticationError;
858
+ }
859
+ function isInsufficientCreditsError(error) {
860
+ return error instanceof InsufficientCreditsError;
861
+ }
862
+ function isNetworkError(error) {
863
+ return error instanceof NetworkError;
864
+ }
865
+ function isRateLimitError(error) {
866
+ return error instanceof RateLimitError;
867
+ }
868
+ function parseError(error) {
869
+ if (isAppError(error)) {
870
+ return error;
871
+ }
872
+ if (error instanceof Error) {
873
+ const message = error.message.toLowerCase();
874
+ if (message.includes("api key") || message.includes("authentication")) {
875
+ return new AuthenticationError(error.message);
876
+ }
877
+ if (message.includes("credits") || message.includes("insufficient")) {
878
+ return new InsufficientCreditsError();
879
+ }
880
+ if (message.includes("network") || message.includes("fetch")) {
881
+ return new NetworkError(error.message);
882
+ }
883
+ return new AppError(error.message);
884
+ }
885
+ if (typeof error === "string") {
886
+ return new AppError(error);
887
+ }
888
+ return new AppError("An unexpected error occurred");
889
+ }
890
+ function getUserFriendlyMessage(error) {
891
+ const appError = parseError(error);
892
+ const friendlyMessages = {
893
+ AUTH_ERROR: "Invalid API key. Please check your credentials.",
894
+ INSUFFICIENT_CREDITS: "You need more credits. Purchase credits to continue.",
895
+ RATE_LIMIT: "You're doing that too fast. Please wait a moment.",
896
+ NETWORK_ERROR: "Connection problem. Please check your internet.",
897
+ GENERATION_ERROR: "Unable to create your character. Please try again."
898
+ };
899
+ return friendlyMessages[appError.code] || appError.message;
900
+ }
901
+
902
+ // src/client.ts
903
+ var DEFAULT_BASE_URL = "https://mnxzykltetirdcnxugcl.supabase.co/functions/v1";
904
+ var DEFAULT_TIMEOUT = 6e4;
905
+ var DEFAULT_RETRY_CONFIG = {
906
+ maxRetries: 3,
907
+ baseDelayMs: 1e3,
908
+ maxDelayMs: 1e4
909
+ };
910
+ var RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
911
+ function generateCacheKey(config) {
912
+ const sortedKeys = Object.keys(config).sort();
913
+ const stableObj = {};
914
+ for (const key of sortedKeys) {
915
+ if (key === "cache") continue;
916
+ const value = config[key];
917
+ if (value !== void 0) {
918
+ stableObj[key] = value;
919
+ }
920
+ }
921
+ return JSON.stringify(stableObj);
922
+ }
923
+ function calculateBackoffDelay(attempt, baseDelay, maxDelay) {
924
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
925
+ const jitter = Math.random() * 0.3 * exponentialDelay;
926
+ return Math.min(exponentialDelay + jitter, maxDelay);
927
+ }
928
+ function delay(ms) {
929
+ return new Promise((resolve) => setTimeout(resolve, ms));
930
+ }
931
+ function isRetryableError(error) {
932
+ if (error instanceof NetworkError) return true;
933
+ if (error instanceof RateLimitError) return true;
934
+ if (error instanceof ApiError) return true;
935
+ if (error instanceof Error) {
936
+ const message = error.message.toLowerCase();
937
+ return message.includes("network") || message.includes("timeout") || message.includes("fetch") || message.includes("failed to fetch");
938
+ }
939
+ return false;
940
+ }
941
+ function parseHttpError(status, data) {
942
+ const errorData = data;
943
+ const message = errorData.error || "An unexpected error occurred";
944
+ if (status === 401 || message.toLowerCase().includes("api key")) {
945
+ return new AuthenticationError(message);
946
+ }
947
+ if (status === 402 || message.toLowerCase().includes("credits")) {
948
+ return new InsufficientCreditsError();
949
+ }
950
+ if (status === 429) {
951
+ return new RateLimitError();
952
+ }
953
+ if (RETRYABLE_STATUS_CODES.includes(status)) {
954
+ return new ApiError(message, status, "generate-character");
955
+ }
956
+ return new GenerationError(message);
957
+ }
958
+ async function fetchWithTimeout(url, options, timeoutMs) {
959
+ const controller = new AbortController();
960
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
961
+ try {
962
+ const response = await fetch(url, {
963
+ ...options,
964
+ signal: controller.signal
965
+ });
966
+ clearTimeout(timeoutId);
967
+ return response;
968
+ } catch (error) {
969
+ clearTimeout(timeoutId);
970
+ if (error instanceof Error && error.name === "AbortError") {
971
+ throw new NetworkError("Request timeout");
972
+ }
973
+ throw error;
974
+ }
975
+ }
976
+ var CharacterForgeClient = class {
977
+ constructor(config) {
978
+ if (!config.apiKey) {
979
+ throw new AuthenticationError("API key is required");
980
+ }
981
+ this.config = {
982
+ apiKey: config.apiKey,
983
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
984
+ cache: config.cache ?? true,
985
+ cacheManager: config.cacheManager || createCacheManager(),
986
+ timeout: config.timeout || DEFAULT_TIMEOUT,
987
+ retry: config.retry || DEFAULT_RETRY_CONFIG
988
+ };
989
+ this.retryConfig = this.config.retry;
990
+ this.cacheManager = this.config.cacheManager;
991
+ sdkLogger.info("SDK Client initialized", {
992
+ cacheEnabled: this.config.cache,
993
+ baseUrl: this.config.baseUrl
994
+ });
995
+ }
996
+ /**
997
+ * Generate a character image based on the configuration
998
+ * Includes caching, retry logic, and comprehensive error handling
999
+ */
1000
+ async generate(characterConfig, onStatusUpdate) {
1001
+ const shouldCache = this.config.cache && characterConfig.cache !== false;
1002
+ const cacheKey = generateCacheKey(characterConfig);
1003
+ sdkLogger.debug("Starting generation", {
1004
+ shouldCache,
1005
+ gender: characterConfig.gender
1006
+ });
1007
+ if (shouldCache) {
1008
+ try {
1009
+ const cachedUrl = await this.cacheManager.get(cacheKey);
1010
+ if (cachedUrl) {
1011
+ sdkLogger.info("Cache hit");
1012
+ onStatusUpdate?.("Retrieved from Client Cache!");
1013
+ return cachedUrl;
1014
+ }
1015
+ sdkLogger.debug("Cache miss");
1016
+ } catch (cacheError) {
1017
+ sdkLogger.warn("Cache lookup failed", { error: cacheError });
1018
+ }
1019
+ }
1020
+ onStatusUpdate?.("Calling AI Cloud...");
1021
+ const imageUrl = await this.callApiWithRetry(characterConfig);
1022
+ if (shouldCache && imageUrl) {
1023
+ const cachedUrl = await this.cacheResult(cacheKey, imageUrl, onStatusUpdate);
1024
+ if (cachedUrl) {
1025
+ sdkLogger.debug("Returning local cached URL");
1026
+ return cachedUrl;
1027
+ }
1028
+ }
1029
+ return imageUrl;
1030
+ }
1031
+ /**
1032
+ * Call the generation API with retry logic
1033
+ */
1034
+ async callApiWithRetry(config) {
1035
+ let lastError = null;
1036
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
1037
+ try {
1038
+ sdkLogger.debug("API call attempt", { attempt: attempt + 1 });
1039
+ const url = `${this.config.baseUrl}/generate-character`;
1040
+ const response = await fetchWithTimeout(
1041
+ url,
1042
+ {
1043
+ method: "POST",
1044
+ headers: {
1045
+ "Content-Type": "application/json",
1046
+ "Authorization": `Bearer ${this.config.apiKey}`
1047
+ },
1048
+ body: JSON.stringify(config)
1049
+ },
1050
+ this.config.timeout
1051
+ );
1052
+ if (!response.ok) {
1053
+ const data2 = await response.json();
1054
+ const error = parseHttpError(response.status, data2);
1055
+ throw error;
1056
+ }
1057
+ const data = await response.json();
1058
+ if (!data.image) {
1059
+ throw new GenerationError("No image URL in response");
1060
+ }
1061
+ sdkLogger.info("Generation successful", {
1062
+ attempt: attempt + 1
1063
+ });
1064
+ return data.image;
1065
+ } catch (error) {
1066
+ lastError = error instanceof Error ? error : new GenerationError("Unknown error");
1067
+ if (lastError instanceof AuthenticationError || lastError instanceof InsufficientCreditsError) {
1068
+ throw lastError;
1069
+ }
1070
+ if (attempt < this.retryConfig.maxRetries && isRetryableError(error)) {
1071
+ const delayMs = calculateBackoffDelay(
1072
+ attempt,
1073
+ this.retryConfig.baseDelayMs,
1074
+ this.retryConfig.maxDelayMs
1075
+ );
1076
+ sdkLogger.warn("Retrying after error", {
1077
+ attempt: attempt + 1,
1078
+ delayMs,
1079
+ error: lastError.message
1080
+ });
1081
+ await delay(delayMs);
1082
+ continue;
1083
+ }
1084
+ throw lastError;
1085
+ }
1086
+ }
1087
+ throw lastError || new GenerationError("Generation failed after retries");
1088
+ }
1089
+ /**
1090
+ * Cache the generation result
1091
+ */
1092
+ async cacheResult(cacheKey, imageUrl, onStatusUpdate) {
1093
+ try {
1094
+ onStatusUpdate?.("Caching result...");
1095
+ await this.cacheManager.set(cacheKey, imageUrl);
1096
+ const cachedUrl = await this.cacheManager.get(cacheKey);
1097
+ if (cachedUrl) {
1098
+ sdkLogger.debug("Result cached successfully");
1099
+ return cachedUrl;
1100
+ }
1101
+ } catch (cacheError) {
1102
+ sdkLogger.warn("Failed to cache image", { error: cacheError });
1103
+ }
1104
+ return null;
1105
+ }
1106
+ /**
1107
+ * Clear the local cache
1108
+ */
1109
+ async clearCache() {
1110
+ sdkLogger.info("Clearing cache");
1111
+ await this.cacheManager.clear();
1112
+ }
1113
+ /**
1114
+ * Get cache statistics (if supported by cache manager)
1115
+ */
1116
+ async getCacheStats() {
1117
+ return null;
1118
+ }
1119
+ /**
1120
+ * Destroy the client and clean up resources
1121
+ * Call this when you no longer need the client instance
1122
+ */
1123
+ destroy() {
1124
+ sdkLogger.info("Destroying SDK client");
1125
+ if (this.cacheManager && "destroy" in this.cacheManager && typeof this.cacheManager.destroy === "function") {
1126
+ this.cacheManager.destroy();
1127
+ }
1128
+ }
1129
+ };
1130
+ function createCharacterForgeClient(config) {
1131
+ return new CharacterForgeClient(config);
1132
+ }
1133
+
1134
+ // src/index.ts
1135
+ var VERSION = "1.0.0";
1136
+ // Annotate the CommonJS export names for ESM import in node:
1137
+ 0 && (module.exports = {
1138
+ ApiError,
1139
+ AppError,
1140
+ AuthenticationError,
1141
+ AuthorizationError,
1142
+ CacheError,
1143
+ CharacterForgeClient,
1144
+ CharacterForgeError,
1145
+ ConfigValidationError,
1146
+ GenerationError,
1147
+ ImageProcessingError,
1148
+ InsufficientCreditsError,
1149
+ Logger,
1150
+ NativeCacheManager,
1151
+ NetworkError,
1152
+ PaymentError,
1153
+ RateLimitError,
1154
+ VERSION,
1155
+ ValidationError,
1156
+ WebCacheManager,
1157
+ createCacheManager,
1158
+ createCharacterForgeClient,
1159
+ getUserFriendlyMessage,
1160
+ isAppError,
1161
+ isAuthenticationError,
1162
+ isBrowser,
1163
+ isInsufficientCreditsError,
1164
+ isNetworkError,
1165
+ isRateLimitError,
1166
+ isReactNative,
1167
+ logger,
1168
+ parseError,
1169
+ sdkLogger
1170
+ });