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