├── .eslintrc.js
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── cli.js
├── crx2.js
├── crx3.js
├── crx3.js.pb
├── index.js
└── resolver.js
└── test
├── expectations
├── updateCRX2.xml
├── updateCRX3.xml
└── updateProdVersionMin.xml
├── index.js
├── key.pem
└── myFirstExtension
├── icon.png
├── key.pem
└── manifest.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | commonjs: true,
5 | es6: true,
6 | node: true
7 | },
8 | extends: "eslint:recommended",
9 | parserOptions: {
10 | ecmaVersion: 2016
11 | },
12 | rules: {
13 | indent: ["error", 2],
14 | "linebreak-style": ["error", "unix"],
15 | quotes: ["error", "double"],
16 | semi: ["error", "always"]
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Node.js Unit Tests
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | build-front:
9 | runs-on: ${{ matrix.os }}
10 | continue-on-error: ${{ matrix.allow-failure || false }}
11 |
12 | strategy:
13 | matrix:
14 | node-version: [8.x, 10.x, 14.x]
15 | os: [ubuntu-latest]
16 | include:
17 | - node-version: 8.x
18 | allow-failure: true
19 | - node-version: 14.x
20 | os: windows-latest
21 | allow-failure: true
22 |
23 | steps:
24 | - uses: actions/checkout@v2
25 |
26 | - name: Use Node.js ${{ matrix.node-version }}
27 | uses: actions/setup-node@v2
28 | with:
29 | node-version: ${{ matrix.node-version }}
30 |
31 | - name: Install and run tests
32 | run: npm install-ci-test
33 |
34 | - name: Run CLI commands
35 | run: |
36 | npm install --global .
37 |
38 | crx keygen ./test --force
39 | crx pack ./test/myFirstExtension -p ./test/key.pem -o test.crx
40 | crx pack ./test/myFirstExtension --zip-output test.zip
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | test/*.crx
2 | test/*.zip
3 | test/*.xml
4 | node_modules
5 | coverage
6 | .nyc_output
7 | tmp
8 |
9 | build.crx
10 | update.xml
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test
2 | .travis.yml
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Change Log
2 |
3 | ### 5.0.1 (2019/07/22 09:57 +00:00)
4 | - [#107](https://github.com/oncletom/crx/pull/107) fix: loading relative path (@ahwayakchih)
5 |
6 | ### v5.0.0 (2019/04/17 08:00 +00:00)
7 | - [#106](https://github.com/oncletom/crx/pull/106) chore: update CHANGELOG (@ahwayakchih)
8 | - [#105](https://github.com/oncletom/crx/pull/105) Update dependencies and prepare for new release (v5.0.0) (@ahwayakchih)
9 | - [#104](https://github.com/oncletom/crx/pull/104) fix: create private key file outside extension directory by default (@ahwayakchih)
10 | - [#103](https://github.com/oncletom/crx/pull/103) Move CLI script to src folder (@arkon)
11 | - [#100](https://github.com/oncletom/crx/pull/100) Move CRX2 logic to separate file (@arkon)
12 | - [#102](https://github.com/oncletom/crx/pull/102) feat: use `manifest.minimum_chrome_version` as XML's `prodversionmin` (@ahwayakchih)
13 | - [#99](https://github.com/oncletom/crx/pull/99) docs: add `--crx-format` to README (@ahwayakchih)
14 | - [#98](https://github.com/oncletom/crx/pull/98) feat: add support for CRXv3 (@ahwayakchih)
15 | - [#97](https://github.com/oncletom/crx/pull/97) chore: update dependencies (#97) (@ahwayakchih)
16 | - [#78](https://github.com/oncletom/crx/pull/78) Add generateAppId sample (#78) (@NN---)
17 |
18 | ### v4.0.1 (2019/02/03 16:17 +00:00)
19 | - [#96](https://github.com/oncletom/crx/pull/96) Remove deprecated crx.writeFile() (@oncletom)
20 |
21 | ### v4.0.0 (2019/02/03 15:57 +00:00)
22 | - [#95](https://github.com/oncletom/crx/pull/95) Release crx@4 (@oncletom)
23 | - [#93](https://github.com/oncletom/crx/pull/93) fix demo code (@g8up)
24 | - [#88](https://github.com/oncletom/crx/pull/88) Bump Node.js version requirement (@oncletom)
25 | - [#90](https://github.com/oncletom/crx/pull/90) Fix syntax in module usage (@blimmer)
26 | - [#83](https://github.com/oncletom/crx/pull/83) Update dependencies (@oncletom)
27 | - [#81](https://github.com/oncletom/crx/pull/81) Fix extension ID calculation from path (@oncletom, @conioh)
28 | - [#76](https://github.com/oncletom/crx/pull/76) Add Appveyor configuration to test build on Windows (@oncletom)
29 | - [#71](https://github.com/oncletom/crx/pull/71) Remove the manifest data from cache on crx.load() (@binhqx)
30 | - [#75](https://github.com/oncletom/crx/pull/75) [Snyk Update] New fixes for 1 vulnerable dependency path (@snyk-bot)
31 |
32 | ### v3.2.1 (2016/10/13 13:13 +00:00)
33 | - [#67](https://github.com/oncletom/crx/pull/67) Drop iojs from package.engines (@dsblv)
34 |
35 | ### v3.2.0 (2016/09/22 23:25 +00:00)
36 | - [#66](https://github.com/oncletom/crx/pull/66) Add the ability to load a list of files in addition to a path. (@oncletom)
37 |
38 | ### v3.1.0 (2016/09/22 14:06 +00:00)
39 | - [#62](https://github.com/oncletom/crx/pull/62) Remove intermediate copy and use of temporary files (@oncletom)
40 | - [#63](https://github.com/oncletom/crx/pull/63) Add code coverage and add additional tests (@oncletom)
41 |
42 | ### v3.0.4 (2016/09/21 13:00 +00:00)
43 | - [#58](https://github.com/oncletom/crx/pull/58) [security fix] Updated archiver dependency; drops support for node 0.8.x (@PavelVanecek)
44 | - [#56](https://github.com/oncletom/crx/pull/56) Update CLI documentation with -o instead of deprecated -f (@nhoizey)
45 | - [#52](https://github.com/oncletom/crx/pull/52) Generate 2048-bit keys at the keygen CLI (@ngyikp)
46 | - [#50](https://github.com/oncletom/crx/pull/50) test crx on node 4.1 and 5 (@joscha)
47 |
48 | ### v3.0.3 (2015/07/22 16:10 +00:00)
49 | - [#47](https://github.com/oncletom/crx/pull/47) added .npmignore file (@PavelVanecek)
50 |
51 | ### v3.0.2 (2015/02/06 11:28 +00:00)
52 | - [#39](https://github.com/oncletom/crx/pull/39) Do not concatenate a possible null buffer. (@oncletom)
53 | - [#37](https://github.com/oncletom/crx/pull/37) fix pack instruction in Module example (@qbarlas)
54 |
55 | ### v2.0.1 (2015/01/15 18:28 +00:00)
56 | - [#34](https://github.com/oncletom/crx/pull/34) Fix various issues with CLI and creating .pem and .crx files (@Batterii)
57 | - [#35](https://github.com/oncletom/crx/pull/35) destroy() is not used (@okuryu)
58 | - [#26](https://github.com/oncletom/crx/pull/26) fixes minor code problems, such as missing semicolons, etc. (@joscha)
59 | - [#29](https://github.com/oncletom/crx/pull/29) generatePublicKey promise and generateAppId state (@joscha)
60 | - [#30](https://github.com/oncletom/crx/pull/30) use temp module (@joscha)
61 | - [#27](https://github.com/oncletom/crx/pull/27) throw error if public key is not set, yet (@joscha)
62 |
63 | ### v2.0.0 (2014/11/29 16:39 +00:00)
64 | - [#24](https://github.com/oncletom/crx/pull/24) Unsigned archives (@oncletom)
65 |
66 | ### v1.1.0 (2014/11/29 15:55 +00:00)
67 | - [#25](https://github.com/oncletom/crx/pull/25) Pure JavaScript public key (@oncletom)
68 |
69 | ### v1.0.0 (2014/11/25 11:45 +00:00)
70 | - [#22](https://github.com/oncletom/crx/pull/22) Remove system ssh-keygen dependency (@oncletom)
71 | - [#20](https://github.com/oncletom/crx/pull/20) Promise-based interface (@oncletom)
72 | - [#19](https://github.com/oncletom/crx/pull/19) Event 'finished' no longer valid #18 (@yuryoparin)
73 | - [#17](https://github.com/oncletom/crx/pull/17) Fix deleting remaining ./tmp directory (@jokesterfr)
74 | - [#16](https://github.com/oncletom/crx/pull/16) removed bufferstream dependency (@christian-bromann)
75 | - [#14](https://github.com/oncletom/crx/pull/14) Support long private keys in latest Chrome 32 (@vitalets)
76 | - [#10](https://github.com/oncletom/crx/pull/10) Windows Compatibility (@adotout)
77 | - [#8](https://github.com/oncletom/crx/pull/8) Use proper stdin options in child_process.spawn() instead of passing a file child.stdin.end() (@nkakuev)
78 | - [#3](https://github.com/oncletom/crx/pull/3) Added support for maxBuffer (@oncletom)
79 | - [#2](https://github.com/oncletom/crx/pull/2) Updated the README example upon `crx` API (@oncletom)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Jed Schmidt, Thomas Parisot and contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # crx [](http://travis-ci.org/oncletom/crx) [](https://ci.appveyor.com/project/oncletom/crx)
2 |
3 | > crx is a utility to **package Google Chrome extensions** via a *Node API* and the *command line*. It is written **purely in JavaScript** and **does not require OpenSSL**!
4 |
5 | Packages are available to use `crx` with:
6 |
7 | - *grunt*: [grunt-crx](https://npmjs.com/grunt-crx)
8 | - *gulp*: [gulp-crx-pack](https://npmjs.com/gulp-crx-pack)
9 | - *webpack*: [crx-webpack-plugin](https://npmjs.com/crx-webpack-plugin)
10 |
11 | Massive hat tip to the [node-rsa project](https://npmjs.com/node-rsa) for the pure JavaScript encryption!
12 |
13 | **Compatibility**: this extension is compatible with `node>=10`.
14 |
15 | ## Install
16 |
17 | ```bash
18 | $ npm install crx
19 | ```
20 |
21 | ## Module API
22 |
23 | Asynchronous functions returns a native ECMAScript Promise.
24 |
25 | ```js
26 | const fs = require('fs');
27 | const path = require('path');
28 |
29 | const ChromeExtension = require('crx');
30 |
31 | const crx = new ChromeExtension({
32 | codebase: 'http://localhost:8000/myExtension.crx',
33 | privateKey: fs.readFileSync('./key.pem')
34 | });
35 |
36 | crx.load( path.resolve(__dirname, './myExtension') )
37 | .then(crx => crx.pack())
38 | .then(crxBuffer => {
39 | const updateXML = crx.generateUpdateXML()
40 |
41 | fs.writeFileSync('../update.xml', updateXML);
42 | fs.writeFileSync('../myExtension.crx', crxBuffer);
43 | })
44 | .catch(err=>{
45 | console.error( err );
46 | });
47 | ```
48 |
49 | ### ChromeExtension = require("crx")
50 | ### crx = new ChromeExtension(attrs)
51 |
52 | This module exports the `ChromeExtension` constructor directly, which can take an optional attribute object, which is used to extend the instance.
53 |
54 | ### crx.load(path|files)
55 |
56 | Prepares the temporary workspace for the Chrome Extension located at `path` — which is expected to directly contain `manifest.json`.
57 |
58 | ```js
59 | crx.load('/path/to/extension').then(crx => {
60 | // ...
61 | });
62 | ```
63 |
64 | Alternatively, you can pass a list of files — the first `manifest.json` file to be found will be considered as the root of the application.
65 |
66 | ```js
67 | crx.load(['/my/extension/manifest.json', '/my/extension/background.json']).then(crx => {
68 | // ...
69 | });
70 | ```
71 |
72 | ### crx.pack()
73 |
74 | Packs the Chrome Extension and resolves the promise with a Buffer containing the `.crx` file.
75 |
76 | ```js
77 | crx.load('/path/to/extension')
78 | .then(crx => crx.pack())
79 | .then(crxBuffer => {
80 | fs.writeFileSync('/tmp/foobar.crx', crxBuffer);
81 | });
82 | ```
83 |
84 | ### crx.generateUpdateXML()
85 |
86 | Returns a Buffer containing the update.xml file used for `autoupdate`, as specified for `update_url` in the manifest. In this case, the instance must have a property called `codebase`.
87 |
88 | ```js
89 | const crx = new ChromeExtension({ ..., codebase: 'https://autoupdateserver.com/myFirstExtension.crx' });
90 |
91 | crx.load('/path/to/extension')
92 | .then(crx => crx.pack())
93 | .then(crxBuffer => {
94 | // ...
95 | const xmlBuffer = crx.generateUpdateXML();
96 | fs.writeFileSync('/foo/bar/update.xml', xmlBuffer);
97 | });
98 | ```
99 |
100 | ### crx.generateAppId
101 |
102 | Generates application id (extension id) from given path.
103 |
104 | ```js
105 | new crx().generateAppId('/path/to/ext') // epgkjnfaepceeghkjflpimappmlalchn
106 | ```
107 |
108 | ## CLI API
109 |
110 | ### crx pack [directory] [--crx-version number] [-o file] [--zip-output file] [-p private-key]
111 |
112 | Pack the specified directory into a .crx package, and output it to stdout. If no directory is specified, the current working directory is used.
113 |
114 | Use the `--crx-version` option to specify which CRX format version to output. Can be either "2" or "3", defaults to "3".
115 |
116 | Use the `-o` option to write the signed extension to a file instead of stdout.
117 |
118 | Use the `--zip-output` option to write the unsigned extension to a file.
119 |
120 | Use the `-p` option to specify an external private key. If this is not used, `key.pem` is used from within the directory. If this option is not used and no `key.pem` file exists, one will be generated automatically.
121 |
122 | Use the `-b` option to specify the maximum buffer allowed to generate extension. By default, will rely on `node` internal setting (~200KB).
123 |
124 | ### crx keygen [directory]
125 |
126 | Generate a 2048-bit RSA private key within the directory. This is called automatically if a key is not specified, and `key.pem` does not exist.
127 |
128 | Use the `--force` option to overwrite an existing private key located in the same given folder.
129 |
130 | ### crx --help
131 |
132 | Show information about using this utility, generated by [commander](https://github.com/visionmedia/commander.js).
133 |
134 | ## CLI example
135 |
136 | Given the following directory structure:
137 |
138 | ```
139 | └─┬ myFirstExtension
140 | ├── manifest.json
141 | └── icon.png
142 | ```
143 |
144 | run this:
145 |
146 | ```bash
147 | $ cd myFirstExtension
148 | $ crx pack -o
149 | ```
150 |
151 | to generate this:
152 |
153 | ```bash
154 | ├─┬ myFirstExtension
155 | │ ├── manifest.json
156 | │ ├── icon.png
157 | │ └── key.pem
158 | └── myFirstExtension.crx
159 | ```
160 |
161 | You can also name the output file like this:
162 |
163 | ```bash
164 | $ cd myFirstExtension
165 | $ crx pack -o myFirstExtension.crx
166 | ```
167 |
168 | to get the same results, or also pipe to the file manually like this.
169 |
170 | ```bash
171 | $ cd myFirstExtension
172 | $ crx pack > ../myFirstExtension.crx
173 | ```
174 |
175 | As you can see a key is generated for you at `key.pem` if none exists. You can also specify an external key. So if you have this:
176 |
177 | ```
178 | ├─┬ myFirstExtension
179 | │ ├── manifest.json
180 | │ └── icon.png
181 | └── myPrivateKey.pem
182 | ```
183 |
184 | you can run this:
185 |
186 | ```bash
187 | $ crx pack myFirstExtension -p myPrivateKey.pem -o
188 | ```
189 |
190 | to sign your package without keeping the key in the directory.
191 |
192 | # License
193 |
194 | [MIT License](LICENSE).
195 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Jed Schmidt
(http://jed.is)",
3 | "name": "crx",
4 | "description": "crx is a utility to package Google Chrome extensions via a Node API and the command line",
5 | "version": "5.0.1",
6 | "license": "MIT",
7 | "homepage": "https://github.com/oncletom/crx",
8 | "repository": {
9 | "type": "git",
10 | "url": "git://github.com/oncletom/crx.git"
11 | },
12 | "main": "./src/index.js",
13 | "bin": {
14 | "crx": "./src/cli.js"
15 | },
16 | "engines": {
17 | "node": ">=10"
18 | },
19 | "scripts": {
20 | "test": "nyc tape ./test/*.js",
21 | "posttest": "eslint src/*.js",
22 | "version": "npm run changelog && git add CHANGELOG.md",
23 | "changelog": "github-changes -o oncletom -r crx -n ${npm_package_version} --only-pulls --use-commit-body"
24 | },
25 | "nyc": {
26 | "functions": 100,
27 | "statements": 100,
28 | "branches": 100,
29 | "check-coverage": true,
30 | "reporter": [
31 | "text",
32 | "html"
33 | ]
34 | },
35 | "dependencies": {
36 | "archiver": "^4.0.2",
37 | "commander": "^2.20.0",
38 | "node-rsa": "^1.0.5",
39 | "pbf": "^3.2.0"
40 | },
41 | "devDependencies": {
42 | "adm-zip": "^0.4.13",
43 | "eslint": "^5.16.0",
44 | "github-changes": "^2.0.0",
45 | "nyc": "^14.1.1",
46 | "tape": "^4.11.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require("path");
4 | var fs = require("fs");
5 | var rsa = require("node-rsa");
6 | var {promisify} = require("util");
7 | var writeFile = promisify(fs.writeFile);
8 | var readFile = promisify(fs.readFile);
9 |
10 | var program = require("commander");
11 | var ChromeExtension = require(".");
12 | var pkg = require("../package.json");
13 |
14 | var resolve = path.resolve;
15 | var join = path.join;
16 |
17 | var cwd = process.cwd();
18 |
19 | program.version(pkg.version);
20 | // coming soon
21 | // .option("-x, --xml", "output autoupdate xml instead of extension ")
22 |
23 | program
24 | .command("keygen [directory]")
25 | .option("--force", "overwrite the private key if it exists")
26 | .option(
27 | "-c, --crx-version [number]",
28 | "CRX format version, can be either 2 or 3, defaults to 3",
29 | parseInt
30 | )
31 | .description("generate a private key in [directory]/key.pem")
32 | .action(keygen);
33 |
34 | program
35 | .command("pack [directory]")
36 | .description("pack [directory] into a .crx extension")
37 | .option(
38 | "-o, --output ",
39 | "write the crx content to instead of stdout"
40 | )
41 | .option("--zip-output ", "write the zip content to ")
42 | .option(
43 | "-p, --private-key ",
44 | "relative path to private key [key.pem], defaults to [directory/../key.pem]")
45 | .option(
46 | "-b, --max-buffer ",
47 | "max amount of memory allowed to generate the crx, in byte"
48 | )
49 | .option(
50 | "-c, --crx-version [number]",
51 | "CRX format version, can be either 2 or 3, defaults to 3",
52 | parseInt
53 | )
54 | .action(pack);
55 |
56 | program.parse(process.argv);
57 |
58 |
59 | /**
60 | * Generate a new key file
61 | * @param {String} keyPath path of the key file to create
62 | * @param {Object} opts
63 | * @returns {Promise}
64 | */
65 | function generateKeyFile(keyPath, opts) {
66 | // Chromium (tested on 72.0.3626.109) which generates CRX v3 files requires pkcs8 key
67 | var pkcs = "pkcs" + (opts.crxVersion === 2 ? "1" : "8") + "-private-pem";
68 |
69 | return Promise.resolve(new rsa({ b: 2048 }))
70 | .then(key => key.exportKey(pkcs))
71 | .then(keyVal => writeFile(keyPath, keyVal))
72 | ;
73 | }
74 |
75 | function keygen(dir, program) {
76 | dir = dir ? resolve(cwd, dir) : cwd;
77 |
78 | var keyPath = join(dir, "key.pem");
79 |
80 | fs.exists(keyPath, function(exists) {
81 | if (exists && !program.force) {
82 | throw new Error("key.pem already exists in the given location.");
83 | }
84 |
85 | generateKeyFile(keyPath, program);
86 | });
87 | }
88 |
89 | function pack(dir, program) {
90 | var input = dir ? resolve(cwd, dir) : cwd;
91 | var keyPath = program.privateKey
92 | ? resolve(cwd, program.privateKey)
93 | : join(input, "..", "key.pem");
94 | var output;
95 |
96 | if (program.output) {
97 | if (path.extname(program.output) !== ".crx") {
98 | throw new Error(
99 | "-o file is expected to have a `.crx` suffix: [" +
100 | program.output +
101 | "] was given."
102 | );
103 | }
104 | }
105 |
106 | if (program.zipOutput) {
107 | if (path.extname(program.zipOutput) !== ".zip") {
108 | throw new Error(
109 | "--zip-output file is expected to have a `.zip` suffix: [" +
110 | program.zipOutput +
111 | "] was given."
112 | );
113 | }
114 | }
115 |
116 | var crx = new ChromeExtension({
117 | rootDirectory: input,
118 | maxBuffer: program.maxBuffer,
119 | version: program.crxVersion || 3
120 | });
121 |
122 | readFile(keyPath)
123 | .then(null, function(err) {
124 | // If the key file doesn't exist, create one
125 | if (err.code === "ENOENT") {
126 | return generateKeyFile(keyPath, program).then(() => {
127 | process.stderr.write("Created new private key at: " + keyPath + ".\n");
128 | return readFile(keyPath);
129 | });
130 | } else {
131 | throw err;
132 | }
133 | })
134 | .then(function(key) {
135 | crx.privateKey = key;
136 | })
137 | .then(function() {
138 | crx
139 | .load()
140 | .then(() => crx.loadContents())
141 | .then(function(fileBuffer) {
142 | if (program.zipOutput) {
143 | var outFile = resolve(cwd, program.zipOutput);
144 |
145 | fs.createWriteStream(outFile).end(fileBuffer);
146 | }
147 | else {
148 | return crx.pack(fileBuffer);
149 | }
150 | })
151 | .then(function(crxBuffer) {
152 | if (program.zipOutput) {
153 | return;
154 | }
155 | else if (program.output) {
156 | output = program.output;
157 | }
158 | else {
159 | output = path.basename(cwd) + ".crx";
160 | }
161 |
162 | var outFile = resolve(cwd, output);
163 | if (outFile) {
164 | fs.createWriteStream(outFile).end(crxBuffer);
165 | }
166 | else {
167 | process.stdout.end(crxBuffer);
168 | }
169 | });
170 | });
171 | }
172 |
173 | module.exports = program;
174 |
--------------------------------------------------------------------------------
/src/crx2.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var crypto = require("crypto");
4 |
5 | /**
6 | * Generates and returns a signed package from extension content.
7 | *
8 | * BC BREAK `this.package` is not stored anymore (since 1.0.0)
9 | *
10 | * @param {Buffer} signature
11 | * @param {Buffer} publicKey
12 | * @param {Buffer} contents
13 | * @returns {Buffer}
14 | */
15 | module.exports = function generatePackage (privateKey, publicKey, contents) {
16 | var signature = generateSignature(privateKey, contents);
17 |
18 | var keyLength = publicKey.length;
19 | var sigLength = signature.length;
20 | var zipLength = contents.length;
21 | var length = 16 + keyLength + sigLength + zipLength;
22 |
23 | var crx = Buffer.alloc(length);
24 |
25 | crx.write("Cr24" + new Array(13).join("\x00"), "binary");
26 |
27 | crx[4] = 2;
28 | crx.writeUInt32LE(keyLength, 8);
29 | crx.writeUInt32LE(sigLength, 12);
30 |
31 | publicKey.copy(crx, 16);
32 | signature.copy(crx, 16 + keyLength);
33 | contents.copy(crx, 16 + keyLength + sigLength);
34 |
35 | return crx;
36 | };
37 |
38 | /**
39 | * Generates a SHA1 package signature.
40 | *
41 | * BC BREAK `this.signature` is not stored anymore (since 1.0.0)
42 | *
43 | * @param {Buffer} privateKey
44 | * @param {Buffer} contents
45 | * @returns {Buffer}
46 | */
47 | function generateSignature (privateKey, contents) {
48 | return Buffer.from(
49 | crypto
50 | .createSign("sha1")
51 | .update(contents)
52 | .sign(privateKey),
53 | "binary"
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/crx3.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var PBf = require("pbf");
4 | var crypto = require("crypto");
5 | var crx = require("./crx3.js.pb");
6 |
7 | /**
8 | * Generates and returns a signed package from extension content.
9 | *
10 | * Based on `crx_creator` from Chromium project.
11 | *
12 | * @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx_creator.cc}
13 | * @param {Buffer} privateKey
14 | * @param {Buffer} publicKey
15 | * @param {Buffer} contents
16 | * @returns {Buffer}
17 | */
18 | module.exports = function generatePackage (privateKey, publicKey, contents) {
19 | var pb;
20 |
21 | pb = new PBf();
22 | crx.SignedData.write({
23 | crx_id: getCrxId(publicKey)
24 | }, pb);
25 | var signedHeaderData = pb.finish();
26 |
27 | pb = new PBf();
28 | crx.CrxFileHeader.write({
29 | sha256_with_rsa: [{
30 | public_key: publicKey,
31 | signature : generateSignature(privateKey, signedHeaderData, contents)
32 | }],
33 | signed_header_data: signedHeaderData
34 | }, pb);
35 | var header = Buffer.from(pb.finish());
36 |
37 | var size =
38 | kSignature.length + // Magic constant
39 | kVersion.length + // Version number
40 | SIZE_BYTES + // Header size
41 | header.length +
42 | contents.length;
43 |
44 | var result = Buffer.allocUnsafe(size);
45 |
46 | var index = 0;
47 | kSignature.copy(result, index);
48 | kVersion.copy(result, index += kSignature.length);
49 | result.writeUInt32LE(header.length, index += kVersion.length);
50 | header.copy(result, index += SIZE_BYTES);
51 | contents.copy(result, index += header.length);
52 |
53 | return result;
54 | };
55 |
56 | /**
57 | * CRX IDs are 16 bytes long
58 | * @constant
59 | */
60 | const CRX_ID_SIZE = 16;
61 |
62 | /**
63 | * CRX3 uses 32bit numbers in various places,
64 | * so let's prepare size constant for that.
65 | * @constant
66 | */
67 | const SIZE_BYTES = 4;
68 |
69 | /**
70 | * Used for file format.
71 | * @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto}
72 | * @constant
73 | */
74 | const kSignature = Buffer.from("Cr24", "utf8");
75 |
76 | /**
77 | * Used for file format.
78 | * @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto}
79 | * @constant
80 | */
81 | const kVersion = Buffer.from([3, 0, 0, 0]);
82 |
83 | /**
84 | * Used for generating package signatures.
85 | * @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto}
86 | * @constant
87 | */
88 | const kSignatureContext = Buffer.from("CRX3 SignedData\x00", "utf8");
89 |
90 | /**
91 | * Given public key data, returns CRX ID.
92 | *
93 | * @param {Buffer} publicKey
94 | * @returns {Buffer}
95 | */
96 | function getCrxId (publicKey) {
97 | var hash = crypto.createHash("sha256");
98 | hash.update(publicKey);
99 | return hash.digest().slice(0, CRX_ID_SIZE);
100 | }
101 |
102 | /**
103 | * Generates and returns a signature.
104 | *
105 | * @param {Buffer} privateKey
106 | * @param {Buffer} signedHeaderData
107 | * @param {Buffer} contents
108 | * @returns {Buffer}
109 | */
110 | function generateSignature (privateKey, signedHeaderData, contents) {
111 | var hash = crypto.createSign("sha256");
112 |
113 | // Magic constant
114 | hash.update(kSignatureContext);
115 |
116 | // Size of signed_header_data
117 | var sizeOctets = Buffer.allocUnsafe(SIZE_BYTES);
118 | sizeOctets.writeUInt32LE(signedHeaderData.length, 0);
119 | hash.update(sizeOctets);
120 |
121 | // Content of signed_header_data
122 | hash.update(signedHeaderData);
123 |
124 | // ZIP content
125 | hash.update(contents);
126 |
127 | return Buffer.from(hash.sign(privateKey), "binary");
128 | }
129 |
--------------------------------------------------------------------------------
/src/crx3.js.pb:
--------------------------------------------------------------------------------
1 | 'use strict'; // code generated by pbf v3.1.0 from https://github.com/chromium/chromium/blob/e4a3bada6aab7aed90460ec7d27f8c7167c5666e/components/crx_file/crx3.proto
2 |
3 | // CrxFileHeader ========================================
4 |
5 | var CrxFileHeader = exports.CrxFileHeader = {};
6 |
7 | CrxFileHeader.read = function (pbf, end) {
8 | return pbf.readFields(CrxFileHeader._readField, {sha256_with_rsa: [], sha256_with_ecdsa: [], signed_header_data: null}, end);
9 | };
10 | CrxFileHeader._readField = function (tag, obj, pbf) {
11 | if (tag === 2) obj.sha256_with_rsa.push(AsymmetricKeyProof.read(pbf, pbf.readVarint() + pbf.pos));
12 | else if (tag === 3) obj.sha256_with_ecdsa.push(AsymmetricKeyProof.read(pbf, pbf.readVarint() + pbf.pos));
13 | else if (tag === 10000) obj.signed_header_data = pbf.readBytes();
14 | };
15 | CrxFileHeader.write = function (obj, pbf) {
16 | if (obj.sha256_with_rsa) for (var i = 0; i < obj.sha256_with_rsa.length; i++) pbf.writeMessage(2, AsymmetricKeyProof.write, obj.sha256_with_rsa[i]);
17 | if (obj.sha256_with_ecdsa) for (i = 0; i < obj.sha256_with_ecdsa.length; i++) pbf.writeMessage(3, AsymmetricKeyProof.write, obj.sha256_with_ecdsa[i]);
18 | if (obj.signed_header_data) pbf.writeBytesField(10000, obj.signed_header_data);
19 | };
20 |
21 | // AsymmetricKeyProof ========================================
22 |
23 | var AsymmetricKeyProof = exports.AsymmetricKeyProof = {};
24 |
25 | AsymmetricKeyProof.read = function (pbf, end) {
26 | return pbf.readFields(AsymmetricKeyProof._readField, {public_key: null, signature: null}, end);
27 | };
28 | AsymmetricKeyProof._readField = function (tag, obj, pbf) {
29 | if (tag === 1) obj.public_key = pbf.readBytes();
30 | else if (tag === 2) obj.signature = pbf.readBytes();
31 | };
32 | AsymmetricKeyProof.write = function (obj, pbf) {
33 | if (obj.public_key) pbf.writeBytesField(1, obj.public_key);
34 | if (obj.signature) pbf.writeBytesField(2, obj.signature);
35 | };
36 |
37 | // SignedData ========================================
38 |
39 | var SignedData = exports.SignedData = {};
40 |
41 | SignedData.read = function (pbf, end) {
42 | return pbf.readFields(SignedData._readField, {crx_id: null}, end);
43 | };
44 | SignedData._readField = function (tag, obj, pbf) {
45 | if (tag === 1) obj.crx_id = pbf.readBytes();
46 | };
47 | SignedData.write = function (obj, pbf) {
48 | if (obj.crx_id) pbf.writeBytesField(1, obj.crx_id);
49 | };
50 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var path = require("path");
4 | var join = path.join;
5 | var crypto = require("crypto");
6 | var RSA = require("node-rsa");
7 | var archiver = require("archiver");
8 | var resolve = require("./resolver.js");
9 | var crx2 = require("./crx2.js");
10 | var crx3 = require("./crx3.js");
11 |
12 | const DEFAULTS = {
13 | appId: null,
14 | rootDirectory: "",
15 | publicKey: null,
16 | privateKey: null,
17 | codebase: null,
18 | path: null,
19 | src: "**",
20 | version: 3,
21 | };
22 |
23 | class ChromeExtension {
24 | constructor(attrs) {
25 | // Setup defaults
26 | Object.assign(this, DEFAULTS, attrs);
27 |
28 | this.loaded = false;
29 | }
30 |
31 | /**
32 | * Packs the content of the extension in a crx file.
33 | *
34 | * @param {Buffer=} contentsBuffer
35 | * @returns {Promise}
36 | * @example
37 | *
38 | * crx.pack().then(function(crxContent){
39 | * // do something with the crxContent binary data
40 | * });
41 | *
42 | */
43 | pack (contentsBuffer) {
44 | if (!this.loaded) {
45 | return this.load().then(this.pack.bind(this, contentsBuffer));
46 | }
47 |
48 | var selfie = this;
49 | var packP = [
50 | this.generatePublicKey(),
51 | contentsBuffer || selfie.loadContents()
52 | ];
53 |
54 | return Promise.all(packP).then(function(outputs) {
55 | var publicKey = outputs[0];
56 | var contents = outputs[1];
57 |
58 | selfie.publicKey = publicKey;
59 |
60 | if (selfie.version === 2) {
61 | return crx2(selfie.privateKey, publicKey, contents);
62 | }
63 |
64 | return crx3(selfie.privateKey, publicKey, contents);
65 | });
66 | }
67 |
68 | /**
69 | * Loads extension manifest and copies its content to a workable path.
70 | *
71 | * @param {string=} path
72 | * @returns {Promise}
73 | */
74 | load (path) {
75 | var selfie = this;
76 |
77 | return resolve(path || selfie.rootDirectory).then(function(metadata) {
78 | selfie.path = metadata.path;
79 | selfie.src = metadata.src;
80 |
81 | var manifestPath = join(selfie.path, "manifest.json");
82 | delete require.cache[manifestPath];
83 |
84 | selfie.manifest = require(manifestPath);
85 | selfie.loaded = true;
86 |
87 | return selfie;
88 | });
89 | }
90 |
91 | /**
92 | * Generates a public key.
93 | *
94 | * BC BREAK `this.publicKey` is not stored anymore (since 1.0.0)
95 | * BC BREAK callback parameter has been removed in favor to the promise interface.
96 | *
97 | * @returns {Promise} Resolves to {Buffer} containing the public key
98 | * @example
99 | *
100 | * crx.generatePublicKey(function(publicKey){
101 | * // do something with publicKey
102 | * });
103 | */
104 | generatePublicKey () {
105 | var privateKey = this.privateKey;
106 |
107 | return new Promise(function(resolve, reject) {
108 | if (!privateKey) {
109 | return reject(
110 | "Impossible to generate a public key: privateKey option has not been defined or is empty."
111 | );
112 | }
113 |
114 | var key = new RSA(privateKey);
115 |
116 | resolve(key.exportKey("pkcs8-public-der"));
117 | });
118 | }
119 |
120 | /**
121 | *
122 | * BC BREAK `this.contents` is not stored anymore (since 1.0.0)
123 | *
124 | * @returns {Promise}
125 | */
126 | loadContents () {
127 | var selfie = this;
128 |
129 | return new Promise(function(resolve, reject) {
130 | var archive = archiver("zip", { zlib: { level: 9 }});
131 | var contents = Buffer.from("");
132 |
133 | if (!selfie.loaded) {
134 | throw new Error(
135 | "crx.load needs to be called first in order to prepare the workspace."
136 | );
137 | }
138 |
139 | archive.on("error", reject);
140 |
141 | /*
142 | TODO: Remove in v4.
143 | It will be better to resolve an archive object
144 | rather than fitting everything in memory.
145 |
146 | @see https://github.com/oncletom/crx/issues/61
147 | */
148 | archive.on("data", function(buf) {
149 | contents = Buffer.concat([contents, buf]);
150 | });
151 |
152 | archive.on("finish", function() {
153 | resolve(contents);
154 | });
155 |
156 | archive
157 | .glob(selfie.src, {
158 | cwd: selfie.path,
159 | matchBase: true,
160 | ignore: ["*.pem", ".git", "*.crx"]
161 | })
162 | .finalize();
163 | });
164 | }
165 |
166 | /**
167 | * Generates an appId from the publicKey.
168 | * Public key has to be set for this to work, otherwise an error is thrown.
169 | *
170 | * BC BREAK `this.appId` is not stored anymore (since 1.0.0)
171 | * BC BREAK introduced `publicKey` parameter as it is not stored any more since 2.0.0
172 | *
173 | * @param {Buffer|string} [publicKey] the public key to use to generate the app ID
174 | * @returns {string}
175 | */
176 | generateAppId (keyOrPath) {
177 | keyOrPath = keyOrPath || this.publicKey;
178 |
179 | if (typeof keyOrPath !== "string" && !(keyOrPath instanceof Buffer)) {
180 | throw new Error("Public key is neither set, nor given");
181 | }
182 |
183 | // Handling Windows Path
184 | // Possibly to be moved in a different method
185 | if (typeof keyOrPath === "string") {
186 | var charCode = keyOrPath.charCodeAt(0);
187 |
188 | // 65 (A) < charCode < 122 (z)
189 | if (charCode >= 65 && charCode <= 122 && keyOrPath[1] === ":") {
190 | keyOrPath = keyOrPath[0].toUpperCase() + keyOrPath.slice(1);
191 |
192 | keyOrPath = Buffer.from(keyOrPath, "utf-16le");
193 | }
194 | }
195 |
196 | return crypto
197 | .createHash("sha256")
198 | .update(keyOrPath)
199 | .digest()
200 | .toString("hex")
201 | .split("")
202 | .map(x => (parseInt(x, 16) + 0x0a).toString(26))
203 | .join("")
204 | .slice(0, 32);
205 | }
206 |
207 | /**
208 | * Generates an updateXML file from the extension content.
209 | *
210 | * If manifest does not include `minimum_chrome_version`, defaults to:
211 | * - '29.0.0' for CRX2, which is earliest extensions API available
212 | * - '64.0.3242' for CRX3, which is when Chrome etension packager switched to CRX3
213 | *
214 | * BC BREAK `this.updateXML` is not stored anymore (since 1.0.0)
215 | *
216 | * @see
217 | * [Chrome Extensions APIs]{@link https://developer.chrome.com/extensions/api_index}
218 | * @see
219 | * [Chrome verions]{@link https://en.wikipedia.org/wiki/Google_Chrome_version_history}
220 | * @see
221 | * [Chromium switches to CRX3]{@link https://chromium.googlesource.com/chromium/src.git/+/b8bc9f99ef4ad6223dfdcafd924051561c05ac75}
222 | * @returns {Buffer}
223 | */
224 | generateUpdateXML () {
225 | if (!this.codebase) {
226 | throw new Error("No URL provided for update.xml.");
227 | }
228 |
229 | var browserVersion = this.manifest.minimum_chrome_version
230 | || (this.version < 3 && "29.0.0") // Earliest version with extensions API
231 | || "64.0.3242"; // Chrome started generating CRX3 packages
232 |
233 | return Buffer.from(`
234 |
235 |
236 |
237 |
238 | `);
239 | }
240 | }
241 |
242 | module.exports = ChromeExtension;
243 |
--------------------------------------------------------------------------------
/src/resolver.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var path = require("path");
4 |
5 | module.exports = function resolve(pathOrFiles) {
6 | return new Promise(function(resolve, reject) {
7 | // legacy and original mode
8 | if (typeof pathOrFiles === "string") {
9 | return resolve({
10 | path: path.resolve(pathOrFiles),
11 | src: "**"
12 | });
13 | }
14 |
15 | // new mode, with a list of files
16 | else if (Array.isArray(pathOrFiles)) {
17 | var manifestFile = "";
18 |
19 | pathOrFiles.some(function(f) {
20 | if (/(^|\/)manifest.json$/.test(f)) {
21 | manifestFile = f;
22 | return true;
23 | }
24 | });
25 |
26 | if (!manifestFile) {
27 | return reject(
28 | new Error("Unable to locate a manifest file in your list of files.")
29 | );
30 | }
31 |
32 | var manifestDir = path.dirname(manifestFile);
33 |
34 | return resolve({
35 | path: path.resolve(manifestDir),
36 | src:
37 | "{" +
38 | pathOrFiles
39 | .map(function(f) {
40 | return path.relative(manifestDir, f);
41 | })
42 | .join(",") +
43 | "}"
44 | });
45 | }
46 |
47 | //
48 | else {
49 | reject(
50 | new Error(
51 | "load path is none of a folder location nor a list of files to pack"
52 | )
53 | );
54 | }
55 | });
56 | };
57 |
--------------------------------------------------------------------------------
/test/expectations/updateCRX2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test/expectations/updateCRX3.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test/expectations/updateProdVersionMin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | /* global require, __dirname, Buffer */
2 | 'use strict';
3 |
4 | var fs = require("fs");
5 | var test = require("tape");
6 | var Zip = require("adm-zip");
7 | var ChromeExtension = require("../");
8 | var join = require("path").join;
9 | var privateKey = fs.readFileSync(join(__dirname, "key.pem"));
10 | var updateXml2 = fs.readFileSync(join(__dirname, "expectations", "updateCRX2.xml"));
11 | var updateXml3 = fs.readFileSync(join(__dirname, "expectations", "updateCRX3.xml"));
12 | var updateXmlCustom = fs.readFileSync(join(__dirname, "expectations", "updateProdVersionMin.xml"));
13 |
14 | function newCrx(opts){
15 | return new ChromeExtension(Object.assign({
16 | privateKey: privateKey,
17 | path: '/tmp',
18 | codebase: "http://localhost:8000/myFirstExtension.crx",
19 | rootDirectory: join(__dirname, "myFirstExtension")
20 | }, opts));
21 | }
22 |
23 | const TESTS = {};
24 |
25 | TESTS.ChromeExtension = function(t, opts){
26 | t.plan(2);
27 |
28 | t.throws(() => ChromeExtension({}));
29 | t.ok(newCrx(opts));
30 | };
31 |
32 |
33 | TESTS.load = function(t, opts){
34 | t.plan(6);
35 |
36 | newCrx(opts).load().then(t.pass);
37 |
38 | // Test relative path
39 | newCrx().load("./test/myFirstExtension").then(function(crx){
40 | t.ok(crx);
41 | }).catch(t.error.bind(t));
42 |
43 | // Test absolute path
44 | newCrx().load(join(__dirname, "myFirstExtension")).then(function(crx){
45 | t.ok(crx);
46 | }).catch(t.error.bind(t));
47 |
48 | // Test list of files
49 | var fileList = [
50 | 'test/myFirstExtension/manifest.json',
51 | 'test/myFirstExtension/icon.png',
52 | ];
53 |
54 | newCrx(opts).load(fileList).then(function(crx){
55 | t.ok(crx);
56 | });
57 |
58 | var fileList = [
59 | 'test/myFirstExtension/icon.png'
60 | ];
61 |
62 | newCrx(opts).load(fileList).catch(function(err){
63 | t.ok(err);
64 | });
65 |
66 | newCrx(opts).load(Buffer.from('')).catch(function(err){
67 | t.ok(err);
68 | })
69 | };
70 |
71 | TESTS.pack = function(t, opts){
72 | t.plan(1);
73 |
74 | var crx = newCrx(opts);
75 | crx.pack().then(function(packageData){
76 | t.ok(packageData instanceof Buffer);
77 | })
78 | .catch(t.error.bind(t));
79 | };
80 |
81 | TESTS.writeFile = function(t, opts){
82 | t.plan(1);
83 |
84 | var crx = newCrx(opts);
85 |
86 | t.throws(() => crx.writeFile('/tmp/crx'));
87 | };
88 |
89 | TESTS.loadContents = function(t, opts){
90 | t.plan(3);
91 |
92 | newCrx(opts).loadContents().catch(function(err){
93 | t.ok(err instanceof Error);
94 | });
95 |
96 | var crx = newCrx(opts);
97 |
98 | crx.load().then(function(){
99 | return crx.loadContents();
100 | })
101 | .then(function(contentsBuffer){
102 | t.ok(contentsBuffer instanceof Buffer);
103 |
104 | return contentsBuffer;
105 | })
106 | .then(function(packageData){
107 | var entries = new Zip(packageData)
108 | .getEntries()
109 | .map(function(entry){
110 | return entry.entryName;
111 | })
112 | .sort(function(a, b){
113 | return a.localeCompare(b);
114 | });
115 |
116 | t.deepEqual(entries, ['icon.png', 'manifest.json']);
117 |
118 | return packageData;
119 | })
120 | .catch(t.error.bind(t));
121 | };
122 |
123 |
124 | TESTS.generateUpdateXML = function(t, opts){
125 | t.plan(3);
126 |
127 | t.throws(() => new ChromeExtension({}).generateUpdateXML(), 'No URL provided for update.xml');
128 |
129 | var crx = newCrx(opts);
130 | var expected = crx.version === 2 ? updateXml2 : updateXml3;
131 |
132 | crx.pack().then(function(){
133 | var xmlBuffer = crx.generateUpdateXML();
134 |
135 | t.equals(xmlBuffer.toString(), expected.toString());
136 | })
137 | .catch(t.error.bind(t));
138 |
139 | var crxCustom = newCrx(opts);
140 | crxCustom.load().then(() => {
141 | crxCustom.manifest.minimum_chrome_version = '99.99.99-crxtest';
142 | crxCustom.pack().then(function(){
143 | var xmlBuffer = crxCustom.generateUpdateXML();
144 |
145 | t.equals(xmlBuffer.toString(), updateXmlCustom.toString());
146 | })
147 | .catch(t.error.bind(t));
148 | });
149 | };
150 |
151 | TESTS.generatePublicKey = function(t, opts) {
152 | t.plan(2);
153 |
154 | var crx = newCrx(opts);
155 | crx.privateKey = null;
156 |
157 | crx.generatePublicKey().catch(function(err){
158 | t.ok(err);
159 | });
160 |
161 | newCrx(opts).generatePublicKey().then(function(publicKey){
162 | t.equals(publicKey.length, 162);
163 | });
164 | };
165 |
166 | TESTS.generateAppId = function(t, opts) {
167 | t.plan(4);
168 |
169 | t.throws(function() { newCrx(opts).generateAppId(); }, /Public key is neither set, nor given/);
170 |
171 | var crx = newCrx(opts)
172 |
173 | // from Public Key
174 | crx.generatePublicKey().then(function(publicKey){
175 | t.equals(crx.generateAppId(publicKey), 'eoilidhiokfphdhpmhoaengdkehanjif');
176 | })
177 | .catch(t.error.bind(t));
178 |
179 | // from Linux Path
180 | t.equals(crx.generateAppId('/usr/local/extension'), 'ioglhmppkolgcgoonkfdbjkcedfjhbcd');
181 |
182 | // from Windows Path
183 | t.equals(crx.generateAppId('c:\\a'), 'igchicfaapedlfgmepccnpolhajaphik');
184 | };
185 |
186 | TESTS["end to end"] = function (t, opts) {
187 | var crx = newCrx(opts);
188 |
189 | crx.load()
190 | .then(function(crx) {
191 | return crx.pack();
192 | })
193 | .then(function(crxBuffer) {
194 | fs.writeFile('build.crx', crxBuffer, t.error);
195 | fs.writeFile('update.xml', crx.generateUpdateXML(), t.error);
196 | })
197 | .then(t.end);
198 | };
199 |
200 | // Setup list of different configurations to test
201 | // Each key is the test name prefix.
202 | // Each value is an options obect to be passed to test implementation.
203 | const TEST_OPTIONS = {
204 | "": undefined, // use defaults
205 | v2: {version: 2},
206 | v3: {version: 3}
207 | };
208 |
209 | // Run whole list of tests for each of the configurations
210 | Reflect.ownKeys(TEST_OPTIONS).forEach(key => {
211 | for (const name in TESTS) {
212 | test(`${key}: ${name}`, t => TESTS[name](t, TEST_OPTIONS[key]));
213 | }
214 | });
215 |
--------------------------------------------------------------------------------
/test/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIICWwIBAAKBgQCx1nHfHr1yCt21hPRWE9yKwC8Xuq+y/dPDrkjJ8cVsgPRoq25i
3 | iej2siZEWryDR7WWuAmdGaUtBFgQyRvCyGQS4YtkKot8iOTFzJo656hgHJUvZP2Q
4 | Yy7ERJ3rZRwLxpWmvYQiXx92LSy19eC6Bi5+FAaTMCQNDdklanijb5D6fwIDAQAB
5 | AoGAUQmPSkUPvvAEp7q2PKNAVFnPG9kOR1ozLXA16xApDpCUzz2PR4fgiMoVdgCC
6 | 9q+up8elWdld022vU7bQ16nJL64GFzgCHubRBM7d5UJjSaVkcDL6QbzAEIwwa1IU
7 | LQA7BheshHs3d+gYRjedNGR0kKu3HeUofhhXLC1WXPCOZYECQQDrKqFPro6jRYjD
8 | T65qvoIetRKw1L8J9iw6UhWb1jMtCgvTKS99zmnJzM2hOsCAy+HDj/YwlCWEKIjh
9 | ZICZBFkpAkEAwZellq94wisJd4ptfQvHSWVPdMXR59jQcW23dBqU4D+itc1wCVT/
10 | xg5iIBaujTHgaM9oS0kawhWE1mmQbyyjZwJAHfVvWXRWbYxlMOSMxsKAVyMgP3DK
11 | 6Zz343IjmJfAK0O1X/BGQZOzPGcf5yNR9NaEa2KCrYuh/+UeEwC3tUatiQJANM9/
12 | lomrsZw36upST+hkpvsCH+LPDiYxRqAdiYiu0DXL1ziBtaoAVDEcR5CocVAH3c+m
13 | rdL1f7iLEkqd4hYVRQJAP+uM18zCINdxYeZn+HVF3p5JOJii658+x08+0L69+DtV
14 | FFfRJfW98rCa5F2mQivCkwpChFq4NWSqpLT3x5WuXg==
15 | -----END RSA PRIVATE KEY-----
--------------------------------------------------------------------------------
/test/myFirstExtension/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thom4parisot/crx/23766455f8df72b3d13d3e72101fe632e052383c/test/myFirstExtension/icon.png
--------------------------------------------------------------------------------
/test/myFirstExtension/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIICWwIBAAKBgQCx1nHfHr1yCt21hPRWE9yKwC8Xuq+y/dPDrkjJ8cVsgPRoq25i
3 | iej2siZEWryDR7WWuAmdGaUtBFgQyRvCyGQS4YtkKot8iOTFzJo656hgHJUvZP2Q
4 | Yy7ERJ3rZRwLxpWmvYQiXx92LSy19eC6Bi5+FAaTMCQNDdklanijb5D6fwIDAQAB
5 | AoGAUQmPSkUPvvAEp7q2PKNAVFnPG9kOR1ozLXA16xApDpCUzz2PR4fgiMoVdgCC
6 | 9q+up8elWdld022vU7bQ16nJL64GFzgCHubRBM7d5UJjSaVkcDL6QbzAEIwwa1IU
7 | LQA7BheshHs3d+gYRjedNGR0kKu3HeUofhhXLC1WXPCOZYECQQDrKqFPro6jRYjD
8 | T65qvoIetRKw1L8J9iw6UhWb1jMtCgvTKS99zmnJzM2hOsCAy+HDj/YwlCWEKIjh
9 | ZICZBFkpAkEAwZellq94wisJd4ptfQvHSWVPdMXR59jQcW23dBqU4D+itc1wCVT/
10 | xg5iIBaujTHgaM9oS0kawhWE1mmQbyyjZwJAHfVvWXRWbYxlMOSMxsKAVyMgP3DK
11 | 6Zz343IjmJfAK0O1X/BGQZOzPGcf5yNR9NaEa2KCrYuh/+UeEwC3tUatiQJANM9/
12 | lomrsZw36upST+hkpvsCH+LPDiYxRqAdiYiu0DXL1ziBtaoAVDEcR5CocVAH3c+m
13 | rdL1f7iLEkqd4hYVRQJAP+uM18zCINdxYeZn+HVF3p5JOJii658+x08+0L69+DtV
14 | FFfRJfW98rCa5F2mQivCkwpChFq4NWSqpLT3x5WuXg==
15 | -----END RSA PRIVATE KEY-----
16 |
--------------------------------------------------------------------------------
/test/myFirstExtension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My First Extension",
3 | "manifest_version": 2,
4 | "version": "1.0",
5 | "description": "The first extension that I made.",
6 | "browser_action": {
7 | "default_icon": "icon.png"
8 | },
9 | "permissions": [
10 | "http://api.flickr.com/"
11 | ]
12 | }
--------------------------------------------------------------------------------