@alibarbar/common 1.0.10 → 1.1.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/dist/algorithm.cjs +1 -1
- package/dist/algorithm.js +1 -1
- package/dist/array.cjs +1 -1
- package/dist/array.js +1 -1
- package/dist/color.cjs +1 -1
- package/dist/color.js +1 -1
- package/dist/crypto.cjs +109 -1
- package/dist/crypto.d.mts +48 -1
- package/dist/crypto.d.ts +48 -1
- package/dist/crypto.js +104 -2
- package/dist/data-structure.cjs +1 -1
- package/dist/data-structure.js +1 -1
- package/dist/date.cjs +1 -1
- package/dist/date.js +1 -1
- package/dist/dom.cjs +1 -1
- package/dist/dom.js +1 -1
- package/dist/file.cjs +1 -1
- package/dist/file.js +1 -1
- package/dist/i18n.cjs +1 -1
- package/dist/i18n.js +1 -1
- package/dist/index.cjs +848 -415
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +838 -413
- package/dist/network.cjs +1 -1
- package/dist/network.js +1 -1
- package/dist/number.cjs +1 -1
- package/dist/number.js +1 -1
- package/dist/object.cjs +1 -1
- package/dist/object.js +1 -1
- package/dist/performance.cjs +1 -1
- package/dist/performance.js +1 -1
- package/dist/storage.cjs +509 -104
- package/dist/storage.d.mts +50 -73
- package/dist/storage.d.ts +50 -73
- package/dist/storage.js +505 -102
- package/dist/string.cjs +1 -1
- package/dist/string.js +1 -1
- package/dist/tracking.cjs +1 -1
- package/dist/tracking.js +1 -1
- package/dist/transform.cjs +1 -1
- package/dist/transform.js +1 -1
- package/dist/upload.cjs +2 -2
- package/dist/upload.d.mts +1 -1
- package/dist/upload.d.ts +1 -1
- package/dist/upload.js +2 -2
- package/dist/url.cjs +1 -1
- package/dist/url.js +1 -1
- package/dist/validation.cjs +1 -1
- package/dist/validation.js +1 -1
- package/package.json +3 -1
- /package/dist/{upload-DchqyDBQ.d.mts → index-DchqyDBQ.d.mts} +0 -0
- /package/dist/{upload-DchqyDBQ.d.ts → index-DchqyDBQ.d.ts} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -8,209 +8,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
8
8
|
var Qs__default = /*#__PURE__*/_interopDefault(Qs);
|
|
9
9
|
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
-
var __esm = (fn, res) => function __init() {
|
|
14
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
|
-
};
|
|
16
|
-
var __export = (target, all) => {
|
|
17
|
-
for (var name in all)
|
|
18
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// src/helper/crypto.ts
|
|
22
|
-
var crypto_exports = {};
|
|
23
|
-
__export(crypto_exports, {
|
|
24
|
-
base64Decode: () => base64Decode,
|
|
25
|
-
base64Encode: () => base64Encode,
|
|
26
|
-
exportPrivateKey: () => exportPrivateKey,
|
|
27
|
-
exportPublicKey: () => exportPublicKey,
|
|
28
|
-
generateRSAKeyPair: () => generateRSAKeyPair,
|
|
29
|
-
generateRandomString: () => generateRandomString,
|
|
30
|
-
generateUUID: () => generateUUID,
|
|
31
|
-
hash: () => hash,
|
|
32
|
-
importPrivateKey: () => importPrivateKey,
|
|
33
|
-
importPublicKey: () => importPublicKey,
|
|
34
|
-
rsaDecrypt: () => rsaDecrypt,
|
|
35
|
-
rsaEncrypt: () => rsaEncrypt,
|
|
36
|
-
sha256: () => sha256
|
|
37
|
-
});
|
|
38
|
-
async function sha256(data) {
|
|
39
|
-
const buffer = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
40
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
|
|
41
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
42
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
43
|
-
}
|
|
44
|
-
function base64Encode(data) {
|
|
45
|
-
if (typeof data === "string") {
|
|
46
|
-
return btoa(unescape(encodeURIComponent(data)));
|
|
47
|
-
}
|
|
48
|
-
const bytes = new Uint8Array(data);
|
|
49
|
-
let binary = "";
|
|
50
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
51
|
-
binary += String.fromCharCode(bytes[i]);
|
|
52
|
-
}
|
|
53
|
-
return btoa(binary);
|
|
54
|
-
}
|
|
55
|
-
function base64Decode(data) {
|
|
56
|
-
try {
|
|
57
|
-
return decodeURIComponent(escape(atob(data)));
|
|
58
|
-
} catch {
|
|
59
|
-
throw new Error("Invalid Base64 string");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function generateUUID() {
|
|
63
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
64
|
-
return crypto.randomUUID();
|
|
65
|
-
}
|
|
66
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
67
|
-
const r = Math.random() * 16 | 0;
|
|
68
|
-
const v = c === "x" ? r : r & 3 | 8;
|
|
69
|
-
return v.toString(16);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
function generateRandomString(length, charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") {
|
|
73
|
-
let result = "";
|
|
74
|
-
for (let i = 0; i < length; i++) {
|
|
75
|
-
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
76
|
-
}
|
|
77
|
-
return result;
|
|
78
|
-
}
|
|
79
|
-
function hash(data) {
|
|
80
|
-
let hashValue = 0;
|
|
81
|
-
for (let i = 0; i < data.length; i++) {
|
|
82
|
-
const char = data.charCodeAt(i);
|
|
83
|
-
hashValue = (hashValue << 5) - hashValue + char;
|
|
84
|
-
hashValue = hashValue & hashValue;
|
|
85
|
-
}
|
|
86
|
-
return Math.abs(hashValue);
|
|
87
|
-
}
|
|
88
|
-
async function generateRSAKeyPair(modulusLength = 2048) {
|
|
89
|
-
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
90
|
-
throw new Error("Web Crypto API is not available");
|
|
91
|
-
}
|
|
92
|
-
const keyPair = await crypto.subtle.generateKey(
|
|
93
|
-
{
|
|
94
|
-
name: "RSA-OAEP",
|
|
95
|
-
modulusLength,
|
|
96
|
-
publicExponent: new Uint8Array([1, 0, 1]),
|
|
97
|
-
hash: "SHA-256"
|
|
98
|
-
},
|
|
99
|
-
true,
|
|
100
|
-
["encrypt", "decrypt"]
|
|
101
|
-
);
|
|
102
|
-
return {
|
|
103
|
-
publicKey: keyPair.publicKey,
|
|
104
|
-
privateKey: keyPair.privateKey
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
async function rsaEncrypt(data, publicKey) {
|
|
108
|
-
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
109
|
-
throw new Error("Web Crypto API is not available");
|
|
110
|
-
}
|
|
111
|
-
const encoder = new TextEncoder();
|
|
112
|
-
const dataBuffer = encoder.encode(data);
|
|
113
|
-
const maxChunkSize = 245;
|
|
114
|
-
const chunks = [];
|
|
115
|
-
for (let i = 0; i < dataBuffer.length; i += maxChunkSize) {
|
|
116
|
-
const chunk2 = dataBuffer.slice(i, i + maxChunkSize);
|
|
117
|
-
const encrypted = await crypto.subtle.encrypt(
|
|
118
|
-
{
|
|
119
|
-
name: "RSA-OAEP"
|
|
120
|
-
},
|
|
121
|
-
publicKey,
|
|
122
|
-
chunk2
|
|
123
|
-
);
|
|
124
|
-
chunks.push(encrypted);
|
|
125
|
-
}
|
|
126
|
-
const totalLength = chunks.reduce((sum, chunk2) => sum + chunk2.byteLength, 0);
|
|
127
|
-
const merged = new Uint8Array(totalLength);
|
|
128
|
-
let offset = 0;
|
|
129
|
-
for (const chunk2 of chunks) {
|
|
130
|
-
merged.set(new Uint8Array(chunk2), offset);
|
|
131
|
-
offset += chunk2.byteLength;
|
|
132
|
-
}
|
|
133
|
-
return base64Encode(merged.buffer);
|
|
134
|
-
}
|
|
135
|
-
async function rsaDecrypt(encryptedData, privateKey) {
|
|
136
|
-
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
137
|
-
throw new Error("Web Crypto API is not available");
|
|
138
|
-
}
|
|
139
|
-
const binaryString = atob(encryptedData);
|
|
140
|
-
const encryptedArray = new Uint8Array(binaryString.length);
|
|
141
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
142
|
-
encryptedArray[i] = binaryString.charCodeAt(i);
|
|
143
|
-
}
|
|
144
|
-
const chunkSize = 256;
|
|
145
|
-
const chunks = [];
|
|
146
|
-
for (let i = 0; i < encryptedArray.length; i += chunkSize) {
|
|
147
|
-
const chunk2 = encryptedArray.slice(i, i + chunkSize);
|
|
148
|
-
const decrypted = await crypto.subtle.decrypt(
|
|
149
|
-
{
|
|
150
|
-
name: "RSA-OAEP"
|
|
151
|
-
},
|
|
152
|
-
privateKey,
|
|
153
|
-
chunk2
|
|
154
|
-
);
|
|
155
|
-
const decoder = new TextDecoder();
|
|
156
|
-
chunks.push(decoder.decode(decrypted));
|
|
157
|
-
}
|
|
158
|
-
return chunks.join("");
|
|
159
|
-
}
|
|
160
|
-
async function exportPublicKey(publicKey) {
|
|
161
|
-
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
162
|
-
throw new Error("Web Crypto API is not available");
|
|
163
|
-
}
|
|
164
|
-
const exported = await crypto.subtle.exportKey("spki", publicKey);
|
|
165
|
-
return base64Encode(exported);
|
|
166
|
-
}
|
|
167
|
-
async function exportPrivateKey(privateKey) {
|
|
168
|
-
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
169
|
-
throw new Error("Web Crypto API is not available");
|
|
170
|
-
}
|
|
171
|
-
const exported = await crypto.subtle.exportKey("pkcs8", privateKey);
|
|
172
|
-
return base64Encode(exported);
|
|
173
|
-
}
|
|
174
|
-
async function importPublicKey(keyData) {
|
|
175
|
-
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
176
|
-
throw new Error("Web Crypto API is not available");
|
|
177
|
-
}
|
|
178
|
-
const keyBuffer = base64Decode(keyData);
|
|
179
|
-
const keyArray = new Uint8Array(keyBuffer.split("").map((char) => char.charCodeAt(0)));
|
|
180
|
-
return crypto.subtle.importKey(
|
|
181
|
-
"spki",
|
|
182
|
-
keyArray.buffer,
|
|
183
|
-
{
|
|
184
|
-
name: "RSA-OAEP",
|
|
185
|
-
hash: "SHA-256"
|
|
186
|
-
},
|
|
187
|
-
true,
|
|
188
|
-
["encrypt"]
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
async function importPrivateKey(keyData) {
|
|
192
|
-
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
193
|
-
throw new Error("Web Crypto API is not available");
|
|
194
|
-
}
|
|
195
|
-
const keyBuffer = base64Decode(keyData);
|
|
196
|
-
const keyArray = new Uint8Array(keyBuffer.split("").map((char) => char.charCodeAt(0)));
|
|
197
|
-
return crypto.subtle.importKey(
|
|
198
|
-
"pkcs8",
|
|
199
|
-
keyArray.buffer,
|
|
200
|
-
{
|
|
201
|
-
name: "RSA-OAEP",
|
|
202
|
-
hash: "SHA-256"
|
|
203
|
-
},
|
|
204
|
-
true,
|
|
205
|
-
["decrypt"]
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
var init_crypto = __esm({
|
|
209
|
-
"src/helper/crypto.ts"() {
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// src/core/string.ts
|
|
11
|
+
// src/core/string/index.ts
|
|
214
12
|
function capitalize(str) {
|
|
215
13
|
if (!str) return str;
|
|
216
14
|
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
@@ -301,7 +99,7 @@ function escapeRegex(str) {
|
|
|
301
99
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
302
100
|
}
|
|
303
101
|
|
|
304
|
-
// src/core/array.ts
|
|
102
|
+
// src/core/array/index.ts
|
|
305
103
|
function unique(arr) {
|
|
306
104
|
return Array.from(new Set(arr));
|
|
307
105
|
}
|
|
@@ -436,7 +234,7 @@ function dropWhile(arr, predicate) {
|
|
|
436
234
|
return arr.slice(index);
|
|
437
235
|
}
|
|
438
236
|
|
|
439
|
-
// src/core/object.ts
|
|
237
|
+
// src/core/object/index.ts
|
|
440
238
|
function deepClone(obj) {
|
|
441
239
|
if (obj === null || typeof obj !== "object") {
|
|
442
240
|
return obj;
|
|
@@ -673,7 +471,7 @@ function omitBy(obj, predicate) {
|
|
|
673
471
|
return result;
|
|
674
472
|
}
|
|
675
473
|
|
|
676
|
-
// src/core/date.ts
|
|
474
|
+
// src/core/date/index.ts
|
|
677
475
|
function formatDate(date, format = "YYYY-MM-DD HH:mm:ss") {
|
|
678
476
|
const d = typeof date === "number" ? new Date(date) : date;
|
|
679
477
|
const year = d.getFullYear();
|
|
@@ -798,7 +596,7 @@ function getQuarter(date) {
|
|
|
798
596
|
return Math.floor(d.getMonth() / 3) + 1;
|
|
799
597
|
}
|
|
800
598
|
|
|
801
|
-
// src/core/validation.ts
|
|
599
|
+
// src/core/validation/index.ts
|
|
802
600
|
function isValidEmail(email) {
|
|
803
601
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
804
602
|
return emailRegex.test(email);
|
|
@@ -875,7 +673,7 @@ function isFloat(value) {
|
|
|
875
673
|
return false;
|
|
876
674
|
}
|
|
877
675
|
|
|
878
|
-
// src/format/number.ts
|
|
676
|
+
// src/format/number/index.ts
|
|
879
677
|
function formatNumber(num, options = {}) {
|
|
880
678
|
const { decimals, separator = ",", decimalPoint = ".", minimumFractionDigits } = options;
|
|
881
679
|
let numStr;
|
|
@@ -952,7 +750,7 @@ function formatBytes(bytes, decimals = 2) {
|
|
|
952
750
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
|
|
953
751
|
}
|
|
954
752
|
|
|
955
|
-
// src/format/url.ts
|
|
753
|
+
// src/format/url/index.ts
|
|
956
754
|
function parseUrl(url) {
|
|
957
755
|
try {
|
|
958
756
|
const urlObj = new URL(url);
|
|
@@ -1043,7 +841,7 @@ function normalizeUrl(url) {
|
|
|
1043
841
|
}
|
|
1044
842
|
}
|
|
1045
843
|
|
|
1046
|
-
// src/format/color.ts
|
|
844
|
+
// src/format/color/index.ts
|
|
1047
845
|
function hexToRgb(hex) {
|
|
1048
846
|
const cleanHex = hex.replace("#", "");
|
|
1049
847
|
if (!/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {
|
|
@@ -1170,7 +968,7 @@ function contrast(color1, color2) {
|
|
|
1170
968
|
return (lighter + 0.05) / (darker + 0.05);
|
|
1171
969
|
}
|
|
1172
970
|
|
|
1173
|
-
// src/format/i18n.ts
|
|
971
|
+
// src/format/i18n/index.ts
|
|
1174
972
|
function getLocale() {
|
|
1175
973
|
if (typeof navigator !== "undefined" && navigator.language) {
|
|
1176
974
|
return navigator.language;
|
|
@@ -1248,7 +1046,7 @@ function pluralize(count, singular, plural, locale) {
|
|
|
1248
1046
|
return `${count} ${pluralForm}`;
|
|
1249
1047
|
}
|
|
1250
1048
|
|
|
1251
|
-
// src/browser/file.ts
|
|
1049
|
+
// src/browser/file/index.ts
|
|
1252
1050
|
async function calculateFileMD5(file) {
|
|
1253
1051
|
return new Promise((resolve, reject) => {
|
|
1254
1052
|
if (typeof FileReader === "undefined") {
|
|
@@ -1338,7 +1136,7 @@ var UploadStatus = /* @__PURE__ */ ((UploadStatus2) => {
|
|
|
1338
1136
|
return UploadStatus2;
|
|
1339
1137
|
})(UploadStatus || {});
|
|
1340
1138
|
|
|
1341
|
-
// src/browser/upload.ts
|
|
1139
|
+
// src/browser/upload/index.ts
|
|
1342
1140
|
var ChunkUploader = class {
|
|
1343
1141
|
constructor(file, options = {}) {
|
|
1344
1142
|
this.taskId = null;
|
|
@@ -1545,186 +1343,757 @@ var ChunkUploader = class {
|
|
|
1545
1343
|
method: "POST",
|
|
1546
1344
|
headers: this.options.headers
|
|
1547
1345
|
}
|
|
1548
|
-
);
|
|
1549
|
-
console.log("[\u5B8C\u6210\u4E0A\u4F20] \u63A5\u53E3\u54CD\u5E94:", response);
|
|
1550
|
-
if (response.code !== 200) {
|
|
1551
|
-
throw new Error(response.message || "\u5B8C\u6210\u4E0A\u4F20\u5931\u8D25");
|
|
1346
|
+
);
|
|
1347
|
+
console.log("[\u5B8C\u6210\u4E0A\u4F20] \u63A5\u53E3\u54CD\u5E94:", response);
|
|
1348
|
+
if (response.code !== 200) {
|
|
1349
|
+
throw new Error(response.message || "\u5B8C\u6210\u4E0A\u4F20\u5931\u8D25");
|
|
1350
|
+
}
|
|
1351
|
+
return response.data;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* 开始上传
|
|
1355
|
+
*/
|
|
1356
|
+
async upload() {
|
|
1357
|
+
try {
|
|
1358
|
+
this.status = "uploading" /* UPLOADING */;
|
|
1359
|
+
this.abortController = new AbortController();
|
|
1360
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 1. \u521D\u59CB\u5316\u4E0A\u4F20");
|
|
1361
|
+
const initResponse = await this.initUpload();
|
|
1362
|
+
this.taskId = initResponse.taskId;
|
|
1363
|
+
if (initResponse.instantUpload && initResponse.fileUrl) {
|
|
1364
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] \u79D2\u4F20\u6210\u529F");
|
|
1365
|
+
this.status = "completed" /* COMPLETED */;
|
|
1366
|
+
const result = {
|
|
1367
|
+
taskId: this.taskId,
|
|
1368
|
+
fileUrl: initResponse.fileUrl,
|
|
1369
|
+
fileName: this.file.name,
|
|
1370
|
+
fileSize: this.file.size,
|
|
1371
|
+
fileMd5: "",
|
|
1372
|
+
success: true,
|
|
1373
|
+
message: "\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u79D2\u4F20\u6210\u529F"
|
|
1374
|
+
};
|
|
1375
|
+
this.options.onComplete(result);
|
|
1376
|
+
return result;
|
|
1377
|
+
}
|
|
1378
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 2. \u51C6\u5907\u5206\u7247");
|
|
1379
|
+
this.prepareChunks();
|
|
1380
|
+
const totalChunks = this.chunks.length;
|
|
1381
|
+
console.log(`[\u4E0A\u4F20\u6D41\u7A0B] \u5171 ${totalChunks} \u4E2A\u5206\u7247`);
|
|
1382
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 3. \u68C0\u67E5\u5DF2\u4E0A\u4F20\u5206\u7247");
|
|
1383
|
+
const existingChunks = await this.getUploadedChunks();
|
|
1384
|
+
existingChunks.forEach((index) => this.uploadedChunks.add(index));
|
|
1385
|
+
console.log(
|
|
1386
|
+
`[\u4E0A\u4F20\u6D41\u7A0B] \u5DF2\u4E0A\u4F20 ${existingChunks.length} \u4E2A\u5206\u7247\uFF0C\u8FD8\u9700\u4E0A\u4F20 ${totalChunks - existingChunks.length} \u4E2A`
|
|
1387
|
+
);
|
|
1388
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 4. \u5F00\u59CB\u4E0A\u4F20\u5206\u7247");
|
|
1389
|
+
await this.uploadChunksConcurrently();
|
|
1390
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 5. \u6240\u6709\u5206\u7247\u4E0A\u4F20\u5B8C\u6210");
|
|
1391
|
+
const uploadedCount = this.uploadedChunks.size;
|
|
1392
|
+
if (uploadedCount < totalChunks) {
|
|
1393
|
+
throw new Error(`\u4E0A\u4F20\u9A8C\u8BC1\u5931\u8D25\uFF1A\u5DF2\u4E0A\u4F20 ${uploadedCount}/${totalChunks} \u4E2A\u5206\u7247`);
|
|
1394
|
+
}
|
|
1395
|
+
console.log(`[\u4E0A\u4F20\u6D41\u7A0B] 6. \u9A8C\u8BC1\u901A\u8FC7\uFF1A${uploadedCount}/${totalChunks} \u4E2A\u5206\u7247\u5DF2\u4E0A\u4F20`);
|
|
1396
|
+
const localPercentage = Math.round(uploadedCount / totalChunks * 100);
|
|
1397
|
+
console.log(`[\u4E0A\u4F20\u6D41\u7A0B] 7. \u672C\u5730\u8FDB\u5EA6\uFF1A${localPercentage}%`);
|
|
1398
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 8. \u83B7\u53D6\u670D\u52A1\u7AEF\u8FDB\u5EA6");
|
|
1399
|
+
const serverProgress = await this.getUploadProgress();
|
|
1400
|
+
const serverPercentage = serverProgress?.percentage ?? 0;
|
|
1401
|
+
console.log(`[\u4E0A\u4F20\u6D41\u7A0B] \u670D\u52A1\u7AEF\u8FDB\u5EA6\uFF1A${serverPercentage}%`);
|
|
1402
|
+
const finalPercentage = serverProgress?.percentage ?? localPercentage;
|
|
1403
|
+
console.log(`[\u4E0A\u4F20\u6D41\u7A0B] 9. \u6700\u7EC8\u8FDB\u5EA6\uFF1A${finalPercentage}%`);
|
|
1404
|
+
if (finalPercentage >= 100) {
|
|
1405
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 10. \u2705 \u8FDB\u5EA6\u8FBE\u5230100%\uFF0C\u8C03\u7528\u5B8C\u6210\u63A5\u53E3");
|
|
1406
|
+
const result = await this.completeUpload();
|
|
1407
|
+
this.status = "completed" /* COMPLETED */;
|
|
1408
|
+
this.options.onComplete(result);
|
|
1409
|
+
console.log("[\u4E0A\u4F20\u6D41\u7A0B] \u2705 \u4E0A\u4F20\u5B8C\u6210");
|
|
1410
|
+
return result;
|
|
1411
|
+
} else {
|
|
1412
|
+
console.error(`[\u4E0A\u4F20\u6D41\u7A0B] \u274C \u8FDB\u5EA6\u4E0D\u8DB3100%\uFF1A${finalPercentage}%`);
|
|
1413
|
+
throw new Error(`\u4E0A\u4F20\u672A\u5B8C\u6210\uFF1A\u5F53\u524D\u8FDB\u5EA6 ${finalPercentage.toFixed(2)}%`);
|
|
1414
|
+
}
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
this.status = "failed" /* FAILED */;
|
|
1417
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1418
|
+
this.options.onError(err);
|
|
1419
|
+
console.error("[\u4E0A\u4F20\u6D41\u7A0B] \u274C \u4E0A\u4F20\u5931\u8D25:", err);
|
|
1420
|
+
throw err;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* 暂停上传
|
|
1425
|
+
*/
|
|
1426
|
+
pause() {
|
|
1427
|
+
if (this.status === "uploading" /* UPLOADING */) {
|
|
1428
|
+
this.status = "paused" /* PAUSED */;
|
|
1429
|
+
if (this.abortController) {
|
|
1430
|
+
this.abortController.abort();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* 恢复上传
|
|
1436
|
+
*/
|
|
1437
|
+
async resume() {
|
|
1438
|
+
if (this.status === "paused" /* PAUSED */) {
|
|
1439
|
+
return this.upload();
|
|
1440
|
+
}
|
|
1441
|
+
throw new Error("\u5F53\u524D\u72B6\u6001\u65E0\u6CD5\u6062\u590D\u4E0A\u4F20");
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* 取消上传
|
|
1445
|
+
*/
|
|
1446
|
+
async cancel() {
|
|
1447
|
+
if (this.taskId && this.status === "uploading" /* UPLOADING */) {
|
|
1448
|
+
try {
|
|
1449
|
+
await this.request(`/api/files/common/cancel/${this.taskId}`, {
|
|
1450
|
+
method: "POST",
|
|
1451
|
+
headers: this.options.headers
|
|
1452
|
+
});
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
console.warn("\u53D6\u6D88\u4E0A\u4F20\u5931\u8D25:", error);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
this.status = "cancelled" /* CANCELLED */;
|
|
1458
|
+
if (this.abortController) {
|
|
1459
|
+
this.abortController.abort();
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* 获取当前状态
|
|
1464
|
+
*/
|
|
1465
|
+
getStatus() {
|
|
1466
|
+
return this.status;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* 获取任务ID
|
|
1470
|
+
*/
|
|
1471
|
+
getTaskId() {
|
|
1472
|
+
return this.taskId;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* HTTP请求封装
|
|
1476
|
+
*/
|
|
1477
|
+
async request(url, options = {}) {
|
|
1478
|
+
const fullUrl = `${this.options.baseURL}${url}`;
|
|
1479
|
+
const signal = this.abortController?.signal;
|
|
1480
|
+
const response = await fetch(fullUrl, {
|
|
1481
|
+
...options,
|
|
1482
|
+
signal
|
|
1483
|
+
});
|
|
1484
|
+
if (!response.ok) {
|
|
1485
|
+
throw new Error(`HTTP\u9519\u8BEF: ${response.status} ${response.statusText}`);
|
|
1486
|
+
}
|
|
1487
|
+
return response.json();
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* 延迟函数
|
|
1491
|
+
*/
|
|
1492
|
+
delay(ms) {
|
|
1493
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
function createUploader(file, options) {
|
|
1497
|
+
return new ChunkUploader(file, options);
|
|
1498
|
+
}
|
|
1499
|
+
async function uploadFile(file, options) {
|
|
1500
|
+
const uploader = createUploader(file, options);
|
|
1501
|
+
return uploader.upload();
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/helper/crypto/index.ts
|
|
1505
|
+
async function sha256(data) {
|
|
1506
|
+
const buffer = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1507
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
|
|
1508
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1509
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1510
|
+
}
|
|
1511
|
+
function base64Encode(data) {
|
|
1512
|
+
if (typeof data === "string") {
|
|
1513
|
+
return btoa(unescape(encodeURIComponent(data)));
|
|
1514
|
+
}
|
|
1515
|
+
const bytes = new Uint8Array(data);
|
|
1516
|
+
let binary = "";
|
|
1517
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1518
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1519
|
+
}
|
|
1520
|
+
return btoa(binary);
|
|
1521
|
+
}
|
|
1522
|
+
function base64Decode(data) {
|
|
1523
|
+
try {
|
|
1524
|
+
return decodeURIComponent(escape(atob(data)));
|
|
1525
|
+
} catch {
|
|
1526
|
+
throw new Error("Invalid Base64 string");
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function generateUUID() {
|
|
1530
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1531
|
+
return crypto.randomUUID();
|
|
1532
|
+
}
|
|
1533
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
1534
|
+
const r = Math.random() * 16 | 0;
|
|
1535
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
1536
|
+
return v.toString(16);
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
function generateRandomString(length, charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") {
|
|
1540
|
+
let result = "";
|
|
1541
|
+
for (let i = 0; i < length; i++) {
|
|
1542
|
+
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
1543
|
+
}
|
|
1544
|
+
return result;
|
|
1545
|
+
}
|
|
1546
|
+
function hash(data) {
|
|
1547
|
+
let hashValue = 0;
|
|
1548
|
+
for (let i = 0; i < data.length; i++) {
|
|
1549
|
+
const char = data.charCodeAt(i);
|
|
1550
|
+
hashValue = (hashValue << 5) - hashValue + char;
|
|
1551
|
+
hashValue = hashValue & hashValue;
|
|
1552
|
+
}
|
|
1553
|
+
return Math.abs(hashValue);
|
|
1554
|
+
}
|
|
1555
|
+
async function generateRSAKeyPair(modulusLength = 2048) {
|
|
1556
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1557
|
+
throw new Error("Web Crypto API is not available");
|
|
1558
|
+
}
|
|
1559
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
1560
|
+
{
|
|
1561
|
+
name: "RSA-OAEP",
|
|
1562
|
+
modulusLength,
|
|
1563
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
1564
|
+
hash: "SHA-256"
|
|
1565
|
+
},
|
|
1566
|
+
true,
|
|
1567
|
+
["encrypt", "decrypt"]
|
|
1568
|
+
);
|
|
1569
|
+
return {
|
|
1570
|
+
publicKey: keyPair.publicKey,
|
|
1571
|
+
privateKey: keyPair.privateKey
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
async function rsaEncrypt(data, publicKey) {
|
|
1575
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1576
|
+
throw new Error("Web Crypto API is not available");
|
|
1577
|
+
}
|
|
1578
|
+
const encoder = new TextEncoder();
|
|
1579
|
+
const dataBuffer = encoder.encode(data);
|
|
1580
|
+
const maxChunkSize = 245;
|
|
1581
|
+
const chunks = [];
|
|
1582
|
+
for (let i = 0; i < dataBuffer.length; i += maxChunkSize) {
|
|
1583
|
+
const chunk2 = dataBuffer.slice(i, i + maxChunkSize);
|
|
1584
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
1585
|
+
{
|
|
1586
|
+
name: "RSA-OAEP"
|
|
1587
|
+
},
|
|
1588
|
+
publicKey,
|
|
1589
|
+
chunk2
|
|
1590
|
+
);
|
|
1591
|
+
chunks.push(encrypted);
|
|
1592
|
+
}
|
|
1593
|
+
const totalLength = chunks.reduce((sum, chunk2) => sum + chunk2.byteLength, 0);
|
|
1594
|
+
const merged = new Uint8Array(totalLength);
|
|
1595
|
+
let offset = 0;
|
|
1596
|
+
for (const chunk2 of chunks) {
|
|
1597
|
+
merged.set(new Uint8Array(chunk2), offset);
|
|
1598
|
+
offset += chunk2.byteLength;
|
|
1599
|
+
}
|
|
1600
|
+
return base64Encode(merged.buffer);
|
|
1601
|
+
}
|
|
1602
|
+
async function rsaDecrypt(encryptedData, privateKey) {
|
|
1603
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1604
|
+
throw new Error("Web Crypto API is not available");
|
|
1605
|
+
}
|
|
1606
|
+
const binaryString = atob(encryptedData);
|
|
1607
|
+
const encryptedArray = new Uint8Array(binaryString.length);
|
|
1608
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
1609
|
+
encryptedArray[i] = binaryString.charCodeAt(i);
|
|
1610
|
+
}
|
|
1611
|
+
const chunkSize = 256;
|
|
1612
|
+
const chunks = [];
|
|
1613
|
+
for (let i = 0; i < encryptedArray.length; i += chunkSize) {
|
|
1614
|
+
const chunk2 = encryptedArray.slice(i, i + chunkSize);
|
|
1615
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
1616
|
+
{
|
|
1617
|
+
name: "RSA-OAEP"
|
|
1618
|
+
},
|
|
1619
|
+
privateKey,
|
|
1620
|
+
chunk2
|
|
1621
|
+
);
|
|
1622
|
+
const decoder = new TextDecoder();
|
|
1623
|
+
chunks.push(decoder.decode(decrypted));
|
|
1624
|
+
}
|
|
1625
|
+
return chunks.join("");
|
|
1626
|
+
}
|
|
1627
|
+
async function exportPublicKey(publicKey) {
|
|
1628
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1629
|
+
throw new Error("Web Crypto API is not available");
|
|
1630
|
+
}
|
|
1631
|
+
const exported = await crypto.subtle.exportKey("spki", publicKey);
|
|
1632
|
+
return base64Encode(exported);
|
|
1633
|
+
}
|
|
1634
|
+
async function exportPrivateKey(privateKey) {
|
|
1635
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1636
|
+
throw new Error("Web Crypto API is not available");
|
|
1637
|
+
}
|
|
1638
|
+
const exported = await crypto.subtle.exportKey("pkcs8", privateKey);
|
|
1639
|
+
return base64Encode(exported);
|
|
1640
|
+
}
|
|
1641
|
+
async function importPublicKey(keyData) {
|
|
1642
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1643
|
+
throw new Error("Web Crypto API is not available");
|
|
1644
|
+
}
|
|
1645
|
+
const keyBuffer = base64Decode(keyData);
|
|
1646
|
+
const keyArray = new Uint8Array(keyBuffer.split("").map((char) => char.charCodeAt(0)));
|
|
1647
|
+
return crypto.subtle.importKey(
|
|
1648
|
+
"spki",
|
|
1649
|
+
keyArray.buffer,
|
|
1650
|
+
{
|
|
1651
|
+
name: "RSA-OAEP",
|
|
1652
|
+
hash: "SHA-256"
|
|
1653
|
+
},
|
|
1654
|
+
true,
|
|
1655
|
+
["encrypt"]
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
async function importPrivateKey(keyData) {
|
|
1659
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1660
|
+
throw new Error("Web Crypto API is not available");
|
|
1661
|
+
}
|
|
1662
|
+
const keyBuffer = base64Decode(keyData);
|
|
1663
|
+
const keyArray = new Uint8Array(keyBuffer.split("").map((char) => char.charCodeAt(0)));
|
|
1664
|
+
return crypto.subtle.importKey(
|
|
1665
|
+
"pkcs8",
|
|
1666
|
+
keyArray.buffer,
|
|
1667
|
+
{
|
|
1668
|
+
name: "RSA-OAEP",
|
|
1669
|
+
hash: "SHA-256"
|
|
1670
|
+
},
|
|
1671
|
+
true,
|
|
1672
|
+
["decrypt"]
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
async function generateHMACKey() {
|
|
1676
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1677
|
+
throw new Error("Web Crypto API is not available");
|
|
1678
|
+
}
|
|
1679
|
+
return crypto.subtle.generateKey(
|
|
1680
|
+
{
|
|
1681
|
+
name: "HMAC",
|
|
1682
|
+
hash: "SHA-256"
|
|
1683
|
+
},
|
|
1684
|
+
true,
|
|
1685
|
+
["sign", "verify"]
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
async function computeHMAC(data, key) {
|
|
1689
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1690
|
+
throw new Error("Web Crypto API is not available");
|
|
1691
|
+
}
|
|
1692
|
+
const buffer = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1693
|
+
const signature = await crypto.subtle.sign("HMAC", key, buffer);
|
|
1694
|
+
return base64Encode(signature);
|
|
1695
|
+
}
|
|
1696
|
+
async function verifyHMAC(data, signature, key) {
|
|
1697
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1698
|
+
throw new Error("Web Crypto API is not available");
|
|
1699
|
+
}
|
|
1700
|
+
try {
|
|
1701
|
+
const dataBuffer = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1702
|
+
const signatureBuffer = new Uint8Array(
|
|
1703
|
+
atob(signature).split("").map((char) => char.charCodeAt(0))
|
|
1704
|
+
);
|
|
1705
|
+
return await crypto.subtle.verify("HMAC", key, signatureBuffer, dataBuffer);
|
|
1706
|
+
} catch {
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
async function deriveKeyFromPassword(password, salt, iterations = 1e5, keyLength = 256) {
|
|
1711
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1712
|
+
throw new Error("Web Crypto API is not available");
|
|
1713
|
+
}
|
|
1714
|
+
const saltBuffer = typeof salt === "string" ? new TextEncoder().encode(salt) : salt;
|
|
1715
|
+
const passwordKey = await crypto.subtle.importKey(
|
|
1716
|
+
"raw",
|
|
1717
|
+
new TextEncoder().encode(password),
|
|
1718
|
+
"PBKDF2",
|
|
1719
|
+
false,
|
|
1720
|
+
["deriveBits", "deriveKey"]
|
|
1721
|
+
);
|
|
1722
|
+
return crypto.subtle.deriveKey(
|
|
1723
|
+
{
|
|
1724
|
+
name: "PBKDF2",
|
|
1725
|
+
salt: saltBuffer,
|
|
1726
|
+
iterations,
|
|
1727
|
+
hash: "SHA-256"
|
|
1728
|
+
},
|
|
1729
|
+
passwordKey,
|
|
1730
|
+
{
|
|
1731
|
+
name: "AES-GCM",
|
|
1732
|
+
length: keyLength
|
|
1733
|
+
},
|
|
1734
|
+
false,
|
|
1735
|
+
["encrypt", "decrypt"]
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
async function aesGCMEncrypt(data, key) {
|
|
1739
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1740
|
+
throw new Error("Web Crypto API is not available");
|
|
1741
|
+
}
|
|
1742
|
+
const dataBuffer = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1743
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1744
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
1745
|
+
{
|
|
1746
|
+
name: "AES-GCM",
|
|
1747
|
+
iv
|
|
1748
|
+
},
|
|
1749
|
+
key,
|
|
1750
|
+
dataBuffer
|
|
1751
|
+
);
|
|
1752
|
+
return {
|
|
1753
|
+
encrypted: base64Encode(encrypted),
|
|
1754
|
+
iv: base64Encode(iv.buffer)
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
async function aesGCMDecrypt(encryptedData, iv, key) {
|
|
1758
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1759
|
+
throw new Error("Web Crypto API is not available");
|
|
1760
|
+
}
|
|
1761
|
+
const encryptedBuffer = new Uint8Array(
|
|
1762
|
+
atob(encryptedData).split("").map((char) => char.charCodeAt(0))
|
|
1763
|
+
);
|
|
1764
|
+
const ivBuffer = new Uint8Array(
|
|
1765
|
+
atob(iv).split("").map((char) => char.charCodeAt(0))
|
|
1766
|
+
);
|
|
1767
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
1768
|
+
{
|
|
1769
|
+
name: "AES-GCM",
|
|
1770
|
+
iv: ivBuffer
|
|
1771
|
+
},
|
|
1772
|
+
key,
|
|
1773
|
+
encryptedBuffer
|
|
1774
|
+
);
|
|
1775
|
+
return new TextDecoder().decode(decrypted);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// src/browser/SecureStorage/index.ts
|
|
1779
|
+
var globalKeyPair = null;
|
|
1780
|
+
var globalHMACKey = null;
|
|
1781
|
+
var keyPairInitialized = false;
|
|
1782
|
+
var initOptions = {
|
|
1783
|
+
autoGenerateKeys: true,
|
|
1784
|
+
persistKeys: false,
|
|
1785
|
+
keyStorageKey: void 0,
|
|
1786
|
+
// 将自动生成随机键名
|
|
1787
|
+
keyEncryptionPassword: void 0,
|
|
1788
|
+
pbkdf2Iterations: 1e5,
|
|
1789
|
+
keyModulusLength: 2048,
|
|
1790
|
+
enableHMAC: true,
|
|
1791
|
+
enableTimestampValidation: true,
|
|
1792
|
+
timestampMaxAge: 7 * 24 * 60 * 60 * 1e3,
|
|
1793
|
+
// 7 天
|
|
1794
|
+
isProduction: false
|
|
1795
|
+
};
|
|
1796
|
+
var actualKeyStorageKey = null;
|
|
1797
|
+
var keyUsageCount = 0;
|
|
1798
|
+
var initializationPromise = null;
|
|
1799
|
+
function generateKeyStorageKey() {
|
|
1800
|
+
return `_sk_${generateRandomString(32)}_${Date.now()}`;
|
|
1801
|
+
}
|
|
1802
|
+
async function initializeStorageKeys(options = {}) {
|
|
1803
|
+
if (options.forceReinitialize) {
|
|
1804
|
+
globalKeyPair = null;
|
|
1805
|
+
globalHMACKey = null;
|
|
1806
|
+
keyPairInitialized = false;
|
|
1807
|
+
actualKeyStorageKey = null;
|
|
1808
|
+
keyUsageCount = 0;
|
|
1809
|
+
initializationPromise = null;
|
|
1810
|
+
} else if (keyPairInitialized && globalKeyPair) {
|
|
1811
|
+
initOptions = { ...initOptions, ...options };
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
initOptions = { ...initOptions, ...options };
|
|
1815
|
+
if (initOptions.keyStorageKey) {
|
|
1816
|
+
actualKeyStorageKey = initOptions.keyStorageKey;
|
|
1817
|
+
} else {
|
|
1818
|
+
actualKeyStorageKey = generateKeyStorageKey();
|
|
1819
|
+
}
|
|
1820
|
+
if (initOptions.persistKeys && typeof window !== "undefined" && window.localStorage) {
|
|
1821
|
+
try {
|
|
1822
|
+
const storedKeys = window.localStorage.getItem(actualKeyStorageKey);
|
|
1823
|
+
if (storedKeys) {
|
|
1824
|
+
const keyData = JSON.parse(storedKeys);
|
|
1825
|
+
if (keyData.encrypted && keyData.privateKeyEncrypted && keyData.salt) {
|
|
1826
|
+
if (!initOptions.keyEncryptionPassword) {
|
|
1827
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
1828
|
+
console.error("Encrypted keys found but no password provided");
|
|
1829
|
+
}
|
|
1830
|
+
throw new Error("Password required to decrypt stored keys");
|
|
1831
|
+
}
|
|
1832
|
+
const saltArray = new Uint8Array(
|
|
1833
|
+
atob(keyData.salt).split("").map((char) => char.charCodeAt(0))
|
|
1834
|
+
);
|
|
1835
|
+
const iterations = keyData.pbkdf2Iterations || initOptions.pbkdf2Iterations;
|
|
1836
|
+
const derivedKey = await deriveKeyFromPassword(
|
|
1837
|
+
initOptions.keyEncryptionPassword,
|
|
1838
|
+
saltArray.buffer,
|
|
1839
|
+
iterations
|
|
1840
|
+
);
|
|
1841
|
+
const decryptedPrivateKey = await aesGCMDecrypt(
|
|
1842
|
+
keyData.privateKeyEncrypted,
|
|
1843
|
+
keyData.iv,
|
|
1844
|
+
derivedKey
|
|
1845
|
+
);
|
|
1846
|
+
globalKeyPair = {
|
|
1847
|
+
publicKey: await importPublicKey(keyData.publicKey),
|
|
1848
|
+
privateKey: await importPrivateKey(decryptedPrivateKey)
|
|
1849
|
+
};
|
|
1850
|
+
} else if (keyData.publicKey && keyData.privateKey) {
|
|
1851
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
1852
|
+
console.warn(
|
|
1853
|
+
"\u26A0\uFE0F SECURITY WARNING: Loading unencrypted keys from storage. Consider re-initializing with password encryption."
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
globalKeyPair = {
|
|
1857
|
+
publicKey: await importPublicKey(keyData.publicKey),
|
|
1858
|
+
privateKey: await importPrivateKey(keyData.privateKey)
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
if (globalKeyPair) {
|
|
1862
|
+
keyPairInitialized = true;
|
|
1863
|
+
if (initOptions.enableHMAC && !globalHMACKey) {
|
|
1864
|
+
globalHMACKey = await generateHMACKey();
|
|
1865
|
+
}
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
1871
|
+
console.warn("Failed to load persisted keys, will generate new ones");
|
|
1872
|
+
}
|
|
1552
1873
|
}
|
|
1553
|
-
return response.data;
|
|
1554
1874
|
}
|
|
1555
|
-
|
|
1556
|
-
* 开始上传
|
|
1557
|
-
*/
|
|
1558
|
-
async upload() {
|
|
1875
|
+
if (initOptions.autoGenerateKeys && !globalKeyPair) {
|
|
1559
1876
|
try {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
const initResponse = await this.initUpload();
|
|
1564
|
-
this.taskId = initResponse.taskId;
|
|
1565
|
-
if (initResponse.instantUpload && initResponse.fileUrl) {
|
|
1566
|
-
console.log("[\u4E0A\u4F20\u6D41\u7A0B] \u79D2\u4F20\u6210\u529F");
|
|
1567
|
-
this.status = "completed" /* COMPLETED */;
|
|
1568
|
-
const result = {
|
|
1569
|
-
taskId: this.taskId,
|
|
1570
|
-
fileUrl: initResponse.fileUrl,
|
|
1571
|
-
fileName: this.file.name,
|
|
1572
|
-
fileSize: this.file.size,
|
|
1573
|
-
fileMd5: "",
|
|
1574
|
-
success: true,
|
|
1575
|
-
message: "\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u79D2\u4F20\u6210\u529F"
|
|
1576
|
-
};
|
|
1577
|
-
this.options.onComplete(result);
|
|
1578
|
-
return result;
|
|
1579
|
-
}
|
|
1580
|
-
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 2. \u51C6\u5907\u5206\u7247");
|
|
1581
|
-
this.prepareChunks();
|
|
1582
|
-
const totalChunks = this.chunks.length;
|
|
1583
|
-
console.log(`[\u4E0A\u4F20\u6D41\u7A0B] \u5171 ${totalChunks} \u4E2A\u5206\u7247`);
|
|
1584
|
-
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 3. \u68C0\u67E5\u5DF2\u4E0A\u4F20\u5206\u7247");
|
|
1585
|
-
const existingChunks = await this.getUploadedChunks();
|
|
1586
|
-
existingChunks.forEach((index) => this.uploadedChunks.add(index));
|
|
1587
|
-
console.log(
|
|
1588
|
-
`[\u4E0A\u4F20\u6D41\u7A0B] \u5DF2\u4E0A\u4F20 ${existingChunks.length} \u4E2A\u5206\u7247\uFF0C\u8FD8\u9700\u4E0A\u4F20 ${totalChunks - existingChunks.length} \u4E2A`
|
|
1589
|
-
);
|
|
1590
|
-
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 4. \u5F00\u59CB\u4E0A\u4F20\u5206\u7247");
|
|
1591
|
-
await this.uploadChunksConcurrently();
|
|
1592
|
-
console.log("[\u4E0A\u4F20\u6D41\u7A0B] 5. \u6240\u6709\u5206\u7247\u4E0A\u4F20\u5B8C\u6210");
|
|
1593
|
-
const uploadedCount = this.uploadedChunks.size;
|
|
1594
|
-
if (uploadedCount < totalChunks) {
|
|
1595
|
-
throw new Error(`\u4E0A\u4F20\u9A8C\u8BC1\u5931\u8D25\uFF1A\u5DF2\u4E0A\u4F20 ${uploadedCount}/${totalChunks} \u4E2A\u5206\u7247`);
|
|
1877
|
+
globalKeyPair = await generateRSAKeyPair(initOptions.keyModulusLength);
|
|
1878
|
+
if (initOptions.enableHMAC && !globalHMACKey) {
|
|
1879
|
+
globalHMACKey = await generateHMACKey();
|
|
1596
1880
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1881
|
+
keyPairInitialized = true;
|
|
1882
|
+
keyUsageCount = 0;
|
|
1883
|
+
if (initOptions.persistKeys && typeof window !== "undefined" && window.localStorage) {
|
|
1884
|
+
try {
|
|
1885
|
+
const publicKeyStr = await exportPublicKey(globalKeyPair.publicKey);
|
|
1886
|
+
if (initOptions.keyEncryptionPassword) {
|
|
1887
|
+
const privateKeyStr = await exportPrivateKey(globalKeyPair.privateKey);
|
|
1888
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1889
|
+
const derivedKey = await deriveKeyFromPassword(
|
|
1890
|
+
initOptions.keyEncryptionPassword,
|
|
1891
|
+
salt.buffer,
|
|
1892
|
+
initOptions.pbkdf2Iterations
|
|
1893
|
+
);
|
|
1894
|
+
const { encrypted, iv } = await aesGCMEncrypt(privateKeyStr, derivedKey);
|
|
1895
|
+
const keyData = {
|
|
1896
|
+
encrypted: true,
|
|
1897
|
+
publicKey: publicKeyStr,
|
|
1898
|
+
privateKeyEncrypted: encrypted,
|
|
1899
|
+
iv,
|
|
1900
|
+
salt: base64Encode(salt.buffer),
|
|
1901
|
+
pbkdf2Iterations: initOptions.pbkdf2Iterations
|
|
1902
|
+
};
|
|
1903
|
+
window.localStorage.setItem(actualKeyStorageKey, JSON.stringify(keyData));
|
|
1904
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.info) {
|
|
1905
|
+
console.info("\u2705 Keys encrypted and stored securely");
|
|
1906
|
+
}
|
|
1907
|
+
} else {
|
|
1908
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
1909
|
+
console.warn(
|
|
1910
|
+
"\u26A0\uFE0F SECURITY WARNING: Storing private keys without encryption! Private keys will be stored in plain text (Base64 encoded). Provide keyEncryptionPassword for secure storage."
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
const keyData = {
|
|
1914
|
+
publicKey: publicKeyStr,
|
|
1915
|
+
privateKey: await exportPrivateKey(globalKeyPair.privateKey)
|
|
1916
|
+
};
|
|
1917
|
+
window.localStorage.setItem(actualKeyStorageKey, JSON.stringify(keyData));
|
|
1918
|
+
}
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
1921
|
+
console.error("Failed to persist keys");
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1616
1924
|
}
|
|
1617
1925
|
} catch (error) {
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
this.options.onError(err);
|
|
1621
|
-
console.error("[\u4E0A\u4F20\u6D41\u7A0B] \u274C \u4E0A\u4F20\u5931\u8D25:", err);
|
|
1622
|
-
throw err;
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
/**
|
|
1626
|
-
* 暂停上传
|
|
1627
|
-
*/
|
|
1628
|
-
pause() {
|
|
1629
|
-
if (this.status === "uploading" /* UPLOADING */) {
|
|
1630
|
-
this.status = "paused" /* PAUSED */;
|
|
1631
|
-
if (this.abortController) {
|
|
1632
|
-
this.abortController.abort();
|
|
1926
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
1927
|
+
console.error("Failed to generate storage keys");
|
|
1633
1928
|
}
|
|
1929
|
+
throw error;
|
|
1634
1930
|
}
|
|
1635
1931
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
*/
|
|
1639
|
-
async resume() {
|
|
1640
|
-
if (this.status === "paused" /* PAUSED */) {
|
|
1641
|
-
return this.upload();
|
|
1642
|
-
}
|
|
1643
|
-
throw new Error("\u5F53\u524D\u72B6\u6001\u65E0\u6CD5\u6062\u590D\u4E0A\u4F20");
|
|
1932
|
+
if (initOptions.enableHMAC && !globalHMACKey) {
|
|
1933
|
+
globalHMACKey = await generateHMACKey();
|
|
1644
1934
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1935
|
+
}
|
|
1936
|
+
function setStorageKeyPair(keyPair) {
|
|
1937
|
+
globalKeyPair = keyPair;
|
|
1938
|
+
keyPairInitialized = true;
|
|
1939
|
+
}
|
|
1940
|
+
function getStorageKeyPair() {
|
|
1941
|
+
return globalKeyPair;
|
|
1942
|
+
}
|
|
1943
|
+
function clearPersistedKeys() {
|
|
1944
|
+
if (typeof window !== "undefined" && window.localStorage && actualKeyStorageKey) {
|
|
1945
|
+
try {
|
|
1946
|
+
window.localStorage.removeItem(actualKeyStorageKey);
|
|
1947
|
+
actualKeyStorageKey = null;
|
|
1948
|
+
keyPairInitialized = false;
|
|
1949
|
+
globalKeyPair = null;
|
|
1950
|
+
globalHMACKey = null;
|
|
1951
|
+
keyUsageCount = 0;
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
1954
|
+
console.error("Failed to clear persisted keys");
|
|
1657
1955
|
}
|
|
1658
1956
|
}
|
|
1659
|
-
this.status = "cancelled" /* CANCELLED */;
|
|
1660
|
-
if (this.abortController) {
|
|
1661
|
-
this.abortController.abort();
|
|
1662
|
-
}
|
|
1663
1957
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1958
|
+
}
|
|
1959
|
+
function getKeyUsageCount() {
|
|
1960
|
+
return keyUsageCount;
|
|
1961
|
+
}
|
|
1962
|
+
function resetKeyUsageCount() {
|
|
1963
|
+
keyUsageCount = 0;
|
|
1964
|
+
}
|
|
1965
|
+
async function ensureKeyPair() {
|
|
1966
|
+
if (globalKeyPair) {
|
|
1967
|
+
keyUsageCount++;
|
|
1968
|
+
return;
|
|
1669
1969
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
*/
|
|
1673
|
-
getTaskId() {
|
|
1674
|
-
return this.taskId;
|
|
1970
|
+
if (!initOptions.autoGenerateKeys) {
|
|
1971
|
+
return;
|
|
1675
1972
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const fullUrl = `${this.options.baseURL}${url}`;
|
|
1681
|
-
const signal = this.abortController?.signal;
|
|
1682
|
-
const response = await fetch(fullUrl, {
|
|
1683
|
-
...options,
|
|
1684
|
-
signal
|
|
1685
|
-
});
|
|
1686
|
-
if (!response.ok) {
|
|
1687
|
-
throw new Error(`HTTP\u9519\u8BEF: ${response.status} ${response.statusText}`);
|
|
1973
|
+
if (initializationPromise) {
|
|
1974
|
+
await initializationPromise;
|
|
1975
|
+
if (globalKeyPair) {
|
|
1976
|
+
keyUsageCount++;
|
|
1688
1977
|
}
|
|
1689
|
-
return
|
|
1978
|
+
return;
|
|
1690
1979
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1980
|
+
initializationPromise = initializeStorageKeys();
|
|
1981
|
+
try {
|
|
1982
|
+
await initializationPromise;
|
|
1983
|
+
} finally {
|
|
1984
|
+
initializationPromise = null;
|
|
1985
|
+
}
|
|
1986
|
+
if (globalKeyPair) {
|
|
1987
|
+
keyUsageCount++;
|
|
1696
1988
|
}
|
|
1697
|
-
};
|
|
1698
|
-
function createUploader(file, options) {
|
|
1699
|
-
return new ChunkUploader(file, options);
|
|
1700
1989
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1990
|
+
function safeParseJSON(jsonStr, expectedType) {
|
|
1991
|
+
try {
|
|
1992
|
+
const parsed = JSON.parse(jsonStr);
|
|
1993
|
+
if (expectedType === "object" && (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))) {
|
|
1994
|
+
throw new Error("Expected object but got different type");
|
|
1995
|
+
}
|
|
1996
|
+
if (expectedType === "array" && !Array.isArray(parsed)) {
|
|
1997
|
+
throw new Error("Expected array but got different type");
|
|
1998
|
+
}
|
|
1999
|
+
return parsed;
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
if (error instanceof SyntaxError) {
|
|
2002
|
+
throw new Error(`Invalid JSON format: ${error.message}`);
|
|
2003
|
+
}
|
|
2004
|
+
throw error;
|
|
2005
|
+
}
|
|
1704
2006
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
2007
|
+
function validateTimestamp(timestamp, maxClockSkew = 5 * 60 * 1e3) {
|
|
2008
|
+
if (!initOptions.enableTimestampValidation || !timestamp) {
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
if (initOptions.timestampMaxAge === 0) {
|
|
2012
|
+
return true;
|
|
2013
|
+
}
|
|
2014
|
+
const now = Date.now();
|
|
2015
|
+
const age = now - timestamp;
|
|
2016
|
+
const maxAge = initOptions.timestampMaxAge || 7 * 24 * 60 * 60 * 1e3;
|
|
2017
|
+
if (age < -maxClockSkew) {
|
|
2018
|
+
return false;
|
|
2019
|
+
}
|
|
2020
|
+
return age >= -maxClockSkew && age <= maxAge;
|
|
1711
2021
|
}
|
|
1712
|
-
function
|
|
1713
|
-
|
|
2022
|
+
async function encryptValue(value, publicKey, hmacKey) {
|
|
2023
|
+
const encryptedData = await rsaEncrypt(value, publicKey);
|
|
2024
|
+
if (hmacKey) {
|
|
2025
|
+
const hmac = await computeHMAC(encryptedData, hmacKey);
|
|
2026
|
+
const item = {
|
|
2027
|
+
data: encryptedData,
|
|
2028
|
+
hmac,
|
|
2029
|
+
encrypted: true,
|
|
2030
|
+
timestamp: Date.now()
|
|
2031
|
+
};
|
|
2032
|
+
return JSON.stringify(item);
|
|
2033
|
+
}
|
|
2034
|
+
return encryptedData;
|
|
2035
|
+
}
|
|
2036
|
+
async function handleDecryptError(error, encryptedStr, key) {
|
|
2037
|
+
if (error instanceof Error && error.message.includes("HMAC verification failed")) {
|
|
2038
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
2039
|
+
console.error(
|
|
2040
|
+
"\u26A0\uFE0F SECURITY ALERT: Data integrity check failed! Data may have been tampered with."
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
throw error;
|
|
2044
|
+
}
|
|
2045
|
+
if (error instanceof Error && error.message.includes("Data timestamp validation failed")) {
|
|
2046
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
2047
|
+
console.warn("\u26A0\uFE0F SECURITY WARNING: Data timestamp validation failed");
|
|
2048
|
+
}
|
|
2049
|
+
throw error;
|
|
2050
|
+
}
|
|
2051
|
+
try {
|
|
2052
|
+
const decryptedStr = base64Decode(encryptedStr);
|
|
2053
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
2054
|
+
console.warn(
|
|
2055
|
+
`\u26A0\uFE0F SECURITY WARNING: Reading unencrypted data for key "${key}". This may be a security risk if sensitive data was stored without encryption.`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
return decryptedStr;
|
|
2059
|
+
} catch {
|
|
2060
|
+
throw error;
|
|
2061
|
+
}
|
|
1714
2062
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
2063
|
+
function handleNoKeyDecode(encryptedStr, key) {
|
|
2064
|
+
try {
|
|
2065
|
+
const decryptedStr = base64Decode(encryptedStr);
|
|
2066
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
2067
|
+
console.warn(
|
|
2068
|
+
`\u26A0\uFE0F SECURITY WARNING: Reading unencrypted data for key "${key}" without encryption keys.`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
return decryptedStr;
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
throw new Error(`Failed to decode storage value for key "${key}"`);
|
|
2074
|
+
}
|
|
1717
2075
|
}
|
|
1718
|
-
async function decryptValue(encryptedValue, privateKey) {
|
|
2076
|
+
async function decryptValue(encryptedValue, privateKey, hmacKey) {
|
|
1719
2077
|
try {
|
|
2078
|
+
const item = JSON.parse(encryptedValue);
|
|
2079
|
+
if (item.encrypted && item.data && item.hmac) {
|
|
2080
|
+
if (hmacKey) {
|
|
2081
|
+
const isValid = await verifyHMAC(item.data, item.hmac, hmacKey);
|
|
2082
|
+
if (!isValid) {
|
|
2083
|
+
throw new Error("HMAC verification failed: data may have been tampered with");
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
if (item.timestamp && !validateTimestamp(item.timestamp)) {
|
|
2087
|
+
throw new Error("Data timestamp validation failed: data may be expired or replayed");
|
|
2088
|
+
}
|
|
2089
|
+
return await rsaDecrypt(item.data, privateKey);
|
|
2090
|
+
}
|
|
1720
2091
|
return await rsaDecrypt(encryptedValue, privateKey);
|
|
1721
|
-
} catch {
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
return encryptedValue;
|
|
1725
|
-
} catch {
|
|
1726
|
-
throw new Error("Failed to decrypt storage value");
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
if (error instanceof Error && error.message.includes("HMAC verification failed")) {
|
|
2094
|
+
throw error;
|
|
1727
2095
|
}
|
|
2096
|
+
throw new Error("Failed to decrypt storage value: invalid format or corrupted data");
|
|
1728
2097
|
}
|
|
1729
2098
|
}
|
|
1730
2099
|
var localStorage = {
|
|
@@ -1746,17 +2115,24 @@ var localStorage = {
|
|
|
1746
2115
|
};
|
|
1747
2116
|
try {
|
|
1748
2117
|
const jsonStr = JSON.stringify(item);
|
|
2118
|
+
await ensureKeyPair();
|
|
1749
2119
|
const keyToUse = publicKey || globalKeyPair?.publicKey;
|
|
1750
2120
|
if (keyToUse) {
|
|
1751
|
-
const encrypted = await encryptValue(jsonStr, keyToUse);
|
|
2121
|
+
const encrypted = await encryptValue(jsonStr, keyToUse, globalHMACKey);
|
|
1752
2122
|
window.localStorage.setItem(key, encrypted);
|
|
1753
2123
|
} else {
|
|
1754
|
-
|
|
1755
|
-
|
|
2124
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
2125
|
+
console.warn(
|
|
2126
|
+
`\u26A0\uFE0F SECURITY WARNING: Storing data without encryption for key "${key}". Data is only Base64 encoded, not encrypted!`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
const encoded = base64Encode(jsonStr);
|
|
1756
2130
|
window.localStorage.setItem(key, encoded);
|
|
1757
2131
|
}
|
|
1758
2132
|
} catch (error) {
|
|
1759
|
-
|
|
2133
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
2134
|
+
console.error("localStorage.set error:", error);
|
|
2135
|
+
}
|
|
1760
2136
|
throw error;
|
|
1761
2137
|
}
|
|
1762
2138
|
},
|
|
@@ -1774,34 +2150,36 @@ var localStorage = {
|
|
|
1774
2150
|
try {
|
|
1775
2151
|
const encryptedStr = window.localStorage.getItem(key);
|
|
1776
2152
|
if (!encryptedStr) return defaultValue;
|
|
2153
|
+
await ensureKeyPair();
|
|
1777
2154
|
let decryptedStr;
|
|
1778
2155
|
const keyToUse = privateKey || globalKeyPair?.privateKey;
|
|
1779
2156
|
if (keyToUse) {
|
|
1780
2157
|
try {
|
|
1781
|
-
decryptedStr = await decryptValue(encryptedStr, keyToUse);
|
|
1782
|
-
} catch {
|
|
1783
|
-
const { base64Decode: base64Decode2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
2158
|
+
decryptedStr = await decryptValue(encryptedStr, keyToUse, globalHMACKey);
|
|
2159
|
+
} catch (error) {
|
|
1784
2160
|
try {
|
|
1785
|
-
decryptedStr =
|
|
2161
|
+
decryptedStr = await handleDecryptError(error, encryptedStr, key);
|
|
1786
2162
|
} catch {
|
|
1787
2163
|
return defaultValue;
|
|
1788
2164
|
}
|
|
1789
2165
|
}
|
|
1790
2166
|
} else {
|
|
1791
|
-
const { base64Decode: base64Decode2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
1792
2167
|
try {
|
|
1793
|
-
decryptedStr =
|
|
2168
|
+
decryptedStr = handleNoKeyDecode(encryptedStr, key);
|
|
1794
2169
|
} catch {
|
|
1795
2170
|
return defaultValue;
|
|
1796
2171
|
}
|
|
1797
2172
|
}
|
|
1798
|
-
const item =
|
|
2173
|
+
const item = safeParseJSON(decryptedStr, "object");
|
|
1799
2174
|
if (item.expiry && Date.now() > item.expiry) {
|
|
1800
2175
|
window.localStorage.removeItem(key);
|
|
1801
2176
|
return defaultValue;
|
|
1802
2177
|
}
|
|
1803
2178
|
return item.value;
|
|
1804
|
-
} catch {
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
2181
|
+
console.warn(`Failed to parse storage item for key "${key}"`);
|
|
2182
|
+
}
|
|
1805
2183
|
return defaultValue;
|
|
1806
2184
|
}
|
|
1807
2185
|
},
|
|
@@ -1814,7 +2192,9 @@ var localStorage = {
|
|
|
1814
2192
|
try {
|
|
1815
2193
|
window.localStorage.removeItem(key);
|
|
1816
2194
|
} catch (error) {
|
|
1817
|
-
|
|
2195
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
2196
|
+
console.error("localStorage.remove error");
|
|
2197
|
+
}
|
|
1818
2198
|
}
|
|
1819
2199
|
},
|
|
1820
2200
|
/**
|
|
@@ -1825,7 +2205,9 @@ var localStorage = {
|
|
|
1825
2205
|
try {
|
|
1826
2206
|
window.localStorage.clear();
|
|
1827
2207
|
} catch (error) {
|
|
1828
|
-
|
|
2208
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
2209
|
+
console.error("localStorage.clear error");
|
|
2210
|
+
}
|
|
1829
2211
|
}
|
|
1830
2212
|
},
|
|
1831
2213
|
/**
|
|
@@ -1841,7 +2223,9 @@ var localStorage = {
|
|
|
1841
2223
|
if (key) keys2.push(key);
|
|
1842
2224
|
}
|
|
1843
2225
|
} catch (error) {
|
|
1844
|
-
|
|
2226
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
2227
|
+
console.error("localStorage.keys error");
|
|
2228
|
+
}
|
|
1845
2229
|
}
|
|
1846
2230
|
return keys2;
|
|
1847
2231
|
}
|
|
@@ -1861,17 +2245,24 @@ var sessionStorage = {
|
|
|
1861
2245
|
const { publicKey } = options;
|
|
1862
2246
|
try {
|
|
1863
2247
|
const jsonStr = JSON.stringify(value);
|
|
2248
|
+
await ensureKeyPair();
|
|
1864
2249
|
const keyToUse = publicKey || globalKeyPair?.publicKey;
|
|
1865
2250
|
if (keyToUse) {
|
|
1866
|
-
const encrypted = await encryptValue(jsonStr, keyToUse);
|
|
2251
|
+
const encrypted = await encryptValue(jsonStr, keyToUse, globalHMACKey);
|
|
1867
2252
|
window.sessionStorage.setItem(key, encrypted);
|
|
1868
2253
|
} else {
|
|
1869
|
-
|
|
1870
|
-
|
|
2254
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
2255
|
+
console.warn(
|
|
2256
|
+
`\u26A0\uFE0F SECURITY WARNING: Storing data without encryption for key "${key}". Data is only Base64 encoded, not encrypted!`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
const encoded = base64Encode(jsonStr);
|
|
1871
2260
|
window.sessionStorage.setItem(key, encoded);
|
|
1872
2261
|
}
|
|
1873
2262
|
} catch (error) {
|
|
1874
|
-
|
|
2263
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
2264
|
+
console.error("sessionStorage.set error:", error);
|
|
2265
|
+
}
|
|
1875
2266
|
throw error;
|
|
1876
2267
|
}
|
|
1877
2268
|
},
|
|
@@ -1889,28 +2280,27 @@ var sessionStorage = {
|
|
|
1889
2280
|
try {
|
|
1890
2281
|
const encryptedStr = window.sessionStorage.getItem(key);
|
|
1891
2282
|
if (!encryptedStr) return defaultValue;
|
|
2283
|
+
await ensureKeyPair();
|
|
1892
2284
|
let decryptedStr;
|
|
1893
2285
|
const keyToUse = privateKey || globalKeyPair?.privateKey;
|
|
1894
2286
|
if (keyToUse) {
|
|
1895
2287
|
try {
|
|
1896
|
-
decryptedStr = await decryptValue(encryptedStr, keyToUse);
|
|
1897
|
-
} catch {
|
|
1898
|
-
const { base64Decode: base64Decode2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
2288
|
+
decryptedStr = await decryptValue(encryptedStr, keyToUse, globalHMACKey);
|
|
2289
|
+
} catch (error) {
|
|
1899
2290
|
try {
|
|
1900
|
-
decryptedStr =
|
|
2291
|
+
decryptedStr = await handleDecryptError(error, encryptedStr, key);
|
|
1901
2292
|
} catch {
|
|
1902
2293
|
return defaultValue;
|
|
1903
2294
|
}
|
|
1904
2295
|
}
|
|
1905
2296
|
} else {
|
|
1906
|
-
const { base64Decode: base64Decode2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
1907
2297
|
try {
|
|
1908
|
-
decryptedStr =
|
|
2298
|
+
decryptedStr = handleNoKeyDecode(encryptedStr, key);
|
|
1909
2299
|
} catch {
|
|
1910
2300
|
return defaultValue;
|
|
1911
2301
|
}
|
|
1912
2302
|
}
|
|
1913
|
-
return
|
|
2303
|
+
return safeParseJSON(decryptedStr);
|
|
1914
2304
|
} catch {
|
|
1915
2305
|
return defaultValue;
|
|
1916
2306
|
}
|
|
@@ -1924,7 +2314,9 @@ var sessionStorage = {
|
|
|
1924
2314
|
try {
|
|
1925
2315
|
window.sessionStorage.removeItem(key);
|
|
1926
2316
|
} catch (error) {
|
|
1927
|
-
|
|
2317
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
2318
|
+
console.error("sessionStorage.remove error");
|
|
2319
|
+
}
|
|
1928
2320
|
}
|
|
1929
2321
|
},
|
|
1930
2322
|
/**
|
|
@@ -1935,7 +2327,9 @@ var sessionStorage = {
|
|
|
1935
2327
|
try {
|
|
1936
2328
|
window.sessionStorage.clear();
|
|
1937
2329
|
} catch (error) {
|
|
1938
|
-
|
|
2330
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.error) {
|
|
2331
|
+
console.error("sessionStorage.clear error");
|
|
2332
|
+
}
|
|
1939
2333
|
}
|
|
1940
2334
|
}
|
|
1941
2335
|
};
|
|
@@ -1970,10 +2364,15 @@ var cookie = {
|
|
|
1970
2364
|
if (typeof document === "undefined") return void 0;
|
|
1971
2365
|
const name = encodeURIComponent(key);
|
|
1972
2366
|
const cookies = document.cookie.split(";");
|
|
1973
|
-
for (const
|
|
1974
|
-
const
|
|
2367
|
+
for (const cookieStr of cookies) {
|
|
2368
|
+
const trimmed = cookieStr.trim();
|
|
2369
|
+
const eqIndex = trimmed.indexOf("=");
|
|
2370
|
+
if (eqIndex === -1) continue;
|
|
2371
|
+
const cookieKey = trimmed.substring(0, eqIndex).trim();
|
|
2372
|
+
const cookieValue = trimmed.substring(eqIndex + 1).trim();
|
|
1975
2373
|
if (cookieKey === name) {
|
|
1976
|
-
|
|
2374
|
+
const decoded = decodeURIComponent(cookieValue);
|
|
2375
|
+
return decoded === "" ? void 0 : decoded;
|
|
1977
2376
|
}
|
|
1978
2377
|
}
|
|
1979
2378
|
return void 0;
|
|
@@ -1997,15 +2396,24 @@ var cookie = {
|
|
|
1997
2396
|
if (typeof document === "undefined") return {};
|
|
1998
2397
|
const cookies = {};
|
|
1999
2398
|
document.cookie.split(";").forEach((cookieStr) => {
|
|
2000
|
-
const
|
|
2399
|
+
const trimmed = cookieStr.trim();
|
|
2400
|
+
if (!trimmed) return;
|
|
2401
|
+
const eqIndex = trimmed.indexOf("=");
|
|
2402
|
+
if (eqIndex === -1) return;
|
|
2403
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
2404
|
+
const value = trimmed.substring(eqIndex + 1).trim();
|
|
2001
2405
|
if (key && value) {
|
|
2002
|
-
|
|
2406
|
+
const decodedKey = decodeURIComponent(key);
|
|
2407
|
+
const decodedValue = decodeURIComponent(value);
|
|
2408
|
+
if (decodedValue !== "") {
|
|
2409
|
+
cookies[decodedKey] = decodedValue;
|
|
2410
|
+
}
|
|
2003
2411
|
}
|
|
2004
2412
|
});
|
|
2005
2413
|
return cookies;
|
|
2006
2414
|
}
|
|
2007
2415
|
};
|
|
2008
|
-
var
|
|
2416
|
+
var secureStorage = {
|
|
2009
2417
|
/**
|
|
2010
2418
|
* 设置值(优先使用localStorage,失败则使用sessionStorage)
|
|
2011
2419
|
* @param key - 键
|
|
@@ -2020,7 +2428,9 @@ var storage = {
|
|
|
2020
2428
|
try {
|
|
2021
2429
|
await sessionStorage.set(key, value, { publicKey: options.publicKey });
|
|
2022
2430
|
} catch {
|
|
2023
|
-
|
|
2431
|
+
if (!initOptions.isProduction && typeof console !== "undefined" && console.warn) {
|
|
2432
|
+
console.warn("Both localStorage and sessionStorage are not available");
|
|
2433
|
+
}
|
|
2024
2434
|
}
|
|
2025
2435
|
}
|
|
2026
2436
|
},
|
|
@@ -2050,10 +2460,28 @@ var storage = {
|
|
|
2050
2460
|
clear() {
|
|
2051
2461
|
localStorage.clear();
|
|
2052
2462
|
sessionStorage.clear();
|
|
2463
|
+
},
|
|
2464
|
+
/**
|
|
2465
|
+
* 获取所有键名
|
|
2466
|
+
* @returns 键名数组
|
|
2467
|
+
*/
|
|
2468
|
+
keys() {
|
|
2469
|
+
const localKeys = localStorage.keys();
|
|
2470
|
+
const sessionKeys = [];
|
|
2471
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
2472
|
+
try {
|
|
2473
|
+
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
2474
|
+
const key = window.sessionStorage.key(i);
|
|
2475
|
+
if (key) sessionKeys.push(key);
|
|
2476
|
+
}
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
return Array.from(/* @__PURE__ */ new Set([...localKeys, ...sessionKeys]));
|
|
2053
2481
|
}
|
|
2054
2482
|
};
|
|
2055
2483
|
|
|
2056
|
-
// src/browser/network.ts
|
|
2484
|
+
// src/browser/network/index.ts
|
|
2057
2485
|
async function fetchWithRetry(url, options = {}, retryCount = 3, retryDelay = 1e3) {
|
|
2058
2486
|
let lastError = null;
|
|
2059
2487
|
for (let i = 0; i <= retryCount; i++) {
|
|
@@ -2150,7 +2578,7 @@ async function request(url, options = {}) {
|
|
|
2150
2578
|
return response.text();
|
|
2151
2579
|
}
|
|
2152
2580
|
|
|
2153
|
-
// src/browser/dom.ts
|
|
2581
|
+
// src/browser/dom/index.ts
|
|
2154
2582
|
function $(selector, context = document) {
|
|
2155
2583
|
return context.querySelector(selector);
|
|
2156
2584
|
}
|
|
@@ -2234,7 +2662,7 @@ async function copyToClipboard(text) {
|
|
|
2234
2662
|
}
|
|
2235
2663
|
}
|
|
2236
2664
|
|
|
2237
|
-
// src/data/transform.ts
|
|
2665
|
+
// src/data/transform/index.ts
|
|
2238
2666
|
function csvToJson(csv, options = {}) {
|
|
2239
2667
|
const { delimiter = ",", headers, skipEmptyLines = true } = options;
|
|
2240
2668
|
const lines = csv.split(/\r?\n/).filter((line) => {
|
|
@@ -2533,7 +2961,7 @@ ${valStr}`;
|
|
|
2533
2961
|
return String(value);
|
|
2534
2962
|
}
|
|
2535
2963
|
|
|
2536
|
-
// src/data/data-structure.ts
|
|
2964
|
+
// src/data/data-structure/index.ts
|
|
2537
2965
|
var Stack = class {
|
|
2538
2966
|
constructor() {
|
|
2539
2967
|
this.items = [];
|
|
@@ -3014,7 +3442,7 @@ var LRUCache = class {
|
|
|
3014
3442
|
}
|
|
3015
3443
|
};
|
|
3016
3444
|
|
|
3017
|
-
// src/data/algorithm.ts
|
|
3445
|
+
// src/data/algorithm/index.ts
|
|
3018
3446
|
function binarySearch(array, target, compareFn) {
|
|
3019
3447
|
let left = 0;
|
|
3020
3448
|
let right = array.length - 1;
|
|
@@ -3144,7 +3572,7 @@ function lcm(a, b) {
|
|
|
3144
3572
|
return Math.abs(a * b) / gcd(a, b);
|
|
3145
3573
|
}
|
|
3146
3574
|
|
|
3147
|
-
// src/helper/performance.ts
|
|
3575
|
+
// src/helper/performance/index.ts
|
|
3148
3576
|
function debounce(fn, delay) {
|
|
3149
3577
|
let timeoutId = null;
|
|
3150
3578
|
return function(...args) {
|
|
@@ -3289,10 +3717,7 @@ var Queue = class {
|
|
|
3289
3717
|
}
|
|
3290
3718
|
};
|
|
3291
3719
|
|
|
3292
|
-
// src/index.ts
|
|
3293
|
-
init_crypto();
|
|
3294
|
-
|
|
3295
|
-
// src/helper/tracking.ts
|
|
3720
|
+
// src/helper/tracking/index.ts
|
|
3296
3721
|
var Tracker = class {
|
|
3297
3722
|
constructor(options) {
|
|
3298
3723
|
this.eventQueue = [];
|
|
@@ -3941,6 +4366,8 @@ exports.addClass = addClass;
|
|
|
3941
4366
|
exports.addDays = addDays;
|
|
3942
4367
|
exports.addMonths = addMonths;
|
|
3943
4368
|
exports.addYears = addYears;
|
|
4369
|
+
exports.aesGCMDecrypt = aesGCMDecrypt;
|
|
4370
|
+
exports.aesGCMEncrypt = aesGCMEncrypt;
|
|
3944
4371
|
exports.base64Decode = base64Decode;
|
|
3945
4372
|
exports.base64Encode = base64Encode;
|
|
3946
4373
|
exports.batch = batch;
|
|
@@ -3955,7 +4382,9 @@ exports.ceil = ceil;
|
|
|
3955
4382
|
exports.checkOnline = checkOnline;
|
|
3956
4383
|
exports.chunk = chunk;
|
|
3957
4384
|
exports.clamp = clamp;
|
|
4385
|
+
exports.clearPersistedKeys = clearPersistedKeys;
|
|
3958
4386
|
exports.compact = compact;
|
|
4387
|
+
exports.computeHMAC = computeHMAC;
|
|
3959
4388
|
exports.contrast = contrast;
|
|
3960
4389
|
exports.cookie = cookie;
|
|
3961
4390
|
exports.copyToClipboard = copyToClipboard;
|
|
@@ -3968,6 +4397,7 @@ exports.debounce = debounce;
|
|
|
3968
4397
|
exports.deepClone = deepClone;
|
|
3969
4398
|
exports.deepMerge = deepMerge;
|
|
3970
4399
|
exports.defaults = defaults;
|
|
4400
|
+
exports.deriveKeyFromPassword = deriveKeyFromPassword;
|
|
3971
4401
|
exports.diffDays = diffDays;
|
|
3972
4402
|
exports.difference = difference;
|
|
3973
4403
|
exports.downloadFile = downloadFile;
|
|
@@ -3996,6 +4426,7 @@ exports.formatNumber = formatNumber;
|
|
|
3996
4426
|
exports.formatNumberI18n = formatNumberI18n;
|
|
3997
4427
|
exports.formatRelativeTime = formatRelativeTime;
|
|
3998
4428
|
exports.gcd = gcd;
|
|
4429
|
+
exports.generateHMACKey = generateHMACKey;
|
|
3999
4430
|
exports.generateRSAKeyPair = generateRSAKeyPair;
|
|
4000
4431
|
exports.generateRandomString = generateRandomString;
|
|
4001
4432
|
exports.generateUUID = generateUUID;
|
|
@@ -4004,6 +4435,7 @@ exports.getDateFormatByGMT = getDateFormatByGMT;
|
|
|
4004
4435
|
exports.getElementOffset = getElementOffset;
|
|
4005
4436
|
exports.getFileExtension = getFileExtension;
|
|
4006
4437
|
exports.getFileNameWithoutExtension = getFileNameWithoutExtension;
|
|
4438
|
+
exports.getKeyUsageCount = getKeyUsageCount;
|
|
4007
4439
|
exports.getLocale = getLocale;
|
|
4008
4440
|
exports.getQuarter = getQuarter;
|
|
4009
4441
|
exports.getQueryParams = getQueryParams;
|
|
@@ -4022,6 +4454,7 @@ exports.hslToRgb = hslToRgb;
|
|
|
4022
4454
|
exports.importPrivateKey = importPrivateKey;
|
|
4023
4455
|
exports.importPublicKey = importPublicKey;
|
|
4024
4456
|
exports.initTracker = initTracker;
|
|
4457
|
+
exports.initializeStorageKeys = initializeStorageKeys;
|
|
4025
4458
|
exports.intersection = intersection;
|
|
4026
4459
|
exports.invert = invert;
|
|
4027
4460
|
exports.isAbsoluteUrl = isAbsoluteUrl;
|
|
@@ -4054,7 +4487,6 @@ exports.kebabCase = kebabCase;
|
|
|
4054
4487
|
exports.keys = keys;
|
|
4055
4488
|
exports.lcm = lcm;
|
|
4056
4489
|
exports.lighten = lighten;
|
|
4057
|
-
exports.localStorage = localStorage;
|
|
4058
4490
|
exports.mapKeys = mapKeys;
|
|
4059
4491
|
exports.mapValues = mapValues;
|
|
4060
4492
|
exports.mask = mask;
|
|
@@ -4082,6 +4514,7 @@ exports.removeAccents = removeAccents;
|
|
|
4082
4514
|
exports.removeClass = removeClass;
|
|
4083
4515
|
exports.removeQueryParams = removeQueryParams;
|
|
4084
4516
|
exports.request = request;
|
|
4517
|
+
exports.resetKeyUsageCount = resetKeyUsageCount;
|
|
4085
4518
|
exports.retry = retry;
|
|
4086
4519
|
exports.rgbToHex = rgbToHex;
|
|
4087
4520
|
exports.rgbToHsl = rgbToHsl;
|
|
@@ -4090,7 +4523,7 @@ exports.rsaDecrypt = rsaDecrypt;
|
|
|
4090
4523
|
exports.rsaEncrypt = rsaEncrypt;
|
|
4091
4524
|
exports.sample = sample;
|
|
4092
4525
|
exports.scrollTo = scrollTo;
|
|
4093
|
-
exports.
|
|
4526
|
+
exports.secureStorage = secureStorage;
|
|
4094
4527
|
exports.set = set;
|
|
4095
4528
|
exports.setCommonParams = setCommonParams;
|
|
4096
4529
|
exports.setQueryParams = setQueryParams;
|
|
@@ -4105,7 +4538,6 @@ exports.snakeCase = snakeCase;
|
|
|
4105
4538
|
exports.sortBy = sortBy;
|
|
4106
4539
|
exports.splitFileIntoChunks = splitFileIntoChunks;
|
|
4107
4540
|
exports.startOfDay = startOfDay;
|
|
4108
|
-
exports.storage = storage;
|
|
4109
4541
|
exports.take = take;
|
|
4110
4542
|
exports.takeWhile = takeWhile;
|
|
4111
4543
|
exports.template = template;
|
|
@@ -4127,6 +4559,7 @@ exports.unzip = unzip;
|
|
|
4127
4559
|
exports.updateQueryParams = updateQueryParams;
|
|
4128
4560
|
exports.uploadFile = uploadFile;
|
|
4129
4561
|
exports.values = values;
|
|
4562
|
+
exports.verifyHMAC = verifyHMAC;
|
|
4130
4563
|
exports.xmlToJson = xmlToJson;
|
|
4131
4564
|
exports.yamlToJson = yamlToJson;
|
|
4132
4565
|
exports.zip = zip;
|