Async SF API
This commit is contained in:
8
.babelrc
8
.babelrc
@@ -1,3 +1,9 @@
|
|||||||
{
|
{
|
||||||
"presets": ["env"]
|
"presets": ["env"],
|
||||||
|
"plugins": [
|
||||||
|
["transform-runtime", {
|
||||||
|
"polyfill": false,
|
||||||
|
"regenerator": true
|
||||||
|
}]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class AccountMenu {
|
|||||||
$timeout, storageManager, $compile, archiveManager) {
|
$timeout, storageManager, $compile, archiveManager) {
|
||||||
'ngInject';
|
'ngInject';
|
||||||
|
|
||||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false, email: "a@bitar.io", user_password: "password"};
|
||||||
$scope.user = authManager.user;
|
$scope.user = authManager.user;
|
||||||
$scope.server = syncManager.serverURL;
|
$scope.server = syncManager.serverURL;
|
||||||
|
|
||||||
@@ -262,26 +262,28 @@ class AccountMenu {
|
|||||||
}.bind(this)
|
}.bind(this)
|
||||||
|
|
||||||
if(data.auth_params) {
|
if(data.auth_params) {
|
||||||
SFJS.crypto.computeEncryptionKeysForUser(password, data.auth_params, (keys) => {
|
SFJS.crypto.computeEncryptionKeysForUser(password, data.auth_params).then((keys) => {
|
||||||
try {
|
try {
|
||||||
SFItemTransformer.decryptMultipleItems(data.items, keys, false); /* throws = false as we don't want to interrupt all decryption if just one fails */
|
SFJS.itemTransformer.decryptMultipleItems(data.items, keys, false) /* throws = false as we don't want to interrupt all decryption if just one fails */
|
||||||
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
.then(() => {
|
||||||
data.items.forEach(function(item){
|
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
||||||
item.enc_item_key = null;
|
data.items.forEach(function(item){
|
||||||
item.auth_hash = null;
|
item.enc_item_key = null;
|
||||||
});
|
item.auth_hash = null;
|
||||||
|
});
|
||||||
|
|
||||||
var errorCount = 0;
|
var errorCount = 0;
|
||||||
// Don't import items that didn't decrypt properly
|
// Don't import items that didn't decrypt properly
|
||||||
data.items = data.items.filter(function(item){
|
data.items = data.items.filter(function(item){
|
||||||
if(item.errorDecrypting) {
|
if(item.errorDecrypting) {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
})
|
||||||
|
|
||||||
|
onDataReady(errorCount);
|
||||||
})
|
})
|
||||||
|
|
||||||
onDataReady(errorCount);
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.log("Error decrypting", e);
|
console.log("Error decrypting", e);
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ class PasswordWizard {
|
|||||||
// Ensure value for current password matches what's saved
|
// Ensure value for current password matches what's saved
|
||||||
let authParams = authManager.getAuthParams();
|
let authParams = authManager.getAuthParams();
|
||||||
let password = $scope.formData.currentPassword;
|
let password = $scope.formData.currentPassword;
|
||||||
SFJS.crypto.computeEncryptionKeysForUser(password, authParams, (keys) => {
|
SFJS.crypto.computeEncryptionKeysForUser(password, authParams).then((keys) => {
|
||||||
let success = keys.mk === authManager.keys().mk;
|
let success = keys.mk === authManager.keys().mk;
|
||||||
if(success) {
|
if(success) {
|
||||||
this.currentServerPw = keys.pw;
|
this.currentServerPw = keys.pw;
|
||||||
@@ -202,7 +202,10 @@ class PasswordWizard {
|
|||||||
|
|
||||||
let currentServerPw = this.currentServerPw;
|
let currentServerPw = this.currentServerPw;
|
||||||
|
|
||||||
SFJS.crypto.generateInitialEncryptionKeysForUser(authManager.user.email, newUserPassword, (newKeys, newAuthParams) => {
|
SFJS.crypto.generateInitialEncryptionKeysForUser(authManager.user.email, newUserPassword).then((results) => {
|
||||||
|
let newKeys = results.newKeys;
|
||||||
|
let newAuthParams = results.newAuthParams;
|
||||||
|
|
||||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||||
syncManager.sync((response) => {
|
syncManager.sync((response) => {
|
||||||
authManager.changePassword(currentServerPw, newKeys, newAuthParams, (response) => {
|
authManager.changePassword(currentServerPw, newKeys, newAuthParams, (response) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Item {
|
|||||||
this.observers = [];
|
this.observers = [];
|
||||||
|
|
||||||
if(!this.uuid) {
|
if(!this.uuid) {
|
||||||
this.uuid = SFJS.crypto.generateUUID();
|
this.uuid = SFJS.crypto.generateUUIDSync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,34 +3,35 @@ class ItemParams {
|
|||||||
constructor(item, keys, version) {
|
constructor(item, keys, version) {
|
||||||
this.item = item;
|
this.item = item;
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
this.version = version || SFJS.crypto.version();
|
this.version = version || SFJS.version();
|
||||||
}
|
}
|
||||||
|
|
||||||
paramsForExportFile(includeDeleted) {
|
async paramsForExportFile(includeDeleted) {
|
||||||
this.additionalFields = ["updated_at"];
|
this.additionalFields = ["updated_at"];
|
||||||
this.forExportFile = true;
|
this.forExportFile = true;
|
||||||
if(includeDeleted) {
|
if(includeDeleted) {
|
||||||
return this.__params();
|
return this.__params();
|
||||||
} else {
|
} else {
|
||||||
return _.omit(this.__params(), ["deleted"]);
|
var result = await this.__params();
|
||||||
|
return _.omit(result, ["deleted"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
paramsForExtension() {
|
async paramsForExtension() {
|
||||||
return this.paramsForExportFile();
|
return this.paramsForExportFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
paramsForLocalStorage() {
|
async paramsForLocalStorage() {
|
||||||
this.additionalFields = ["updated_at", "dirty", "errorDecrypting"];
|
this.additionalFields = ["updated_at", "dirty", "errorDecrypting"];
|
||||||
this.forExportFile = true;
|
this.forExportFile = true;
|
||||||
return this.__params();
|
return this.__params();
|
||||||
}
|
}
|
||||||
|
|
||||||
paramsForSync() {
|
async paramsForSync() {
|
||||||
return this.__params();
|
return this.__params();
|
||||||
}
|
}
|
||||||
|
|
||||||
__params() {
|
async __params() {
|
||||||
|
|
||||||
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
|
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ class ItemParams {
|
|||||||
// Items should always be encrypted for export files. Only respect item.doNotEncrypt for remote sync params.
|
// Items should always be encrypted for export files. Only respect item.doNotEncrypt for remote sync params.
|
||||||
var doNotEncrypt = this.item.doNotEncrypt() && !this.forExportFile;
|
var doNotEncrypt = this.item.doNotEncrypt() && !this.forExportFile;
|
||||||
if(this.keys && !doNotEncrypt) {
|
if(this.keys && !doNotEncrypt) {
|
||||||
var encryptedParams = SFItemTransformer.encryptItem(this.item, this.keys, this.version);
|
var encryptedParams = await SFJS.itemTransformer.encryptItem(this.item, this.keys, this.version);
|
||||||
_.merge(params, encryptedParams);
|
_.merge(params, encryptedParams);
|
||||||
|
|
||||||
if(this.version !== "001") {
|
if(this.version !== "001") {
|
||||||
@@ -47,7 +48,7 @@ class ItemParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
params.content = this.forExportFile ? this.item.createContentJSONFromProperties() : "000" + SFJS.crypto.base64(JSON.stringify(this.item.createContentJSONFromProperties()));
|
params.content = this.forExportFile ? this.item.createContentJSONFromProperties() : "000" + await SFJS.crypto.base64(JSON.stringify(this.item.createContentJSONFromProperties()));
|
||||||
if(!this.forExportFile) {
|
if(!this.forExportFile) {
|
||||||
params.enc_item_key = null;
|
params.enc_item_key = null;
|
||||||
params.auth_hash = null;
|
params.auth_hash = null;
|
||||||
|
|||||||
@@ -60,17 +60,18 @@ class ActionsManager {
|
|||||||
switch (action.verb) {
|
switch (action.verb) {
|
||||||
case "get": {
|
case "get": {
|
||||||
|
|
||||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
this.httpManager.getAbsolute(action.url, {}, (response) => {
|
||||||
action.error = false;
|
action.error = false;
|
||||||
var items = response.items || [response.item];
|
var items = response.items || [response.item];
|
||||||
SFItemTransformer.decryptMultipleItems(items, this.authManager.keys());
|
SFJS.itemTransformer.decryptMultipleItems(items, this.authManager.keys()).then(() => {
|
||||||
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
|
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
|
||||||
for(var item of items) {
|
for(var item of items) {
|
||||||
item.setDirty(true);
|
item.setDirty(true);
|
||||||
}
|
}
|
||||||
this.syncManager.sync(null);
|
this.syncManager.sync(null);
|
||||||
customCallback({items: items});
|
customCallback({items: items});
|
||||||
}.bind(this), function(response){
|
})
|
||||||
|
}, (response) => {
|
||||||
action.error = true;
|
action.error = true;
|
||||||
customCallback(null);
|
customCallback(null);
|
||||||
})
|
})
|
||||||
@@ -80,13 +81,13 @@ class ActionsManager {
|
|||||||
|
|
||||||
case "render": {
|
case "render": {
|
||||||
|
|
||||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
this.httpManager.getAbsolute(action.url, {}, (response) => {
|
||||||
action.error = false;
|
action.error = false;
|
||||||
SFItemTransformer.decryptItem(response.item, this.authManager.keys());
|
SFJS.itemTransformer.decryptItem(response.item, this.authManager.keys()).then(() => {
|
||||||
var item = this.modelManager.createItem(response.item, true /* Dont notify observers */);
|
var item = this.modelManager.createItem(response.item, true /* Dont notify observers */);
|
||||||
customCallback({item: item});
|
customCallback({item: item});
|
||||||
|
})
|
||||||
}.bind(this), function(response){
|
}, (response) => {
|
||||||
action.error = true;
|
action.error = true;
|
||||||
customCallback(null);
|
customCallback(null);
|
||||||
})
|
})
|
||||||
@@ -102,22 +103,15 @@ class ActionsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "post": {
|
case "post": {
|
||||||
var params = {};
|
this.outgoingParamsForItem(item, extension, decrypted).then((itemParams) => {
|
||||||
|
var params = {
|
||||||
|
items: [itemParams] // Wrap it in an array
|
||||||
|
}
|
||||||
|
|
||||||
if(action.all) {
|
this.performPost(action, extension, params, function(response){
|
||||||
var items = this.modelManager.allItemsMatchingTypes(action.content_types);
|
customCallback(response);
|
||||||
params.items = items.map(function(item){
|
});
|
||||||
var params = this.outgoingParamsForItem(item, extension, decrypted);
|
})
|
||||||
return params;
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
} else {
|
|
||||||
params.items = [this.outgoingParamsForItem(item, extension, decrypted)];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.performPost(action, extension, params, function(response){
|
|
||||||
customCallback(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -130,7 +124,7 @@ class ActionsManager {
|
|||||||
action.lastExecuted = new Date();
|
action.lastExecuted = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
outgoingParamsForItem(item, extension, decrypted = false) {
|
async outgoingParamsForItem(item, extension, decrypted = false) {
|
||||||
var keys = this.authManager.keys();
|
var keys = this.authManager.keys();
|
||||||
if(decrypted) {
|
if(decrypted) {
|
||||||
keys = null;
|
keys = null;
|
||||||
|
|||||||
@@ -24,22 +24,23 @@ class ArchiveManager {
|
|||||||
protocolVersion = this.authManager.protocolVersion();
|
protocolVersion = this.authManager.protocolVersion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var data = this.__itemsData(keys, authParams, protocolVersion);
|
this.__itemsData(keys, authParams, protocolVersion).then((data) => {
|
||||||
this.__downloadData(data, `SN Archive - ${new Date()}.txt`);
|
this.__downloadData(data, `SN Archive - ${new Date()}.txt`);
|
||||||
|
|
||||||
// download as zipped plain text files
|
// download as zipped plain text files
|
||||||
if(!keys) {
|
if(!keys) {
|
||||||
var notes = this.modelManager.allItemsMatchingTypes(["Note"]);
|
var notes = this.modelManager.allItemsMatchingTypes(["Note"]);
|
||||||
this.__downloadZippedNotes(notes);
|
this.__downloadZippedNotes(notes);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Private
|
Private
|
||||||
*/
|
*/
|
||||||
|
|
||||||
__itemsData(keys, authParams, protocolVersion) {
|
async __itemsData(keys, authParams, protocolVersion) {
|
||||||
let data = this.modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
|
let data = await this.modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
|
||||||
let blobData = new Blob([data], {type: 'text/json'});
|
let blobData = new Blob([data], {type: 'text/json'});
|
||||||
return blobData;
|
return blobData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,37 +83,8 @@ angular.module('app')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.costMinimumForVersion = function(version) {
|
|
||||||
// all current versions have a min of 3000
|
|
||||||
// future versions will increase this
|
|
||||||
return SFJS.crypto.costMinimumForVersion(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isProtocolVersionSupported = function(version) {
|
this.isProtocolVersionSupported = function(version) {
|
||||||
return SFJS.crypto.supportedVersions().includes(version);
|
return SFJS.supportedVersions().includes(version);
|
||||||
}
|
|
||||||
|
|
||||||
/* Upon sign in to an outdated version, the user will be presented with an alert requiring them to confirm
|
|
||||||
understanding they are signing in with an older version of the protocol, and must upgrade immediately after completing sign in.
|
|
||||||
*/
|
|
||||||
this.isProtocolVersionOutdated = function(version) {
|
|
||||||
// YYYY-MM-DD
|
|
||||||
let expirationDates = {
|
|
||||||
"001" : Date.parse("2018-01-01"),
|
|
||||||
"002" : Date.parse("2019-06-01"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let date = expirationDates[version];
|
|
||||||
if(!date) {
|
|
||||||
// No expiration date, is active version
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let expired = new Date() > date;
|
|
||||||
return expired;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.supportsPasswordDerivationCost = function(cost) {
|
|
||||||
return SFJS.crypto.supportsPasswordDerivationCost(cost);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getAuthParamsForEmail = function(url, email, extraParams, callback) {
|
this.getAuthParamsForEmail = function(url, email, extraParams, callback) {
|
||||||
@@ -130,7 +101,7 @@ angular.module('app')
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.login = function(url, email, password, ephemeral, strictSignin, extraParams, callback) {
|
this.login = function(url, email, password, ephemeral, strictSignin, extraParams, callback) {
|
||||||
this.getAuthParamsForEmail(url, email, extraParams, function(authParams){
|
this.getAuthParamsForEmail(url, email, extraParams, (authParams) => {
|
||||||
|
|
||||||
// SF3 requires a unique identifier in the auth params
|
// SF3 requires a unique identifier in the auth params
|
||||||
authParams.identifier = email;
|
authParams.identifier = email;
|
||||||
@@ -147,7 +118,7 @@ angular.module('app')
|
|||||||
|
|
||||||
if(!this.isProtocolVersionSupported(authParams.version)) {
|
if(!this.isProtocolVersionSupported(authParams.version)) {
|
||||||
var message;
|
var message;
|
||||||
if(SFJS.crypto.isVersionNewerThanLibraryVersion(authParams.version)) {
|
if(SFJS.isVersionNewerThanLibraryVersion(authParams.version)) {
|
||||||
// The user has a new account type, but is signing in to an older client.
|
// The user has a new account type, but is signing in to an older client.
|
||||||
message = "This version of the application does not support your newer account type. Please upgrade to the latest version of Standard Notes to sign in.";
|
message = "This version of the application does not support your newer account type. Please upgrade to the latest version of Standard Notes to sign in.";
|
||||||
} else {
|
} else {
|
||||||
@@ -158,14 +129,14 @@ angular.module('app')
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.isProtocolVersionOutdated(authParams.version)) {
|
if(SFJS.isProtocolVersionOutdated(authParams.version)) {
|
||||||
let message = `The encryption version for your account, ${authParams.version}, is outdated and requires upgrade. You may proceed with login, but are advised to follow prompts for Security Updates once inside. Please visit standardnotes.org/help/security for more information.\n\nClick 'OK' to proceed with login.`
|
let message = `The encryption version for your account, ${authParams.version}, is outdated and requires upgrade. You may proceed with login, but are advised to follow prompts for Security Updates once inside. Please visit standardnotes.org/help/security for more information.\n\nClick 'OK' to proceed with login.`
|
||||||
if(!confirm(message)) {
|
if(!confirm(message)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
if(!SFJS.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
||||||
let message = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
let message = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
||||||
"If we attempted to generate your login keys here, it would take hours. " +
|
"If we attempted to generate your login keys here, it would take hours. " +
|
||||||
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to log in."
|
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to log in."
|
||||||
@@ -173,7 +144,7 @@ angular.module('app')
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var minimum = this.costMinimumForVersion(authParams.version);
|
var minimum = SFJS.costMinimumForVersion(authParams.version);
|
||||||
if(authParams.pw_cost < minimum) {
|
if(authParams.pw_cost < minimum) {
|
||||||
let message = "Unable to login due to insecure password parameters. Please visit standardnotes.org/help/security for more information.";
|
let message = "Unable to login due to insecure password parameters. Please visit standardnotes.org/help/security for more information.";
|
||||||
callback({error: {message: message}});
|
callback({error: {message: message}});
|
||||||
@@ -182,7 +153,7 @@ angular.module('app')
|
|||||||
|
|
||||||
if(strictSignin) {
|
if(strictSignin) {
|
||||||
// Refuse sign in if authParams.version is anything but the latest version
|
// Refuse sign in if authParams.version is anything but the latest version
|
||||||
var latestVersion = SFJS.crypto.version();
|
var latestVersion = SFJS.version();
|
||||||
if(authParams.version !== latestVersion) {
|
if(authParams.version !== latestVersion) {
|
||||||
let message = `Strict sign in refused server sign in parameters. The latest security version is ${latestVersion}, but your account is reported to have version ${authParams.version}. If you'd like to proceed with sign in anyway, please disable strict sign in and try again.`;
|
let message = `Strict sign in refused server sign in parameters. The latest security version is ${latestVersion}, but your account is reported to have version ${authParams.version}. If you'd like to proceed with sign in anyway, please disable strict sign in and try again.`;
|
||||||
callback({error: {message: message}});
|
callback({error: {message: message}});
|
||||||
@@ -190,24 +161,25 @@ angular.module('app')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SFJS.crypto.computeEncryptionKeysForUser(password, authParams, function(keys){
|
SFJS.crypto.computeEncryptionKeysForUser(password, authParams).then((keys) => {
|
||||||
var requestUrl = url + "/auth/sign_in";
|
var requestUrl = url + "/auth/sign_in";
|
||||||
var params = _.merge({password: keys.pw, email: email}, extraParams);
|
var params = _.merge({password: keys.pw, email: email}, extraParams);
|
||||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
|
||||||
|
httpManager.postAbsolute(requestUrl, params, (response) => {
|
||||||
this.setEphemeral(ephemeral);
|
this.setEphemeral(ephemeral);
|
||||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||||
this.checkForSecurityUpdate();
|
this.checkForSecurityUpdate();
|
||||||
$timeout(() => callback(response));
|
$timeout(() => callback(response));
|
||||||
}.bind(this), function(response){
|
}, (response) => {
|
||||||
console.error("Error logging in", response);
|
console.error("Error logging in", response);
|
||||||
if(typeof response !== 'object') {
|
if(typeof response !== 'object') {
|
||||||
response = {error: {message: "A server error occurred while trying to sign in. Please try again."}};
|
response = {error: {message: "A server error occurred while trying to sign in. Please try again."}};
|
||||||
}
|
}
|
||||||
$timeout(() => callback(response));
|
$timeout(() => callback(response));
|
||||||
})
|
});
|
||||||
|
|
||||||
}.bind(this));
|
});
|
||||||
}.bind(this))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
||||||
@@ -238,7 +210,10 @@ angular.module('app')
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.register = function(url, email, password, ephemeral, callback) {
|
this.register = function(url, email, password, ephemeral, callback) {
|
||||||
SFJS.crypto.generateInitialEncryptionKeysForUser(email, password, (keys, authParams) => {
|
SFJS.crypto.generateInitialEncryptionKeysForUser(email, password).then((results) => {
|
||||||
|
let keys = results.keys;
|
||||||
|
let authParams = results.authParams;
|
||||||
|
|
||||||
var requestUrl = url + "/auth";
|
var requestUrl = url + "/auth";
|
||||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
var params = _.merge({password: keys.pw, email: email}, authParams);
|
||||||
|
|
||||||
@@ -277,12 +252,12 @@ angular.module('app')
|
|||||||
this.updateAuthParams = function(authParams, callback) {
|
this.updateAuthParams = function(authParams, callback) {
|
||||||
var requestUrl = storageManager.getItem("server") + "/auth/update";
|
var requestUrl = storageManager.getItem("server") + "/auth/update";
|
||||||
var params = authParams;
|
var params = authParams;
|
||||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
httpManager.postAbsolute(requestUrl, params, (response) => {
|
||||||
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
||||||
if(callback) {
|
if(callback) {
|
||||||
callback(response);
|
callback(response);
|
||||||
}
|
}
|
||||||
}.bind(this), function(response){
|
}, function(response){
|
||||||
var error = response;
|
var error = response;
|
||||||
console.error("Update error:", response);
|
console.error("Update error:", response);
|
||||||
if(callback) {
|
if(callback) {
|
||||||
@@ -297,7 +272,7 @@ angular.module('app')
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let latest = SFJS.crypto.version();
|
let latest = SFJS.version();
|
||||||
|
|
||||||
if(this.protocolVersion() !== latest) {
|
if(this.protocolVersion() !== latest) {
|
||||||
// Prompt user to perform security update
|
// Prompt user to perform security update
|
||||||
|
|||||||
@@ -716,7 +716,7 @@ class ComponentManager {
|
|||||||
console.log("Web|componentManager|registerComponentWindow", component);
|
console.log("Web|componentManager|registerComponentWindow", component);
|
||||||
}
|
}
|
||||||
component.window = componentWindow;
|
component.window = componentWindow;
|
||||||
component.sessionKey = SFJS.crypto.generateUUID();
|
component.sessionKey = SFJS.crypto.generateUUIDSync();
|
||||||
this.sendMessageToComponent(component, {
|
this.sendMessageToComponent(component, {
|
||||||
action: "component-registered",
|
action: "component-registered",
|
||||||
sessionKey: component.sessionKey,
|
sessionKey: component.sessionKey,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class DesktopManager {
|
|||||||
Sending a component in its raw state is really slow for the desktop app
|
Sending a component in its raw state is really slow for the desktop app
|
||||||
Keys are not passed into ItemParams, so the result is not encrypted
|
Keys are not passed into ItemParams, so the result is not encrypted
|
||||||
*/
|
*/
|
||||||
convertComponentForTransmission(component) {
|
async convertComponentForTransmission(component) {
|
||||||
return new ItemParams(component).paramsForExportFile(true);
|
return new ItemParams(component).paramsForExportFile(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,14 +44,15 @@ class DesktopManager {
|
|||||||
syncComponentsInstallation(components) {
|
syncComponentsInstallation(components) {
|
||||||
if(!this.isDesktop) return;
|
if(!this.isDesktop) return;
|
||||||
|
|
||||||
var data = components.map((component) => {
|
Promise.all(components.map((component) => {
|
||||||
return this.convertComponentForTransmission(component);
|
return this.convertComponentForTransmission(component);
|
||||||
|
})).then((data) => {
|
||||||
|
this.installationSyncHandler(data);
|
||||||
})
|
})
|
||||||
this.installationSyncHandler(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
installComponent(component) {
|
async installComponent(component) {
|
||||||
this.installComponentHandler(this.convertComponentForTransmission(component));
|
this.installComponentHandler(await this.convertComponentForTransmission(component));
|
||||||
}
|
}
|
||||||
|
|
||||||
registerUpdateObserver(callback) {
|
registerUpdateObserver(callback) {
|
||||||
@@ -128,7 +129,7 @@ class DesktopManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_requestBackupFile() {
|
desktop_requestBackupFile(callback) {
|
||||||
var keys, authParams, protocolVersion;
|
var keys, authParams, protocolVersion;
|
||||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||||
keys = this.passcodeManager.keys();
|
keys = this.passcodeManager.keys();
|
||||||
@@ -140,13 +141,14 @@ class DesktopManager {
|
|||||||
protocolVersion = this.authManager.protocolVersion();
|
protocolVersion = this.authManager.protocolVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = this.modelManager.getAllItemsJSONData(
|
this.modelManager.getAllItemsJSONData(
|
||||||
keys,
|
keys,
|
||||||
authParams,
|
authParams,
|
||||||
protocolVersion,
|
protocolVersion,
|
||||||
true /* return null on empty */
|
true /* return null on empty */
|
||||||
);
|
).then((data) => {
|
||||||
return data;
|
callback(data);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_setMajorDataChangeHandler(handler) {
|
desktop_setMajorDataChangeHandler(handler) {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class ModelManager {
|
|||||||
|
|
||||||
var newItem = this.createItem(item);
|
var newItem = this.createItem(item);
|
||||||
|
|
||||||
newItem.uuid = SFJS.crypto.generateUUID();
|
newItem.uuid = SFJS.crypto.generateUUIDSync();
|
||||||
|
|
||||||
// Update uuids of relationships
|
// Update uuids of relationships
|
||||||
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);
|
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);
|
||||||
@@ -455,24 +455,25 @@ class ModelManager {
|
|||||||
Archives
|
Archives
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getAllItemsJSONData(keys, authParams, protocolVersion, returnNullIfEmpty) {
|
async getAllItemsJSONData(keys, authParams, protocolVersion, returnNullIfEmpty) {
|
||||||
var items = _.map(this.allItems, (item) => {
|
return Promise.all(this.allItems.map((item) => {
|
||||||
var itemParams = new ItemParams(item, keys, protocolVersion);
|
var itemParams = new ItemParams(item, keys, protocolVersion);
|
||||||
return itemParams.paramsForExportFile();
|
return itemParams.paramsForExportFile();
|
||||||
});
|
})).then((items) => {
|
||||||
|
if(returnNullIfEmpty && items.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if(returnNullIfEmpty && items.length == 0) {
|
var data = {items: items}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = {items: items}
|
if(keys) {
|
||||||
|
// auth params are only needed when encrypted with a standard file key
|
||||||
|
data["auth_params"] = authParams;
|
||||||
|
}
|
||||||
|
|
||||||
if(keys) {
|
return JSON.stringify(data, null, 2 /* pretty print */);
|
||||||
// auth params are only needed when encrypted with a standard file key
|
})
|
||||||
data["auth_params"] = authParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(data, null, 2 /* pretty print */);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ angular.module('app')
|
|||||||
|
|
||||||
this.unlock = function(passcode, callback) {
|
this.unlock = function(passcode, callback) {
|
||||||
var params = this.passcodeAuthParams();
|
var params = this.passcodeAuthParams();
|
||||||
SFJS.crypto.computeEncryptionKeysForUser(passcode, params, (keys) => {
|
SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => {
|
||||||
if(keys.pw !== params.hash) {
|
if(keys.pw !== params.hash) {
|
||||||
callback(false);
|
callback(false);
|
||||||
return;
|
return;
|
||||||
@@ -40,16 +40,20 @@ angular.module('app')
|
|||||||
|
|
||||||
this._keys = keys;
|
this._keys = keys;
|
||||||
this._authParams = params;
|
this._authParams = params;
|
||||||
this.decryptLocalStorage(keys, params);
|
this.decryptLocalStorage(keys, params).then(() => {
|
||||||
this._locked = false;
|
this._locked = false;
|
||||||
callback(true);
|
callback(true);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPasscode = (passcode, callback) => {
|
this.setPasscode = (passcode, callback) => {
|
||||||
var uuid = SFJS.crypto.generateUUID();
|
var uuid = SFJS.crypto.generateUUIDSync();
|
||||||
|
|
||||||
|
SFJS.crypto.generateInitialEncryptionKeysForUser(uuid, passcode).then((results) => {
|
||||||
|
let keys = results.keys;
|
||||||
|
let authParams = results.authParams;
|
||||||
|
|
||||||
SFJS.crypto.generateInitialEncryptionKeysForUser(uuid, passcode, (keys, authParams) => {
|
|
||||||
authParams.hash = keys.pw;
|
authParams.hash = keys.pw;
|
||||||
this._keys = keys;
|
this._keys = keys;
|
||||||
this._hasPasscode = true;
|
this._hasPasscode = true;
|
||||||
@@ -83,9 +87,9 @@ angular.module('app')
|
|||||||
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
|
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.decryptLocalStorage = function(keys, authParams) {
|
this.decryptLocalStorage = async function(keys, authParams) {
|
||||||
storageManager.setKeys(keys, authParams);
|
storageManager.setKeys(keys, authParams);
|
||||||
storageManager.decryptStorage();
|
return storageManager.decryptStorage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -154,12 +154,14 @@ class StorageManager {
|
|||||||
|
|
||||||
// Save new encrypted storage in Fixed storage
|
// Save new encrypted storage in Fixed storage
|
||||||
var params = new ItemParams(encryptedStorage, this.encryptedStorageKeys, this.encryptedStorageAuthParams.version);
|
var params = new ItemParams(encryptedStorage, this.encryptedStorageKeys, this.encryptedStorageAuthParams.version);
|
||||||
this.setItem("encryptedStorage", JSON.stringify(params.paramsForSync()), StorageManager.Fixed);
|
params.paramsForSync().then((syncParams) => {
|
||||||
|
this.setItem("encryptedStorage", JSON.stringify(syncParams), StorageManager.Fixed);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptStorage() {
|
async decryptStorage() {
|
||||||
var stored = JSON.parse(this.getItem("encryptedStorage", StorageManager.Fixed));
|
var stored = JSON.parse(this.getItem("encryptedStorage", StorageManager.Fixed));
|
||||||
SFItemTransformer.decryptItem(stored, this.encryptedStorageKeys);
|
await SFJS.itemTransformer.decryptItem(stored, this.encryptedStorageKeys);
|
||||||
var encryptedStorage = new EncryptedStorage(stored);
|
var encryptedStorage = new EncryptedStorage(stored);
|
||||||
|
|
||||||
for(var key of Object.keys(encryptedStorage.storage)) {
|
for(var key of Object.keys(encryptedStorage.storage)) {
|
||||||
|
|||||||
@@ -29,24 +29,26 @@ class SyncManager {
|
|||||||
|
|
||||||
var version = this.authManager.offline() ? this.passcodeManager.protocolVersion() : this.authManager.protocolVersion();
|
var version = this.authManager.offline() ? this.passcodeManager.protocolVersion() : this.authManager.protocolVersion();
|
||||||
var keys = this.authManager.offline() ? this.passcodeManager.keys() : this.authManager.keys();
|
var keys = this.authManager.offline() ? this.passcodeManager.keys() : this.authManager.keys();
|
||||||
var params = items.map(function(item) {
|
|
||||||
|
Promise.all(items.map(async (item) => {
|
||||||
var itemParams = new ItemParams(item, keys, version);
|
var itemParams = new ItemParams(item, keys, version);
|
||||||
itemParams = itemParams.paramsForLocalStorage();
|
itemParams = await itemParams.paramsForLocalStorage();
|
||||||
if(offlineOnly) {
|
if(offlineOnly) {
|
||||||
delete itemParams.dirty;
|
delete itemParams.dirty;
|
||||||
}
|
}
|
||||||
return itemParams;
|
return itemParams;
|
||||||
}.bind(this));
|
})).then((params) => {
|
||||||
|
this.storageManager.saveModels(params, callback);
|
||||||
this.storageManager.saveModels(params, callback);
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLocalItems(callback) {
|
loadLocalItems(callback) {
|
||||||
var params = this.storageManager.getAllModels(function(items){
|
var params = this.storageManager.getAllModels((items) => {
|
||||||
var items = this.handleItemsResponse(items, null, ModelManager.MappingSourceLocalRetrieved);
|
this.handleItemsResponse(items, null, ModelManager.MappingSourceLocalRetrieved).then((items) => {
|
||||||
Item.sortItemsByDate(items);
|
Item.sortItemsByDate(items);
|
||||||
callback(items);
|
callback(items);
|
||||||
}.bind(this))
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
syncOffline(items, callback) {
|
syncOffline(items, callback) {
|
||||||
@@ -54,7 +56,7 @@ class SyncManager {
|
|||||||
for(var item of items) {
|
for(var item of items) {
|
||||||
item.updated_at = new Date();
|
item.updated_at = new Date();
|
||||||
}
|
}
|
||||||
this.writeItemsToLocalStorage(items, true, function(responseItems){
|
this.writeItemsToLocalStorage(items, true, (responseItems) => {
|
||||||
// delete anything needing to be deleted
|
// delete anything needing to be deleted
|
||||||
for(var item of items) {
|
for(var item of items) {
|
||||||
if(item.deleted) {
|
if(item.deleted) {
|
||||||
@@ -70,7 +72,7 @@ class SyncManager {
|
|||||||
if(callback) {
|
if(callback) {
|
||||||
callback({success: true});
|
callback({success: true});
|
||||||
}
|
}
|
||||||
}.bind(this))
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +203,7 @@ class SyncManager {
|
|||||||
this.syncLocked = false;
|
this.syncLocked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sync(callback, options = {}, source) {
|
async sync(callback, options = {}, source) {
|
||||||
|
|
||||||
if(this.syncLocked) {
|
if(this.syncLocked) {
|
||||||
console.log("Sync Locked, Returning;");
|
console.log("Sync Locked, Returning;");
|
||||||
@@ -281,11 +283,14 @@ class SyncManager {
|
|||||||
|
|
||||||
var params = {};
|
var params = {};
|
||||||
params.limit = 150;
|
params.limit = 150;
|
||||||
params.items = _.map(subItems, function(item){
|
|
||||||
|
await Promise.all(subItems.map((item) => {
|
||||||
var itemParams = new ItemParams(item, keys, version);
|
var itemParams = new ItemParams(item, keys, version);
|
||||||
itemParams.additionalFields = options.additionalFields;
|
itemParams.additionalFields = options.additionalFields;
|
||||||
return itemParams.paramsForSync();
|
return itemParams.paramsForSync();
|
||||||
}.bind(this));
|
})).then((itemsParams) => {
|
||||||
|
params.items = itemsParams;
|
||||||
|
})
|
||||||
|
|
||||||
for(var item of subItems) {
|
for(var item of subItems) {
|
||||||
// Reset dirty counter to 0, since we're about to sync it.
|
// Reset dirty counter to 0, since we're about to sync it.
|
||||||
@@ -300,7 +305,7 @@ class SyncManager {
|
|||||||
this.stopCheckingIfSyncIsTakingTooLong();
|
this.stopCheckingIfSyncIsTakingTooLong();
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
|
||||||
var onSyncSuccess = function(response) {
|
var onSyncSuccess = async function(response) {
|
||||||
// Check to make sure any subItem hasn't been marked as dirty again while a sync was ongoing
|
// Check to make sure any subItem hasn't been marked as dirty again while a sync was ongoing
|
||||||
var itemsToClearAsDirty = [];
|
var itemsToClearAsDirty = [];
|
||||||
for(var item of subItems) {
|
for(var item of subItems) {
|
||||||
@@ -325,8 +330,7 @@ class SyncManager {
|
|||||||
|
|
||||||
// Map retrieved items to local data
|
// Map retrieved items to local data
|
||||||
// Note that deleted items will not be returned
|
// Note that deleted items will not be returned
|
||||||
var retrieved
|
var retrieved = await this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved);
|
||||||
= this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved);
|
|
||||||
|
|
||||||
// Append items to master list of retrieved items for this ongoing sync operation
|
// Append items to master list of retrieved items for this ongoing sync operation
|
||||||
this.allRetreivedItems = this.allRetreivedItems.concat(retrieved);
|
this.allRetreivedItems = this.allRetreivedItems.concat(retrieved);
|
||||||
@@ -337,8 +341,7 @@ class SyncManager {
|
|||||||
var omitFields = ["content", "auth_hash"];
|
var omitFields = ["content", "auth_hash"];
|
||||||
|
|
||||||
// Map saved items to local data
|
// Map saved items to local data
|
||||||
var saved =
|
var saved = await this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved);
|
||||||
this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved);
|
|
||||||
|
|
||||||
// Append items to master list of saved items for this ongoing sync operation
|
// Append items to master list of saved items for this ongoing sync operation
|
||||||
this.allSavedItems = this.allSavedItems.concat(saved);
|
this.allSavedItems = this.allSavedItems.concat(saved);
|
||||||
@@ -418,9 +421,9 @@ class SyncManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleItemsResponse(responseItems, omitFields, source) {
|
async handleItemsResponse(responseItems, omitFields, source) {
|
||||||
var keys = this.authManager.keys() || this.passcodeManager.keys();
|
var keys = this.authManager.keys() || this.passcodeManager.keys();
|
||||||
SFItemTransformer.decryptMultipleItems(responseItems, keys);
|
await SFJS.itemTransformer.decryptMultipleItems(responseItems, keys);
|
||||||
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields, source);
|
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields, source);
|
||||||
|
|
||||||
// During the decryption process, items may be marked as "errorDecrypting". If so, we want to be sure
|
// During the decryption process, items may be marked as "errorDecrypting". If so, we want to be sure
|
||||||
@@ -446,7 +449,7 @@ class SyncManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnsavedItemsResponse(unsaved) {
|
async handleUnsavedItemsResponse(unsaved) {
|
||||||
if(unsaved.length == 0) {
|
if(unsaved.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -454,7 +457,7 @@ class SyncManager {
|
|||||||
console.log("Handle unsaved", unsaved);
|
console.log("Handle unsaved", unsaved);
|
||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
var handleNext = () => {
|
var handleNext = async () => {
|
||||||
if(i >= unsaved.length) {
|
if(i >= unsaved.length) {
|
||||||
// Handled all items
|
// Handled all items
|
||||||
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||||
@@ -463,7 +466,7 @@ class SyncManager {
|
|||||||
|
|
||||||
var mapping = unsaved[i];
|
var mapping = unsaved[i];
|
||||||
var itemResponse = mapping.item;
|
var itemResponse = mapping.item;
|
||||||
SFItemTransformer.decryptMultipleItems([itemResponse], this.authManager.keys());
|
await SFJS.itemTransformer.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||||
|
|
||||||
if(!item) {
|
if(!item) {
|
||||||
|
|||||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -1001,6 +1001,15 @@
|
|||||||
"regenerator-transform": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz"
|
"regenerator-transform": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"babel-plugin-transform-runtime": {
|
||||||
|
"version": "6.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz",
|
||||||
|
"integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz"
|
||||||
|
}
|
||||||
|
},
|
||||||
"babel-plugin-transform-strict-mode": {
|
"babel-plugin-transform-strict-mode": {
|
||||||
"version": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz",
|
"version": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz",
|
||||||
"integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=",
|
"integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"angular": "^1.6.1",
|
"angular": "^1.6.1",
|
||||||
"angular-mocks": "^1.6.1",
|
"angular-mocks": "^1.6.1",
|
||||||
"babel-cli": "^6.18.0",
|
"babel-cli": "^6.18.0",
|
||||||
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"babel-preset-env": "^1.1.1",
|
"babel-preset-env": "^1.1.1",
|
||||||
"babel-preset-es2016": "^6.16.0",
|
"babel-preset-es2016": "^6.16.0",
|
||||||
"bower": "^1.8.0",
|
"bower": "^1.8.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user