@blinkdotnew/sdk 0.16.0 → 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/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
@@ -1050,6 +1050,7 @@ interface BlinkAnalytics {
1050
1050
  setUserId(userId: string | null): void;
1051
1051
  setUserEmail(email: string | null): void;
1052
1052
  clearAttribution(): void;
1053
+ destroy(): void;
1053
1054
  }
1054
1055
  declare class BlinkAnalyticsImpl implements BlinkAnalytics {
1055
1056
  private httpClient;
@@ -1071,6 +1072,10 @@ declare class BlinkAnalyticsImpl implements BlinkAnalytics {
1071
1072
  * Disable analytics tracking
1072
1073
  */
1073
1074
  disable(): void;
1075
+ /**
1076
+ * Cleanup analytics instance (remove from global tracking)
1077
+ */
1078
+ destroy(): void;
1074
1079
  /**
1075
1080
  * Enable analytics tracking
1076
1081
  */
@@ -1141,23 +1146,48 @@ declare class BlinkStorageImpl implements BlinkStorage {
1141
1146
  * Upload a file to project storage
1142
1147
  *
1143
1148
  * @param file - File, Blob, or Buffer to upload
1144
- * @param path - Destination path within project storage
1149
+ * @param path - Destination path within project storage (extension will be auto-corrected to match file type)
1145
1150
  * @param options - Upload options including upsert and progress callback
1146
1151
  * @returns Promise resolving to upload response with public URL
1147
1152
  *
1148
1153
  * @example
1149
1154
  * ```ts
1155
+ * // Extension automatically corrected to match actual file type
1150
1156
  * 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
- * }
1157
+ * pngFile,
1158
+ * `avatars/${user.id}`, // No extension needed!
1159
+ * { upsert: true }
1157
1160
  * );
1161
+ * // If file is PNG, final path will be: avatars/user123.png
1162
+ *
1163
+ * // Or with extension (will be corrected if wrong)
1164
+ * const { publicUrl } = await blink.storage.upload(
1165
+ * pngFile,
1166
+ * `avatars/${user.id}.jpg`, // Wrong extension
1167
+ * { upsert: true }
1168
+ * );
1169
+ * // Final path will be: avatars/user123.png (auto-corrected!)
1158
1170
  * ```
1159
1171
  */
1160
1172
  upload(file: File | Blob | Buffer, path: string, options?: StorageUploadOptions): Promise<StorageUploadResponse>;
1173
+ /**
1174
+ * Detect file type from actual file content and correct path extension
1175
+ * This ensures the path extension always matches the actual file type
1176
+ */
1177
+ private detectFileTypeAndCorrectPath;
1178
+ /**
1179
+ * Get the first few bytes of a file to analyze its signature
1180
+ */
1181
+ private getFileSignature;
1182
+ /**
1183
+ * Detect file type from file signature (magic numbers)
1184
+ * This is the most reliable way to detect actual file type
1185
+ */
1186
+ private detectFileTypeFromSignature;
1187
+ /**
1188
+ * Get file extension from MIME type as fallback
1189
+ */
1190
+ private getExtensionFromMimeType;
1161
1191
  /**
1162
1192
  * Get a download URL for a file that triggers browser download
1163
1193
  *
package/dist/index.d.ts CHANGED
@@ -1050,6 +1050,7 @@ interface BlinkAnalytics {
1050
1050
  setUserId(userId: string | null): void;
1051
1051
  setUserEmail(email: string | null): void;
1052
1052
  clearAttribution(): void;
1053
+ destroy(): void;
1053
1054
  }
1054
1055
  declare class BlinkAnalyticsImpl implements BlinkAnalytics {
1055
1056
  private httpClient;
@@ -1071,6 +1072,10 @@ declare class BlinkAnalyticsImpl implements BlinkAnalytics {
1071
1072
  * Disable analytics tracking
1072
1073
  */
1073
1074
  disable(): void;
1075
+ /**
1076
+ * Cleanup analytics instance (remove from global tracking)
1077
+ */
1078
+ destroy(): void;
1074
1079
  /**
1075
1080
  * Enable analytics tracking
1076
1081
  */
@@ -1141,23 +1146,48 @@ declare class BlinkStorageImpl implements BlinkStorage {
1141
1146
  * Upload a file to project storage
1142
1147
  *
1143
1148
  * @param file - File, Blob, or Buffer to upload
1144
- * @param path - Destination path within project storage
1149
+ * @param path - Destination path within project storage (extension will be auto-corrected to match file type)
1145
1150
  * @param options - Upload options including upsert and progress callback
1146
1151
  * @returns Promise resolving to upload response with public URL
1147
1152
  *
1148
1153
  * @example
1149
1154
  * ```ts
1155
+ * // Extension automatically corrected to match actual file type
1150
1156
  * 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
- * }
1157
+ * pngFile,
1158
+ * `avatars/${user.id}`, // No extension needed!
1159
+ * { upsert: true }
1157
1160
  * );
1161
+ * // If file is PNG, final path will be: avatars/user123.png
1162
+ *
1163
+ * // Or with extension (will be corrected if wrong)
1164
+ * const { publicUrl } = await blink.storage.upload(
1165
+ * pngFile,
1166
+ * `avatars/${user.id}.jpg`, // Wrong extension
1167
+ * { upsert: true }
1168
+ * );
1169
+ * // Final path will be: avatars/user123.png (auto-corrected!)
1158
1170
  * ```
1159
1171
  */
1160
1172
  upload(file: File | Blob | Buffer, path: string, options?: StorageUploadOptions): Promise<StorageUploadResponse>;
1173
+ /**
1174
+ * Detect file type from actual file content and correct path extension
1175
+ * This ensures the path extension always matches the actual file type
1176
+ */
1177
+ private detectFileTypeAndCorrectPath;
1178
+ /**
1179
+ * Get the first few bytes of a file to analyze its signature
1180
+ */
1181
+ private getFileSignature;
1182
+ /**
1183
+ * Detect file type from file signature (magic numbers)
1184
+ * This is the most reliable way to detect actual file type
1185
+ */
1186
+ private detectFileTypeFromSignature;
1187
+ /**
1188
+ * Get file extension from MIME type as fallback
1189
+ */
1190
+ private getExtensionFromMimeType;
1161
1191
  /**
1162
1192
  * Get a download URL for a file that triggers browser download
1163
1193
  *
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,132 @@ 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
+ }
1890
2025
  /**
1891
2026
  * Get a download URL for a file that triggers browser download
1892
2027
  *
@@ -3163,6 +3298,15 @@ var BlinkRealtimeChannel = class {
3163
3298
  } catch (err) {
3164
3299
  }
3165
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);
3166
3310
  this.websocket.addEventListener("message", handleResponse);
3167
3311
  this.websocket.send(message);
3168
3312
  }
@@ -3579,6 +3723,15 @@ var BlinkAnalyticsImpl = class {
3579
3723
  this.enabled = false;
3580
3724
  this.clearTimer();
3581
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
+ }
3582
3735
  /**
3583
3736
  * Enable analytics tracking
3584
3737
  */
@@ -3745,19 +3898,37 @@ var BlinkAnalyticsImpl = class {
3745
3898
  }
3746
3899
  }
3747
3900
  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
- });
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);
3761
3932
  }
3762
3933
  setupUnloadListener() {
3763
3934
  window.addEventListener("pagehide", () => {
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,132 @@ 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
+ }
1888
2023
  /**
1889
2024
  * Get a download URL for a file that triggers browser download
1890
2025
  *
@@ -3161,6 +3296,15 @@ var BlinkRealtimeChannel = class {
3161
3296
  } catch (err) {
3162
3297
  }
3163
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);
3164
3308
  this.websocket.addEventListener("message", handleResponse);
3165
3309
  this.websocket.send(message);
3166
3310
  }
@@ -3577,6 +3721,15 @@ var BlinkAnalyticsImpl = class {
3577
3721
  this.enabled = false;
3578
3722
  this.clearTimer();
3579
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
+ }
3580
3733
  /**
3581
3734
  * Enable analytics tracking
3582
3735
  */
@@ -3743,19 +3896,37 @@ var BlinkAnalyticsImpl = class {
3743
3896
  }
3744
3897
  }
3745
3898
  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
- });
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);
3759
3930
  }
3760
3931
  setupUnloadListener() {
3761
3932
  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.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",