extension-from-store 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,7 +107,23 @@ When extraction is disabled, the archive is saved as:
107
107
 
108
108
  - `.crx`: strip CRX header, extract ZIP payload
109
109
  - `.xpi`: treat as ZIP
110
- - No normalization, no rewriting, no formatting
110
+ - No content normalization, no rewriting, no formatting
111
+ - Unix permission bits from the archive are not preserved: directories come
112
+ out `0755` and files come out `0644` (`0755` when the archive marked them
113
+ executable). Some store packages ship entries with modes like `0204` that
114
+ the owner cannot read back after extraction; browsers ignore archive modes
115
+ when installing, so this package does too.
116
+
117
+ ## When the store refuses to serve an extension
118
+
119
+ Stores sometimes decline to serve a listed extension for download. The Chrome
120
+ Web Store update endpoint answers `HTTP 204 No Content` instead of a CRX when
121
+ an extension is blocked, for example when its update check reports
122
+ `_malware="true"` (observed live with ModHeader,
123
+ `idgpnmonknjnojddfkpgkljpfnnfcklj`, on 2026-08-02). This is a store-side
124
+ refusal, not a truncated download: real Chrome clients get the same empty
125
+ response. extension-from-store surfaces it as a `NotPublic` error that names
126
+ the URL that returned no content.
111
127
 
112
128
  ## `extension.meta.json`
113
129
 
package/dist/browser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { strFromU8, unzipSync } from "fflate";
2
- import { createLogger, resolveDownload, extensionFromStoreError, stripCrxHeader, validateInput, parseManifestInfo } from "./588.js";
2
+ import { createLogger, resolveDownload, extensionFromStoreError, stripCrxHeader, validateInput, parseManifestInfo } from "./614.js";
3
3
  const TEXT_FILE_PATTERN = /(^|\/)(?:[^/]+\.(?:txt|md|mdx|json|js|jsx|mjs|cjs|ts|tsx|css|scss|sass|less|html|xml|svg|yml|yaml|toml|ini|conf|map)|\.(?:gitignore|npmrc|editorconfig|prettierrc|eslintrc))$/i;
4
4
  function getDefaultFetch() {
5
5
  if ('function' != typeof globalThis.fetch) throw new extensionFromStoreError('DownloadFailed', 'No fetch implementation was provided');
package/dist/core.js CHANGED
@@ -1 +1 @@
1
- export { asExtensionFromStoreError, createLogger, detectStoreFromUrl, extensionFromStoreError, extractChromeIdFromUrl, extractEdgeIdFromUrl, extractFirefoxSlugFromUrl, getChromeDownloadUrl, getEdgeDownloadUrl, normalizeChromePlatformInfo, parseManifestInfo, resolveDownload, resolveFirefoxDownload, sanitizeSegment, stripCrxHeader, validateInput } from "./588.js";
1
+ export { asExtensionFromStoreError, createLogger, detectStoreFromUrl, extensionFromStoreError, extractChromeIdFromUrl, extractEdgeIdFromUrl, extractFirefoxSlugFromUrl, getChromeDownloadUrl, getEdgeDownloadUrl, normalizeChromePlatformInfo, parseManifestInfo, resolveDownload, resolveFirefoxDownload, sanitizeSegment, stripCrxHeader, validateInput } from "./614.js";
package/dist/extract.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { stripCrxHeader } from './crx';
2
- export declare function extractCrx(crxPath: string, extractDir: string, workDir: string): Promise<void>;
2
+ export declare function extractCrx(crxPath: string, extractDir: string, _workDir?: string): Promise<void>;
3
3
  export declare function extractZipArchive(zipPath: string, extractDir: string): Promise<void>;
package/dist/index.cjs CHANGED
@@ -63,8 +63,7 @@ class errors_extensionFromStoreError extends Error {
63
63
  this.cause = cause;
64
64
  }
65
65
  }
66
- const external_extract_zip_namespaceObject = require("extract-zip");
67
- var external_extract_zip_default = /*#__PURE__*/ __webpack_require__.n(external_extract_zip_namespaceObject);
66
+ const external_fflate_namespaceObject = require("fflate");
68
67
  function readUInt32LE(bytes, offset) {
69
68
  const view = new DataView(bytes.buffer, bytes.byteOffset + offset, Uint32Array.BYTES_PER_ELEMENT);
70
69
  return view.getUint32(0, true);
@@ -92,20 +91,53 @@ function stripCrxHeader(buffer) {
92
91
  }
93
92
  throw new errors_extensionFromStoreError('ExtractionFailed', `Unsupported CRX version ${version}`);
94
93
  }
95
- async function extractCrx(crxPath, extractDir, workDir) {
94
+ async function normalizeExtractedModes(dir) {
95
+ await promises_default().chmod(dir, 493);
96
+ const entries = await promises_default().readdir(dir, {
97
+ withFileTypes: true
98
+ });
99
+ for (const entry of entries){
100
+ const target = external_node_path_default().join(dir, entry.name);
101
+ if (entry.isDirectory()) await normalizeExtractedModes(target);
102
+ else if (entry.isFile()) {
103
+ const { mode } = await promises_default().stat(target);
104
+ await promises_default().chmod(target, 73 & mode ? 493 : 420);
105
+ }
106
+ }
107
+ }
108
+ async function writeZipEntries(zipData, extractDir) {
109
+ const root = external_node_path_default().resolve(extractDir);
110
+ const entries = (0, external_fflate_namespaceObject.unzipSync)(zipData);
111
+ await promises_default().mkdir(root, {
112
+ recursive: true
113
+ });
114
+ for (const [name, data] of Object.entries(entries)){
115
+ const normalized = name.replace(/\\/g, '/');
116
+ const target = external_node_path_default().resolve(root, normalized);
117
+ const relative = external_node_path_default().relative(root, target);
118
+ if (!relative || relative.startsWith('..') || external_node_path_default().isAbsolute(relative)) throw new Error(`Refusing to extract zip entry outside the extraction dir: ${name}`);
119
+ if (normalized.endsWith('/')) {
120
+ await promises_default().mkdir(target, {
121
+ recursive: true
122
+ });
123
+ continue;
124
+ }
125
+ await promises_default().mkdir(external_node_path_default().dirname(target), {
126
+ recursive: true
127
+ });
128
+ await promises_default().writeFile(target, data);
129
+ }
130
+ }
131
+ async function extractCrx(crxPath, extractDir, _workDir) {
96
132
  const crxBuffer = await promises_default().readFile(crxPath);
97
133
  const zipBuffer = Buffer.from(stripCrxHeader(crxBuffer));
98
- const zipPath = external_node_path_default().join(workDir, 'payload.zip');
99
- await promises_default().writeFile(zipPath, zipBuffer);
100
- await external_extract_zip_default()(zipPath, {
101
- dir: extractDir
102
- });
103
- await promises_default().unlink(zipPath).catch(()=>void 0);
134
+ await writeZipEntries(new Uint8Array(zipBuffer), extractDir);
135
+ await normalizeExtractedModes(extractDir);
104
136
  }
105
137
  async function extractZipArchive(zipPath, extractDir) {
106
- await external_extract_zip_default()(zipPath, {
107
- dir: extractDir
108
- });
138
+ const zipBuffer = await promises_default().readFile(zipPath);
139
+ await writeZipEntries(new Uint8Array(zipBuffer), extractDir);
140
+ await normalizeExtractedModes(extractDir);
109
141
  }
110
142
  const external_node_fs_namespaceObject = require("node:fs");
111
143
  var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
@@ -166,7 +198,13 @@ async function downloadToFile(url, filePath, options, maxRedirects = 5) {
166
198
  if (401 === statusCode || 403 === statusCode) throw new errors_extensionFromStoreError('NotPublic', 'Extension is not publicly downloadable');
167
199
  throw new errors_extensionFromStoreError('DownloadFailed', `Failed to download ${url} (HTTP ${statusCode})`);
168
200
  }
201
+ if (204 === statusCode) {
202
+ stream.resume();
203
+ throw new errors_extensionFromStoreError('NotPublic', `Store returned no content for ${url}; the extension is not being served for download`);
204
+ }
169
205
  await (0, external_node_stream_promises_namespaceObject.pipeline)(stream, external_node_fs_default().createWriteStream(filePath));
206
+ const { size } = await external_node_fs_default().promises.stat(filePath);
207
+ if (0 === size) throw new errors_extensionFromStoreError('DownloadFailed', `Downloaded file from ${url} is empty`);
170
208
  }
171
209
  async function requestJson(url, options, maxRedirects = 5) {
172
210
  const { statusCode, headers, stream } = await request(url, options);
package/dist/index.js CHANGED
@@ -1,27 +1,60 @@
1
1
  import promises from "node:fs/promises";
2
2
  import node_os from "node:os";
3
3
  import node_path from "node:path";
4
- import extract_zip from "extract-zip";
4
+ import { unzipSync } from "fflate";
5
5
  import node_fs from "node:fs";
6
6
  import node_http from "node:http";
7
7
  import node_https from "node:https";
8
8
  import { pipeline } from "node:stream/promises";
9
9
  import { URL } from "node:url";
10
- import { createLogger, stripCrxHeader, extensionFromStoreError as errors_extensionFromStoreError, resolveDownload, sanitizeSegment, validateInput, parseManifestInfo } from "./588.js";
11
- async function extractCrx(crxPath, extractDir, workDir) {
10
+ import { createLogger, stripCrxHeader, extensionFromStoreError as errors_extensionFromStoreError, resolveDownload, sanitizeSegment, validateInput, parseManifestInfo } from "./614.js";
11
+ async function normalizeExtractedModes(dir) {
12
+ await promises.chmod(dir, 493);
13
+ const entries = await promises.readdir(dir, {
14
+ withFileTypes: true
15
+ });
16
+ for (const entry of entries){
17
+ const target = node_path.join(dir, entry.name);
18
+ if (entry.isDirectory()) await normalizeExtractedModes(target);
19
+ else if (entry.isFile()) {
20
+ const { mode } = await promises.stat(target);
21
+ await promises.chmod(target, 73 & mode ? 493 : 420);
22
+ }
23
+ }
24
+ }
25
+ async function writeZipEntries(zipData, extractDir) {
26
+ const root = node_path.resolve(extractDir);
27
+ const entries = unzipSync(zipData);
28
+ await promises.mkdir(root, {
29
+ recursive: true
30
+ });
31
+ for (const [name, data] of Object.entries(entries)){
32
+ const normalized = name.replace(/\\/g, '/');
33
+ const target = node_path.resolve(root, normalized);
34
+ const relative = node_path.relative(root, target);
35
+ if (!relative || relative.startsWith('..') || node_path.isAbsolute(relative)) throw new Error(`Refusing to extract zip entry outside the extraction dir: ${name}`);
36
+ if (normalized.endsWith('/')) {
37
+ await promises.mkdir(target, {
38
+ recursive: true
39
+ });
40
+ continue;
41
+ }
42
+ await promises.mkdir(node_path.dirname(target), {
43
+ recursive: true
44
+ });
45
+ await promises.writeFile(target, data);
46
+ }
47
+ }
48
+ async function extractCrx(crxPath, extractDir, _workDir) {
12
49
  const crxBuffer = await promises.readFile(crxPath);
13
50
  const zipBuffer = Buffer.from(stripCrxHeader(crxBuffer));
14
- const zipPath = node_path.join(workDir, 'payload.zip');
15
- await promises.writeFile(zipPath, zipBuffer);
16
- await extract_zip(zipPath, {
17
- dir: extractDir
18
- });
19
- await promises.unlink(zipPath).catch(()=>void 0);
51
+ await writeZipEntries(new Uint8Array(zipBuffer), extractDir);
52
+ await normalizeExtractedModes(extractDir);
20
53
  }
21
54
  async function extractZipArchive(zipPath, extractDir) {
22
- await extract_zip(zipPath, {
23
- dir: extractDir
24
- });
55
+ const zipBuffer = await promises.readFile(zipPath);
56
+ await writeZipEntries(new Uint8Array(zipBuffer), extractDir);
57
+ await normalizeExtractedModes(extractDir);
25
58
  }
26
59
  const DEFAULT_USER_AGENT = 'extension-from-store';
27
60
  function request(url, options) {
@@ -67,7 +100,13 @@ async function downloadToFile(url, filePath, options, maxRedirects = 5) {
67
100
  if (401 === statusCode || 403 === statusCode) throw new errors_extensionFromStoreError('NotPublic', 'Extension is not publicly downloadable');
68
101
  throw new errors_extensionFromStoreError('DownloadFailed', `Failed to download ${url} (HTTP ${statusCode})`);
69
102
  }
103
+ if (204 === statusCode) {
104
+ stream.resume();
105
+ throw new errors_extensionFromStoreError('NotPublic', `Store returned no content for ${url}; the extension is not being served for download`);
106
+ }
70
107
  await pipeline(stream, node_fs.createWriteStream(filePath));
108
+ const { size } = await node_fs.promises.stat(filePath);
109
+ if (0 === size) throw new errors_extensionFromStoreError('DownloadFailed', `Downloaded file from ${url} is empty`);
71
110
  }
72
111
  async function requestJson(url, options, maxRedirects = 5) {
73
112
  const { statusCode, headers, stream } = await request(url, options);
@@ -203,5 +242,5 @@ async function fetchExtensionFromStore(url, options = {}) {
203
242
  }).catch(()=>void 0);
204
243
  }
205
244
  }
206
- export { extensionFromStoreError } from "./588.js";
245
+ export { extensionFromStoreError } from "./614.js";
207
246
  export { fetchExtensionFromStore };
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "engineStrict": false,
11
11
  "name": "extension-from-store",
12
- "version": "0.2.3",
12
+ "version": "0.2.5",
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",
@@ -79,7 +79,6 @@
79
79
  "extension-download"
80
80
  ],
81
81
  "dependencies": {
82
- "extract-zip": "^2.0.1",
83
82
  "fflate": "^0.8.3"
84
83
  },
85
84
  "pnpm": {
@@ -97,12 +96,12 @@
97
96
  }
98
97
  },
99
98
  "devDependencies": {
100
- "@rslib/core": "^0.23.0",
101
- "@types/node": "^25.9.3",
102
- "eslint": "^10.5.0",
103
- "eslint-config-auditor": "^2.3.1",
99
+ "@rslib/core": "^0.23.2",
100
+ "@types/node": "^26.1.1",
101
+ "eslint": "^10.8.0",
102
+ "eslint-config-auditor": "^3.0.0",
104
103
  "typescript": "^6.0.3",
105
- "vitest": "^4.1.9"
104
+ "vitest": "^4.1.10"
106
105
  },
107
106
  "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1",
108
107
  "bugs": "https://github.com/cezaraugusto/extension-from-store/issues",
File without changes