@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 +9 -1
- package/dist/index.d.mts +37 -7
- package/dist/index.d.ts +37 -7
- package/dist/index.js +192 -21
- package/dist/index.mjs +192 -21
- package/package.json +1 -1
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
|
|
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
|
-
*
|
|
1152
|
-
* `avatars/${user.id}
|
|
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
|
-
*
|
|
1152
|
-
* `avatars/${user.id}
|
|
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
|
-
*
|
|
1828
|
-
* `avatars/${user.id}
|
|
1829
|
-
* {
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
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
|
-
*
|
|
1826
|
-
* `avatars/${user.id}
|
|
1827
|
-
* {
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
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