From 61580d6f9be18e63d90fe684625bb5b400d80e26 Mon Sep 17 00:00:00 2001 From: cgeek <cem.moreau@gmail.com> Date: Sun, 4 Jun 2017 00:16:53 +0200 Subject: [PATCH] [enh] #957 Add `msPeriod` parameter --- app/lib/computation/blockchainContext.js | 2 + app/lib/constants.js | 3 + app/lib/dal/fileDAL.js | 20 +++-- app/lib/dal/sqliteDAL/MetaDAL.js | 23 ++++- app/lib/dal/sqliteDAL/index/MIndexDAL.js | 1 + doc/Protocol.md | 16 ++++ index.js | 2 +- package.json | 4 +- server.js | 9 +- test/dal/dal.js | 2 +- test/integration/membership_chainability.js | 93 +++++++++++++++++++++ test/integration/tools/toolbox.js | 7 +- yarn.lock | 54 +++++++----- 13 files changed, 197 insertions(+), 39 deletions(-) create mode 100644 test/integration/membership_chainability.js diff --git a/app/lib/computation/blockchainContext.js b/app/lib/computation/blockchainContext.js index 7cc75c5be..87cf93d00 100644 --- a/app/lib/computation/blockchainContext.js +++ b/app/lib/computation/blockchainContext.js @@ -154,6 +154,8 @@ function BlockchainContext(BlockchainService) { if (indexer.ruleIdentityWritability(iindex, conf) === false) throw Error('ruleIdentityWritability'); // BR_G64 if (indexer.ruleMembershipWritability(mindex, conf) === false) throw Error('ruleMembershipWritability'); + // BR_G108 + if (indexer.ruleMembershipPeriod(mindex) === false) throw Error('ruleMembershipPeriod'); // BR_G65 if (indexer.ruleCertificationWritability(cindex, conf) === false) throw Error('ruleCertificationWritability'); // BR_G66 diff --git a/app/lib/constants.js b/app/lib/constants.js index aefa526e6..0e8e784f7 100644 --- a/app/lib/constants.js +++ b/app/lib/constants.js @@ -11,6 +11,8 @@ const IPV6_REGEXP = /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1, module.exports = { + TIME_TO_TURN_ON_BRG_107: 1498860000, + ERROR: { PEER: { @@ -124,6 +126,7 @@ module.exports = { STEPMAX: 3, SIGDELAY: 3600 * 24 * 365 * 5, SIGPERIOD: 0, // Instant + MSPERIOD: 0, // Instant SIGSTOCK: 40, SIGWINDOW: 3600 * 24 * 7, // a week SIGVALIDITY: 3600 * 24 * 365, diff --git a/app/lib/dal/fileDAL.js b/app/lib/dal/fileDAL.js index cb83fd43c..9742f9f40 100644 --- a/app/lib/dal/fileDAL.js +++ b/app/lib/dal/fileDAL.js @@ -62,14 +62,14 @@ function FileDAL(params) { 'cindexDAL': that.cindexDAL }; - this.init = () => co(function *() { + this.init = (conf) => co(function *() { const dalNames = _.keys(that.newDals); for (const dalName of dalNames) { const dal = that.newDals[dalName]; yield dal.init(); } logger.debug("Upgrade database..."); - yield that.metaDAL.upgradeDatabase(); + yield that.metaDAL.upgradeDatabase(conf); const latestMember = yield that.iindexDAL.getLatestMember(); if (latestMember && that.wotb.getWoTSize() > latestMember.wotb_id + 1) { logger.warn('Maintenance: cleaning wotb...'); @@ -374,9 +374,19 @@ function FileDAL(params) { return _(pending).sortBy((ms) => -ms.number)[0]; }); - this.findNewcomers = () => co(function*() { - const mss = yield that.msDAL.getPendingIN(); - return _.chain(mss).sortBy((ms) => -ms.sigDate).value(); + this.findNewcomers = (blockMedianTime) => co(function*() { + const pending = yield that.msDAL.getPendingIN() + const mss = yield pending.map(p => co(function*() { + const reduced = yield that.mindexDAL.getReducedMS(p.issuer) + if (!reduced || !reduced.chainable_on || blockMedianTime >= reduced.chainable_on || blockMedianTime < constants.TIME_TO_TURN_ON_BRG_107) { + return p + } + return null + })) + return _.chain(mss) + .filter(ms => ms) + .sortBy((ms) => -ms.sigDate) + .value() }); this.findLeavers = () => co(function*() { diff --git a/app/lib/dal/sqliteDAL/MetaDAL.js b/app/lib/dal/sqliteDAL/MetaDAL.js index 017cdf2b2..b90353c54 100644 --- a/app/lib/dal/sqliteDAL/MetaDAL.js +++ b/app/lib/dal/sqliteDAL/MetaDAL.js @@ -267,6 +267,21 @@ function MetaDAL(driver) { wallet.balance = amountsRemaining.reduce((sum, src) => sum + src.amount * Math.pow(10, src.base), 0) yield walletDAL.saveWallet(wallet) } + }), + + /** + * Feeds the m_index.chainable_on + */ + 21: (conf) => co(function*() { + let blockDAL = new (require('./BlockDAL'))(driver); + let mindexDAL = new (require('./index/MIndexDAL'))(driver); + yield mindexDAL.exec('ALTER TABLE m_index ADD COLUMN chainable_on INTEGER NULL;') + const memberships = yield mindexDAL.query('SELECT * FROM m_index WHERE op = ?', [common.constants.IDX_CREATE]) + for (const ms of memberships) { + const reference = yield blockDAL.getBlock(parseInt(ms.written_on.split('-')[0])) + const updateQuery = 'UPDATE m_index SET chainable_on = ' + (reference.medianTime + conf.msPeriod) + ' WHERE pub = \'' + ms.pub + '\' AND op = \'CREATE\'' + yield mindexDAL.exec(updateQuery) + } }) }; @@ -280,7 +295,7 @@ function MetaDAL(driver) { 'COMMIT;', []); }); - function executeMigration(migration) { + function executeMigration(migration, conf) { return co(function *() { try { if (typeof migration == "string") { @@ -291,7 +306,7 @@ function MetaDAL(driver) { } else if (typeof migration == "function") { // JS function to execute - yield migration(); + yield migration(conf); } } catch (e) { @@ -300,10 +315,10 @@ function MetaDAL(driver) { }); } - this.upgradeDatabase = () => co(function *() { + this.upgradeDatabase = (conf) => co(function *() { let version = yield that.getVersion(); while(migrations[version]) { - yield executeMigration(migrations[version]); + yield executeMigration(migrations[version], conf); // Automated increment yield that.exec('UPDATE meta SET version = version + 1'); version++; diff --git a/app/lib/dal/sqliteDAL/index/MIndexDAL.js b/app/lib/dal/sqliteDAL/index/MIndexDAL.js index 72052c6f9..ab8ae3876 100644 --- a/app/lib/dal/sqliteDAL/index/MIndexDAL.js +++ b/app/lib/dal/sqliteDAL/index/MIndexDAL.js @@ -28,6 +28,7 @@ function MIndexDAL(driver) { 'expired_on', 'revokes_on', 'revoked_on', + 'chainable_on', 'leaving', 'revocation' ]; diff --git a/doc/Protocol.md b/doc/Protocol.md index 8d4bded3b..612afb4ea 100644 --- a/doc/Protocol.md +++ b/doc/Protocol.md @@ -1057,6 +1057,7 @@ ud0 | UD(0), i.e. initial Universal Dividend udTime0 | Time of first UD. udReevalTime0 | Time of first reevaluation of the UD. sigPeriod | Minimum delay between 2 certifications of a same issuer, in seconds. Must be positive or zero. +msPeriod | Minimum delay between 2 memberships of a same issuer, in seconds. Must be positive or zero. sigStock | Maximum quantity of active certifications made by member. sigWindow | Maximum delay a certification can wait before being expired for non-writing. sigValidity | Maximum age of an active signature (in seconds) @@ -1198,6 +1199,7 @@ Each identity produces 2 new entries: expired_on = 0 expires_on = MedianTime + msValidity revokes_on = MedianTime + msValidity*2 + chainable_on = MedianTime + msPeriod type = 'JOIN' revoked_on = null leaving = false @@ -1226,6 +1228,7 @@ Each join whose `PUBLIC_KEY` **does not match** a local MINDEX `CREATE, PUBLIC_K expired_on = 0 expires_on = MedianTime + msValidity revokes_on = MedianTime + msValidity*2 + chainable_on = MedianTime + msPeriod type = 'JOIN' revoked_on = null leaving = null @@ -1242,6 +1245,7 @@ Each active produces 1 new entry: written_on = BLOCKSTAMP expires_on = MedianTime + msValidity revokes_on = MedianTime + msValidity*2 + chainable_on = MedianTime + msPeriod type = 'RENEW' revoked_on = null leaving = null @@ -2088,6 +2092,12 @@ For each ENTRY in local MINDEX where `revoked_on == null`: For each ENTRY in local MINDEX where `revoked_on != null`: ENTRY.isBeingRevoked = true + +####### BR_G107 - ENTRY.unchainables +F +If `HEAD.number > 0`: + + ENTRY.unchainables = COUNT(GLOBAL_MINDEX[issuer=ENTRY.issuer, chainable_on > HEAD~1.medianTime])) ###### Local CINDEX augmentation @@ -2327,6 +2337,12 @@ Rule: ENTRY.age <= [msWindow] +###### BR_G108 - Membership period + +Rule: + + ENTRY.unchainables == 0 + ###### BR_G65 - Certification writability Rule: diff --git a/index.js b/index.js index 56cd2c9f9..a96bed320 100644 --- a/index.js +++ b/index.js @@ -296,7 +296,7 @@ function Stack(dependencies) { return yield command.onConfiguredExecute(server, conf, program, params, wizardTasks, that); } // Second possible class of commands: post-service - yield server.initDAL(); + yield server.initDAL(conf); /** * Service injection diff --git a/package.json b/package.json index 3d9bd6949..cd215319e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "colors": "1.1.2", "commander": "2.9.0", "daemonize2": "0.4.2", - "duniter-common": "1.3.x", + "duniter-common": "^1.3.5", "event-stream": "3.3.4", "heapdump": "^0.3.9", "inquirer": "3.0.6", @@ -71,7 +71,7 @@ "duniter-bma": "1.3.x", "duniter-crawler": "1.3.x", "duniter-keypair": "1.3.X", - "duniter-prover": "1.3.x", + "duniter-prover": "^1.3.3", "duniter-ui": "1.3.x", "eslint": "3.13.1", "eslint-plugin-mocha": "4.8.0", diff --git a/server.js b/server.js index 8c9bff582..bdb435dfb 100644 --- a/server.js +++ b/server.js @@ -89,6 +89,7 @@ function Server (home, memoryOnly, overrideConf) { ud0: constants.CONTRACT.DEFAULT.UD0, stepMax: constants.CONTRACT.DEFAULT.STEPMAX, sigPeriod: constants.CONTRACT.DEFAULT.SIGPERIOD, + msPeriod: constants.CONTRACT.DEFAULT.MSPERIOD, sigStock: constants.CONTRACT.DEFAULT.SIGSTOCK, sigWindow: constants.CONTRACT.DEFAULT.SIGWINDOW, sigValidity: constants.CONTRACT.DEFAULT.SIGVALIDITY, @@ -110,6 +111,8 @@ function Server (home, memoryOnly, overrideConf) { that.conf[key] = defaultValues[key]; } }); + // 1.3.X: the msPeriod = msWindow + that.conf.msPeriod = that.conf.msPeriod || that.conf.msWindow // Default keypair if (!that.conf.pair || !that.conf.pair.pub || !that.conf.pair.sec) { // Create a random key @@ -171,8 +174,8 @@ function Server (home, memoryOnly, overrideConf) { this.submitP = (obj, isInnerWrite) => Q.nbind(this.submit, this)(obj, isInnerWrite); - this.initDAL = () => co(function*() { - yield that.dal.init(); + this.initDAL = (conf) => co(function*() { + yield that.dal.init(conf); // Maintenance let head_1 = yield that.dal.bindexDAL.head(1); if (head_1) { @@ -352,7 +355,7 @@ function Server (home, memoryOnly, overrideConf) { this.writeRaw = (raw, type) => co(function *() { const parser = documentsMapping[type] && documentsMapping[type].parser; - const obj = parser.syncWrite(raw); + const obj = parser.syncWrite(raw, logger); return yield that.singleWritePromise(obj); }); diff --git a/test/dal/dal.js b/test/dal/dal.js index 9415f79c5..e31968f1e 100644 --- a/test/dal/dal.js +++ b/test/dal/dal.js @@ -103,7 +103,7 @@ describe("DAL", function(){ it('should have DB version 21', () => co(function *() { let version = yield fileDAL.getDBVersion(); should.exist(version); - version.should.equal(21); + version.should.equal(22); })); it('should have no peer in a first time', function(){ diff --git a/test/integration/membership_chainability.js b/test/integration/membership_chainability.js new file mode 100644 index 000000000..62aa5e99c --- /dev/null +++ b/test/integration/membership_chainability.js @@ -0,0 +1,93 @@ +"use strict" + +const co = require('co') +const should = require('should') +const toolbox = require('./tools/toolbox') + +describe("Membership chainability", function() { + + describe("before July 2017", () => { + + const now = 1482220000 + let s1, cat + + const conf = { + msPeriod: 20, + nbCores: 1, + msValidity: 10000, + udTime0: now, + udReevalTime0: now, + sigQty: 1, + medianTimeBlocks: 1 // The medianTime always equals previous block's medianTime + } + + before(() => co(function*() { + const res1 = yield toolbox.simpleNodeWith2Users(conf) + s1 = res1.s1 + cat = res1.cat + yield s1.commit({ time: now }) + yield s1.commit({ time: now }) + yield s1.commit({ time: now, actives: [ + 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd:QA2gKg6x2PhqMyKhi3hWBXuRJuRwd8G6WGHGNZIEicUR2kjE8Y3WScLyaMNQAZF3s7ewvUvpWkewopd5ugr+Bg==:1-4A21CEA1EA7C3BB0A22DEC87C5AECB38E69DB70A269CEC3644B8149B322C7669:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:cat' + ]}) + })) + + it('current should be the 2nd', () => s1.expect('/blockchain/current', (res) => { + res.should.have.property('number').equal(2) + res.should.have.property('actives').length(1) + })) + }) + + describe("after July 2017", () => { + + const now = 1498860000 + let s1, cat + + const conf = { + msPeriod: 20, + nbCores: 1, + msValidity: 10000, + udTime0: now, + udReevalTime0: now, + sigQty: 1, + medianTimeBlocks: 1 // The medianTime always equals previous block's medianTime + } + + before(() => co(function*() { + const res1 = yield toolbox.simpleNodeWith2Users(conf) + s1 = res1.s1 + cat = res1.cat + yield s1.commit({ time: now }) + yield s1.commit({ time: now + 20 }) + })) + + it('should refuse a block with a too early membership in it', () => co(function*() { + yield toolbox.shouldFail(s1.commit({ + time: now + 20, + actives: ['HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd:H2jum4LLenc/69vZAFw2OppLxVQgNtp+7XL+M9nSvAGjxMf8jBEAeQ/nrfDP3Lrk2SvDvp5Hice5jFboHVdxAQ==:1-2989DEFA8BD18F111B3686EB14ED91EE7C509C9D74EE5C96AECBD4F3CA5E0FB6:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:cat'] + }), '500 - "{\\n \\"ucode\\": 1002,\\n \\"message\\": \\"ruleMembershipPeriod\\"\\n}"') + })) + + it('should not be able to renew immediately', () => co(function*() { + yield cat.join() + yield s1.commit({ time: now + 20 }) + yield s1.expect('/blockchain/block/2', (res) => { + res.should.have.property('number').equal(2) + res.should.have.property('joiners').length(0) + }) + })) + + it('should be able to renew after 20 sec', () => co(function*() { + yield s1.commit({ time: now + 20 }) + yield s1.expect('/blockchain/block/3', (res) => { + res.should.have.property('number').equal(3) + res.should.have.property('actives').length(1) + }) + })) + + it('current should be the 4th', () => s1.expect('/blockchain/current', (res) => { + res.should.have.property('number').equal(3) + res.should.have.property('actives').length(1) + })) + }) +}) diff --git a/test/integration/tools/toolbox.js b/test/integration/tools/toolbox.js index 8a42e7fef..aadc5d3c8 100644 --- a/test/integration/tools/toolbox.js +++ b/test/integration/tools/toolbox.js @@ -30,9 +30,12 @@ module.exports = { shouldFail: (promise, message) => co(function*() { try { yield promise; - throw { "message": '{ "message": "Should have thrown an error" }' }; + throw '{ "message": "Should have thrown an error" }' } catch(e) { - const err = JSON.parse(e) + let err = e + if (typeof e === "string") { + err = JSON.parse(e) + } err.should.have.property('message').equal(message); } }), diff --git a/yarn.lock b/yarn.lock index a352398a8..90f8d17a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,8 +87,8 @@ ansi@^0.3.0, ansi@~0.3.1: resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" aproba@^1.0.3: - version "1.1.1" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.1.tgz#95d3600f07710aa0e9298c726ad5ecf2eacbabab" + version "1.1.2" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" archiver-utils@^1.3.0: version "1.3.0" @@ -229,6 +229,12 @@ base-x@^2.0.1: dependencies: safe-buffer "^5.0.1" +base-x@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.2.tgz#bf873861b7514279b7969f340929eab87c11d130" + dependencies: + safe-buffer "^5.0.1" + basic-auth@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" @@ -312,17 +318,23 @@ brace-expansion@^1.0.0, brace-expansion@^1.1.7: balanced-match "^0.4.1" concat-map "0.0.1" -bs58@4.0.0, bs58@^4.0.0: +bs58@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.0.tgz#65f5deaf6d74e6135a99f763ca6209ab424b9172" dependencies: base-x "^2.0.1" +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + dependencies: + base-x "^3.0.2" + buffer-crc32@^0.2.1: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" -buffer-shims@^1.0.0, buffer-shims@~1.0.0: +buffer-shims@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" @@ -739,9 +751,9 @@ duniter-bma@1.3.x: underscore "1.8.3" ws "1.1.1" -duniter-common@1.3.x, duniter-common@^1.3.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/duniter-common/-/duniter-common-1.3.4.tgz#5f666930d373447ec202207b89a7803581cb8b1d" +duniter-common@1.3.x, duniter-common@^1.3.0, duniter-common@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/duniter-common/-/duniter-common-1.3.5.tgz#b727117a2c9463d0486b7c0feb845df60b65e247" dependencies: bs58 "^4.0.0" co "4.6.0" @@ -781,9 +793,9 @@ duniter-keypair@1.3.X, duniter-keypair@1.3.x: tweetnacl "0.14.5" tweetnacl-util "0.15.0" -duniter-prover@1.3.x: - version "1.3.2" - resolved "https://registry.yarnpkg.com/duniter-prover/-/duniter-prover-1.3.2.tgz#8923ece7619a1170d2f4f43ee3cadac65999958e" +duniter-prover@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/duniter-prover/-/duniter-prover-1.3.3.tgz#2575119c7996b16bde3ad0032d248ce179080064" dependencies: async "2.2.0" co "4.6.0" @@ -857,8 +869,8 @@ errorhandler@1.5.0: escape-html "~1.0.3" es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.21" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.21.tgz#19a725f9e51d0300bbc1e8e821109fd9daf55925" + version "0.10.22" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.22.tgz#1876c51f990769c112c781ea3ebe89f84fd39071" dependencies: es6-iterator "2" es6-symbol "~3.1" @@ -912,11 +924,11 @@ escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@1.0.2, escape-string-regexp@^1.0.2: +escape-string-regexp@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" -escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -2483,14 +2495,14 @@ readable-stream@1.1.x: string_decoder "~0.10.x" readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2: - version "2.2.9" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.9.tgz#cf78ec6f4a6d1eb43d26488cac97f042e74b7fc8" + version "2.2.10" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.10.tgz#effe72bb7c884c0dd335e2379d526196d9d011ee" dependencies: - buffer-shims "~1.0.0" core-util-is "~1.0.0" inherits "~2.0.1" isarray "~1.0.0" process-nextick-args "~1.0.6" + safe-buffer "^5.0.1" string_decoder "~1.0.0" util-deprecate "~1.0.1" @@ -2689,8 +2701,8 @@ rx@^4.1.0: resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" safe-buffer@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" + version "5.1.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223" sax@>=0.1.1: version "1.2.2" @@ -3163,8 +3175,8 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" uglify-js@^2.6: - version "2.8.27" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.27.tgz#47787f912b0f242e5b984343be8e35e95f694c9c" + version "2.8.28" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.28.tgz#e335032df9bb20dcb918f164589d5af47f38834a" dependencies: source-map "~0.5.1" yargs "~3.10.0" -- GitLab