@blinkdotnew/sdk 0.14.13 → 0.17.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 CHANGED
@@ -1817,20 +1817,27 @@ var BlinkStorageImpl = class {
1817
1817
  * Upload a file to project storage
1818
1818
  *
1819
1819
  * @param file - File, Blob, or Buffer to upload
1820
- * @param path - Destination path within project storage
1820
+ * @param path - Destination path within project storage (extension will be auto-corrected to match file type)
1821
1821
  * @param options - Upload options including upsert and progress callback
1822
1822
  * @returns Promise resolving to upload response with public URL
1823
1823
  *
1824
1824
  * @example
1825
1825
  * ```ts
1826
+ * // Extension automatically corrected to match actual file type
1826
1827
  * const { publicUrl } = await blink.storage.upload(
1827
- * fileInput.files[0],
1828
- * `avatars/${user.id}.png`,
1829
- * {
1830
- * upsert: true,
1831
- * onProgress: pct => console.log(`${pct}%`)
1832
- * }
1828
+ * pngFile,
1829
+ * `avatars/${user.id}`, // No extension needed!
1830
+ * { upsert: true }
1831
+ * );
1832
+ * // If file is PNG, final path will be: avatars/user123.png
1833
+ *
1834
+ * // Or with extension (will be corrected if wrong)
1835
+ * const { publicUrl } = await blink.storage.upload(
1836
+ * pngFile,
1837
+ * `avatars/${user.id}.jpg`, // Wrong extension
1838
+ * { upsert: true }
1833
1839
  * );
1840
+ * // Final path will be: avatars/user123.png (auto-corrected!)
1834
1841
  * ```
1835
1842
  */
1836
1843
  async upload(file, path, options = {}) {
@@ -1851,10 +1858,12 @@ var BlinkStorageImpl = class {
1851
1858
  if (fileSize > maxSize) {
1852
1859
  throw new BlinkStorageError(`File size (${Math.round(fileSize / 1024 / 1024)}MB) exceeds maximum allowed size (50MB)`);
1853
1860
  }
1861
+ const { correctedPath, detectedContentType } = await this.detectFileTypeAndCorrectPath(file, path);
1854
1862
  const response = await this.httpClient.uploadFile(
1855
1863
  `/api/storage/${this.httpClient.projectId}/upload`,
1856
1864
  file,
1857
- path,
1865
+ correctedPath,
1866
+ // Use corrected path with proper extension
1858
1867
  {
1859
1868
  upsert: options.upsert,
1860
1869
  onProgress: options.onProgress
@@ -1887,6 +1896,200 @@ var BlinkStorageImpl = class {
1887
1896
  );
1888
1897
  }
1889
1898
  }
1899
+ /**
1900
+ * Detect file type from actual file content and correct path extension
1901
+ * This ensures the path extension always matches the actual file type
1902
+ */
1903
+ async detectFileTypeAndCorrectPath(file, originalPath) {
1904
+ try {
1905
+ const fileSignature = await this.getFileSignature(file);
1906
+ const detectedType = this.detectFileTypeFromSignature(fileSignature);
1907
+ let detectedContentType = detectedType.mimeType;
1908
+ let detectedExtension = detectedType.extension;
1909
+ if (!detectedContentType && file instanceof File && file.type) {
1910
+ detectedContentType = file.type;
1911
+ detectedExtension = this.getExtensionFromMimeType(file.type);
1912
+ }
1913
+ if (!detectedContentType) {
1914
+ detectedContentType = "application/octet-stream";
1915
+ detectedExtension = "bin";
1916
+ }
1917
+ const pathParts = originalPath.split("/");
1918
+ const fileName = pathParts[pathParts.length - 1];
1919
+ const directory = pathParts.slice(0, -1).join("/");
1920
+ if (!fileName) {
1921
+ throw new Error("Invalid path: filename cannot be empty");
1922
+ }
1923
+ const nameWithoutExt = fileName.includes(".") ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName;
1924
+ const correctedFileName = `${nameWithoutExt}.${detectedExtension}`;
1925
+ const correctedPath = directory ? `${directory}/${correctedFileName}` : correctedFileName;
1926
+ return {
1927
+ correctedPath,
1928
+ detectedContentType
1929
+ };
1930
+ } catch (error) {
1931
+ console.warn("File type detection failed, using original path:", error);
1932
+ return {
1933
+ correctedPath: originalPath,
1934
+ detectedContentType: "application/octet-stream"
1935
+ };
1936
+ }
1937
+ }
1938
+ /**
1939
+ * Get the first few bytes of a file to analyze its signature
1940
+ */
1941
+ async getFileSignature(file) {
1942
+ const bytesToRead = 12;
1943
+ if (typeof Buffer !== "undefined" && file instanceof Buffer) {
1944
+ return new Uint8Array(file.slice(0, bytesToRead));
1945
+ }
1946
+ if (file instanceof File || file instanceof Blob) {
1947
+ const slice = file.slice(0, bytesToRead);
1948
+ const arrayBuffer = await slice.arrayBuffer();
1949
+ return new Uint8Array(arrayBuffer);
1950
+ }
1951
+ throw new Error("Unsupported file type for signature detection");
1952
+ }
1953
+ /**
1954
+ * Detect file type from file signature (magic numbers)
1955
+ * This is the most reliable way to detect actual file type
1956
+ */
1957
+ detectFileTypeFromSignature(signature) {
1958
+ const hex = Array.from(signature).map((b) => b.toString(16).padStart(2, "0")).join("");
1959
+ const signatures = {
1960
+ // Images
1961
+ "ffd8ff": { mimeType: "image/jpeg", extension: "jpg" },
1962
+ "89504e47": { mimeType: "image/png", extension: "png" },
1963
+ "47494638": { mimeType: "image/gif", extension: "gif" },
1964
+ "52494646": { mimeType: "image/webp", extension: "webp" },
1965
+ // RIFF (WebP container)
1966
+ "424d": { mimeType: "image/bmp", extension: "bmp" },
1967
+ "49492a00": { mimeType: "image/tiff", extension: "tiff" },
1968
+ "4d4d002a": { mimeType: "image/tiff", extension: "tiff" },
1969
+ // Documents
1970
+ "25504446": { mimeType: "application/pdf", extension: "pdf" },
1971
+ "504b0304": { mimeType: "application/zip", extension: "zip" },
1972
+ // Also used by docx, xlsx
1973
+ "d0cf11e0": { mimeType: "application/msword", extension: "doc" },
1974
+ // Audio
1975
+ "494433": { mimeType: "audio/mpeg", extension: "mp3" },
1976
+ "664c6143": { mimeType: "audio/flac", extension: "flac" },
1977
+ "4f676753": { mimeType: "audio/ogg", extension: "ogg" },
1978
+ // Video
1979
+ "000000": { mimeType: "video/mp4", extension: "mp4" },
1980
+ // ftyp box
1981
+ "1a45dfa3": { mimeType: "video/webm", extension: "webm" },
1982
+ // Text
1983
+ "efbbbf": { mimeType: "text/plain", extension: "txt" }
1984
+ // UTF-8 BOM
1985
+ };
1986
+ for (const [sig, type] of Object.entries(signatures)) {
1987
+ if (hex.startsWith(sig)) {
1988
+ return type;
1989
+ }
1990
+ }
1991
+ if (hex.startsWith("52494646") && hex.substring(16, 24) === "57454250") {
1992
+ return { mimeType: "image/webp", extension: "webp" };
1993
+ }
1994
+ if (hex.substring(8, 16) === "66747970") {
1995
+ return { mimeType: "video/mp4", extension: "mp4" };
1996
+ }
1997
+ return { mimeType: "", extension: "" };
1998
+ }
1999
+ /**
2000
+ * Get file extension from MIME type as fallback
2001
+ */
2002
+ getExtensionFromMimeType(mimeType) {
2003
+ const mimeToExt = {
2004
+ "image/jpeg": "jpg",
2005
+ "image/png": "png",
2006
+ "image/gif": "gif",
2007
+ "image/webp": "webp",
2008
+ "image/bmp": "bmp",
2009
+ "image/svg+xml": "svg",
2010
+ "application/pdf": "pdf",
2011
+ "text/plain": "txt",
2012
+ "text/html": "html",
2013
+ "text/css": "css",
2014
+ "application/javascript": "js",
2015
+ "application/json": "json",
2016
+ "audio/mpeg": "mp3",
2017
+ "audio/wav": "wav",
2018
+ "audio/ogg": "ogg",
2019
+ "video/mp4": "mp4",
2020
+ "video/webm": "webm",
2021
+ "application/zip": "zip"
2022
+ };
2023
+ return mimeToExt[mimeType] || "bin";
2024
+ }
2025
+ /**
2026
+ * Get a download URL for a file that triggers browser download
2027
+ *
2028
+ * @param path - Path to the file in project storage
2029
+ * @param options - Download options including custom filename
2030
+ * @returns Promise resolving to download response with download URL
2031
+ *
2032
+ * @example
2033
+ * ```ts
2034
+ * // Download with original filename
2035
+ * const { downloadUrl, filename } = await blink.storage.download('images/photo.jpg');
2036
+ * window.open(downloadUrl, '_blank');
2037
+ *
2038
+ * // Download with custom filename
2039
+ * const { downloadUrl } = await blink.storage.download(
2040
+ * 'images/photo.jpg',
2041
+ * { filename: 'my-photo.jpg' }
2042
+ * );
2043
+ *
2044
+ * // Create download link in React
2045
+ * <a href={downloadUrl} download={filename}>Download Image</a>
2046
+ * ```
2047
+ */
2048
+ async download(path, options = {}) {
2049
+ try {
2050
+ if (!path || typeof path !== "string" || !path.trim()) {
2051
+ throw new BlinkStorageError("Path must be a non-empty string");
2052
+ }
2053
+ const response = await this.httpClient.request(
2054
+ `/api/storage/${this.httpClient.projectId}/download`,
2055
+ {
2056
+ method: "GET",
2057
+ searchParams: {
2058
+ path: path.trim(),
2059
+ ...options.filename && { filename: options.filename }
2060
+ }
2061
+ }
2062
+ );
2063
+ if (response.data?.downloadUrl) {
2064
+ return {
2065
+ downloadUrl: response.data.downloadUrl,
2066
+ filename: response.data.filename || options.filename || path.split("/").pop() || "download",
2067
+ contentType: response.data.contentType,
2068
+ size: response.data.size
2069
+ };
2070
+ } else {
2071
+ throw new BlinkStorageError("Invalid response format: missing downloadUrl");
2072
+ }
2073
+ } catch (error) {
2074
+ if (error instanceof BlinkStorageError) {
2075
+ throw error;
2076
+ }
2077
+ if (error instanceof Error && "status" in error) {
2078
+ const status = error.status;
2079
+ if (status === 404) {
2080
+ throw new BlinkStorageError("File not found", 404);
2081
+ }
2082
+ if (status === 400) {
2083
+ throw new BlinkStorageError("Invalid request parameters", 400);
2084
+ }
2085
+ }
2086
+ throw new BlinkStorageError(
2087
+ `Download failed: ${error instanceof Error ? error.message : "Unknown error"}`,
2088
+ void 0,
2089
+ { originalError: error }
2090
+ );
2091
+ }
2092
+ }
1890
2093
  /**
1891
2094
  * Remove one or more files from project storage
1892
2095
  *
@@ -2354,15 +2557,15 @@ var BlinkAIImpl = class {
2354
2557
  }
2355
2558
  }
2356
2559
  /**
2357
- * Generates images from text descriptions using AI image models.
2560
+ * Generates images from text descriptions using AI.
2358
2561
  *
2359
2562
  * @param options - Object containing:
2360
- * - `prompt`: Text description of the image to generate (required)
2361
- * - `size`: Image dimensions (e.g., "1024x1024", "512x512") - varies by model
2362
- * - `quality`: Image quality ("standard" or "hd")
2563
+ * - `prompt`: Text description of the desired image (required)
2564
+ * - `size`: Image dimensions (default: "1024x1024")
2565
+ * - `quality`: Image quality ("standard" or "hd", default: "standard")
2363
2566
  * - `n`: Number of images to generate (default: 1)
2364
- * - `response_format`: Output format ("url" or "b64_json")
2365
- * - Plus optional model, signal parameters
2567
+ * - `background`: Background handling ("auto", "transparent", "opaque", default: "auto")
2568
+ * - Plus optional signal parameter
2366
2569
  *
2367
2570
  * @example
2368
2571
  * ```ts
@@ -2375,29 +2578,25 @@ var BlinkAIImpl = class {
2375
2578
  * // High-quality image with specific size
2376
2579
  * const { data } = await blink.ai.generateImage({
2377
2580
  * prompt: "A futuristic city skyline with flying cars",
2378
- * size: "1792x1024",
2581
+ * size: "1536x1024",
2379
2582
  * quality: "hd",
2380
- * model: "dall-e-3"
2583
+ * background: "transparent"
2381
2584
  * });
2382
2585
  *
2383
2586
  * // Multiple images
2384
2587
  * const { data } = await blink.ai.generateImage({
2385
2588
  * prompt: "A cute robot mascot for a tech company",
2386
2589
  * n: 3,
2387
- * size: "1024x1024"
2590
+ * size: "1024x1024",
2591
+ * quality: "hd"
2388
2592
  * });
2389
2593
  * data.forEach((img, i) => console.log(`Image ${i+1}:`, img.url));
2390
- *
2391
- * // Base64 format for direct embedding
2392
- * const { data } = await blink.ai.generateImage({
2393
- * prompt: "A minimalist logo design",
2394
- * response_format: "b64_json"
2395
- * });
2396
- * console.log("Base64 data:", data[0].b64_json);
2397
2594
  * ```
2398
2595
  *
2399
2596
  * @returns Promise<ImageGenerationResponse> - Object containing:
2400
- * - `data`: Array of generated images with url or b64_json
2597
+ * - `data`: Array of generated images with URLs
2598
+ * - `created`: Timestamp of generation
2599
+ * - `usage`: Token usage information
2401
2600
  */
2402
2601
  async generateImage(options) {
2403
2602
  try {
@@ -2407,11 +2606,12 @@ var BlinkAIImpl = class {
2407
2606
  const response = await this.httpClient.aiImage(
2408
2607
  options.prompt,
2409
2608
  {
2410
- model: options.model,
2609
+ model: "gpt-image-1",
2411
2610
  size: options.size,
2412
2611
  quality: options.quality,
2413
2612
  n: options.n,
2414
- response_format: options.response_format,
2613
+ background: options.background,
2614
+ response_format: "url",
2415
2615
  signal: options.signal
2416
2616
  }
2417
2617
  );
@@ -2431,10 +2631,8 @@ var BlinkAIImpl = class {
2431
2631
  return { url: item };
2432
2632
  } else if (item.url) {
2433
2633
  return item;
2434
- } else if (item.b64_json) {
2435
- return { b64_json: item.b64_json };
2436
2634
  } else {
2437
- return { url: item };
2635
+ throw new BlinkAIError("Invalid image response format");
2438
2636
  }
2439
2637
  });
2440
2638
  return imageResponse;
@@ -2449,6 +2647,129 @@ var BlinkAIImpl = class {
2449
2647
  );
2450
2648
  }
2451
2649
  }
2650
+ /**
2651
+ * Modifies existing images using AI with text prompts for image-to-image editing.
2652
+ *
2653
+ * @param options - Object containing:
2654
+ * - `images`: Array of public image URLs to modify (required, up to 16 images)
2655
+ * - `prompt`: Text description of desired modifications (required)
2656
+ * - `size`: Output image dimensions (default: "auto")
2657
+ * - `quality`: Image quality ("standard" or "hd", default: "standard")
2658
+ * - `n`: Number of output images to generate (default: 1)
2659
+ * - `background`: Background handling ("auto", "transparent", "opaque", default: "auto")
2660
+ * - Plus optional signal parameter
2661
+ *
2662
+ * @example
2663
+ * ```ts
2664
+ * // Professional headshots from casual photos
2665
+ * const { data } = await blink.ai.modifyImage({
2666
+ * images: [
2667
+ * "https://storage.example.com/user-photo-1.jpg",
2668
+ * "https://storage.example.com/user-photo-2.jpg"
2669
+ * ],
2670
+ * prompt: "Transform into professional business headshots with studio lighting",
2671
+ * quality: "hd",
2672
+ * n: 4
2673
+ * });
2674
+ * data.forEach((img, i) => console.log(`Headshot ${i+1}:`, img.url));
2675
+ *
2676
+ * // Artistic style transformation
2677
+ * const { data } = await blink.ai.modifyImage({
2678
+ * images: ["https://storage.example.com/portrait.jpg"],
2679
+ * prompt: "Transform into oil painting style with dramatic lighting",
2680
+ * quality: "hd",
2681
+ * size: "1024x1024"
2682
+ * });
2683
+ *
2684
+ * // Background replacement
2685
+ * const { data } = await blink.ai.modifyImage({
2686
+ * images: ["https://storage.example.com/product.jpg"],
2687
+ * prompt: "Remove background and place on clean white studio background",
2688
+ * background: "transparent",
2689
+ * n: 2
2690
+ * });
2691
+ *
2692
+ * // Batch processing multiple photos
2693
+ * const userPhotos = [
2694
+ * "https://storage.example.com/photo1.jpg",
2695
+ * "https://storage.example.com/photo2.jpg",
2696
+ * "https://storage.example.com/photo3.jpg"
2697
+ * ];
2698
+ * const { data } = await blink.ai.modifyImage({
2699
+ * images: userPhotos,
2700
+ * prompt: "Convert to black and white vintage style photographs",
2701
+ * quality: "hd"
2702
+ * });
2703
+ * ```
2704
+ *
2705
+ * @returns Promise<ImageGenerationResponse> - Object containing:
2706
+ * - `data`: Array of modified images with URLs
2707
+ * - `created`: Timestamp of generation
2708
+ * - `usage`: Token usage information
2709
+ */
2710
+ async modifyImage(options) {
2711
+ try {
2712
+ if (!options.prompt) {
2713
+ throw new BlinkAIError("Prompt is required");
2714
+ }
2715
+ if (!options.images || !Array.isArray(options.images) || options.images.length === 0) {
2716
+ throw new BlinkAIError("Images array is required and must contain at least one image URL");
2717
+ }
2718
+ if (options.images.length > 16) {
2719
+ throw new BlinkAIError("Maximum 16 images allowed");
2720
+ }
2721
+ for (let i = 0; i < options.images.length; i++) {
2722
+ const validation = this.validateImageUrl(options.images[i]);
2723
+ if (!validation.isValid) {
2724
+ throw new BlinkAIError(`Image ${i + 1}: ${validation.error}`);
2725
+ }
2726
+ }
2727
+ const response = await this.httpClient.aiImage(
2728
+ options.prompt,
2729
+ // Non-null assertion since we validated above
2730
+ {
2731
+ model: "gpt-image-1",
2732
+ images: options.images,
2733
+ size: options.size,
2734
+ quality: options.quality,
2735
+ n: options.n,
2736
+ background: options.background,
2737
+ response_format: "url",
2738
+ signal: options.signal
2739
+ }
2740
+ );
2741
+ let imageResponse;
2742
+ if (response.data?.result?.data) {
2743
+ imageResponse = response.data.result;
2744
+ } else if (response.data?.data) {
2745
+ imageResponse = response.data;
2746
+ } else {
2747
+ throw new BlinkAIError("Invalid response format: missing image data");
2748
+ }
2749
+ if (!Array.isArray(imageResponse.data)) {
2750
+ throw new BlinkAIError("Invalid response format: data should be an array");
2751
+ }
2752
+ imageResponse.data = imageResponse.data.map((item) => {
2753
+ if (typeof item === "string") {
2754
+ return { url: item };
2755
+ } else if (item.url) {
2756
+ return item;
2757
+ } else {
2758
+ throw new BlinkAIError("Invalid image response format");
2759
+ }
2760
+ });
2761
+ return imageResponse;
2762
+ } catch (error) {
2763
+ if (error instanceof BlinkAIError) {
2764
+ throw error;
2765
+ }
2766
+ throw new BlinkAIError(
2767
+ `Image modification failed: ${error instanceof Error ? error.message : "Unknown error"}`,
2768
+ void 0,
2769
+ { originalError: error }
2770
+ );
2771
+ }
2772
+ }
2452
2773
  /**
2453
2774
  * Converts text to speech using AI voice synthesis models.
2454
2775
  *
@@ -2977,6 +3298,15 @@ var BlinkRealtimeChannel = class {
2977
3298
  } catch (err) {
2978
3299
  }
2979
3300
  };
3301
+ const originalTimeout = timeout;
3302
+ const cleanupTimeout = setTimeout(() => {
3303
+ if (this.websocket) {
3304
+ this.websocket.removeEventListener("message", handleResponse);
3305
+ }
3306
+ reject(new BlinkRealtimeError("Message send timeout - no response from server"));
3307
+ }, 1e4);
3308
+ queuedMessage.timeout = cleanupTimeout;
3309
+ clearTimeout(originalTimeout);
2980
3310
  this.websocket.addEventListener("message", handleResponse);
2981
3311
  this.websocket.send(message);
2982
3312
  }
@@ -3393,6 +3723,15 @@ var BlinkAnalyticsImpl = class {
3393
3723
  this.enabled = false;
3394
3724
  this.clearTimer();
3395
3725
  }
3726
+ /**
3727
+ * Cleanup analytics instance (remove from global tracking)
3728
+ */
3729
+ destroy() {
3730
+ this.disable();
3731
+ if (typeof window !== "undefined") {
3732
+ window.__blinkAnalyticsInstances?.delete(this);
3733
+ }
3734
+ }
3396
3735
  /**
3397
3736
  * Enable analytics tracking
3398
3737
  */
@@ -3559,19 +3898,37 @@ var BlinkAnalyticsImpl = class {
3559
3898
  }
3560
3899
  }
3561
3900
  setupRouteChangeListener() {
3562
- const originalPushState = history.pushState;
3563
- const originalReplaceState = history.replaceState;
3564
- history.pushState = (...args) => {
3565
- originalPushState.apply(history, args);
3566
- this.log("pageview");
3567
- };
3568
- history.replaceState = (...args) => {
3569
- originalReplaceState.apply(history, args);
3570
- this.log("pageview");
3571
- };
3572
- window.addEventListener("popstate", () => {
3573
- this.log("pageview");
3574
- });
3901
+ if (!window.__blinkAnalyticsSetup) {
3902
+ const originalPushState = history.pushState;
3903
+ const originalReplaceState = history.replaceState;
3904
+ const analyticsInstances = /* @__PURE__ */ new Set();
3905
+ window.__blinkAnalyticsInstances = analyticsInstances;
3906
+ history.pushState = (...args) => {
3907
+ originalPushState.apply(history, args);
3908
+ analyticsInstances.forEach((instance) => {
3909
+ if (instance.isEnabled()) {
3910
+ instance.log("pageview");
3911
+ }
3912
+ });
3913
+ };
3914
+ history.replaceState = (...args) => {
3915
+ originalReplaceState.apply(history, args);
3916
+ analyticsInstances.forEach((instance) => {
3917
+ if (instance.isEnabled()) {
3918
+ instance.log("pageview");
3919
+ }
3920
+ });
3921
+ };
3922
+ window.addEventListener("popstate", () => {
3923
+ analyticsInstances.forEach((instance) => {
3924
+ if (instance.isEnabled()) {
3925
+ instance.log("pageview");
3926
+ }
3927
+ });
3928
+ });
3929
+ window.__blinkAnalyticsSetup = true;
3930
+ }
3931
+ window.__blinkAnalyticsInstances?.add(this);
3575
3932
  }
3576
3933
  setupUnloadListener() {
3577
3934
  window.addEventListener("pagehide", () => {