extension-from-store 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import * as __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__ from "node:fs/promises";
2
- import * as __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__ from "node:path";
3
2
  import * as __WEBPACK_EXTERNAL_MODULE_node_os_74b4b876__ from "node:os";
3
+ import * as __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__ from "node:path";
4
+ import * as __WEBPACK_EXTERNAL_MODULE_extract_zip_c4acec5a__ from "extract-zip";
4
5
  import * as __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__ from "node:fs";
5
6
  import * as __WEBPACK_EXTERNAL_MODULE_node_http_2dc67212__ from "node:http";
6
7
  import * as __WEBPACK_EXTERNAL_MODULE_node_https_626f33a7__ from "node:https";
7
8
  import * as __WEBPACK_EXTERNAL_MODULE_node_stream_promises_5adae1f2__ from "node:stream/promises";
8
9
  import * as __WEBPACK_EXTERNAL_MODULE_node_url_e96de089__ from "node:url";
9
- import * as __WEBPACK_EXTERNAL_MODULE_extract_zip_c4acec5a__ from "extract-zip";
10
10
  function _define_property(obj, key, value) {
11
11
  if (key in obj) Object.defineProperty(obj, key, {
12
12
  value: value,
@@ -24,6 +24,48 @@ class errors_extensionFromStoreError extends Error {
24
24
  this.cause = cause;
25
25
  }
26
26
  }
27
+ function readUInt32LE(bytes, offset) {
28
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset, Uint32Array.BYTES_PER_ELEMENT);
29
+ return view.getUint32(0, true);
30
+ }
31
+ function readMagic(bytes) {
32
+ let out = '';
33
+ const slice = bytes.subarray(0, 4);
34
+ for(let index = 0; index < slice.length; index += 1)out += String.fromCharCode(slice[index]);
35
+ return out;
36
+ }
37
+ function stripCrxHeader(buffer) {
38
+ if (buffer.length < 16) throw new errors_extensionFromStoreError('ExtractionFailed', 'CRX file too small');
39
+ const magic = readMagic(buffer);
40
+ if ('Cr24' !== magic) throw new errors_extensionFromStoreError('ExtractionFailed', 'Invalid CRX header');
41
+ const version = readUInt32LE(buffer, 4);
42
+ if (2 === version) {
43
+ const publicKeyLength = readUInt32LE(buffer, 8);
44
+ const signatureLength = readUInt32LE(buffer, 12);
45
+ const headerSize = 16 + publicKeyLength + signatureLength;
46
+ return buffer.subarray(headerSize);
47
+ }
48
+ if (3 === version) {
49
+ const headerSize = readUInt32LE(buffer, 8);
50
+ return buffer.subarray(12 + headerSize);
51
+ }
52
+ throw new errors_extensionFromStoreError('ExtractionFailed', `Unsupported CRX version ${version}`);
53
+ }
54
+ async function extractCrx(crxPath, extractDir, workDir) {
55
+ const crxBuffer = await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].readFile(crxPath);
56
+ const zipBuffer = Buffer.from(stripCrxHeader(crxBuffer));
57
+ const zipPath = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(workDir, 'payload.zip');
58
+ await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].writeFile(zipPath, zipBuffer);
59
+ await (0, __WEBPACK_EXTERNAL_MODULE_extract_zip_c4acec5a__["default"])(zipPath, {
60
+ dir: extractDir
61
+ });
62
+ await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].unlink(zipPath).catch(()=>void 0);
63
+ }
64
+ async function extractZipArchive(zipPath, extractDir) {
65
+ await (0, __WEBPACK_EXTERNAL_MODULE_extract_zip_c4acec5a__["default"])(zipPath, {
66
+ dir: extractDir
67
+ });
68
+ }
27
69
  function createLogger(logger) {
28
70
  return {
29
71
  info: (message)=>{
@@ -54,7 +96,8 @@ function request(url, options) {
54
96
  hostname: target.hostname,
55
97
  port: target.port,
56
98
  path: `${target.pathname}${target.search}`,
57
- headers
99
+ headers,
100
+ agent: false
58
101
  }, (res)=>{
59
102
  resolve({
60
103
  statusCode: res.statusCode || 0,
@@ -105,75 +148,59 @@ async function requestJson(url, options, maxRedirects = 5) {
105
148
  throw new errors_extensionFromStoreError('StoreIncompatibility', `Invalid JSON response from ${url}`, error);
106
149
  }
107
150
  }
108
- function stripCrxHeader(buffer) {
109
- if (buffer.length < 16) throw new errors_extensionFromStoreError('ExtractionFailed', 'CRX file too small');
110
- const magic = buffer.subarray(0, 4).toString('ascii');
111
- if ('Cr24' !== magic) throw new errors_extensionFromStoreError('ExtractionFailed', 'Invalid CRX header');
112
- const version = buffer.readUInt32LE(4);
113
- if (2 === version) {
114
- const publicKeyLength = buffer.readUInt32LE(8);
115
- const signatureLength = buffer.readUInt32LE(12);
116
- const headerSize = 16 + publicKeyLength + signatureLength;
117
- return buffer.subarray(headerSize);
118
- }
119
- if (3 === version) {
120
- const headerSize = buffer.readUInt32LE(8);
121
- const offset = 12 + headerSize;
122
- return buffer.subarray(offset);
123
- }
124
- throw new errors_extensionFromStoreError('ExtractionFailed', `Unsupported CRX version ${version}`);
125
- }
126
- async function extractCrx(crxPath, extractDir, workDir) {
127
- const crxBuffer = await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].readFile(crxPath);
128
- const zipBuffer = stripCrxHeader(crxBuffer);
129
- const zipPath = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(workDir, 'payload.zip');
130
- await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].writeFile(zipPath, zipBuffer);
131
- await (0, __WEBPACK_EXTERNAL_MODULE_extract_zip_c4acec5a__["default"])(zipPath, {
132
- dir: extractDir
133
- });
134
- await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].unlink(zipPath).catch(()=>void 0);
135
- }
136
- async function extractZipArchive(zipPath, extractDir) {
137
- await (0, __WEBPACK_EXTERNAL_MODULE_extract_zip_c4acec5a__["default"])(zipPath, {
138
- dir: extractDir
139
- });
140
- }
141
- async function readManifestInfo(manifestPath) {
142
- let raw = '';
143
- try {
144
- raw = await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].readFile(manifestPath, 'utf8');
145
- } catch (error) {
146
- throw new errors_extensionFromStoreError('ExtractionFailed', 'manifest.json was not found after extraction', error);
147
- }
151
+ function parseManifestInfo(raw) {
148
152
  let parsed;
149
153
  try {
150
154
  parsed = JSON.parse(raw);
151
155
  } catch (error) {
152
156
  throw new errors_extensionFromStoreError('ExtractionFailed', 'manifest.json is not valid JSON', error);
153
157
  }
154
- const manifestVersion = null == parsed ? void 0 : parsed.manifest_version;
155
- const extensionVersion = null == parsed ? void 0 : parsed.version;
158
+ if (!parsed || 'object' != typeof parsed || Array.isArray(parsed)) throw new errors_extensionFromStoreError('ExtractionFailed', 'manifest.json must contain an object');
159
+ const manifest = parsed;
160
+ const manifestVersion = manifest.manifest_version;
161
+ const extensionVersion = manifest.version;
156
162
  if (2 !== manifestVersion && 3 !== manifestVersion) throw new errors_extensionFromStoreError('ExtractionFailed', 'manifest_version must be 2 or 3');
157
163
  if (!extensionVersion || 'string' != typeof extensionVersion) throw new errors_extensionFromStoreError('ExtractionFailed', 'manifest.json is missing a version');
158
164
  return {
165
+ manifest,
159
166
  manifestVersion,
160
167
  extensionVersion
161
168
  };
162
169
  }
163
- function getPlatformInfo() {
164
- const platform = process.platform;
165
- const arch = process.arch;
166
- const os = 'darwin' === platform ? 'mac' : 'win32' === platform ? 'win' : 'linux';
167
- const archName = 'x64' === arch ? 'x64' : 'arm64' === arch ? 'arm64' : 'ia32' === arch ? 'x86' : 'x64';
170
+ async function readManifestInfo(manifestPath) {
171
+ let raw = '';
172
+ try {
173
+ raw = await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].readFile(manifestPath, 'utf8');
174
+ } catch (error) {
175
+ throw new errors_extensionFromStoreError('ExtractionFailed', 'manifest.json was not found after extraction', error);
176
+ }
177
+ return parseManifestInfo(raw);
178
+ }
179
+ function getNodeArch() {
180
+ if ('arm64' === process.arch) return 'arm64';
181
+ if ('ia32' === process.arch) return 'x86';
182
+ return 'x64';
183
+ }
184
+ function getNodeChromePlatformInfo() {
168
185
  return {
169
- os,
170
- arch: archName,
171
- naclArch: archName
186
+ os: 'darwin' === process.platform ? 'mac' : 'win32' === process.platform ? 'win' : 'linux',
187
+ arch: getNodeArch()
172
188
  };
173
189
  }
174
- function getChromeDownloadUrl(id) {
190
+ function normalizeChromePlatformInfo(platform) {
191
+ return {
192
+ os: platform.os,
193
+ arch: platform.arch,
194
+ naclArch: platform.naclArch || platform.arch
195
+ };
196
+ }
197
+ const DEFAULT_CHROME_PLATFORM = {
198
+ os: 'linux',
199
+ arch: 'x64'
200
+ };
201
+ function getChromeDownloadUrl(id, platformInfo = DEFAULT_CHROME_PLATFORM) {
175
202
  const encoded = encodeURIComponent(id);
176
- const platform = getPlatformInfo();
203
+ const platform = normalizeChromePlatformInfo(platformInfo);
177
204
  const productId = 'chromiumcrx';
178
205
  const productChannel = 'unknown';
179
206
  const productVersion = '9999.0.9999.0';
@@ -203,12 +230,12 @@ function getEdgeDownloadUrl(id) {
203
230
  async function resolveFirefoxDownload(idOrSlug, versionHint, options) {
204
231
  var _addon_current_version_file, _addon_current_version, _addon_current_version1;
205
232
  const baseUrl = `https://addons.mozilla.org/api/v5/addons/addon/${encodeURIComponent(idOrSlug)}/`;
206
- const addon = await requestJson(baseUrl, options);
233
+ const addon = await options.requestJson(baseUrl, options);
207
234
  const slugOrId = addon.slug || idOrSlug;
208
235
  if (versionHint) {
209
236
  var _version_file;
210
237
  const versionUrl = `${baseUrl}versions/${encodeURIComponent(versionHint)}/`;
211
- const version = await requestJson(versionUrl, options);
238
+ const version = await options.requestJson(versionUrl, options);
212
239
  const downloadUrl = null == (_version_file = version.file) ? void 0 : _version_file.url;
213
240
  if (!downloadUrl) throw new errors_extensionFromStoreError('NotPublic', `Version ${versionHint} is not publicly downloadable`);
214
241
  return {
@@ -253,48 +280,20 @@ function extractFirefoxSlugFromUrl(url) {
253
280
  function validateInput(url) {
254
281
  if (!url || 'string' != typeof url) throw new errors_extensionFromStoreError('InvalidInput', 'URL is required');
255
282
  }
256
- function defaultOutputDir() {
257
- return __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].resolve(process.cwd(), 'extensions');
258
- }
259
283
  function sanitizeSegment(value, label) {
260
284
  const sanitized = value.replace(/[\\/]/g, '-').trim();
261
285
  if (!sanitized) throw new errors_extensionFromStoreError('InvalidInput', `${label} is not a valid path segment`);
262
286
  return sanitized;
263
287
  }
264
- async function ensureDirExists(dir) {
265
- await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].mkdir(dir, {
266
- recursive: true
267
- });
268
- }
269
- async function pathExists(target) {
270
- try {
271
- await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].access(target);
272
- return true;
273
- } catch {
274
- return false;
275
- }
276
- }
277
- async function moveDir(source, destination) {
278
- try {
279
- await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].rename(source, destination);
280
- } catch {
281
- await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].cp(source, destination, {
282
- recursive: true
283
- });
284
- await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].rm(source, {
285
- recursive: true,
286
- force: true
287
- });
288
- }
289
- }
290
- async function resolveDownload(url, version, options) {
288
+ async function resolveDownload(url, options) {
291
289
  const store = detectStoreFromUrl(url);
292
290
  if (!store) throw new errors_extensionFromStoreError('UnsupportedStore', 'URL does not match a supported store');
293
291
  if ('chrome' === store) {
294
292
  const downloadId = extractChromeIdFromUrl(url);
295
293
  if (!downloadId) throw new errors_extensionFromStoreError('NotFound', 'Chrome extension id not found in URL');
296
294
  return {
297
- downloadUrl: getChromeDownloadUrl(downloadId),
295
+ store,
296
+ downloadUrl: getChromeDownloadUrl(downloadId, options.platform),
298
297
  archiveType: 'crx',
299
298
  downloadId,
300
299
  slugOrId: downloadId
@@ -304,6 +303,7 @@ async function resolveDownload(url, version, options) {
304
303
  const downloadId = extractEdgeIdFromUrl(url);
305
304
  if (!downloadId) throw new errors_extensionFromStoreError('NotFound', 'Edge extension id not found in URL');
306
305
  return {
306
+ store,
307
307
  downloadUrl: getEdgeDownloadUrl(downloadId),
308
308
  archiveType: 'crx',
309
309
  downloadId,
@@ -312,14 +312,48 @@ async function resolveDownload(url, version, options) {
312
312
  }
313
313
  const slug = extractFirefoxSlugFromUrl(url);
314
314
  if (!slug) throw new errors_extensionFromStoreError('NotFound', 'Firefox extension slug not found in URL');
315
- const firefox = await resolveFirefoxDownload(slug, version, options);
315
+ const firefox = await resolveFirefoxDownload(slug, options.version, {
316
+ userAgent: options.userAgent,
317
+ logger: options.logger,
318
+ requestJson: options.requestJson
319
+ });
316
320
  return {
321
+ store,
317
322
  downloadUrl: firefox.downloadUrl,
318
323
  archiveType: 'xpi',
319
324
  versionHint: firefox.version,
320
325
  slugOrId: firefox.slugOrId
321
326
  };
322
327
  }
328
+ function defaultOutputDir() {
329
+ return __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].resolve(process.cwd(), 'extensions');
330
+ }
331
+ async function ensureDirExists(dir) {
332
+ await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].mkdir(dir, {
333
+ recursive: true
334
+ });
335
+ }
336
+ async function pathExists(target) {
337
+ try {
338
+ await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].access(target);
339
+ return true;
340
+ } catch {
341
+ return false;
342
+ }
343
+ }
344
+ async function moveDir(source, destination) {
345
+ try {
346
+ await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].rename(source, destination);
347
+ } catch {
348
+ await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].cp(source, destination, {
349
+ recursive: true
350
+ });
351
+ await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].rm(source, {
352
+ recursive: true,
353
+ force: true
354
+ });
355
+ }
356
+ }
323
357
  function errorWithCode(code, message, cause) {
324
358
  return new errors_extensionFromStoreError(code, message, cause);
325
359
  }
@@ -328,7 +362,13 @@ async function fetchExtensionFromStore(url, options = {}) {
328
362
  const log = createLogger(options.logger);
329
363
  const outDir = options.outDir ? __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].resolve(options.outDir) : defaultOutputDir();
330
364
  await ensureDirExists(outDir);
331
- const resolved = await resolveDownload(url, options.version, options);
365
+ const resolved = await resolveDownload(url, {
366
+ version: options.version,
367
+ userAgent: options.userAgent,
368
+ logger: options.logger,
369
+ platform: getNodeChromePlatformInfo(),
370
+ requestJson: requestJson
371
+ });
332
372
  if (options.version && !url.includes('addons.mozilla.org')) log.warn('Version hints are best-effort and only supported on Firefox at the moment.');
333
373
  const workDir = await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].mkdtemp(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(__WEBPACK_EXTERNAL_MODULE_node_os_74b4b876__["default"].tmpdir(), 'extension-from-store-'));
334
374
  const archivePath = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(workDir, `archive.${resolved.archiveType}`);
@@ -363,7 +403,7 @@ async function fetchExtensionFromStore(url, options = {}) {
363
403
  await moveDir(extractDir, finalDir);
364
404
  const metaPath = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(finalDir, 'extension.meta.json');
365
405
  const meta = {
366
- store: detectStoreFromUrl(url) || 'chrome',
406
+ store: resolved.store,
367
407
  identifier: resolved.slugOrId,
368
408
  version: resolvedVersion,
369
409
  manifestVersion: manifestInfo.manifestVersion
@@ -0,0 +1,7 @@
1
+ export type ExtensionManifest = Record<string, unknown>;
2
+ export type ManifestInfo = {
3
+ extensionVersion: string;
4
+ manifestVersion: 2 | 3;
5
+ manifest: ExtensionManifest;
6
+ };
7
+ export declare function parseManifestInfo(raw: string): ManifestInfo;
package/dist/meta.d.ts CHANGED
@@ -1,5 +1,2 @@
1
- export type ManifestInfo = {
2
- extensionVersion: string;
3
- manifestVersion: 2 | 3;
4
- };
1
+ import { type ManifestInfo } from './manifest';
5
2
  export declare function readManifestInfo(manifestPath: string): Promise<ManifestInfo>;
@@ -0,0 +1,2 @@
1
+ import type { ChromePlatformInfo } from './platform';
2
+ export declare function getNodeChromePlatformInfo(): ChromePlatformInfo;
@@ -0,0 +1,8 @@
1
+ export type ChromePlatformOs = 'mac' | 'win' | 'linux';
2
+ export type ChromePlatformArch = 'x64' | 'arm64' | 'x86';
3
+ export type ChromePlatformInfo = {
4
+ os: ChromePlatformOs;
5
+ arch: ChromePlatformArch;
6
+ naclArch?: ChromePlatformArch;
7
+ };
8
+ export declare function normalizeChromePlatformInfo(platform: ChromePlatformInfo): Required<ChromePlatformInfo>;
@@ -0,0 +1,21 @@
1
+ import type { Logger } from './logger';
2
+ import type { ChromePlatformInfo } from './platform';
3
+ import { type JsonRequester } from './stores/firefox';
4
+ export type ResolvedDownload = {
5
+ store: 'chrome' | 'edge' | 'firefox';
6
+ downloadUrl: string;
7
+ archiveType: 'crx' | 'xpi';
8
+ versionHint?: string;
9
+ downloadId?: string;
10
+ slugOrId: string;
11
+ };
12
+ export type ResolveDownloadOptions = {
13
+ version?: string;
14
+ userAgent?: string;
15
+ logger?: Logger;
16
+ platform?: ChromePlatformInfo;
17
+ requestJson: JsonRequester;
18
+ };
19
+ export declare function validateInput(url: string): void;
20
+ export declare function sanitizeSegment(value: string, label: string): string;
21
+ export declare function resolveDownload(url: string, options: ResolveDownloadOptions): Promise<ResolvedDownload>;
@@ -1 +1,2 @@
1
- export declare function getChromeDownloadUrl(id: string): string;
1
+ import { type ChromePlatformInfo } from '../platform';
2
+ export declare function getChromeDownloadUrl(id: string, platformInfo?: ChromePlatformInfo): string;
@@ -1,7 +1,12 @@
1
1
  import type { Logger } from '../logger';
2
+ export type JsonRequester = <T>(url: string, options: {
3
+ userAgent?: string;
4
+ logger?: Logger;
5
+ }) => Promise<T>;
2
6
  export declare function resolveFirefoxDownload(idOrSlug: string, versionHint: string | undefined, options: {
3
7
  userAgent?: string;
4
8
  logger?: Logger;
9
+ requestJson: JsonRequester;
5
10
  }): Promise<{
6
11
  downloadUrl: string;
7
12
  version: string;
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "engineStrict": false,
11
11
  "name": "extension-from-store",
12
- "version": "0.1.1",
12
+ "version": "0.2.0",
13
13
  "description": "Download public browser extensions from official stores",
14
14
  "homepage": "https://www.npmjs.com/package/extension-from-store",
15
15
  "type": "module",
@@ -18,6 +18,16 @@
18
18
  "types": "./dist/index.d.ts",
19
19
  "import": "./dist/index.js",
20
20
  "require": "./dist/index.cjs"
21
+ },
22
+ "./browser": {
23
+ "types": "./dist/browser.d.ts",
24
+ "import": "./dist/browser.js",
25
+ "require": "./dist/browser.cjs"
26
+ },
27
+ "./core": {
28
+ "types": "./dist/core.d.ts",
29
+ "import": "./dist/core.js",
30
+ "require": "./dist/core.cjs"
21
31
  }
22
32
  },
23
33
  "main": "./dist/index.cjs",
@@ -32,6 +42,7 @@
32
42
  },
33
43
  "scripts": {
34
44
  "build": "rslib build",
45
+ "typecheck": "tsc --noEmit",
35
46
  "check": "pnpm dlx @biomejs/biome@1.9.4 check --write",
36
47
  "dev": "rslib build --watch",
37
48
  "format": "pnpm dlx @biomejs/biome@1.9.4 format --write",
@@ -69,7 +80,8 @@
69
80
  "extension-download"
70
81
  ],
71
82
  "dependencies": {
72
- "extract-zip": "^2.0.1"
83
+ "extract-zip": "^2.0.1",
84
+ "fflate": "^0.8.2"
73
85
  },
74
86
  "devDependencies": {
75
87
  "@biomejs/biome": "^1.9.4",