extension-from-store 0.2.3 → 0.2.4

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/index.cjs CHANGED
@@ -92,6 +92,20 @@ function stripCrxHeader(buffer) {
92
92
  }
93
93
  throw new errors_extensionFromStoreError('ExtractionFailed', `Unsupported CRX version ${version}`);
94
94
  }
95
+ async function normalizeExtractedModes(dir) {
96
+ await promises_default().chmod(dir, 493);
97
+ const entries = await promises_default().readdir(dir, {
98
+ withFileTypes: true
99
+ });
100
+ for (const entry of entries){
101
+ const target = external_node_path_default().join(dir, entry.name);
102
+ if (entry.isDirectory()) await normalizeExtractedModes(target);
103
+ else if (entry.isFile()) {
104
+ const { mode } = await promises_default().stat(target);
105
+ await promises_default().chmod(target, 73 & mode ? 493 : 420);
106
+ }
107
+ }
108
+ }
95
109
  async function extractCrx(crxPath, extractDir, workDir) {
96
110
  const crxBuffer = await promises_default().readFile(crxPath);
97
111
  const zipBuffer = Buffer.from(stripCrxHeader(crxBuffer));
@@ -100,12 +114,14 @@ async function extractCrx(crxPath, extractDir, workDir) {
100
114
  await external_extract_zip_default()(zipPath, {
101
115
  dir: extractDir
102
116
  });
117
+ await normalizeExtractedModes(extractDir);
103
118
  await promises_default().unlink(zipPath).catch(()=>void 0);
104
119
  }
105
120
  async function extractZipArchive(zipPath, extractDir) {
106
121
  await external_extract_zip_default()(zipPath, {
107
122
  dir: extractDir
108
123
  });
124
+ await normalizeExtractedModes(extractDir);
109
125
  }
110
126
  const external_node_fs_namespaceObject = require("node:fs");
111
127
  var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
@@ -166,7 +182,13 @@ async function downloadToFile(url, filePath, options, maxRedirects = 5) {
166
182
  if (401 === statusCode || 403 === statusCode) throw new errors_extensionFromStoreError('NotPublic', 'Extension is not publicly downloadable');
167
183
  throw new errors_extensionFromStoreError('DownloadFailed', `Failed to download ${url} (HTTP ${statusCode})`);
168
184
  }
185
+ if (204 === statusCode) {
186
+ stream.resume();
187
+ throw new errors_extensionFromStoreError('NotPublic', `Store returned no content for ${url}; the extension is not being served for download`);
188
+ }
169
189
  await (0, external_node_stream_promises_namespaceObject.pipeline)(stream, external_node_fs_default().createWriteStream(filePath));
190
+ const { size } = await external_node_fs_default().promises.stat(filePath);
191
+ if (0 === size) throw new errors_extensionFromStoreError('DownloadFailed', `Downloaded file from ${url} is empty`);
170
192
  }
171
193
  async function requestJson(url, options, maxRedirects = 5) {
172
194
  const { statusCode, headers, stream } = await request(url, options);
package/dist/index.js CHANGED
@@ -7,7 +7,21 @@ 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";
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
+ }
11
25
  async function extractCrx(crxPath, extractDir, workDir) {
12
26
  const crxBuffer = await promises.readFile(crxPath);
13
27
  const zipBuffer = Buffer.from(stripCrxHeader(crxBuffer));
@@ -16,12 +30,14 @@ async function extractCrx(crxPath, extractDir, workDir) {
16
30
  await extract_zip(zipPath, {
17
31
  dir: extractDir
18
32
  });
33
+ await normalizeExtractedModes(extractDir);
19
34
  await promises.unlink(zipPath).catch(()=>void 0);
20
35
  }
21
36
  async function extractZipArchive(zipPath, extractDir) {
22
37
  await extract_zip(zipPath, {
23
38
  dir: extractDir
24
39
  });
40
+ await normalizeExtractedModes(extractDir);
25
41
  }
26
42
  const DEFAULT_USER_AGENT = 'extension-from-store';
27
43
  function request(url, options) {
@@ -67,7 +83,13 @@ async function downloadToFile(url, filePath, options, maxRedirects = 5) {
67
83
  if (401 === statusCode || 403 === statusCode) throw new errors_extensionFromStoreError('NotPublic', 'Extension is not publicly downloadable');
68
84
  throw new errors_extensionFromStoreError('DownloadFailed', `Failed to download ${url} (HTTP ${statusCode})`);
69
85
  }
86
+ if (204 === statusCode) {
87
+ stream.resume();
88
+ throw new errors_extensionFromStoreError('NotPublic', `Store returned no content for ${url}; the extension is not being served for download`);
89
+ }
70
90
  await pipeline(stream, node_fs.createWriteStream(filePath));
91
+ const { size } = await node_fs.promises.stat(filePath);
92
+ if (0 === size) throw new errors_extensionFromStoreError('DownloadFailed', `Downloaded file from ${url} is empty`);
71
93
  }
72
94
  async function requestJson(url, options, maxRedirects = 5) {
73
95
  const { statusCode, headers, stream } = await request(url, options);
@@ -203,5 +225,5 @@ async function fetchExtensionFromStore(url, options = {}) {
203
225
  }).catch(()=>void 0);
204
226
  }
205
227
  }
206
- export { extensionFromStoreError } from "./588.js";
228
+ export { extensionFromStoreError } from "./614.js";
207
229
  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.4",
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",
@@ -97,12 +97,12 @@
97
97
  }
98
98
  },
99
99
  "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",
100
+ "@rslib/core": "^0.23.2",
101
+ "@types/node": "^26.1.1",
102
+ "eslint": "^10.8.0",
103
+ "eslint-config-auditor": "^3.0.0",
104
104
  "typescript": "^6.0.3",
105
- "vitest": "^4.1.9"
105
+ "vitest": "^4.1.10"
106
106
  },
107
107
  "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1",
108
108
  "bugs": "https://github.com/cezaraugusto/extension-from-store/issues",
File without changes