diff --git a/CHANGELOG.md b/CHANGELOG.md index b024022..d0a42a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Webpack bundling +### Changed +- Updated openpgpjs to 5.1.0 ## [3.2.0] - 2021-11-07 ### Added diff --git a/package.json b/package.json index 3d136cb..6255381 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "bent": "^7.3.12", "body-parser": "^1.19.0", "dialog-polyfill": "^0.5.6", - "doipjs": "^0.14.0", + "doipjs": "^0.15.2", "dotenv": "^8.2.0", "express": "^4.17.1", "express-validator": "^6.13.0", @@ -16,7 +16,7 @@ "got": "^11.8.2", "jstransformer-markdown-it": "^2.1.0", "libravatar": "^3.0.0", - "openpgp": "^4.10.9", + "openpgp": "^5.1.0", "pug": "^3.0.0", "qrcode": "^1.4.4", "string-replace-middleware": "^1.0.2" diff --git a/server/index.js b/server/index.js index d05c5b0..67c0a11 100644 --- a/server/index.js +++ b/server/index.js @@ -174,7 +174,7 @@ const computeExtraData = async (key, keyData) => { // Query libravatar to get the avatar url return { - avatarURL: await libravatar.get_avatar_url({ email: primaryUser.user.userId.email, size: 128, default: 'mm', https: true }) + avatarURL: await libravatar.get_avatar_url({ email: primaryUser.user.userID.email, size: 128, default: 'mm', https: true }) } } diff --git a/server/keys.js b/server/keys.js index 6f13a51..d336179 100644 --- a/server/keys.js +++ b/server/keys.js @@ -81,7 +81,9 @@ const fetchWKD = (id) => { } try { - output.publicKey = (await openpgp.key.read(plaintext)).keys[0] + output.publicKey = await openpgp.readKey({ + binaryKey: plaintext + }) } catch(error) { reject(new Error(`No public keys could be read from the data fetched using WKD`)) } @@ -136,7 +138,9 @@ const fetchSignature = (signature) => { // Check validity of signature let signatureData try { - signatureData = await openpgp.cleartext.readArmored(signature) + signatureData = await openpgp.readCleartextMessage({ + cleartextMessage: signature + }) } catch (error) { reject(new Error(`Signature could not be properly read (${error.message})`)) } @@ -159,10 +163,10 @@ const fetchSignature = (signature) => { // Check validity of signature const verified = await openpgp.verify({ message: signatureData, - publicKeys: output.publicKey + verificationKeys: output.publicKey }) - const { valid } = verified.signatures[0] - if (!valid) { + + if (!await verified.signatures[0].verified) { reject(new Error('Signature was invalid')) } diff --git a/server/utils.js b/server/utils.js index 550312e..8848e9c 100644 --- a/server/utils.js +++ b/server/utils.js @@ -27,12 +27,12 @@ You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . */ -const openpgp = require('openpgp') +const crypto = require('crypto').webcrypto -exports.computeWKDLocalPart = async (message) => { - const data = openpgp.util.str_to_Uint8Array(message.toLowerCase()); - const hash = await openpgp.crypto.hash.sha1(data); - return openpgp.util.encodeZBase32(hash); +exports.computeWKDLocalPart = async (localPart) => { + const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase()); + const localPartHashed = new Uint8Array(await crypto.subtle.digest('SHA-1', localPartEncoded)); + return encodeZBase32(localPartHashed); } exports.generatePageTitle = (type, data) => { @@ -50,3 +50,33 @@ exports.generatePageTitle = (type, data) => { break } } + +// Copied from https://github.com/openpgpjs/wkd-client/blob/0d074519e011a5139a8953679cf5f807e4cd2378/src/wkd.js +function encodeZBase32(data) { + if (data.length === 0) { + return ""; + } + const ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769"; + const SHIFT = 5; + const MASK = 31; + let buffer = data[0]; + let index = 1; + let bitsLeft = 8; + let result = ''; + while (bitsLeft > 0 || index < data.length) { + if (bitsLeft < SHIFT) { + if (index < data.length) { + buffer <<= 8; + buffer |= data[index++] & 0xff; + bitsLeft += 8; + } else { + const pad = SHIFT - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } + } + bitsLeft -= SHIFT; + result += ALPHABET[MASK & (buffer >> bitsLeft)]; + } + return result; +} \ No newline at end of file diff --git a/static-src/ui.js b/static-src/ui.js index a07b8c7..b590c14 100644 --- a/static-src/ui.js +++ b/static-src/ui.js @@ -91,11 +91,11 @@ export function init() { } if (elUtilQR) { - runQRUtility + runQRUtility() } if (elUtilProfileURL) { - runProfileURLUtility + runProfileURLUtility() } } @@ -113,11 +113,13 @@ const runEncryptionForm = () => { config.show_version = false; let encrypted = await openpgp.encrypt({ - message: openpgp.message.fromText(elFormEncrypt.querySelector('.input').value), - publicKeys: window.kx.key.object, + message: await openpgp.createMessage({ + text: elFormEncrypt.querySelector('.input').value + }), + encryptionKeys: window.kx.key.object, config: config }); - elFormEncrypt.querySelector('.output').value = encrypted.data; + elFormEncrypt.querySelector('.output').value = encrypted; } catch (e) { console.error(e); elFormEncrypt.querySelector('.output').value = `Could not encrypt message!\n==========================\n${e.message ? e.message : e}`; @@ -136,24 +138,27 @@ const runVerificationForm = () => { // Try two different methods of signature reading let signature = null, verified = null, readError = null; try { - signature = await openpgp.message.readArmored(elFormVerify.querySelector('.input').value); + signature = await openpgp.readMessage({ + armoredMessage: elFormVerify.querySelector('.input').value + }); } catch(e) { - readError = e; - } - try { - signature = await openpgp.cleartext.readArmored(elFormVerify.querySelector('.input').value); - } catch(e) { - readError = e; + try { + signature = await openpgp.readCleartextMessage({ + cleartextMessage: elFormVerify.querySelector('.input').value + }); + } catch(e) { + readError = e; + } } if (signature == null) { throw(readError) }; // Verify the signature verified = await openpgp.verify({ message: signature, - publicKeys: window.kx.key.object + verificationKeys: window.kx.key.object }); - if (verified.signatures[0].valid) { + if (await verified.signatures[0].verified) { elFormVerify.querySelector('.output').value = `The message was signed by the profile's key.`; } else { elFormVerify.querySelector('.output').value = `The message was NOT signed by the profile's key.`; diff --git a/static-src/utils.js b/static-src/utils.js index 270f133..b0445a3 100644 --- a/static-src/utils.js +++ b/static-src/utils.js @@ -31,10 +31,10 @@ import * as openpgp from 'openpgp' import QRCode from 'qrcode' // Compute local part of Web Key Directory URL -export async function computeWKDLocalPart(message) { - const data = openpgp.util.str_to_Uint8Array(message.toLowerCase()); - const hash = await openpgp.crypto.hash.sha1(data); - return openpgp.util.encodeZBase32(hash); +export async function computeWKDLocalPart(localPart) { + const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase()); + const localPartHashed = new Uint8Array(await crypto.subtle.digest('SHA-1', localPartEncoded)); + return encodeZBase32(localPartHashed); } // Generate Keyoxide profile URL @@ -68,22 +68,26 @@ export async function generateProfileURL(data) { // Fetch OpenPGP key based on information stored in window export async function fetchProfileKey() { - if (window.kx.key.object && window.kx.key.object instanceof openpgp.key.Key) { + if (window.kx.key.object && window.kx.key.object instanceof openpgp.PublicKey) { return; } const rawKeyData = await fetch(window.kx.key.url) let key, errorMsg - + try { - key = (await openpgp.key.read(new Uint8Array(await rawKeyData.clone().arrayBuffer()))).keys[0] + key = (await openpgp.readKey({ + binaryKey: new Uint8Array(await rawKeyData.clone().arrayBuffer()) + })) } catch(error) { errorMsg = error.message } if (!key) { try { - key = (await openpgp.key.readArmored(await rawKeyData.clone().text())).keys[0] + key = (await openpgp.readKey({ + armoredKey: await rawKeyData.clone().text() + })) } catch (error) { errorMsg = error.message } @@ -132,3 +136,33 @@ export function showQR(input, type) { qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height); } } + +// Copied from https://github.com/openpgpjs/wkd-client/blob/0d074519e011a5139a8953679cf5f807e4cd2378/src/wkd.js +export function encodeZBase32(data) { + if (data.length === 0) { + return ""; + } + const ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769"; + const SHIFT = 5; + const MASK = 31; + let buffer = data[0]; + let index = 1; + let bitsLeft = 8; + let result = ''; + while (bitsLeft > 0 || index < data.length) { + if (bitsLeft < SHIFT) { + if (index < data.length) { + buffer <<= 8; + buffer |= data[index++] & 0xff; + bitsLeft += 8; + } else { + const pad = SHIFT - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } + } + bitsLeft -= SHIFT; + result += ALPHABET[MASK & (buffer >> bitsLeft)]; + } + return result; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a7a02ef..f2a207e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -305,6 +305,49 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" +"@openpgp/hkp-client@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@openpgp/hkp-client/-/hkp-client-0.0.2.tgz#d8737358efcf6412c8273f89385e020766613e88" + integrity sha512-hA71RhqfLfNltZsy/USTQehE2QAVB3eK4xx8p76XtFJy5Zg6gK2XbZvOC/x/yG8i2Ipbyul1DMTMxH9v8rfPKw== + dependencies: + node-fetch "^2.6.1" + +"@openpgp/wkd-client@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@openpgp/wkd-client/-/wkd-client-0.0.3.tgz#e9f137ed21ee8631451782e22a2740fd781a2534" + integrity sha512-qe+uWtCJetuG78KhfiQyEA+ZciC/qeECXRj+LCm4m0s98qR2wPwYHRI1u8aFbtkN6G4ZMyKN+opY++fJS5l3vg== + dependencies: + "@peculiar/webcrypto" "^1.1.6" + node-fetch "^2.6.1" + +"@peculiar/asn1-schema@^2.0.44": + version "2.0.44" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.44.tgz#dcb1b8f84a4dd5f07f674028beade9c3de43cc06" + integrity sha512-uaCnjQ9A9WwQSMuDJcNOCYEPXTahgKbFMvI7eMOMd8lXgx0J1eU7F3BoMsK5PFxa3dVUxjSQbaOjfgGoeHGgoQ== + dependencies: + "@types/asn1js" "^2.0.2" + asn1js "^2.1.1" + pvtsutils "^1.2.1" + tslib "^2.3.0" + +"@peculiar/json-schema@^1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" + integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== + dependencies: + tslib "^2.0.0" + +"@peculiar/webcrypto@^1.1.6": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.3.0.tgz#9b597b90470afdb566040e703e6aaaf704d9be7c" + integrity sha512-gV+1F0jQmZmxChl8kNqhdBEYqQbSBvvzT/6r05FSj95/wXmAP+EIAKqMM8ehM2aCxHYTQpOV7bs7y+RQvAwZaw== + dependencies: + "@peculiar/asn1-schema" "^2.0.44" + "@peculiar/json-schema" "^1.1.12" + pvtsutils "^1.2.1" + tslib "^2.3.1" + webcrypto-core "^1.4.0" + "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" @@ -334,6 +377,11 @@ dependencies: defer-to-connect "^2.0.0" +"@types/asn1js@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-2.0.2.tgz#bb1992291381b5f06e22a829f2ae009267cdf8c5" + integrity sha512-t4YHCgtD+ERvH0FyxvNlYwJ2ezhqw7t+Ygh4urQ7dJER8i185JPv6oIM3ey5YQmGN6Zp9EMbpohkjZi9t3UxwA== + "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz" @@ -989,6 +1037,13 @@ asn1@~0.2.3: dependencies: safer-buffer "~2.1.0" +asn1js@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-2.2.0.tgz#d890fcdda86b8a005693df14a986bfb2c2069c57" + integrity sha512-oagLNqpfNv7CvmyMoexMDNyVDSiq1rya0AEUgcLlNHdHgNl6U/hi8xY370n5y+ZIFEXOx0J4B1qF2NDjMRxklA== + dependencies: + pvutils latest + assert-never@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz" @@ -1029,6 +1084,13 @@ aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +axios@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" + integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== + dependencies: + follow-redirects "^1.14.7" + babel-plugin-jsx-pragmatic@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/babel-plugin-jsx-pragmatic/-/babel-plugin-jsx-pragmatic-1.0.2.tgz" @@ -1790,14 +1852,16 @@ doctypes@^1.1.0: resolved "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz" integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= -doipjs@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-0.14.0.tgz#902e6981a40326860b58ff3113fc7eac450bdd34" - integrity sha512-FX0rRv4h+/IaCCS2YSrEdx7nbM1iHimJfXhGDi5OU+0ALw5868RLDUQoWNpTfS+PtKX4vOgqmMAGuGlPLJiqkA== +doipjs@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-0.15.2.tgz#1a27d80c3c066d675baf2bbc637521da2c1c35b8" + integrity sha512-AiUqOOp6dqF1yEV6IbFYLGH/FClxT95Qa/izFXH3vHqm2IHbwxiLeqTrhRUMGO7V52nsBmnb29kCTVURXrg0pA== dependencies: + "@openpgp/hkp-client" "^0.0.2" + "@openpgp/wkd-client" "^0.0.3" "@xmpp/client" "^0.12.0" "@xmpp/debug" "^0.12.0" - bent "^7.3.12" + axios "^0.25.0" browser-or-node "^1.3.0" cors "^2.8.5" dotenv "^8.2.0" @@ -1806,7 +1870,7 @@ doipjs@^0.14.0: irc-upd "^0.11.0" jsdom "^16.5.1" merge-options "^3.0.3" - openpgp "^4.10.9" + openpgp "^5.0" query-string "^6.14.1" valid-url "^1.0.9" validator "^13.5.2" @@ -2238,6 +2302,11 @@ find-up@^4.0.0: locate-path "^5.0.0" path-exists "^4.0.0" +follow-redirects@^1.14.7: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz" @@ -3543,18 +3612,11 @@ node-environment-flags@^1.0.5: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" -node-fetch@^2.1.2, node-fetch@^2.6.1: +node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-localstorage@~1.3.0: - version "1.3.1" - resolved "https://registry.npmjs.org/node-localstorage/-/node-localstorage-1.3.1.tgz" - integrity sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ== - dependencies: - write-file-atomic "^1.1.4" - node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz" @@ -3705,14 +3767,12 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -openpgp@^4.10.9: - version "4.10.10" - resolved "https://registry.npmjs.org/openpgp/-/openpgp-4.10.10.tgz" - integrity sha512-Ub48OogGPjNsr0G/wnJ/SyAQzt/tfcXZTWVZdjKFpXCQV1Ca+upFdSPPkBlGG3lb9EQGOKZJ2tzYNH6ZyKMkDQ== +openpgp@^5.0, openpgp@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.1.0.tgz#4da3880ad04d9d75b3f0470451f6862d43252568" + integrity sha512-keCno6QPMXWwfjrOOtT8fwZ5XgCcB7vZH80xb44SbJ49qQ11Efl2fFfqHpaie7jTQFjRKxgT8hSFPXJUjogNPw== dependencies: asn1.js "^5.0.0" - node-fetch "^2.1.2" - node-localstorage "~1.3.0" optionator@^0.8.1: version "0.8.3" @@ -4101,6 +4161,18 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" +pvtsutils@^1.2.0, pvtsutils@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.2.2.tgz#62ef6bc0513cbc255ee02574dedeaa41272d6101" + integrity sha512-OALo5ZEdqiI127i64+CXwkCOyFHUA+tCQgaUO/MvRDFXWPr53f2sx28ECNztUEzuyu5xvuuD1EB/szg9mwJoGA== + dependencies: + tslib "^2.3.1" + +pvutils@latest: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.2.tgz#8d046cabeb81040d430744b0e76f4b2970c9e81f" + integrity sha512-wlo0BUInyP+ZgBJHV8PnJW8S2HubdQfMMip8B9yXr9aFlauJFuF1jZ/RWFmzGYitC7GxkxqXdwbY9/R97v+Cqg== + qrcode@^1.4.4: version "1.4.4" resolved "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz" @@ -4607,11 +4679,6 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slide@^1.1.5: - version "1.1.6" - resolved "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz" - integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz" @@ -4984,6 +5051,11 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +tslib@^2.0.0, tslib@^2.3.0, tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" @@ -5201,6 +5273,17 @@ watchpack@^2.3.1: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +webcrypto-core@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.4.0.tgz#9a395920792bcfa4605dc64aaf264156f79e873e" + integrity sha512-HY3Zo0GcRIQUUDnlZ/shGjN+4f7LVMkdJZoGPog+oHhJsJdMz6iM8Za5xZ0t6qg7Fx/JXXz+oBv2J2p982hGTQ== + dependencies: + "@peculiar/asn1-schema" "^2.0.44" + "@peculiar/json-schema" "^1.1.12" + asn1js "^2.1.1" + pvtsutils "^1.2.0" + tslib "^2.3.1" + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz" @@ -5372,15 +5455,6 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^1.1.4: - version "1.3.4" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz" - integrity sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8= - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - slide "^1.1.5" - write-file-atomic@^3.0.0: version "3.0.3" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz"