plugins first implementation

This commit is contained in:
antelle 2017-02-18 23:46:59 +01:00
parent 07c8150c2c
commit 20cc765fb5
15 changed files with 753 additions and 16 deletions

View File

@ -24,5 +24,8 @@ indent_size = 4
[*.scss]
indent_size = 2
[*.css]
indent_size = 2
[*.nsh]
indent_size = 2

View File

@ -2,26 +2,10 @@
const AppSettingsModel = require('../models/app-settings-model');
const Libs = {
backbone: require('backbone'),
_: require('underscore'),
underscore: require('underscore'),
$: require('jquery'),
jquery: require('jquery'),
kdbxweb: require('kdbxweb'),
hbs: require('hbs'),
pikaday: require('pikaday'),
filesaver: require('filesaver'),
qrcode: require('qrcode')
};
const ExportApi = {
settings: {
get: function(key) { return key ? AppSettingsModel.instance.get(key) : AppSettingsModel.instance.toJSON(); },
set: function(key, value) { AppSettingsModel.instance.set(key, value); }
},
require: function(module) {
return Libs[module] || require('../' + module);
}
};

View File

@ -24,6 +24,7 @@
"shortcuts": "Shortcuts",
"help": "Help",
"settings": "Settings",
"plugins": "Plugins",
"history": "history",
"cache": "cache",
@ -444,6 +445,12 @@
"setShAutoTypeGlobal": "auto-type (when app is in background)",
"setShLock": "lock database",
"setPlInstallTitle": "Install new plugins",
"setPlInstallDesc": "KeeWeb plugins add features, themes and locales to KeeWeb. Plugins run with the same privileges as KeeWeb, they can access and manage all your passwords. Never install plugins you don't trust.",
"setPlInstallLabel": "Plugin URL",
"setPlInstallBtn": "Install",
"setPlInstallBtnProgress": "Installing",
"setAboutTitle": "About",
"setAboutBuilt": "This app is built with these awesome tools",
"setAboutLic": "License",

View File

@ -45,6 +45,7 @@ const MenuModel = Backbone.Model.extend({
this.generalSection = new MenuSectionModel([{ locTitle: 'menuSetGeneral', icon: 'cog', page: 'general', active: true }]);
this.shortcutsSection = new MenuSectionModel([{ locTitle: 'shortcuts', icon: 'keyboard-o', page: 'shortcuts' }]);
this.pluginsSection = new MenuSectionModel([{ locTitle: 'plugins', icon: 'puzzle-piece', page: 'plugins' }]);
this.aboutSection = new MenuSectionModel([{ locTitle: 'menuSetAbout', icon: 'info', page: 'about' }]);
this.helpSection = new MenuSectionModel([{ locTitle: 'help', icon: 'question', page: 'help' }]);
this.filesSection = new MenuSectionModel();
@ -52,6 +53,7 @@ const MenuModel = Backbone.Model.extend({
this.menus.settings = new MenuSectionCollection([
this.generalSection,
this.shortcutsSection,
this.pluginsSection,
this.aboutSection,
this.helpSection,
this.filesSection

View File

@ -0,0 +1,22 @@
'use strict';
const Libs = {
backbone: require('backbone'),
_: require('underscore'),
underscore: require('underscore'),
$: require('jquery'),
jquery: require('jquery'),
kdbxweb: require('kdbxweb'),
hbs: require('hbs'),
pikaday: require('pikaday'),
filesaver: require('filesaver'),
qrcode: require('qrcode')
};
const PluginApi = {
require(module) {
return Libs[module] || require('../' + module);
}
};
module.exports = PluginApi;

View File

@ -0,0 +1,28 @@
'use strict';
// const Logger = require('../util/logger');
const Plugin = require('./plugin');
// const logger = new Logger('plugin-manager');
const PluginManager = {
plugins: {},
install(url) {
return Plugin.load(url).then(plugin => {
const name = plugin.get('name');
return Promise.resolve().then(() => {
if (this.plugins[name]) {
return this.plugins[name].uninstall().then(() => {
delete this.plugins[name];
});
}
}).then(() => {
this.plugins[name] = plugin;
return plugin.install();
});
});
}
};
module.exports = PluginManager;

View File

@ -0,0 +1,291 @@
'use strict';
const kdbxweb = require('kdbxweb');
const Backbone = require('backbone');
const PluginApi = require('./plugin-api')
const Logger = require('../util/logger');
const commonLogger = new Logger('plugin');
const Plugin = Backbone.Model.extend({
defaults: {
name: '',
manifest: '',
url: '',
status: 'inactive'
},
resources: null,
initialize(manifest, url) {
this.set('name', manifest.name);
this.set('manifest', manifest);
this.set('url', url);
this.logger = new Logger(`plugin:${manifest.name}`);
},
install() {
this.set('status', 'installing');
return Promise.resolve().then(() => {
const error = this.validateManifest();
if (error) {
this.logger.error('Manifest validation error', error);
this.set('status', 'invalid');
throw 'Plugin validation error: ' + error;
}
return this.installWithManifest();
});
},
validateManifest() {
const manifest = this.get('manifest');
if (!manifest.name) {
return 'No plugin name';
}
if (!manifest.description) {
return 'No plugin description';
}
if (!/^\d+\.\d+\.\d+$/.test(manifest.version || '')) {
return 'Invalid plugin version';
}
if (manifest.manifestVersion !== '0.1.0') {
return 'Invalid manifest version ' + manifest.manifestVersion;
}
if (!manifest.author || !manifest.author.email || !manifest.author.name || !manifest.author.url) {
return 'Invalid plugin author';
}
if (!manifest.url) {
return 'No plugin url';
}
if (!manifest.publicKey) {
return 'No plugin public key';
}
if (!manifest.resources || !Object.keys(manifest.resources).length) {
return 'No plugin resources';
}
if (manifest.resources.loc &&
(!manifest.locale || !manifest.locale.title || !/^[a-z]{2}(-[A-Z]{2})?$/.test(manifest.locale.name))) {
return 'Bad plugin locale';
}
},
installWithManifest() {
const manifest = this.get('manifest');
const url = this.get('url');
this.logger.info('Loading plugin with resources', Object.keys(manifest.resources).join(', '));
this.resources = {};
const ts = this.logger.ts();
const results = [];
if (manifest.resources.css) {
results.push(this.loadResource('css', url + 'plugin.css'));
}
if (manifest.resources.js) {
results.push(this.loadResource('js', url + 'plugin.js'));
}
if (manifest.resources.loc) {
results.push(this.loadResource('js', url + manifest.locale.name + '.json'));
}
return Promise.all(results)
.catch(() => { throw 'Error loading plugin resources'; })
.then(() => this.installWithResources())
.then(() => {
this.logger.info('Install complete', this.logger.ts(ts));
});
},
loadResource(type, url) {
let ts = this.logger.ts();
const manifest = this.get('manifest');
return httpGet(url, true).then(data => {
this.logger.debug('Resource data loaded', type, this.logger.ts(ts));
ts = this.logger.ts();
const key = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(manifest.publicKey));
const signature = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(manifest.resources[type]));
const algo = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } };
return kdbxweb.CryptoEngine.subtle.importKey('spki', key, algo, false, ['verify'])
.then(subtleKey => kdbxweb.CryptoEngine.subtle.verify(algo, subtleKey, signature, data))
.catch(e => {
this.logger.error('Error validating resource signature', type, e);
throw e;
})
.then(valid => {
if (valid) {
this.logger.debug('Resource signature valid', type, this.logger.ts(ts));
this.resources[type] = data;
} else {
this.logger.error('Resource signature invalid', type);
throw `Signature invalid: ${type}`;
}
});
});
},
installWithResources() {
this.logger.info('Installing loaded plugin');
const manifest = this.get('manifest');
const promises = [];
if (this.resources.css) {
promises.push(this.applyCss(manifest.name, this.resources.css));
}
if (this.resources.js) {
promises.push(this.applyJs(manifest.name, this.resources.js));
}
if (this.resources.loc) {
promises.push(this.applyLoc(manifest.locale, this.resources.loc));
}
this.set('status', 'active');
return Promise.all(promises)
.catch(e => {
this.logger.info('Install error', e);
this.uninstall();
throw e;
});
},
applyCss(name, data) {
return Promise.resolve().then(() => {
const text = kdbxweb.ByteUtils.bytesToString(data);
this.createElementInHead('style', 'plugin-css-' + name, 'text/css', text);
this.logger.debug('Plugin style installed');
});
},
applyJs(name, data) {
return Promise.resolve().then(() => {
let text = kdbxweb.ByteUtils.bytesToString(data);
this.module = {exports: {}};
const id = 'plugin-' + Date.now().toString() + Math.random().toString();
global[id] = {
require: PluginApi.require,
module: this.module
};
text = `(function(require, module){${text}})(window["${id}"].require,window["${id}"].module);`;
const ts = this.logger.ts();
this.createElementInHead('script', 'plugin-js-' + name, 'text/javascript', text);
return new Promise((resolve, reject) => {
setTimeout(() => {
delete global[id];
if (this.module.exports.uninstall) {
this.logger.debug('Plugin script installed', this.logger.ts(ts));
resolve();
} else {
reject('Plugin script installation failed');
}
}, 0);
});
});
},
createElementInHead(tagName, id, type, text) {
let el = document.getElementById(id);
if (el) {
el.parentNode.removeChild(el);
}
el = document.createElement(tagName);
el.appendChild(document.createTextNode(text));
el.setAttribute('id', id);
el.setAttribute('type', type);
document.head.appendChild(el);
},
removeElement(id) {
const el = document.getElementById(id);
if (el) {
el.parentNode.removeChild(el);
}
},
applyLoc(locale, data) {
return Promise.resolve().then(() => {
data = JSON.parse(data);
// TODO: apply locale
this.logger.debug('Plugin locale installed');
});
},
removeLoc(locale) {
// TODO: remove locale
},
uninstall() {
const manifest = this.get('manifest');
this.logger.info('Uninstalling plugin with resources', Object.keys(manifest.resources).join(', '));
const ts = this.logger.ts();
return Promise.resolve().then(() => {
if (manifest.resources.css) {
this.removeElement('plugin-css-' + this.get('name'));
}
if (manifest.resources.js) {
try {
this.module.exports.uninstall();
} catch (e) {
this.logger.error('Plugin uninstall method returned an error', e);
}
this.removeElement('plugin-js-' + this.get('name'));
}
if (manifest.resources.loc) {
this.removeLoc(this.get('manifest').locale);
}
this.logger.info('Uninstall complete', this.logger.ts(ts));
});
}
});
Plugin.load = function(url) {
if (url[url.length - 1] !== '/') {
url += '/';
}
commonLogger.info('Installing plugin from url', url);
const manifestUrl = url + 'manifest.json';
return httpGet(manifestUrl)
.catch(e => {
commonLogger.error('Error loading plugin manifest', e);
throw 'Error loading plugin manifest';
})
.then(manifest => {
try {
manifest = JSON.parse(manifest);
} catch (e) {
commonLogger.error('Failed to parse manifest', manifest);
throw 'Failed to parse manifest';
}
commonLogger.debug('Loaded manifest', manifest);
return new Plugin(manifest, url);
});
};
function httpGet(url, binary) {
commonLogger.debug('GET', url);
const ts = commonLogger.ts();
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
commonLogger.debug('GET OK', url, commonLogger.ts(ts));
resolve(xhr.response);
} else {
commonLogger.debug('GET error', url, xhr.status);
reject(xhr.status ? `HTTP status ${xhr.status}` : 'network error');
}
});
xhr.addEventListener('error', () => {
commonLogger.debug('GET error', url, xhr.status);
reject(xhr.status ? `HTTP status ${xhr.status}` : 'network error');
});
xhr.addEventListener('abort', () => {
commonLogger.debug('GET aborted', url);
reject('Network request timeout');
});
xhr.addEventListener('timeout', () => {
commonLogger.debug('GET timeout', url);
reject('Network request timeout');
});
if (binary) {
xhr.responseType = binary ? 'arraybuffer' : 'text';
}
xhr.open('GET', url);
xhr.send();
});
}
module.exports = Plugin;

View File

@ -0,0 +1,50 @@
'use strict';
const Backbone = require('backbone');
const Locale = require('../../util/locale');
const PluginManager = require('../../plugins/plugin-manager');
const SettingsPluginsView = Backbone.View.extend({
template: require('templates/settings/settings-plugins.hbs'),
events: {
'click .settings_plugins-install-btn': 'installClick'
},
render() {
this.renderTemplate({
plugins: []
});
},
installClick() {
const installBtn = this.$el.find('.settings_plugins-install-btn');
const urlTextBox = this.$el.find('#settings__plugins-install-url');
const errorBox = this.$el.find('.settings__plugins-install-error');
errorBox.html('');
const url = urlTextBox.val().trim();
if (!url) {
return;
}
urlTextBox.prop('disabled', true);
installBtn.text(Locale.setPlInstallBtnProgress + '...').prop('disabled', true);
PluginManager.install(url)
.then(() => {
this.installFinished();
urlTextBox.val('');
})
.catch(e => {
this.installFinished();
errorBox.text(e.toString());
});
},
installFinished() {
const installBtn = this.$el.find('.settings_plugins-install-btn');
const urlTextBox = this.$el.find('#settings__plugins-install-url');
urlTextBox.prop('disabled', false);
installBtn.text(Locale.setPlInstallBtn).prop('disabled', false);
}
});
module.exports = SettingsPluginsView;

View File

@ -146,4 +146,9 @@
&--error { color: $red; }
}
}
&__plugins {
&-install-error {
margin-top: $base-padding-h;
}
}
}

View File

@ -0,0 +1,12 @@
<div>
<h1><i class="fa fa-puzzle-piece"></i> {{res 'plugins'}}</h1>
<h2>{{res 'setPlInstallTitle'}}</h2>
<div class="settings__plugins-list"></div>
<div class="settings__plugins-install">
<div>{{res 'setPlInstallDesc'}}</div>
<label for="settings__plugins-install-url">{{res 'setPlInstallLabel'}}</label>
<input type="text" class="settings__input input-base" id="settings__plugins-install-url" value="http://localhost:8085/plugins/example/" />
<button class="settings_plugins-install-btn">{{res 'setPlInstallBtn'}}</button>
<div class="error-color settings__plugins-install-error"></div>
</div>
</div>

3
plugins/example/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
*.log
*.pem

View File

@ -0,0 +1,18 @@
{
"version": "0.0.1",
"manifestVersion": "0.1.0",
"name": "example",
"description": "KeeWeb plugin example",
"author": {
"name": "antelle",
"email": "antelle.net@gmail.com",
"url": "http://antelle.net"
},
"licence": "MIT",
"url": "https://keeweb.info",
"resources": {
"js": "X/xB9Y09Pkctu0Cwbl5i17+t9ngE2iD43kU/sv6rrXdw030N8mHWc667jE0OJQnDslOSleg5FCnnQ11/An3xZy/hgmDHZfgIu4nqJmkMwJVgxqiaBR28yl664PhJEY9te9XYqH213OSrM3HwWkqFx5qGn7lNZOPD5Wbq5lEflj2a+yTnQlx5AO1vRhdLBYl1OatsQ9smRhPb24AfTtWbNfOnoYCwgKtbvhReSu6R5Z7iei9TV6VeTFrCspXIQr9NWCHzbRunGTZjbtK0zYFK1+w4xAIxKUdfuMx00lrebq5y4Xn6Kv6NEFF9iouPUxH4oQ8lXeNd4FpsMdGOOQNlzQ==",
"css": "ny7Aj5PURIaD6Y6CLE6FMaIaj/z77XN6j7eUIA2N6agFLvwRBsZDVtM02oidivtVDA5nimc7/5iKzS2ppijFK67g1zMKfdZwZHOz5VdDGijBA8Zu4IybfSu4FWnbnWVjkCKLdaCS0kf6q/ivjgvovTqTMnuhzobN9jgTyV+TgYbof+dK1EiQCeQxf/ylJJTa3Sj9IMOdHNmu1y8Gxm1Zo58mlYA8ZQsImVjL6aNNn7Lc5yooexBxTzpHqBHe2KNlJoGorZG4VUOb1lIdnUlUke17pmbidueZt5tYJzJXFUPpd8yHxDDqAGxmaTEbohVCwslgDptEF56V1Xe9y0IzCw=="
},
"publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1QB+yQofkqIHbHFVAtWhjEFjaxvNekyQx/aK7nEpZzqM8ReoVWbJGVA+7z7MhymwZanbL8uAUrSpNTp5eFWltDksxqHqmXT4UcFU+4reLjYfgwjIaA3c9Q+2JA1Iowqbv3NDcDKm6Ug+dROr04VDCfYE4WRYgGdTYHDbJs5svxUgQ25uc0KKUWAvhYbSKsw43AwmbPbKkHdfZHiS5ZST99HVEJWxn3Kd2zLY9Kk70nu9MzypMLDqxUqjKgdeRCIyZeAYzB75miH3B5vMKhFcdmA8+D6WU2N+6gzKsY5BfqF729uFKSTo4JUKQ5fMU0lKSDHG4qGrkgnURfAUuj9YMQIDAQAB"
}

View File

@ -0,0 +1,11 @@
i, svg {
animation: psychodelic-spin 1s linear 0s infinite;
transform-origin: center center;
}
@keyframes psychodelic-spin {
0% { transform: rotate(0deg); }
25% { transform: rotate(10deg); color: darkviolet; }
50% { transform: rotate(0deg); }
75% { transform: rotate(-10deg); color: hotpink; }
100% { transform: rotate(0deg); }
}

18
plugins/example/plugin.js Normal file
View File

@ -0,0 +1,18 @@
/**
* KeeWeb plugin: example
* @author antelle
* @license MIT
*/
const DetailsView = require('views/details/details-view');
const detailsViewRender = DetailsView.prototype.render;
DetailsView.prototype.render = function() {
detailsViewRender.apply(this);
this.$el.find('.details__header:first').addClass('input-shake');
return this;
};
module.exports.uninstall = function() {
DetailsView.prototype.render = detailsViewRender;
};

View File

@ -0,0 +1,283 @@
/**
* KeeWeb plugin creator
* (C) Antelle 2017, MIT license https://github.com/keeweb/keeweb
*/
/* eslint-disable no-console */
const path = require('path');
const fs = require('fs');
const childProcess = require('child_process');
const readline = require('readline');
const crypto = require('crypto');
const args = process.argv.splice(2);
const op = args.shift();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
showBanner();
switch (op) {
case 'create':
createPlugin();
break;
case 'sign':
signPlugin();
break;
default:
showHelp();
}
function showBanner() {
console.log('KeeWeb plugin control');
}
function showHelp() {
rl.close();
console.log('Usage:');
console.log(' - node kw-plugin-control.js create');
console.log(' - node kw-plugin-control.js sign <plugin_name>');
}
function createPlugin() {
gatherPluginData(data => {
console.log('Creating plugin:', data.name);
createPluginFolder(data);
generateKeyPair(data);
createManifest(data);
createResources(data);
console.log('Plugin created, now it\'s time to make something awesome!');
});
}
function gatherPluginData(callback) {
const data = {
version: '0.0.1',
manifestVersion: '0.1.0'
};
const questions = [{
text: 'Plugin name',
callback: name => {
if (!name) {
return true;
}
if (fs.existsSync(name)) {
console.log(`Folder '${name}' already exists, please select another name`);
return true;
}
data.name = name;
next();
}
}, {
text: 'Description',
callback: description => {
if (!description) {
return true;
}
data.description = description;
next();
}
}, {
text: 'Author name',
callback: authorName => {
if (!authorName) {
return true;
}
data.author = { name: authorName };
next();
}
}, {
text: 'Author email',
callback: authorEmail => {
if (!authorEmail || authorEmail.indexOf('@') < 0 || authorEmail.indexOf('.') < 0) {
return true;
}
data.author.email = authorEmail;
next();
}
}, {
text: 'Author url',
callback: authorUrl => {
if (!authorUrl || authorUrl.indexOf('http://') < 0 && authorUrl.indexOf('https://') < 0) {
return true;
}
data.author.url = authorUrl;
next();
}
}, {
text: 'License (default: MIT)',
callback: licence => {
data.licence = licence || 'MIT';
next();
}
}, {
text: 'Plugin page URL',
callback: pluginUrl => {
if (!pluginUrl || pluginUrl.indexOf('http://') < 0 && pluginUrl.indexOf('https://') < 0) {
return true;
}
data.url = pluginUrl;
next();
}
}, {
text: 'There are different types of KeeWeb plugins:\n ' +
'1: js only\n ' +
'2: js + css\n ' +
'3: css only\n ' +
'4: locale\n' +
'Your plugin type is',
callback: res => {
const placeholder = `<resource signature will be here, please run 'kw-plugin-control sign ${data.name}'>`;
switch (res) {
case '1':
data.resources = { js: placeholder };
break;
case '2':
data.resources = { js: placeholder, css: placeholder };
break;
case '3':
data.resources = { css: placeholder };
break;
case '4':
data.resources = { loc: placeholder };
break;
default:
console.error('Please enter number from 1 to 4');
return true;
}
next();
}
}, {
text: 'Locale code (format: xx or xx-XX)',
if: () => data.resources.loc,
callback: loc => {
if (!/^[a-z]{2}(-[A-Z]{2})?$/.test(loc) || loc === 'en') {
console.error('Invalid locale');
return true;
}
data.locale = { name: loc };
next();
}
}, {
text: 'Locale name (human-readable)',
if: () => data.resources.loc,
callback: loc => {
if (!loc || loc.toLowerCase() === 'english' || loc.toLowerCase() === 'default') {
console.error('Invalid locale');
return true;
}
data.locale.title = loc;
next();
}
}];
next();
function next() {
const question = questions.shift();
if (!question) {
callback(data);
rl.close();
} else {
if (question.if && !question.if()) {
return next();
}
ask(question.text, question.callback);
}
}
}
function ask(question, callback) {
rl.question(`${question}: `, answer => {
const res = callback(answer.trim());
if (res) {
ask(res.question || question, callback);
}
});
}
function createPluginFolder(data) {
console.log('Creating plugin folder...');
fs.mkdirSync(data.name);
}
function generateKeyPair(data) {
console.log('Generating key pair...');
const privateKeyPath = path.join(data.name, 'private_key.pem');
const publicKeyPath = path.join(data.name, 'public_key.pem');
childProcess.execSync(`openssl genpkey -algorithm RSA -out "${privateKeyPath}" -pkeyopt rsa_keygen_bits:2048`);
childProcess.execSync(`openssl rsa -pubout -in "${privateKeyPath}" -out "${publicKeyPath}"`);
data.publicKey = fs.readFileSync(path.join(data.name, 'public_key.pem'), 'utf8')
.match(/-+BEGIN PUBLIC KEY-+([\s\S]+?)-+END PUBLIC KEY-+/)[1]
.replace(/\n/g, '');
fs.unlinkSync(publicKeyPath);
}
function createManifest(data) {
console.log('Creating manifest...');
fs.writeFileSync(path.join(data.name, 'manifest.json'), JSON.stringify(data, null, 2));
}
function createResources(data) {
console.log('Adding default files...');
if (data.resources.js) {
fs.writeFileSync(path.join(data.name, 'plugin.js'), `/**
* KeeWeb plugin: ${data.name}
* @author ${data.author.name}
* @license ${data.licence}
*/
/* global kw */
`);
}
if (data.resources.css) {
fs.writeFileSync(path.join(data.name, 'plugin.css'), '');
}
if (data.resources.loc) {
fs.writeFileSync(path.join(data.name, data.locale.name + '.json'), '{\n}');
}
fs.writeFileSync(path.join(data.name, '.gitignore'), ['.DS_Store', '*.log', '*.pem'].join('\n'));
}
function signPlugin() {
rl.close();
const packageName = args.shift();
if (!packageName) {
showHelp();
return;
}
if (!fs.existsSync(packageName)) {
console.error('Package folder does not exist');
return process.exit(1);
}
const manifest = JSON.parse(fs.readFileSync(path.join(packageName, 'manifest.json')));
const privateKey = fs.readFileSync(path.join(packageName, 'private_key.pem'), 'binary');
for (const res of Object.keys(manifest.resources)) {
console.log(`Signing ${res}...`);
let fileName;
switch (res) {
case 'js':
fileName = 'plugin.js';
break;
case 'css':
fileName = 'plugin.css';
break;
case 'loc':
fileName = manifest.locale.name + '.json';
break;
}
fileName = path.join(packageName, fileName);
const sign = crypto.createSign('RSA-SHA256');
sign.write(fs.readFileSync(fileName));
sign.end();
manifest.resources[res] = sign.sign(privateKey).toString('base64');
}
fs.writeFileSync(path.join(packageName, 'manifest.json'), JSON.stringify(manifest, null, 2));
console.log('Done, package manifest updated');
}