@blinkdotnew/sdk 0.16.0 → 0.17.1

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/README.md CHANGED
@@ -705,12 +705,20 @@ try {
705
705
  // Upload files (returns public URL directly)
706
706
  const { publicUrl } = await blink.storage.upload(
707
707
  file,
708
- 'path/to/file.jpg',
708
+ 'path/to/file',
709
709
  {
710
710
  upsert: true,
711
711
  onProgress: (percent) => console.log(`${percent}%`)
712
712
  }
713
713
  )
714
+ // If file is PNG, final path will be: path/to/file.png
715
+
716
+ // 💡 Pro tip: Use the actual filename (file.name) in your path to avoid confusion
717
+ const { publicUrl } = await blink.storage.upload(
718
+ file,
719
+ `uploads/${file.name}`, // Uses actual filename with correct extension
720
+ { upsert: true }
721
+ )
714
722
 
715
723
  // Remove files
716
724
  await blink.storage.remove('file1.jpg', 'file2.jpg')
package/dist/index.d.mts CHANGED
@@ -625,6 +625,7 @@ declare class HttpClient {
625
625
  uploadFile(path: string, file: File | Blob | Buffer, filePath: string, options?: {
626
626
  upsert?: boolean;
627
627
  onProgress?: (percent: number) => void;
628
+ contentType?: string;
628
629
  }): Promise<BlinkResponse<any>>;
629
630
  /**
630
631
  * Upload with progress tracking using XMLHttpRequest
@@ -1050,6 +1051,7 @@ interface BlinkAnalytics {
1050
1051
  setUserId(userId: string | null): void;
1051
1052
  setUserEmail(email: string | null): void;
1052
1053
  clearAttribution(): void;
1054
+ destroy(): void;
1053
1055
  }
1054
1056
  declare class BlinkAnalyticsImpl implements BlinkAnalytics {
1055
1057
  private httpClient;
@@ -1071,6 +1073,10 @@ declare class BlinkAnalyticsImpl implements BlinkAnalytics {
1071
1073
  * Disable analytics tracking
1072
1074
  */
1073
1075
  disable(): void;
1076
+ /**
1077
+ * Cleanup analytics instance (remove from global tracking)
1078
+ */
1079
+ destroy(): void;
1074
1080
  /**
1075
1081
  * Enable analytics tracking
1076
1082
  */
@@ -1141,23 +1147,48 @@ declare class BlinkStorageImpl implements BlinkStorage {
1141
1147
  * Upload a file to project storage
1142
1148
  *
1143
1149
  * @param file - File, Blob, or Buffer to upload
1144
- * @param path - Destination path within project storage
1150
+ * @param path - Destination path within project storage (extension will be auto-corrected to match file type)
1145
1151
  * @param options - Upload options including upsert and progress callback
1146
1152
  * @returns Promise resolving to upload response with public URL
1147
1153
  *
1148
1154
  * @example
1149
1155
  * ```ts
1156
+ * // Extension automatically corrected to match actual file type
1150
1157
  * const { publicUrl } = await blink.storage.upload(
1151
- * fileInput.files[0],
1152
- * `avatars/${user.id}.png`,
1153
- * {
1154
- * upsert: true,
1155
- * onProgress: pct => console.log(`${pct}%`)
1156
- * }
1158
+ * pngFile,
1159
+ * `avatars/${user.id}`, // No extension needed!
1160
+ * { upsert: true }
1157
1161
  * );
1162
+ * // If file is PNG, final path will be: avatars/user123.png
1163
+ *
1164
+ * // Or with extension (will be corrected if wrong)
1165
+ * const { publicUrl } = await blink.storage.upload(
1166
+ * pngFile,
1167
+ * `avatars/${user.id}.jpg`, // Wrong extension
1168
+ * { upsert: true }
1169
+ * );
1170
+ * // Final path will be: avatars/user123.png (auto-corrected!)
1158
1171
  * ```
1159
1172
  */
1160
1173
  upload(file: File | Blob | Buffer, path: string, options?: StorageUploadOptions): Promise<StorageUploadResponse>;
1174
+ /**
1175
+ * Detect file type from actual file content and correct path extension
1176
+ * This ensures the path extension always matches the actual file type
1177
+ */
1178
+ private detectFileTypeAndCorrectPath;
1179
+ /**
1180
+ * Get the first few bytes of a file to analyze its signature
1181
+ */
1182
+ private getFileSignature;
1183
+ /**
1184
+ * Detect file type from file signature (magic numbers)
1185
+ * This is the most reliable way to detect actual file type
1186
+ */
1187
+ private detectFileTypeFromSignature;
1188
+ /**
1189
+ * Get file extension from MIME type as fallback
1190
+ */
1191
+ private getExtensionFromMimeType;
1161
1192
  /**
1162
1193
  * Get a download URL for a file that triggers browser download
1163
1194
  *
package/dist/index.d.ts CHANGED
@@ -625,6 +625,7 @@ declare class HttpClient {
625
625
  uploadFile(path: string, file: File | Blob | Buffer, filePath: string, options?: {
626
626
  upsert?: boolean;
627
627
  onProgress?: (percent: number) => void;
628
+ contentType?: string;
628
629
  }): Promise<BlinkResponse<any>>;
629
630
  /**
630
631
  * Upload with progress tracking using XMLHttpRequest
@@ -1050,6 +1051,7 @@ interface BlinkAnalytics {
1050
1051
  setUserId(userId: string | null): void;
1051
1052
  setUserEmail(email: string | null): void;
1052
1053
  clearAttribution(): void;
1054
+ destroy(): void;
1053
1055
  }
1054
1056
  declare class BlinkAnalyticsImpl implements BlinkAnalytics {
1055
1057
  private httpClient;
@@ -1071,6 +1073,10 @@ declare class BlinkAnalyticsImpl implements BlinkAnalytics {
1071
1073
  * Disable analytics tracking
1072
1074
  */
1073
1075
  disable(): void;
1076
+ /**
1077
+ * Cleanup analytics instance (remove from global tracking)
1078
+ */
1079
+ destroy(): void;
1074
1080
  /**
1075
1081
  * Enable analytics tracking
1076
1082
  */
@@ -1141,23 +1147,48 @@ declare class BlinkStorageImpl implements BlinkStorage {
1141
1147
  * Upload a file to project storage
1142
1148
  *
1143
1149
  * @param file - File, Blob, or Buffer to upload
1144
- * @param path - Destination path within project storage
1150
+ * @param path - Destination path within project storage (extension will be auto-corrected to match file type)
1145
1151
  * @param options - Upload options including upsert and progress callback
1146
1152
  * @returns Promise resolving to upload response with public URL
1147
1153
  *
1148
1154
  * @example
1149
1155
  * ```ts
1156
+ * // Extension automatically corrected to match actual file type
1150
1157
  * const { publicUrl } = await blink.storage.upload(
1151
- * fileInput.files[0],
1152
- * `avatars/${user.id}.png`,
1153
- * {
1154
- * upsert: true,
1155
- * onProgress: pct => console.log(`${pct}%`)
1156
- * }
1158
+ * pngFile,
1159
+ * `avatars/${user.id}`, // No extension needed!
1160
+ * { upsert: true }
1157
1161
  * );
1162
+ * // If file is PNG, final path will be: avatars/user123.png
1163
+ *
1164
+ * // Or with extension (will be corrected if wrong)
1165
+ * const { publicUrl } = await blink.storage.upload(
1166
+ * pngFile,
1167
+ * `avatars/${user.id}.jpg`, // Wrong extension
1168
+ * { upsert: true }
1169
+ * );
1170
+ * // Final path will be: avatars/user123.png (auto-corrected!)
1158
1171
  * ```
1159
1172
  */
1160
1173
  upload(file: File | Blob | Buffer, path: string, options?: StorageUploadOptions): Promise<StorageUploadResponse>;
1174
+ /**
1175
+ * Detect file type from actual file content and correct path extension
1176
+ * This ensures the path extension always matches the actual file type
1177
+ */
1178
+ private detectFileTypeAndCorrectPath;
1179
+ /**
1180
+ * Get the first few bytes of a file to analyze its signature
1181
+ */
1182
+ private getFileSignature;
1183
+ /**
1184
+ * Detect file type from file signature (magic numbers)
1185
+ * This is the most reliable way to detect actual file type
1186
+ */
1187
+ private detectFileTypeFromSignature;
1188
+ /**
1189
+ * Get file extension from MIME type as fallback
1190
+ */
1191
+ private getExtensionFromMimeType;
1161
1192
  /**
1162
1193
  * Get a download URL for a file that triggers browser download
1163
1194
  *
package/dist/index.js CHANGED
@@ -403,9 +403,10 @@ var HttpClient = class {
403
403
  if (file instanceof File) {
404
404
  formData.append("file", file);
405
405
  } else if (file instanceof Blob) {
406
- formData.append("file", file);
406
+ const blobWithType = options.contentType ? new Blob([file], { type: options.contentType }) : file;
407
+ formData.append("file", blobWithType);
407
408
  } else if (typeof Buffer !== "undefined" && file instanceof Buffer) {
408
- const blob = new Blob([file]);
409
+ const blob = new Blob([file], { type: options.contentType || "application/octet-stream" });
409
410
  formData.append("file", blob);
410
411
  } else {
411
412
  throw new BlinkValidationError("Unsupported file type");
@@ -1817,20 +1818,27 @@ var BlinkStorageImpl = class {
1817
1818
  * Upload a file to project storage
1818
1819
  *
1819
1820
  * @param file - File, Blob, or Buffer to upload
1820
- * @param path - Destination path within project storage
1821
+ * @param path - Destination path within project storage (extension will be auto-corrected to match file type)
1821
1822
  * @param options - Upload options including upsert and progress callback
1822
1823
  * @returns Promise resolving to upload response with public URL
1823
1824
  *
1824
1825
  * @example
1825
1826
  * ```ts
1827
+ * // Extension automatically corrected to match actual file type
1826
1828
  * 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
- * }
1829
+ * pngFile,
1830
+ * `avatars/${user.id}`, // No extension needed!
1831
+ * { upsert: true }
1832
+ * );
1833
+ * // If file is PNG, final path will be: avatars/user123.png
1834
+ *
1835
+ * // Or with extension (will be corrected if wrong)
1836
+ * const { publicUrl } = await blink.storage.upload(
1837
+ * pngFile,
1838
+ * `avatars/${user.id}.jpg`, // Wrong extension
1839
+ * { upsert: true }
1833
1840
  * );
1841
+ * // Final path will be: avatars/user123.png (auto-corrected!)
1834
1842
  * ```
1835
1843
  */
1836
1844
  async upload(file, path, options = {}) {
@@ -1851,13 +1859,17 @@ var BlinkStorageImpl = class {
1851
1859
  if (fileSize > maxSize) {
1852
1860
  throw new BlinkStorageError(`File size (${Math.round(fileSize / 1024 / 1024)}MB) exceeds maximum allowed size (50MB)`);
1853
1861
  }
1862
+ const { correctedPath, detectedContentType } = await this.detectFileTypeAndCorrectPath(file, path);
1854
1863
  const response = await this.httpClient.uploadFile(
1855
1864
  `/api/storage/${this.httpClient.projectId}/upload`,
1856
1865
  file,
1857
- path,
1866
+ correctedPath,
1867
+ // Use corrected path with proper extension
1858
1868
  {
1859
1869
  upsert: options.upsert,
1860
- onProgress: options.onProgress
1870
+ onProgress: options.onProgress,
1871
+ contentType: detectedContentType
1872
+ // Pass detected content type
1861
1873
  }
1862
1874
  );
1863
1875
  if (response.data?.data?.publicUrl) {
@@ -1887,6 +1899,132 @@ var BlinkStorageImpl = class {
1887
1899
  );
1888
1900
  }
1889
1901
  }
1902
+ /**
1903
+ * Detect file type from actual file content and correct path extension
1904
+ * This ensures the path extension always matches the actual file type
1905
+ */
1906
+ async detectFileTypeAndCorrectPath(file, originalPath) {
1907
+ try {
1908
+ const fileSignature = await this.getFileSignature(file);
1909
+ const detectedType = this.detectFileTypeFromSignature(fileSignature);
1910
+ let detectedContentType = detectedType.mimeType;
1911
+ let detectedExtension = detectedType.extension;
1912
+ if (!detectedContentType && file instanceof File && file.type) {
1913
+ detectedContentType = file.type;
1914
+ detectedExtension = this.getExtensionFromMimeType(file.type);
1915
+ }
1916
+ if (!detectedContentType) {
1917
+ detectedContentType = "application/octet-stream";
1918
+ detectedExtension = "bin";
1919
+ }
1920
+ const pathParts = originalPath.split("/");
1921
+ const fileName = pathParts[pathParts.length - 1];
1922
+ const directory = pathParts.slice(0, -1).join("/");
1923
+ if (!fileName) {
1924
+ throw new Error("Invalid path: filename cannot be empty");
1925
+ }
1926
+ const nameWithoutExt = fileName.includes(".") ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName;
1927
+ const correctedFileName = `${nameWithoutExt}.${detectedExtension}`;
1928
+ const correctedPath = directory ? `${directory}/${correctedFileName}` : correctedFileName;
1929
+ return {
1930
+ correctedPath,
1931
+ detectedContentType
1932
+ };
1933
+ } catch (error) {
1934
+ console.warn("File type detection failed, using original path:", error);
1935
+ return {
1936
+ correctedPath: originalPath,
1937
+ detectedContentType: "application/octet-stream"
1938
+ };
1939
+ }
1940
+ }
1941
+ /**
1942
+ * Get the first few bytes of a file to analyze its signature
1943
+ */
1944
+ async getFileSignature(file) {
1945
+ const bytesToRead = 12;
1946
+ if (typeof Buffer !== "undefined" && file instanceof Buffer) {
1947
+ return new Uint8Array(file.slice(0, bytesToRead));
1948
+ }
1949
+ if (file instanceof File || file instanceof Blob) {
1950
+ const slice = file.slice(0, bytesToRead);
1951
+ const arrayBuffer = await slice.arrayBuffer();
1952
+ return new Uint8Array(arrayBuffer);
1953
+ }
1954
+ throw new Error("Unsupported file type for signature detection");
1955
+ }
1956
+ /**
1957
+ * Detect file type from file signature (magic numbers)
1958
+ * This is the most reliable way to detect actual file type
1959
+ */
1960
+ detectFileTypeFromSignature(signature) {
1961
+ const hex = Array.from(signature).map((b) => b.toString(16).padStart(2, "0")).join("");
1962
+ const signatures = {
1963
+ // Images
1964
+ "ffd8ff": { mimeType: "image/jpeg", extension: "jpg" },
1965
+ "89504e47": { mimeType: "image/png", extension: "png" },
1966
+ "47494638": { mimeType: "image/gif", extension: "gif" },
1967
+ "52494646": { mimeType: "image/webp", extension: "webp" },
1968
+ // RIFF (WebP container)
1969
+ "424d": { mimeType: "image/bmp", extension: "bmp" },
1970
+ "49492a00": { mimeType: "image/tiff", extension: "tiff" },
1971
+ "4d4d002a": { mimeType: "image/tiff", extension: "tiff" },
1972
+ // Documents
1973
+ "25504446": { mimeType: "application/pdf", extension: "pdf" },
1974
+ "504b0304": { mimeType: "application/zip", extension: "zip" },
1975
+ // Also used by docx, xlsx
1976
+ "d0cf11e0": { mimeType: "application/msword", extension: "doc" },
1977
+ // Audio
1978
+ "494433": { mimeType: "audio/mpeg", extension: "mp3" },
1979
+ "664c6143": { mimeType: "audio/flac", extension: "flac" },
1980
+ "4f676753": { mimeType: "audio/ogg", extension: "ogg" },
1981
+ // Video
1982
+ "000000": { mimeType: "video/mp4", extension: "mp4" },
1983
+ // ftyp box
1984
+ "1a45dfa3": { mimeType: "video/webm", extension: "webm" },
1985
+ // Text
1986
+ "efbbbf": { mimeType: "text/plain", extension: "txt" }
1987
+ // UTF-8 BOM
1988
+ };
1989
+ for (const [sig, type] of Object.entries(signatures)) {
1990
+ if (hex.startsWith(sig)) {
1991
+ return type;
1992
+ }
1993
+ }
1994
+ if (hex.startsWith("52494646") && hex.substring(16, 24) === "57454250") {
1995
+ return { mimeType: "image/webp", extension: "webp" };
1996
+ }
1997
+ if (hex.substring(8, 16) === "66747970") {
1998
+ return { mimeType: "video/mp4", extension: "mp4" };
1999
+ }
2000
+ return { mimeType: "", extension: "" };
2001
+ }
2002
+ /**
2003
+ * Get file extension from MIME type as fallback
2004
+ */
2005
+ getExtensionFromMimeType(mimeType) {
2006
+ const mimeToExt = {
2007
+ "image/jpeg": "jpg",
2008
+ "image/png": "png",
2009
+ "image/gif": "gif",
2010
+ "image/webp": "webp",
2011
+ "image/bmp": "bmp",
2012
+ "image/svg+xml": "svg",
2013
+ "application/pdf": "pdf",
2014
+ "text/plain": "txt",
2015
+ "text/html": "html",
2016
+ "text/css": "css",
2017
+ "application/javascript": "js",
2018
+ "application/json": "json",
2019
+ "audio/mpeg": "mp3",
2020
+ "audio/wav": "wav",
2021
+ "audio/ogg": "ogg",
2022
+ "video/mp4": "mp4",
2023
+ "video/webm": "webm",
2024
+ "application/zip": "zip"
2025
+ };
2026
+ return mimeToExt[mimeType] || "bin";
2027
+ }
1890
2028
  /**
1891
2029
  * Get a download URL for a file that triggers browser download
1892
2030
  *
@@ -3163,6 +3301,15 @@ var BlinkRealtimeChannel = class {
3163
3301
  } catch (err) {
3164
3302
  }
3165
3303
  };
3304
+ const originalTimeout = timeout;
3305
+ const cleanupTimeout = setTimeout(() => {
3306
+ if (this.websocket) {
3307
+ this.websocket.removeEventListener("message", handleResponse);
3308
+ }
3309
+ reject(new BlinkRealtimeError("Message send timeout - no response from server"));
3310
+ }, 1e4);
3311
+ queuedMessage.timeout = cleanupTimeout;
3312
+ clearTimeout(originalTimeout);
3166
3313
  this.websocket.addEventListener("message", handleResponse);
3167
3314
  this.websocket.send(message);
3168
3315
  }
@@ -3579,6 +3726,15 @@ var BlinkAnalyticsImpl = class {
3579
3726
  this.enabled = false;
3580
3727
  this.clearTimer();
3581
3728
  }
3729
+ /**
3730
+ * Cleanup analytics instance (remove from global tracking)
3731
+ */
3732
+ destroy() {
3733
+ this.disable();
3734
+ if (typeof window !== "undefined") {
3735
+ window.__blinkAnalyticsInstances?.delete(this);
3736
+ }
3737
+ }
3582
3738
  /**
3583
3739
  * Enable analytics tracking
3584
3740
  */
@@ -3745,19 +3901,37 @@ var BlinkAnalyticsImpl = class {
3745
3901
  }
3746
3902
  }
3747
3903
  setupRouteChangeListener() {
3748
- const originalPushState = history.pushState;
3749
- const originalReplaceState = history.replaceState;
3750
- history.pushState = (...args) => {
3751
- originalPushState.apply(history, args);
3752
- this.log("pageview");
3753
- };
3754
- history.replaceState = (...args) => {
3755
- originalReplaceState.apply(history, args);
3756
- this.log("pageview");
3757
- };
3758
- window.addEventListener("popstate", () => {
3759
- this.log("pageview");
3760
- });
3904
+ if (!window.__blinkAnalyticsSetup) {
3905
+ const originalPushState = history.pushState;
3906
+ const originalReplaceState = history.replaceState;
3907
+ const analyticsInstances = /* @__PURE__ */ new Set();
3908
+ window.__blinkAnalyticsInstances = analyticsInstances;
3909
+ history.pushState = (...args) => {
3910
+ originalPushState.apply(history, args);
3911
+ analyticsInstances.forEach((instance) => {
3912
+ if (instance.isEnabled()) {
3913
+ instance.log("pageview");
3914
+ }
3915
+ });
3916
+ };
3917
+ history.replaceState = (...args) => {
3918
+ originalReplaceState.apply(history, args);
3919
+ analyticsInstances.forEach((instance) => {
3920
+ if (instance.isEnabled()) {
3921
+ instance.log("pageview");
3922
+ }
3923
+ });
3924
+ };
3925
+ window.addEventListener("popstate", () => {
3926
+ analyticsInstances.forEach((instance) => {
3927
+ if (instance.isEnabled()) {
3928
+ instance.log("pageview");
3929
+ }
3930
+ });
3931
+ });
3932
+ window.__blinkAnalyticsSetup = true;
3933
+ }
3934
+ window.__blinkAnalyticsInstances?.add(this);
3761
3935
  }
3762
3936
  setupUnloadListener() {
3763
3937
  window.addEventListener("pagehide", () => {
package/dist/index.mjs CHANGED
@@ -401,9 +401,10 @@ var HttpClient = class {
401
401
  if (file instanceof File) {
402
402
  formData.append("file", file);
403
403
  } else if (file instanceof Blob) {
404
- formData.append("file", file);
404
+ const blobWithType = options.contentType ? new Blob([file], { type: options.contentType }) : file;
405
+ formData.append("file", blobWithType);
405
406
  } else if (typeof Buffer !== "undefined" && file instanceof Buffer) {
406
- const blob = new Blob([file]);
407
+ const blob = new Blob([file], { type: options.contentType || "application/octet-stream" });
407
408
  formData.append("file", blob);
408
409
  } else {
409
410
  throw new BlinkValidationError("Unsupported file type");
@@ -1815,20 +1816,27 @@ var BlinkStorageImpl = class {
1815
1816
  * Upload a file to project storage
1816
1817
  *
1817
1818
  * @param file - File, Blob, or Buffer to upload
1818
- * @param path - Destination path within project storage
1819
+ * @param path - Destination path within project storage (extension will be auto-corrected to match file type)
1819
1820
  * @param options - Upload options including upsert and progress callback
1820
1821
  * @returns Promise resolving to upload response with public URL
1821
1822
  *
1822
1823
  * @example
1823
1824
  * ```ts
1825
+ * // Extension automatically corrected to match actual file type
1824
1826
  * 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
- * }
1827
+ * pngFile,
1828
+ * `avatars/${user.id}`, // No extension needed!
1829
+ * { upsert: true }
1830
+ * );
1831
+ * // If file is PNG, final path will be: avatars/user123.png
1832
+ *
1833
+ * // Or with extension (will be corrected if wrong)
1834
+ * const { publicUrl } = await blink.storage.upload(
1835
+ * pngFile,
1836
+ * `avatars/${user.id}.jpg`, // Wrong extension
1837
+ * { upsert: true }
1831
1838
  * );
1839
+ * // Final path will be: avatars/user123.png (auto-corrected!)
1832
1840
  * ```
1833
1841
  */
1834
1842
  async upload(file, path, options = {}) {
@@ -1849,13 +1857,17 @@ var BlinkStorageImpl = class {
1849
1857
  if (fileSize > maxSize) {
1850
1858
  throw new BlinkStorageError(`File size (${Math.round(fileSize / 1024 / 1024)}MB) exceeds maximum allowed size (50MB)`);
1851
1859
  }
1860
+ const { correctedPath, detectedContentType } = await this.detectFileTypeAndCorrectPath(file, path);
1852
1861
  const response = await this.httpClient.uploadFile(
1853
1862
  `/api/storage/${this.httpClient.projectId}/upload`,
1854
1863
  file,
1855
- path,
1864
+ correctedPath,
1865
+ // Use corrected path with proper extension
1856
1866
  {
1857
1867
  upsert: options.upsert,
1858
- onProgress: options.onProgress
1868
+ onProgress: options.onProgress,
1869
+ contentType: detectedContentType
1870
+ // Pass detected content type
1859
1871
  }
1860
1872
  );
1861
1873
  if (response.data?.data?.publicUrl) {
@@ -1885,6 +1897,132 @@ var BlinkStorageImpl = class {
1885
1897
  );
1886
1898
  }
1887
1899
  }
1900
+ /**
1901
+ * Detect file type from actual file content and correct path extension
1902
+ * This ensures the path extension always matches the actual file type
1903
+ */
1904
+ async detectFileTypeAndCorrectPath(file, originalPath) {
1905
+ try {
1906
+ const fileSignature = await this.getFileSignature(file);
1907
+ const detectedType = this.detectFileTypeFromSignature(fileSignature);
1908
+ let detectedContentType = detectedType.mimeType;
1909
+ let detectedExtension = detectedType.extension;
1910
+ if (!detectedContentType && file instanceof File && file.type) {
1911
+ detectedContentType = file.type;
1912
+ detectedExtension = this.getExtensionFromMimeType(file.type);
1913
+ }
1914
+ if (!detectedContentType) {
1915
+ detectedContentType = "application/octet-stream";
1916
+ detectedExtension = "bin";
1917
+ }
1918
+ const pathParts = originalPath.split("/");
1919
+ const fileName = pathParts[pathParts.length - 1];
1920
+ const directory = pathParts.slice(0, -1).join("/");
1921
+ if (!fileName) {
1922
+ throw new Error("Invalid path: filename cannot be empty");
1923
+ }
1924
+ const nameWithoutExt = fileName.includes(".") ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName;
1925
+ const correctedFileName = `${nameWithoutExt}.${detectedExtension}`;
1926
+ const correctedPath = directory ? `${directory}/${correctedFileName}` : correctedFileName;
1927
+ return {
1928
+ correctedPath,
1929
+ detectedContentType
1930
+ };
1931
+ } catch (error) {
1932
+ console.warn("File type detection failed, using original path:", error);
1933
+ return {
1934
+ correctedPath: originalPath,
1935
+ detectedContentType: "application/octet-stream"
1936
+ };
1937
+ }
1938
+ }
1939
+ /**
1940
+ * Get the first few bytes of a file to analyze its signature
1941
+ */
1942
+ async getFileSignature(file) {
1943
+ const bytesToRead = 12;
1944
+ if (typeof Buffer !== "undefined" && file instanceof Buffer) {
1945
+ return new Uint8Array(file.slice(0, bytesToRead));
1946
+ }
1947
+ if (file instanceof File || file instanceof Blob) {
1948
+ const slice = file.slice(0, bytesToRead);
1949
+ const arrayBuffer = await slice.arrayBuffer();
1950
+ return new Uint8Array(arrayBuffer);
1951
+ }
1952
+ throw new Error("Unsupported file type for signature detection");
1953
+ }
1954
+ /**
1955
+ * Detect file type from file signature (magic numbers)
1956
+ * This is the most reliable way to detect actual file type
1957
+ */
1958
+ detectFileTypeFromSignature(signature) {
1959
+ const hex = Array.from(signature).map((b) => b.toString(16).padStart(2, "0")).join("");
1960
+ const signatures = {
1961
+ // Images
1962
+ "ffd8ff": { mimeType: "image/jpeg", extension: "jpg" },
1963
+ "89504e47": { mimeType: "image/png", extension: "png" },
1964
+ "47494638": { mimeType: "image/gif", extension: "gif" },
1965
+ "52494646": { mimeType: "image/webp", extension: "webp" },
1966
+ // RIFF (WebP container)
1967
+ "424d": { mimeType: "image/bmp", extension: "bmp" },
1968
+ "49492a00": { mimeType: "image/tiff", extension: "tiff" },
1969
+ "4d4d002a": { mimeType: "image/tiff", extension: "tiff" },
1970
+ // Documents
1971
+ "25504446": { mimeType: "application/pdf", extension: "pdf" },
1972
+ "504b0304": { mimeType: "application/zip", extension: "zip" },
1973
+ // Also used by docx, xlsx
1974
+ "d0cf11e0": { mimeType: "application/msword", extension: "doc" },
1975
+ // Audio
1976
+ "494433": { mimeType: "audio/mpeg", extension: "mp3" },
1977
+ "664c6143": { mimeType: "audio/flac", extension: "flac" },
1978
+ "4f676753": { mimeType: "audio/ogg", extension: "ogg" },
1979
+ // Video
1980
+ "000000": { mimeType: "video/mp4", extension: "mp4" },
1981
+ // ftyp box
1982
+ "1a45dfa3": { mimeType: "video/webm", extension: "webm" },
1983
+ // Text
1984
+ "efbbbf": { mimeType: "text/plain", extension: "txt" }
1985
+ // UTF-8 BOM
1986
+ };
1987
+ for (const [sig, type] of Object.entries(signatures)) {
1988
+ if (hex.startsWith(sig)) {
1989
+ return type;
1990
+ }
1991
+ }
1992
+ if (hex.startsWith("52494646") && hex.substring(16, 24) === "57454250") {
1993
+ return { mimeType: "image/webp", extension: "webp" };
1994
+ }
1995
+ if (hex.substring(8, 16) === "66747970") {
1996
+ return { mimeType: "video/mp4", extension: "mp4" };
1997
+ }
1998
+ return { mimeType: "", extension: "" };
1999
+ }
2000
+ /**
2001
+ * Get file extension from MIME type as fallback
2002
+ */
2003
+ getExtensionFromMimeType(mimeType) {
2004
+ const mimeToExt = {
2005
+ "image/jpeg": "jpg",
2006
+ "image/png": "png",
2007
+ "image/gif": "gif",
2008
+ "image/webp": "webp",
2009
+ "image/bmp": "bmp",
2010
+ "image/svg+xml": "svg",
2011
+ "application/pdf": "pdf",
2012
+ "text/plain": "txt",
2013
+ "text/html": "html",
2014
+ "text/css": "css",
2015
+ "application/javascript": "js",
2016
+ "application/json": "json",
2017
+ "audio/mpeg": "mp3",
2018
+ "audio/wav": "wav",
2019
+ "audio/ogg": "ogg",
2020
+ "video/mp4": "mp4",
2021
+ "video/webm": "webm",
2022
+ "application/zip": "zip"
2023
+ };
2024
+ return mimeToExt[mimeType] || "bin";
2025
+ }
1888
2026
  /**
1889
2027
  * Get a download URL for a file that triggers browser download
1890
2028
  *
@@ -3161,6 +3299,15 @@ var BlinkRealtimeChannel = class {
3161
3299
  } catch (err) {
3162
3300
  }
3163
3301
  };
3302
+ const originalTimeout = timeout;
3303
+ const cleanupTimeout = setTimeout(() => {
3304
+ if (this.websocket) {
3305
+ this.websocket.removeEventListener("message", handleResponse);
3306
+ }
3307
+ reject(new BlinkRealtimeError("Message send timeout - no response from server"));
3308
+ }, 1e4);
3309
+ queuedMessage.timeout = cleanupTimeout;
3310
+ clearTimeout(originalTimeout);
3164
3311
  this.websocket.addEventListener("message", handleResponse);
3165
3312
  this.websocket.send(message);
3166
3313
  }
@@ -3577,6 +3724,15 @@ var BlinkAnalyticsImpl = class {
3577
3724
  this.enabled = false;
3578
3725
  this.clearTimer();
3579
3726
  }
3727
+ /**
3728
+ * Cleanup analytics instance (remove from global tracking)
3729
+ */
3730
+ destroy() {
3731
+ this.disable();
3732
+ if (typeof window !== "undefined") {
3733
+ window.__blinkAnalyticsInstances?.delete(this);
3734
+ }
3735
+ }
3580
3736
  /**
3581
3737
  * Enable analytics tracking
3582
3738
  */
@@ -3743,19 +3899,37 @@ var BlinkAnalyticsImpl = class {
3743
3899
  }
3744
3900
  }
3745
3901
  setupRouteChangeListener() {
3746
- const originalPushState = history.pushState;
3747
- const originalReplaceState = history.replaceState;
3748
- history.pushState = (...args) => {
3749
- originalPushState.apply(history, args);
3750
- this.log("pageview");
3751
- };
3752
- history.replaceState = (...args) => {
3753
- originalReplaceState.apply(history, args);
3754
- this.log("pageview");
3755
- };
3756
- window.addEventListener("popstate", () => {
3757
- this.log("pageview");
3758
- });
3902
+ if (!window.__blinkAnalyticsSetup) {
3903
+ const originalPushState = history.pushState;
3904
+ const originalReplaceState = history.replaceState;
3905
+ const analyticsInstances = /* @__PURE__ */ new Set();
3906
+ window.__blinkAnalyticsInstances = analyticsInstances;
3907
+ history.pushState = (...args) => {
3908
+ originalPushState.apply(history, args);
3909
+ analyticsInstances.forEach((instance) => {
3910
+ if (instance.isEnabled()) {
3911
+ instance.log("pageview");
3912
+ }
3913
+ });
3914
+ };
3915
+ history.replaceState = (...args) => {
3916
+ originalReplaceState.apply(history, args);
3917
+ analyticsInstances.forEach((instance) => {
3918
+ if (instance.isEnabled()) {
3919
+ instance.log("pageview");
3920
+ }
3921
+ });
3922
+ };
3923
+ window.addEventListener("popstate", () => {
3924
+ analyticsInstances.forEach((instance) => {
3925
+ if (instance.isEnabled()) {
3926
+ instance.log("pageview");
3927
+ }
3928
+ });
3929
+ });
3930
+ window.__blinkAnalyticsSetup = true;
3931
+ }
3932
+ window.__blinkAnalyticsInstances?.add(this);
3759
3933
  }
3760
3934
  setupUnloadListener() {
3761
3935
  window.addEventListener("pagehide", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/sdk",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
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",