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