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 +17 -1
- package/dist/browser.js +1 -1
- package/dist/core.js +1 -1
- package/dist/extract.d.ts +1 -1
- package/dist/index.cjs +50 -12
- package/dist/index.js +52 -13
- package/package.json +6 -7
- /package/dist/{588.js → 614.js} +0 -0
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 "./
|
|
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 "./
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
99
|
-
await
|
|
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
|
|
107
|
-
|
|
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
|
|
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 "./
|
|
11
|
-
async function
|
|
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
|
-
|
|
15
|
-
await
|
|
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
|
|
23
|
-
|
|
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 "./
|
|
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.
|
|
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.
|
|
101
|
-
"@types/node": "^
|
|
102
|
-
"eslint": "^10.
|
|
103
|
-
"eslint-config-auditor": "^
|
|
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.
|
|
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",
|
/package/dist/{588.js → 614.js}
RENAMED
|
File without changes
|