n8n-nodes-nvk-browser 1.0.76 → 1.0.78

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.
@@ -51,7 +51,8 @@ exports.createProfileFields = [
51
51
  rows: 4,
52
52
  },
53
53
  default: '',
54
- description: 'Chrome Store extension IDs or local paths, one per line',
54
+ placeholder: 'nmmhkkegccagdldgiimedpiccmgmieda\nhttps://example.com/extension.crx\n./extensions/my-extension',
55
+ description: 'Extensions to install: Chrome Store ID (32 chars), URL (http://...), or local path. One extension per line. Examples:\n- Chrome Store ID: nmmhkkegccagdldgiimedpiccmgmieda\n- URL: https://example.com/extension.crx\n- Local path: ./extensions/my-extension or /absolute/path/to/extension',
55
56
  displayOptions: {
56
57
  show: {
57
58
  resource: ['profile'],
@@ -95,8 +95,29 @@ class CreateProfile {
95
95
  const extensionsInput = this.getNodeParameter('extensions', i) || '';
96
96
  // Parse extensions
97
97
  const extensions = ExtensionHandler_1.ExtensionHandler.parseExtensions(extensionsInput);
98
- // Tạo profile
99
- const profile = profileManager.createProfile(profileName, proxy, note, extensions);
98
+ // Validate extensions và lấy resolved paths
99
+ const extensionPaths = [];
100
+ if (extensions.length > 0) {
101
+ console.log(`[CreateProfile] Validating ${extensions.length} extension(s)...`);
102
+ for (const ext of extensions) {
103
+ try {
104
+ const validation = await ExtensionHandler_1.ExtensionHandler.validateExtension(ext, resolvedProfilesDir);
105
+ if (validation.valid && validation.path) {
106
+ extensionPaths.push(validation.path);
107
+ console.log(`[CreateProfile] ✓ Extension validated: ${ext} -> ${validation.path}`);
108
+ }
109
+ else {
110
+ console.error(`[CreateProfile] ✗ Extension validation failed: ${ext} - ${validation.error || 'Unknown error'}`);
111
+ }
112
+ }
113
+ catch (error) {
114
+ console.error(`[CreateProfile] ✗ Error validating extension "${ext}":`, error);
115
+ }
116
+ }
117
+ console.log(`[CreateProfile] Successfully validated ${extensionPaths.length}/${extensions.length} extension(s)`);
118
+ }
119
+ // Tạo profile với cả extensionPaths đã resolve
120
+ const profile = profileManager.createProfile(profileName, proxy, note, extensions, extensionPaths);
100
121
  // Lưu proxy authentication vào profile nếu có
101
122
  // QUAN TRỌNG: Phải gọi SAU khi createProfile và setProfileName để Preferences file đã được tạo
102
123
  // Nhưng phải đảm bảo không bị ghi đè
@@ -180,6 +201,7 @@ class CreateProfile {
180
201
  proxy: profile.proxy,
181
202
  note: profile.note,
182
203
  extensions: profile.extensions,
204
+ extensionPaths: profile.extensionPaths,
183
205
  createdAt: profile.createdAt,
184
206
  },
185
207
  },
@@ -97,8 +97,38 @@ class BrowserManager {
97
97
  args.push(...ProxyHandler_1.ProxyHandler.getChromeProxyArgs(proxyConfig));
98
98
  }
99
99
  // Extension configuration
100
- if (profile.extensions && profile.extensions.length > 0) {
101
- args.push(...ExtensionHandler_1.ExtensionHandler.getExtensionArgs(profile.extensions));
100
+ // Sử dụng extensionPaths đã resolve thay vì validate lại
101
+ if (profile.extensionPaths && profile.extensionPaths.length > 0) {
102
+ // Kiểm tra paths còn tồn tại không
103
+ const validPaths = [];
104
+ for (const extPath of profile.extensionPaths) {
105
+ const manifestPath = path.join(extPath, 'manifest.json');
106
+ if (fs.existsSync(manifestPath)) {
107
+ validPaths.push(extPath);
108
+ }
109
+ else {
110
+ console.warn(`[BrowserManager] Extension path no longer valid (missing manifest.json): ${extPath}`);
111
+ }
112
+ }
113
+ if (validPaths.length > 0) {
114
+ args.push(`--load-extension=${validPaths.join(',')}`);
115
+ console.log(`[BrowserManager] Loading ${validPaths.length} extension(s) from cached paths`);
116
+ }
117
+ else {
118
+ console.warn(`[BrowserManager] No valid extension paths found, skipping extensions`);
119
+ }
120
+ }
121
+ else if (profile.extensions && profile.extensions.length > 0) {
122
+ // Fallback: Nếu không có extensionPaths, validate lại (cho backward compatibility)
123
+ console.log(`[BrowserManager] No extensionPaths found, validating extensions on-the-fly (legacy mode)`);
124
+ try {
125
+ const extensionArgs = await ExtensionHandler_1.ExtensionHandler.getExtensionArgs(profile.extensions, this.profilesDir);
126
+ args.push(...extensionArgs);
127
+ }
128
+ catch (error) {
129
+ console.error(`[BrowserManager] Error loading extensions:`, error);
130
+ // Continue without extensions rather than failing completely
131
+ }
102
132
  }
103
133
  // Window configuration
104
134
  if (windowConfig) {
@@ -1,9 +1,70 @@
1
+ interface ExtensionValidationResult {
2
+ valid: boolean;
3
+ path?: string;
4
+ error?: string;
5
+ type?: 'store-id' | 'url' | 'local-path';
6
+ }
1
7
  export declare class ExtensionHandler {
8
+ private static readonly CACHE_DIR_NAME;
9
+ private static readonly DOWNLOAD_TIMEOUT;
10
+ private static readonly MAX_RETRIES;
11
+ /**
12
+ * Parse extensions từ input string (mỗi extension một dòng)
13
+ */
2
14
  static parseExtensions(extensionInput: string): string[];
3
- static validateExtension(extension: string): {
4
- valid: boolean;
5
- path?: string;
6
- error?: string;
7
- };
8
- static getExtensionArgs(extensions: string[]): string[];
15
+ /**
16
+ * Xác định loại extension: Chrome Store ID, URL, hoặc Local Path
17
+ */
18
+ private static detectExtensionType;
19
+ /**
20
+ * Validate resolve extension path
21
+ */
22
+ static validateExtension(extension: string, workspacePath?: string): Promise<ExtensionValidationResult>;
23
+ /**
24
+ * Lấy cache directory path
25
+ */
26
+ private static getCacheDir;
27
+ /**
28
+ * Đảm bảo cache directory tồn tại
29
+ */
30
+ private static ensureCacheDir;
31
+ /**
32
+ * Tạo cache key từ extension identifier
33
+ */
34
+ private static getCacheKey;
35
+ /**
36
+ * Kiểm tra extension đã được cache chưa
37
+ */
38
+ private static getCachedExtensionPath;
39
+ /**
40
+ * Download file từ URL
41
+ */
42
+ private static downloadFile;
43
+ /**
44
+ * Extract CRX file
45
+ * CRX format: Header (magic number + version + pub key len + sig len + pub key + sig) + ZIP content
46
+ */
47
+ private static extractCrxFile;
48
+ /**
49
+ * Extract ZIP buffer to directory
50
+ */
51
+ private static extractZipBuffer;
52
+ /**
53
+ * Manual ZIP extraction
54
+ * Supports stored (method 0) and deflated (method 8) compression
55
+ */
56
+ private static extractZipManually;
57
+ /**
58
+ * Download extension từ Chrome Web Store
59
+ */
60
+ private static downloadAndExtractChromeStoreExtension;
61
+ /**
62
+ * Download và extract extension từ URL
63
+ */
64
+ private static downloadAndExtractExtensionFromUrl;
65
+ /**
66
+ * Get Chrome arguments để load extensions
67
+ */
68
+ static getExtensionArgs(extensions: string[], workspacePath?: string): Promise<string[]>;
9
69
  }
70
+ export {};
@@ -26,7 +26,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.ExtensionHandler = void 0;
27
27
  const fs = __importStar(require("fs"));
28
28
  const path = __importStar(require("path"));
29
+ const https = __importStar(require("https"));
30
+ const http = __importStar(require("http"));
31
+ const zlib = __importStar(require("zlib"));
32
+ const crypto = __importStar(require("crypto"));
29
33
  class ExtensionHandler {
34
+ /**
35
+ * Parse extensions từ input string (mỗi extension một dòng)
36
+ */
30
37
  static parseExtensions(extensionInput) {
31
38
  if (!extensionInput || !extensionInput.trim()) {
32
39
  return [];
@@ -38,49 +45,500 @@ class ExtensionHandler {
38
45
  .filter(line => line.length > 0);
39
46
  return lines;
40
47
  }
41
- static validateExtension(extension) {
42
- // Nếu ID từ Chrome Store (thường 32 tự alphanumeric)
43
- if (/^[a-z]{32}$/i.test(extension)) {
44
- return { valid: true };
45
- }
46
- // Nếu là đường dẫn local
47
- if (path.isAbsolute(extension) || extension.startsWith('./') || extension.startsWith('../')) {
48
- const resolvedPath = path.resolve(extension);
49
- if (fs.existsSync(resolvedPath)) {
50
- // Kiểm tra xem có phải là extension directory không
51
- const manifestPath = path.join(resolvedPath, 'manifest.json');
52
- if (fs.existsSync(manifestPath)) {
53
- return { valid: true, path: resolvedPath };
48
+ /**
49
+ * Xác định loại extension: Chrome Store ID, URL, hoặc Local Path
50
+ */
51
+ static detectExtensionType(extension) {
52
+ // Chrome Store ID: 32 ký tự alphanumeric
53
+ if (/^[a-z0-9]{32}$/i.test(extension)) {
54
+ return 'store-id';
55
+ }
56
+ // URL: bắt đầu với http:// hoặc https://
57
+ if (/^https?:\/\//i.test(extension)) {
58
+ return 'url';
59
+ }
60
+ // Local path
61
+ return 'local-path';
62
+ }
63
+ /**
64
+ * Validate và resolve extension path
65
+ */
66
+ static async validateExtension(extension, workspacePath) {
67
+ const type = this.detectExtensionType(extension);
68
+ if (type === 'store-id') {
69
+ // Chrome Store ID - cần download
70
+ try {
71
+ const extensionPath = await this.downloadAndExtractChromeStoreExtension(extension, workspacePath);
72
+ return { valid: true, path: extensionPath, type: 'store-id' };
73
+ }
74
+ catch (error) {
75
+ return {
76
+ valid: false,
77
+ error: `Failed to download Chrome Store extension: ${error instanceof Error ? error.message : String(error)}`,
78
+ type: 'store-id',
79
+ };
80
+ }
81
+ }
82
+ if (type === 'url') {
83
+ // URL - cần download
84
+ try {
85
+ const extensionPath = await this.downloadAndExtractExtensionFromUrl(extension, workspacePath);
86
+ return { valid: true, path: extensionPath, type: 'url' };
87
+ }
88
+ catch (error) {
89
+ return {
90
+ valid: false,
91
+ error: `Failed to download extension from URL: ${error instanceof Error ? error.message : String(error)}`,
92
+ type: 'url',
93
+ };
94
+ }
95
+ }
96
+ // Local path
97
+ let resolvedPath;
98
+ if (path.isAbsolute(extension)) {
99
+ resolvedPath = extension;
100
+ }
101
+ else if (extension.startsWith('./') || extension.startsWith('../')) {
102
+ resolvedPath = path.resolve(extension);
103
+ }
104
+ else {
105
+ // Relative path từ workspace
106
+ const basePath = workspacePath || path.resolve(__dirname, '..');
107
+ resolvedPath = path.resolve(basePath, extension);
108
+ }
109
+ if (!fs.existsSync(resolvedPath)) {
110
+ return {
111
+ valid: false,
112
+ error: `Extension path does not exist: ${resolvedPath}`,
113
+ type: 'local-path',
114
+ };
115
+ }
116
+ // Kiểm tra xem có phải là extension directory không
117
+ const manifestPath = path.join(resolvedPath, 'manifest.json');
118
+ if (fs.existsSync(manifestPath)) {
119
+ return { valid: true, path: resolvedPath, type: 'local-path' };
120
+ }
121
+ // Có thể là CRX file - cần extract
122
+ if (resolvedPath.toLowerCase().endsWith('.crx')) {
123
+ try {
124
+ const extractedPath = await this.extractCrxFile(resolvedPath, workspacePath);
125
+ return { valid: true, path: extractedPath, type: 'local-path' };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ valid: false,
130
+ error: `Failed to extract CRX file: ${error instanceof Error ? error.message : String(error)}`,
131
+ type: 'local-path',
132
+ };
133
+ }
134
+ }
135
+ return {
136
+ valid: false,
137
+ error: 'Extension directory does not contain manifest.json',
138
+ type: 'local-path',
139
+ };
140
+ }
141
+ /**
142
+ * Lấy cache directory path
143
+ */
144
+ static getCacheDir(workspacePath) {
145
+ const basePath = workspacePath || path.resolve(__dirname, '..');
146
+ return path.join(basePath, '..', this.CACHE_DIR_NAME);
147
+ }
148
+ /**
149
+ * Đảm bảo cache directory tồn tại
150
+ */
151
+ static ensureCacheDir(workspacePath) {
152
+ const cacheDir = this.getCacheDir(workspacePath);
153
+ if (!fs.existsSync(cacheDir)) {
154
+ fs.mkdirSync(cacheDir, { recursive: true });
155
+ }
156
+ return cacheDir;
157
+ }
158
+ /**
159
+ * Tạo cache key từ extension identifier
160
+ */
161
+ static getCacheKey(identifier) {
162
+ return crypto.createHash('md5').update(identifier).digest('hex');
163
+ }
164
+ /**
165
+ * Kiểm tra extension đã được cache chưa
166
+ */
167
+ static getCachedExtensionPath(cacheKey, workspacePath) {
168
+ const cacheDir = this.getCacheDir(workspacePath);
169
+ const cachedPath = path.join(cacheDir, cacheKey);
170
+ const manifestPath = path.join(cachedPath, 'manifest.json');
171
+ if (fs.existsSync(manifestPath)) {
172
+ console.log(`[ExtensionHandler] Using cached extension: ${cachedPath}`);
173
+ return cachedPath;
174
+ }
175
+ return null;
176
+ }
177
+ /**
178
+ * Download file từ URL
179
+ */
180
+ static async downloadFile(url, outputPath, retries = this.MAX_RETRIES) {
181
+ return new Promise((resolve, reject) => {
182
+ const attemptDownload = (downloadUrl, attempt) => {
183
+ console.log(`[ExtensionHandler] Downloading ${downloadUrl} (attempt ${attempt}/${retries})...`);
184
+ const file = fs.createWriteStream(outputPath);
185
+ const protocol = downloadUrl.startsWith('https:') ? https : http;
186
+ const request = protocol.get(downloadUrl, (response) => {
187
+ // Handle redirects
188
+ if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
189
+ file.close();
190
+ fs.unlinkSync(outputPath);
191
+ if (response.headers.location) {
192
+ const redirectUrl = response.headers.location.startsWith('http')
193
+ ? response.headers.location
194
+ : new URL(response.headers.location, downloadUrl).href;
195
+ return attemptDownload(redirectUrl, attempt);
196
+ }
197
+ }
198
+ if (response.statusCode !== 200) {
199
+ file.close();
200
+ fs.unlinkSync(outputPath);
201
+ if (attempt < retries) {
202
+ return attemptDownload(downloadUrl, attempt + 1);
203
+ }
204
+ return reject(new Error(`Download failed with status ${response.statusCode}`));
205
+ }
206
+ response.pipe(file);
207
+ file.on('finish', () => {
208
+ file.close();
209
+ console.log(`[ExtensionHandler] Downloaded to ${outputPath}`);
210
+ resolve();
211
+ });
212
+ });
213
+ request.on('error', (error) => {
214
+ file.close();
215
+ if (fs.existsSync(outputPath)) {
216
+ fs.unlinkSync(outputPath);
217
+ }
218
+ if (attempt < retries) {
219
+ console.log(`[ExtensionHandler] Download error, retrying... (${error.message})`);
220
+ setTimeout(() => attemptDownload(downloadUrl, attempt + 1), 1000 * attempt);
221
+ }
222
+ else {
223
+ reject(error);
224
+ }
225
+ });
226
+ request.setTimeout(this.DOWNLOAD_TIMEOUT, () => {
227
+ request.destroy();
228
+ file.close();
229
+ if (fs.existsSync(outputPath)) {
230
+ fs.unlinkSync(outputPath);
231
+ }
232
+ if (attempt < retries) {
233
+ attemptDownload(downloadUrl, attempt + 1);
234
+ }
235
+ else {
236
+ reject(new Error('Download timeout'));
237
+ }
238
+ });
239
+ };
240
+ attemptDownload(url, 1);
241
+ });
242
+ }
243
+ /**
244
+ * Extract CRX file
245
+ * CRX format: Header (magic number + version + pub key len + sig len + pub key + sig) + ZIP content
246
+ */
247
+ static async extractCrxFile(crxPath, workspacePath) {
248
+ const cacheKey = this.getCacheKey(`local-crx-${crxPath}`);
249
+ const cachedPath = this.getCachedExtensionPath(cacheKey, workspacePath);
250
+ if (cachedPath) {
251
+ return cachedPath;
252
+ }
253
+ console.log(`[ExtensionHandler] Extracting CRX file: ${crxPath}`);
254
+ const cacheDir = this.ensureCacheDir(workspacePath);
255
+ const outputDir = path.join(cacheDir, cacheKey);
256
+ if (fs.existsSync(outputDir)) {
257
+ fs.rmSync(outputDir, { recursive: true, force: true });
258
+ }
259
+ fs.mkdirSync(outputDir, { recursive: true });
260
+ const crxBuffer = fs.readFileSync(crxPath);
261
+ // Check magic number (Cr24 = 0x43723234)
262
+ const magic = crxBuffer.readUInt32LE(0);
263
+ if (magic !== 0x34327243) {
264
+ throw new Error('Invalid CRX file: wrong magic number');
265
+ }
266
+ // Read version (4 bytes at offset 4)
267
+ const version = crxBuffer.readUInt32LE(4);
268
+ if (version === 2) {
269
+ // CRX v2: magic (4) + version (4) + pub key len (4) + sig len (4) + pub key + sig + zip
270
+ const pubKeyLen = crxBuffer.readUInt32LE(8);
271
+ const sigLen = crxBuffer.readUInt32LE(12);
272
+ const headerSize = 16 + pubKeyLen + sigLen;
273
+ const zipBuffer = crxBuffer.slice(headerSize);
274
+ await this.extractZipBuffer(zipBuffer, outputDir);
275
+ }
276
+ else if (version === 3) {
277
+ // CRX v3: similar but with additional header
278
+ const headerSize = crxBuffer.readUInt32LE(8);
279
+ const zipBuffer = crxBuffer.slice(headerSize);
280
+ await this.extractZipBuffer(zipBuffer, outputDir);
281
+ }
282
+ else {
283
+ // Try to find ZIP header manually (PK\x03\x04)
284
+ let zipStart = -1;
285
+ for (let i = 0; i < crxBuffer.length - 4; i++) {
286
+ if (crxBuffer[i] === 0x50 &&
287
+ crxBuffer[i + 1] === 0x4b &&
288
+ crxBuffer[i + 2] === 0x03 &&
289
+ crxBuffer[i + 3] === 0x04) {
290
+ zipStart = i;
291
+ break;
54
292
  }
55
- return { valid: false, error: 'Extension directory does not contain manifest.json' };
56
293
  }
57
- return { valid: false, error: 'Extension path does not exist' };
294
+ if (zipStart === -1) {
295
+ throw new Error('Invalid CRX file: ZIP content not found');
296
+ }
297
+ const zipBuffer = crxBuffer.slice(zipStart);
298
+ await this.extractZipBuffer(zipBuffer, outputDir);
299
+ }
300
+ // Validate manifest.json
301
+ const manifestPath = path.join(outputDir, 'manifest.json');
302
+ if (!fs.existsSync(manifestPath)) {
303
+ throw new Error('Extracted extension does not contain manifest.json');
58
304
  }
59
- // Nếu relative path từ workspace
60
- const workspacePath = path.resolve(__dirname, '..', extension);
61
- if (fs.existsSync(workspacePath)) {
62
- const manifestPath = path.join(workspacePath, 'manifest.json');
63
- if (fs.existsSync(manifestPath)) {
64
- return { valid: true, path: workspacePath };
305
+ console.log(`[ExtensionHandler] Extracted CRX to: ${outputDir}`);
306
+ return outputDir;
307
+ }
308
+ /**
309
+ * Extract ZIP buffer to directory
310
+ */
311
+ static async extractZipBuffer(zipBuffer, outputDir) {
312
+ // ZIP files are not zlib-compressed, they have their own format
313
+ // Use manual extraction
314
+ return this.extractZipManually(zipBuffer, outputDir);
315
+ }
316
+ /**
317
+ * Manual ZIP extraction
318
+ * Supports stored (method 0) and deflated (method 8) compression
319
+ */
320
+ static async extractZipManually(zipBuffer, outputDir) {
321
+ const files = [];
322
+ let offset = 0;
323
+ // First pass: collect all file entries
324
+ while (offset < zipBuffer.length - 4) {
325
+ // Look for local file header signature (0x04034b50 = "PK\x03\x04")
326
+ if (zipBuffer[offset] !== 0x50 || zipBuffer[offset + 1] !== 0x4b) {
327
+ offset++;
328
+ continue;
329
+ }
330
+ const signature = zipBuffer.readUInt32LE(offset);
331
+ if (signature !== 0x04034b50) {
332
+ offset++;
333
+ continue;
334
+ }
335
+ // Read file header
336
+ const version = zipBuffer.readUInt16LE(offset + 4);
337
+ const flags = zipBuffer.readUInt16LE(offset + 6);
338
+ const method = zipBuffer.readUInt16LE(offset + 8);
339
+ const compressedSize = zipBuffer.readUInt32LE(offset + 18);
340
+ const uncompressedSize = zipBuffer.readUInt32LE(offset + 22);
341
+ const fileNameLength = zipBuffer.readUInt16LE(offset + 26);
342
+ const extraFieldLength = zipBuffer.readUInt16LE(offset + 28);
343
+ if (offset + 30 + fileNameLength + extraFieldLength > zipBuffer.length) {
344
+ break;
65
345
  }
66
- return { valid: false, error: 'Extension directory does not contain manifest.json' };
346
+ const fileName = zipBuffer
347
+ .slice(offset + 30, offset + 30 + fileNameLength)
348
+ .toString('utf-8')
349
+ .replace(/\\/g, '/'); // Normalize path separators
350
+ const dataOffset = offset + 30 + fileNameLength + extraFieldLength;
351
+ if (dataOffset + compressedSize > zipBuffer.length) {
352
+ break;
353
+ }
354
+ const fileData = zipBuffer.slice(dataOffset, dataOffset + compressedSize);
355
+ if (!fileName.endsWith('/')) {
356
+ files.push({
357
+ name: fileName,
358
+ data: fileData,
359
+ method: method,
360
+ });
361
+ }
362
+ offset = dataOffset + compressedSize;
363
+ }
364
+ // Second pass: extract files
365
+ for (const file of files) {
366
+ try {
367
+ const filePath = path.join(outputDir, file.name);
368
+ const dir = path.dirname(filePath);
369
+ // Skip if path traversal attempt
370
+ if (!filePath.startsWith(path.resolve(outputDir))) {
371
+ console.warn(`[ExtensionHandler] Skipping potentially dangerous path: ${file.name}`);
372
+ continue;
373
+ }
374
+ if (!fs.existsSync(dir)) {
375
+ fs.mkdirSync(dir, { recursive: true });
376
+ }
377
+ let extractedData;
378
+ if (file.method === 0) {
379
+ // Stored (no compression)
380
+ extractedData = file.data;
381
+ }
382
+ else if (file.method === 8) {
383
+ // Deflated - use zlib
384
+ try {
385
+ extractedData = zlib.inflateRawSync(file.data);
386
+ }
387
+ catch (error) {
388
+ console.warn(`[ExtensionHandler] Failed to inflate ${file.name}, trying stored method`);
389
+ extractedData = file.data;
390
+ }
391
+ }
392
+ else {
393
+ console.warn(`[ExtensionHandler] Unsupported compression method ${file.method} for ${file.name}, skipping`);
394
+ continue;
395
+ }
396
+ fs.writeFileSync(filePath, extractedData);
397
+ }
398
+ catch (error) {
399
+ console.warn(`[ExtensionHandler] Error extracting file ${file.name}:`, error);
400
+ // Continue with other files
401
+ }
402
+ }
403
+ if (files.length === 0) {
404
+ throw new Error('No valid files found in ZIP archive');
67
405
  }
68
- return { valid: false, error: 'Invalid extension format. Must be Chrome Store ID or valid path' };
69
406
  }
70
- static getExtensionArgs(extensions) {
407
+ /**
408
+ * Download extension từ Chrome Web Store
409
+ */
410
+ static async downloadAndExtractChromeStoreExtension(extensionId, workspacePath) {
411
+ const cacheKey = this.getCacheKey(extensionId);
412
+ const cachedPath = this.getCachedExtensionPath(cacheKey, workspacePath);
413
+ if (cachedPath) {
414
+ return cachedPath;
415
+ }
416
+ console.log(`[ExtensionHandler] Downloading Chrome Store extension: ${extensionId}`);
417
+ const cacheDir = this.ensureCacheDir(workspacePath);
418
+ const tempCrxPath = path.join(cacheDir, `${cacheKey}.crx`);
419
+ const outputDir = path.join(cacheDir, cacheKey);
420
+ try {
421
+ // Chrome Web Store download URL
422
+ const downloadUrl = `https://clients2.google.com/service/update2/crx?response=redirect&x=id%3D${extensionId}%26uc`;
423
+ // Download CRX file
424
+ await this.downloadFile(downloadUrl, tempCrxPath);
425
+ // Extract CRX
426
+ const extractedPath = await this.extractCrxFile(tempCrxPath, workspacePath);
427
+ // Move to cache directory with proper name
428
+ if (extractedPath !== outputDir) {
429
+ if (fs.existsSync(outputDir)) {
430
+ fs.rmSync(outputDir, { recursive: true, force: true });
431
+ }
432
+ fs.renameSync(extractedPath, outputDir);
433
+ }
434
+ // Clean up temp CRX file
435
+ if (fs.existsSync(tempCrxPath)) {
436
+ fs.unlinkSync(tempCrxPath);
437
+ }
438
+ return outputDir;
439
+ }
440
+ catch (error) {
441
+ // Clean up on error
442
+ if (fs.existsSync(tempCrxPath)) {
443
+ fs.unlinkSync(tempCrxPath);
444
+ }
445
+ if (fs.existsSync(outputDir)) {
446
+ fs.rmSync(outputDir, { recursive: true, force: true });
447
+ }
448
+ throw error;
449
+ }
450
+ }
451
+ /**
452
+ * Download và extract extension từ URL
453
+ */
454
+ static async downloadAndExtractExtensionFromUrl(url, workspacePath) {
455
+ const cacheKey = this.getCacheKey(url);
456
+ const cachedPath = this.getCachedExtensionPath(cacheKey, workspacePath);
457
+ if (cachedPath) {
458
+ return cachedPath;
459
+ }
460
+ console.log(`[ExtensionHandler] Downloading extension from URL: ${url}`);
461
+ const cacheDir = this.ensureCacheDir(workspacePath);
462
+ const tempFilePath = path.join(cacheDir, `${cacheKey}.tmp`);
463
+ const outputDir = path.join(cacheDir, cacheKey);
464
+ try {
465
+ // Download file
466
+ await this.downloadFile(url, tempFilePath);
467
+ // Determine file type and extract
468
+ const fullBuffer = fs.readFileSync(tempFilePath);
469
+ const fileHeader = fullBuffer.slice(0, 4);
470
+ const headerStr = fileHeader.toString('ascii', 0, 4);
471
+ const isCrx = url.toLowerCase().endsWith('.crx') || headerStr === 'Cr24';
472
+ if (isCrx || tempFilePath.toLowerCase().endsWith('.crx')) {
473
+ // Extract CRX
474
+ const extractedPath = await this.extractCrxFile(tempFilePath, workspacePath);
475
+ if (extractedPath !== outputDir) {
476
+ if (fs.existsSync(outputDir)) {
477
+ fs.rmSync(outputDir, { recursive: true, force: true });
478
+ }
479
+ fs.renameSync(extractedPath, outputDir);
480
+ }
481
+ }
482
+ else {
483
+ // Try as ZIP
484
+ const zipBuffer = fs.readFileSync(tempFilePath);
485
+ if (!fs.existsSync(outputDir)) {
486
+ fs.mkdirSync(outputDir, { recursive: true });
487
+ }
488
+ await this.extractZipManually(zipBuffer, outputDir);
489
+ // Validate manifest.json
490
+ const manifestPath = path.join(outputDir, 'manifest.json');
491
+ if (!fs.existsSync(manifestPath)) {
492
+ throw new Error('Downloaded file does not contain manifest.json');
493
+ }
494
+ }
495
+ // Clean up temp file
496
+ if (fs.existsSync(tempFilePath)) {
497
+ fs.unlinkSync(tempFilePath);
498
+ }
499
+ return outputDir;
500
+ }
501
+ catch (error) {
502
+ // Clean up on error
503
+ if (fs.existsSync(tempFilePath)) {
504
+ fs.unlinkSync(tempFilePath);
505
+ }
506
+ if (fs.existsSync(outputDir)) {
507
+ fs.rmSync(outputDir, { recursive: true, force: true });
508
+ }
509
+ throw error;
510
+ }
511
+ }
512
+ /**
513
+ * Get Chrome arguments để load extensions
514
+ */
515
+ static async getExtensionArgs(extensions, workspacePath) {
71
516
  const args = [];
72
517
  const validPaths = [];
73
518
  for (const ext of extensions) {
74
- const validation = this.validateExtension(ext);
75
- if (validation.valid && validation.path) {
76
- validPaths.push(validation.path);
519
+ try {
520
+ console.log(`[ExtensionHandler] Processing extension: ${ext}`);
521
+ const validation = await this.validateExtension(ext, workspacePath);
522
+ if (validation.valid && validation.path) {
523
+ validPaths.push(validation.path);
524
+ console.log(`[ExtensionHandler] ✓ Extension validated: ${validation.path}`);
525
+ }
526
+ else {
527
+ console.error(`[ExtensionHandler] ✗ Extension validation failed: ${validation.error}`);
528
+ }
529
+ }
530
+ catch (error) {
531
+ console.error(`[ExtensionHandler] ✗ Error processing extension "${ext}":`, error);
77
532
  }
78
- // Chrome Store IDs sẽ được load sau khi browser khởi động
79
533
  }
80
534
  if (validPaths.length > 0) {
81
535
  args.push(`--load-extension=${validPaths.join(',')}`);
536
+ console.log(`[ExtensionHandler] Load extensions: ${validPaths.length} extension(s)`);
82
537
  }
83
538
  return args;
84
539
  }
85
540
  }
541
+ ExtensionHandler.CACHE_DIR_NAME = 'extensions-cache';
542
+ ExtensionHandler.DOWNLOAD_TIMEOUT = 30000; // 30 seconds
543
+ ExtensionHandler.MAX_RETRIES = 3;
86
544
  exports.ExtensionHandler = ExtensionHandler;
@@ -5,7 +5,7 @@ export declare class ProfileManager {
5
5
  private ensureProfilesDir;
6
6
  private getProfileMetadataPath;
7
7
  private getProfileDir;
8
- createProfile(name: string, proxy?: string, note?: string, extensions?: string[]): ProfileData;
8
+ createProfile(name: string, proxy?: string, note?: string, extensions?: string[], extensionPaths?: string[]): ProfileData;
9
9
  private setProfileName;
10
10
  getProfile(profileId: string): ProfileData | null;
11
11
  getAllProfiles(): ProfileData[];
@@ -43,7 +43,7 @@ class ProfileManager {
43
43
  getProfileDir(profileId) {
44
44
  return path.join(this.profilesDir, profileId);
45
45
  }
46
- createProfile(name, proxy, note, extensions) {
46
+ createProfile(name, proxy, note, extensions, extensionPaths) {
47
47
  const profileId = (0, uuid_1.v4)();
48
48
  const profileDir = this.getProfileDir(profileId);
49
49
  // Tạo thư mục profile
@@ -54,6 +54,7 @@ class ProfileManager {
54
54
  proxy,
55
55
  note,
56
56
  extensions: extensions || [],
57
+ extensionPaths: extensionPaths || [],
57
58
  createdAt: new Date().toISOString(),
58
59
  updatedAt: new Date().toISOString(),
59
60
  };
@@ -4,6 +4,7 @@ export interface ProfileData {
4
4
  proxy?: string;
5
5
  note?: string;
6
6
  extensions?: string[];
7
+ extensionPaths?: string[];
7
8
  createdAt: string;
8
9
  updatedAt: string;
9
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-nvk-browser",
3
- "version": "1.0.76",
3
+ "version": "1.0.78",
4
4
  "description": "n8n nodes for managing Chrome browser profiles and page interactions with Puppeteer automation",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",