├── .babelrc ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── dist └── .gitkeep ├── package.json └── src ├── index.js └── libs ├── chrome-webstore.js └── getToken.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 pastak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-webstore-manager 2 | 3 | [![](https://nodei.co/npm-dl/chrome-webstore-manager.png?months=3)](https://www.npmjs.com/package/chrome-webstore-manager) 4 | 5 | [![](https://nodei.co/npm/chrome-webstore-manager.png?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/chrome-webstore-manager) 6 | 7 | ## INSTALL 8 | 9 | `$ npm install -g chrome-webstore-manager` 10 | 11 | ## HOW TO USE 12 | 13 | ### Create new item 14 | 15 | - `$ chrome-webstore-manager insert /path/to/your_extension.zip` 16 | - **caution**: it requires `zip` NOT `crx` 17 | - return value: chrome-web-store-item-id 18 | 19 | Only this command, your items is under draft. So you should publish item. I write about it on below. 20 | 21 | ### Publish item 22 | 23 | - `$ chrome-webstore-manager publish ITEM_ID` 24 | - `ITEM_ID`: chrome-web-store-item-id 25 | 26 | ### Update item 27 | 28 | - `$ chrome-webstore-manager update ITEM_ID /path/to/your_extension.zip` 29 | - `ITEM_ID`: chrome-web-store-item-id 30 | 31 | ## Example 32 | 33 | Sample webapp for release chrome extenison on heroku 34 | 35 | https://github.com/pastak/chrome-extension-release-heroku 36 | 37 | ## Use on NodeJS 38 | 39 | ```js 40 | const ChromeWebstore = require('chrome-webstore-manager') 41 | const fileBin = fs.readFileSync('./extension.zip') 42 | // chrome web store item id 43 | const itemId = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' 44 | 45 | // Initialize with ClientID and ClinetSecret 46 | const chromeWebstore = new ChromeWebstore(client_id, client_secret) 47 | 48 | // Get authorize URL 49 | const getCodeUrl = chromeWebstore.getCodeUrl() 50 | 51 | const code = open_browser_and_input_authorized_code(getCodeUrl) 52 | 53 | // Get OAuth access token 54 | const token = chromeWebstore.getAccessToken(code) 55 | 56 | // Create new item 57 | chromeWebstore.insertItem(token, fileBin).then((data) => { do_something }) 58 | 59 | // Update item 60 | chromeWebstore.updateItem(token, fileBin, itemId).then((data) => { do_something }) 61 | 62 | // Make item publish 63 | // target is 'trustedTesters' OR 'default'. default is 'default' 64 | const target = 'trustedTesters' 65 | chromeWebstore.publishItem(token, itemId, target).then((data) => { do_something }) 66 | 67 | // Get new token with refresh_token 68 | chromeWebstore.getRefreshToken(refreshToken).then(function (data) { 69 | const json = JSON.parse(data) 70 | const newToken = json.access_token 71 | }) 72 | 73 | ``` 74 | 75 | ## More Info about token 76 | 77 | ### Get access token 78 | 79 | - Prepare the client ID and client secret to use Chrome Web Store API according to https://developer.chrome.com/webstore/using_webstore_api#beforeyoubegin 80 | - You can get access token via this commands. 81 | - `$ chrome-webstore-manager token --client_id [YOUR_CLIENT_ID] --client_secret [YOUR_CLIENT_SECRET]` 82 | - After a while, open your browser then accept Google OAuth. 83 | 84 | ### Set your access_token 85 | 86 | - You can pass `access_token` to command. 87 | - You can set command's optional value `-t` or `--token` 88 | - You can set `access_token` as environment value named `WEBSTORE_TOKEN` 89 | - If you use it on \*CI, it is useful `export WEBSTORE_TOKEN=[YOUR_ACCESS_TOKEN]` 90 | 91 | ### Get access token using refresh token 92 | 93 | - You get new access token with `$ chrome-webstore-manager refresh_token --client_id [YOUR_CLIENT_ID] --client_secret [YOUR_CLIENT_SECRET] --refresh_token [YOUR_REFRESH_TOKEN]` 94 | - You can use environment variable `WEBSTORE_REFRESH_TOKEN` instead of `refresh_token` 95 | 96 | **Usage Example** 97 | 98 | `$ WEBSTORE_TOKEN=$(chrome-webstore-manager refresh_token ...) chrome-webstore-manager update ITEM_ID extension.zip` 99 | -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastak/chrome-webstore-manager/305cee14699ba89a165875c20c905fe586d61179/dist/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-webstore-manager", 3 | "version": "0.5.0", 4 | "description": "Command Line Tool for Publish Chrome WebStore", 5 | "main": "./dist/libs/chrome-webstore.js", 6 | "scripts": { 7 | "test": "standard ./src/**/*js", 8 | "build": "babel src -d dist", 9 | "prepublish": "npm run build" 10 | }, 11 | "bin": { 12 | "chrome-webstore-manager": "./dist/index.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "ssh://git@github.com/pastak/chrome-webstore-manager.git" 17 | }, 18 | "author": "pastak", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/pastak/chrome-webstore-manager/issues" 22 | }, 23 | "homepage": "https://github.com/pastak/chrome-webstore-manager", 24 | "dependencies": { 25 | "commander": "^2.20.3", 26 | "got": "^11.8.2", 27 | "open": "^8.0.7" 28 | }, 29 | "devDependencies": { 30 | "@babel/cli": "^7.13.16", 31 | "@babel/core": "^7.14.0", 32 | "@babel/preset-env": "^7.14.1", 33 | "standard": "^16.0.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const readline = require('readline') 4 | const fs = require('fs') 5 | const program = require('commander') 6 | const open = require('open') 7 | const getToken = require('./libs/getToken') 8 | const ChromeWebstore = require('./libs/chrome-webstore.js') 9 | 10 | const getAccessToken = function (cid, cs, code) { 11 | const chromeWebstore = new ChromeWebstore(cid, cs) 12 | chromeWebstore.getAccessToken(code) 13 | .then(function (json) { 14 | console.log('Your token: ' + json.access_token) 15 | console.log('Your refresh_token: ' + json.refresh_token) 16 | process.exit() 17 | }) 18 | } 19 | 20 | program 21 | .version(require('../package.json').version) 22 | 23 | program 24 | .command('token') 25 | .description('Get token for the first time') 26 | .option('--client_id [Client_ID]', 'Your Client ID') 27 | .option('--client_secret [Client_Secret]', 'Your Client Secret') 28 | .option('--code [CODE]', 'Your authorized code') 29 | .action(function (options) { 30 | const rli = readline.createInterface(process.stdin, process.stdout) 31 | const cid = options.client_id 32 | const cs = options.client_secret 33 | if (!(cid && cs)) { 34 | console.error('Require Client ID and Client Secret') 35 | process.exit(1) 36 | } 37 | if (options.code) { 38 | return getAccessToken(cid, cs, options.code) 39 | } 40 | const chromeWebstore = new ChromeWebstore(cid, cs) 41 | const getCodeUrl = chromeWebstore.getCodeUrl() 42 | open(getCodeUrl) 43 | rli.setPrompt('Your CODE: ') 44 | rli.on('line', function (code) { 45 | getAccessToken(cid, cs, code) 46 | }) 47 | rli.prompt() 48 | }) 49 | 50 | program 51 | .command('insert [zipFile]') 52 | .description('create new item on Chrome Web Store') 53 | .option('-t, --token [YOUR_TOKEN]', 'Your token') 54 | .action(function (zipFile, options) { 55 | const token = getToken(options) 56 | const fileBin = fs.readFileSync(zipFile) 57 | const chromeWebstore = new ChromeWebstore() 58 | chromeWebstore.insertItem(token, fileBin).then(function (json) { 59 | if (json.itemError) { 60 | console.error(json.itemError) 61 | return process.exit(1) 62 | } 63 | console.log(json.id) 64 | }) 65 | }) 66 | 67 | program 68 | .command('update [itemId] [zipFile]') 69 | .description('update your item') 70 | .option('-t, --token [YOUR_TOKEN]', 'Your token') 71 | .action(function (itemId, zipFile, options) { 72 | const fileBin = fs.readFileSync(zipFile) 73 | const token = getToken(options) 74 | const chromeWebstore = new ChromeWebstore() 75 | chromeWebstore.updateItem(token, fileBin, itemId).then(function (json) { 76 | if (json.itemError) { 77 | console.error(json.itemError) 78 | return process.exit(1) 79 | } 80 | console.log(json.id) 81 | }) 82 | }) 83 | 84 | program 85 | .command('get [itemId]') 86 | .description('get item from chrome webstore') 87 | .option('-t, --token [YOUR_TOKEN]', 'Your token') 88 | .option('--projection [PROJECTION_TYPE]', '"DRAFT" or "PUBLISHED"') 89 | .action(function (itemId, options) { 90 | const token = getToken(options) 91 | const projection = options.projection || 'DRAFT' 92 | const chromeWebstore = new ChromeWebstore() 93 | chromeWebstore.getItem(token, itemId, projection).then(function (json) { 94 | if (json.itemError) { 95 | console.error(json.itemError) 96 | return process.exit(1) 97 | } 98 | console.log(json.id) 99 | }) 100 | }) 101 | 102 | program 103 | .command('publish [itemId]') 104 | .description('make item publish') 105 | .option('-t, --token [YOUR_TOKEN]', 'Your token') 106 | .option('--target [TARGET_TYPE]', '"trustedTesters" or "default"') 107 | .action(function (itemId, options) { 108 | const token = getToken(options) 109 | const target = options.target || 'default' 110 | const chromeWebstore = new ChromeWebstore() 111 | chromeWebstore.publishItem(token, itemId, target).then(function (json) { 112 | if (json.itemError) { 113 | console.error(json.itemError) 114 | return process.exit(1) 115 | } 116 | console.log(json.id) 117 | }) 118 | }) 119 | program 120 | .command('refresh_token') 121 | .description('Get token by refresh_token') 122 | .option('--client_id [Client_ID]', 'Your Client ID') 123 | .option('--client_secret [Client_Secret]', 'Your Client Secret') 124 | .option('--refresh_token [Refresh_Token]', 'Your Refresh Token') 125 | .action(function (options) { 126 | const cid = options.client_id 127 | const cs = options.client_secret 128 | if (!(cid && cs)) { 129 | console.error('Require Client ID and Client Secret') 130 | process.exit(1) 131 | } 132 | if (!(process.env.WEBSTORE_REFRESH_TOKEN || options.refresh_token)) { 133 | console.error('Require refresh_token') 134 | process.exit(1) 135 | } 136 | const refreshToken = options.refresh_token || process.env.WEBSTORE_REFRESH_TOKEN 137 | const chromeWebstore = new ChromeWebstore(cid, cs) 138 | chromeWebstore.getRefreshToken(refreshToken).then(function (json) { 139 | console.log(json.access_token) 140 | }) 141 | }) 142 | 143 | program.parse(process.argv) 144 | 145 | if (!program.args.length) { 146 | program.help() 147 | } 148 | -------------------------------------------------------------------------------- /src/libs/chrome-webstore.js: -------------------------------------------------------------------------------- 1 | const got = require('got') 2 | module.exports = class ChromeWebStore { 3 | constructor (cid, cs, redirectUrl) { 4 | this.cid = cid 5 | this.cs = cs 6 | this.redirectUrl = redirectUrl || 'urn:ietf:wg:oauth:2.0:oob' 7 | } 8 | 9 | getCodeUrl (redirectUrl, state) { 10 | state = state || '' 11 | state = encodeURIComponent(state) 12 | return 'https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&client_id=' + this.cid + '&state' + state + '&redirect_uri=' + (redirectUrl || this.redirectUrl) 13 | } 14 | 15 | getAccessToken (code, redirectUrl) { 16 | return got.post('https://accounts.google.com/o/oauth2/token', 17 | { 18 | form: { 19 | client_id: this.cid, 20 | client_secret: this.cs, 21 | code: code, 22 | grant_type: 'authorization_code', 23 | redirect_uri: redirectUrl || this.redirectUrl 24 | } 25 | }).json() 26 | } 27 | 28 | insertItem (token, fileBin) { 29 | return got.post('https://www.googleapis.com/upload/chromewebstore/v1.1/items', 30 | { 31 | headers: { 32 | Authorization: 'Bearer ' + token, 33 | 'x-goog-api-version': 2 34 | }, 35 | body: fileBin 36 | }).json() 37 | } 38 | 39 | getItem (token, itemId, projection = 'DRAFT') { 40 | return got.get('https://www.googleapis.com//chromewebstore/v1.1/items/' + itemId + '?projection=' + projection, 41 | { 42 | headers: { 43 | Authorization: 'Bearer ' + token, 44 | 'x-goog-api-version': 2 45 | } 46 | }).json() 47 | } 48 | 49 | updateItem (token, fileBin, itemId) { 50 | return got.put('https://www.googleapis.com/upload/chromewebstore/v1.1/items/' + itemId, 51 | { 52 | headers: { 53 | Authorization: 'Bearer ' + token, 54 | 'x-goog-api-version': 2 55 | }, 56 | body: fileBin, 57 | timeout: 120 * 1000 58 | }).json() 59 | } 60 | 61 | publishItem (token, itemId, target = 'default') { 62 | return got.post('https://www.googleapis.com//chromewebstore/v1.1/items/' + itemId + '/publish?publishTarget=' + target, 63 | { 64 | headers: { 65 | Authorization: 'Bearer ' + token, 66 | 'x-goog-api-version': 2 67 | } 68 | }) 69 | } 70 | 71 | getRefreshToken (refreshToken) { 72 | return got.post('https://www.googleapis.com/oauth2/v3/token', 73 | { 74 | form: { 75 | client_id: this.cid, 76 | client_secret: this.cs, 77 | grant_type: 'refresh_token', 78 | refresh_token: refreshToken 79 | } 80 | }).json() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/libs/getToken.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | if (typeof options === 'string') { 3 | return options 4 | } 5 | if (typeof options === 'object' && options.token) { 6 | return options.token 7 | } 8 | if (process.env.WEBSTORE_TOKEN) { 9 | return process.env.WEBSTORE_TOKEN 10 | } 11 | console.error("Require Chrome Webstore API access_token. Please set into `--token` or ENV['WEBSTORE_TOKEN']") 12 | process.exit(1) 13 | } 14 | --------------------------------------------------------------------------------