mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 18:25:24 +00:00
Removed nodejs crypto inclusion for node-forge
Added loadConfiguration function Moved completely from Openssl for signature generation and crypto for sha1 generation to node-forge Moved configuration to another file Added init function
This commit is contained in:
@@ -2,12 +2,12 @@
|
|||||||
"certificates": {
|
"certificates": {
|
||||||
"dir": "certificates",
|
"dir": "certificates",
|
||||||
"files": {
|
"files": {
|
||||||
"wwdr_pem": "WWDR.pem",
|
"wwdr": "WWDR.pem",
|
||||||
"certificate": "passcertificate.pem",
|
"signerCert": "passcertificate.pem",
|
||||||
"key": "passkey.pem"
|
"signerKey": "passkey.pem"
|
||||||
},
|
},
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"dev_pem_key": "123456"
|
"privateKeySecret": "123456"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
|
|||||||
294
index.js
294
index.js
@@ -1,17 +1,19 @@
|
|||||||
const os = require("os");
|
const os = require("os");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const crypto = require("crypto");
|
const forge = require("node-forge");
|
||||||
const { spawn } = require("child_process");
|
const { spawn } = require("child_process");
|
||||||
const archiver = require("archiver");
|
const archiver = require("archiver");
|
||||||
const async = require("async");
|
const async = require("async");
|
||||||
|
|
||||||
const _configuration = Object.freeze(require("./config.json"));
|
let _configuration = Object.freeze(require("./config.json"));
|
||||||
|
|
||||||
const supportedTypesOfPass = /(boardingPass|eventTicket|coupon|generic|storeCard)/i;
|
const supportedTypesOfPass = /(boardingPass|eventTicket|coupon|generic|storeCard)/i;
|
||||||
const passModelsDir = _configuration.models.dir;
|
const passModelsDir = _configuration.models.dir;
|
||||||
const outputDir = _configuration.output.dir;
|
const outputDir = _configuration.output.dir;
|
||||||
const Certificates = _configuration.certificates;
|
const Certificates = {
|
||||||
|
status: false
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Apply a filter to arg0 to remove hidden files names (starting with dot)
|
Apply a filter to arg0 to remove hidden files names (starting with dot)
|
||||||
@@ -28,6 +30,61 @@ function capitalizeFirst(str) {
|
|||||||
return str[0].toUpperCase()+str.slice(1);
|
return str[0].toUpperCase()+str.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadConfiguration(configurationPath) {
|
||||||
|
let setup = require(path.resolve(__dirname, configurationPath));
|
||||||
|
let reqFilesKeys = ["wwdr", "signerCert", "signerKey"];
|
||||||
|
|
||||||
|
// Node-Forge also accepts .cer certificates
|
||||||
|
if (!setup.certificates.dir || fs.accessSync(path.resolve(setup.certificates.dir)) !== undefined) {
|
||||||
|
throw new Error("Unable to load certificates directory. Check its existence or the permissions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setup.certificates.files) {
|
||||||
|
throw new Error("Expected key 'files' in configuration file but not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setup.certificates.files.wwdr) {
|
||||||
|
throw new Error("Expected file path or content for key certificates.files.wwdr. Please provide a valid certificate from https://apple.co/2sc2pvv");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setup.certificates.files.signerCert) {
|
||||||
|
throw new Error("Expected file path or content for key certificates.files.signerCert. Please provide a valid signer certificate.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setup.certificates.files.signerKey || !setup.certificates.credentials.privateKeySecret) {
|
||||||
|
throw new Error("Expected file path or content for key certificates.files.signerKey with an associated password at certificates.credentials.privateKeySecret but not found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let certPaths = reqFilesKeys.map(e => path.resolve(setup.certificates.dir, setup.certificates.files[e]));
|
||||||
|
|
||||||
|
return new Promise(function(success, reject) {
|
||||||
|
let docStruct = {};
|
||||||
|
|
||||||
|
async.concat(certPaths, fs.readFile, function(err, contents) {
|
||||||
|
// contents is a Buffer array
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success(
|
||||||
|
contents.map(function(file, index) {
|
||||||
|
if (file.includes("PRIVATE KEY")) {
|
||||||
|
return forge.pki.decryptRsaPrivateKey(
|
||||||
|
file,
|
||||||
|
setup.certificates.credentials.privateKeySecret
|
||||||
|
);
|
||||||
|
} else if (file.includes("CERTIFICATE")) {
|
||||||
|
return forge.pki.certificateFromPem(file);
|
||||||
|
} else {
|
||||||
|
throw new Error("File not allowed in configuration. Only .pems files containing certificates and private keys are allowed");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@function fileStreamToBuffer
|
@function fileStreamToBuffer
|
||||||
@params {String} path - the path of the file to be read
|
@params {String} path - the path of the file to be read
|
||||||
@@ -62,11 +119,11 @@ function fileStreamToBuffer(path, ...callbacks) {
|
|||||||
|
|
||||||
function checkSignatureRequirements() {
|
function checkSignatureRequirements() {
|
||||||
let checkCertificate = new Promise(function(available, notAvailable) {
|
let checkCertificate = new Promise(function(available, notAvailable) {
|
||||||
fs.access(`${Certificates.dir}/${Certificates.files.certificate}`, (e) => (!!e ? notAvailable : available)() );
|
fs.access(path.resolve(Certificates.dir, Certificates.files.signerCert), (e) => (!!e ? notAvailable : available)() );
|
||||||
});
|
});
|
||||||
|
|
||||||
let checkKey = new Promise(function(available, notAvailable) {
|
let checkKey = new Promise(function(available, notAvailable) {
|
||||||
fs.access(`${Certificates.dir}/${Certificates.files.key}`, (e) => (!!e ? notAvailable : available)() );
|
fs.access(path.resolve(Certificates.dir, Certificates.files.signerKey), (e) => (!!e ? notAvailable : available)() );
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([checkCertificate, checkKey]);
|
return Promise.all([checkCertificate, checkKey]);
|
||||||
@@ -88,49 +145,107 @@ function UUIDGen(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+
|
|||||||
@returns {Object} Promise
|
@returns {Object} Promise
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function generateManifestSignature(manifestUUID) {
|
|
||||||
return new Promise(function(done, rejected) {
|
|
||||||
checkSignatureRequirements()
|
|
||||||
.then(function() {
|
|
||||||
let opensslError = false;
|
|
||||||
let opensslBuffer = [];
|
|
||||||
|
|
||||||
let opensslProcess = spawn("openssl", [
|
function generateManifestSignature(manifest) {
|
||||||
"smime",
|
// return new Promise(function(done, rejected) {
|
||||||
"-binary",
|
let signature = forge.pkcs7.createSignedData();
|
||||||
"-sign",
|
|
||||||
"-certfile", path.resolve(Certificates.dir, Certificates.files["wwdr_pem"]),
|
|
||||||
"-signer", path.resolve(Certificates.dir, Certificates.files["certificate"]),
|
|
||||||
"-inkey", path.resolve(Certificates.dir, Certificates.files["key"]),
|
|
||||||
"-in", path.resolve(`${os.tmpdir()}/manifest-${manifestUUID}.json`),
|
|
||||||
// "-out", path.resolve("passCreator", "event.pass", "./signature"),
|
|
||||||
"-outform", "DER",
|
|
||||||
"-passin", `pass:${Certificates.credentials["dev_pem_key"]}`
|
|
||||||
]);
|
|
||||||
|
|
||||||
opensslProcess.stdout.on("data", function(data) {
|
if (typeof manifest === "object") {
|
||||||
opensslBuffer.push(data);
|
signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8")
|
||||||
});
|
} else if (typeof manifest === "string") {
|
||||||
|
signature.content = manifest;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Manifest content must be a string or an object. Unable to accept manifest of type ${typeof manifest}`);
|
||||||
|
}
|
||||||
|
|
||||||
opensslProcess.stderr.on("data", function(data) {
|
signature.addCertificate(Certificates.wwdr);
|
||||||
opensslBuffer.push(data);
|
signature.addCertificate(Certificates.signerCert);
|
||||||
opensslError = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
opensslProcess.stdout.on("end", function() {
|
signature.addSigner({
|
||||||
if (opensslError) {
|
key: Certificates.signerKey,
|
||||||
return rejected(Buffer.concat(opensslBuffer));
|
certificate: Certificates.signerCert,
|
||||||
}
|
digestAlgorithm: forge.pki.oids.sha1,
|
||||||
|
authenticatedAttributes: [{
|
||||||
return done(Buffer.concat(opensslBuffer));
|
type: forge.pki.oids.contentType,
|
||||||
});
|
value: forge.pki.oids.data
|
||||||
})
|
}, {
|
||||||
.catch(function(e) {
|
type: forge.pki.oids.messageDigest,
|
||||||
return rejected(`Cannot fulfill signature requirements.\n${e}`);
|
}, {
|
||||||
|
// the value is autogenerated
|
||||||
|
type: forge.pki.oids.signingTime,
|
||||||
|
}]
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
signature.sign();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Signing creates in contentInfo a JSON object nested BER/TLV (X.690 standard) structure.
|
||||||
|
* Each object represents a component of ASN.1 (Abstract Syntax Notation)
|
||||||
|
* For a more complete reference, refer to: https://en.wikipedia.org/wiki/X.690#BER_encoding
|
||||||
|
*
|
||||||
|
* signature.contentInfo.type => SEQUENCE OF (16)
|
||||||
|
* signature.contentInfo.value[0].type => OBJECT IDENTIFIER (6)
|
||||||
|
* signature.contantInfo.value[1].type => END OF CONTENT (EOC - 0)
|
||||||
|
*
|
||||||
|
* EOC are only present only in constructed indefinite-length methods
|
||||||
|
* Since `signature.contentInfo.value[1].value` contains an object whose value contains the content we passed,
|
||||||
|
* we have to pop the whole object away to avoid signature content invalidation.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
signature.contentInfo.value.pop();
|
||||||
|
|
||||||
|
// Converting the JSON Structure into a DER (which is a subset of BER), ASN.1 valid structure
|
||||||
|
// Returning the buffer of the signature
|
||||||
|
// return done(Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), 'binary'));
|
||||||
|
|
||||||
|
return Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), 'binary');
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// function generateManifestSignature(manifestUUID) {
|
||||||
|
// return new Promise(function(done, rejected) {
|
||||||
|
// checkSignatureRequirements()
|
||||||
|
// .then(function() {
|
||||||
|
// let opensslError = false;
|
||||||
|
// let opensslBuffer = [];
|
||||||
|
|
||||||
|
// let opensslProcess = spawn("openssl", [
|
||||||
|
// "smime",
|
||||||
|
// "-binary",
|
||||||
|
// "-sign",
|
||||||
|
// "-certfile", path.resolve(Certificates.dir, Certificates.files["wwdr_pem"]),
|
||||||
|
// "-signer", path.resolve(Certificates.dir, Certificates.files["certificate"]),
|
||||||
|
// "-inkey", path.resolve(Certificates.dir, Certificates.files["key"]),
|
||||||
|
// "-in", path.resolve(`${os.tmpdir()}/manifest-${manifestUUID}.json`),
|
||||||
|
// // "-out", path.resolve("passCreator", "event.pass", "./signature"),
|
||||||
|
// "-outform", "DER",
|
||||||
|
// "-passin", `pass:${Certificates.credentials["privateKeySecret"]}`
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// opensslProcess.stdout.on("data", function(data) {
|
||||||
|
// opensslBuffer.push(data);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// opensslProcess.stderr.on("data", function(data) {
|
||||||
|
// opensslBuffer.push(data);
|
||||||
|
// opensslError = true;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// opensslProcess.stdout.on("end", function() {
|
||||||
|
// if (opensslError) {
|
||||||
|
// return rejected(Buffer.concat(opensslBuffer));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return done(Buffer.concat(opensslBuffer));
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
// .catch(function(e) {
|
||||||
|
// return rejected(`Cannot fulfill signature requirements.\n${e}`);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Generates a Buffer of JSON file (manifest)
|
Generates a Buffer of JSON file (manifest)
|
||||||
@function generateManifest
|
@function generateManifest
|
||||||
@@ -157,7 +272,8 @@ function generateManifest(fromObject, manifestUUID) {
|
|||||||
manifestWS.write(source);
|
manifestWS.write(source);
|
||||||
manifestWS.end();
|
manifestWS.end();
|
||||||
|
|
||||||
return done(Buffer.from(source));
|
//return done(Buffer.from(source));
|
||||||
|
return done(Buffer.from(source).toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,8 +361,11 @@ function editPassStructure(options, passBuffer) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function RequestHandler(request, response) {
|
function RequestHandler(request, response) {
|
||||||
|
if (!Certificates.status) {
|
||||||
|
throw new Error("passkit requires initialization by calling .init() method.");
|
||||||
|
}
|
||||||
|
|
||||||
if (!supportedTypesOfPass.test(request.params.type)) {
|
if (!supportedTypesOfPass.test(request.params.type)) {
|
||||||
// 😊
|
// 😊
|
||||||
response.set("Content-Type", "application/json");
|
response.set("Content-Type", "application/json");
|
||||||
@@ -283,11 +402,12 @@ function RequestHandler(request, response) {
|
|||||||
fileStreamToBuffer(`${passModelsDir}/${request.params.type}.pass/pass.json`, function _returnBuffer(bufferResult) {
|
fileStreamToBuffer(`${passModelsDir}/${request.params.type}.pass/pass.json`, function _returnBuffer(bufferResult) {
|
||||||
editPassStructure(filterPassOptions(options), bufferResult).then(function _afterJSONParse(passFileBuffer) {
|
editPassStructure(filterPassOptions(options), bufferResult).then(function _afterJSONParse(passFileBuffer) {
|
||||||
// Manifest dictionary
|
// Manifest dictionary
|
||||||
let manifestRaw = {};
|
let manifest = {};
|
||||||
let archive = archiver("zip");
|
let archive = archiver("zip");
|
||||||
|
|
||||||
archive.append(passFileBuffer, { name: "pass.json" });
|
archive.append(passFileBuffer, { name: "pass.json" });
|
||||||
manifestRaw["pass.json"] = crypto.createHash("sha1").update(passFileBuffer).digest("hex").trim();
|
|
||||||
|
manifest["pass.json"] = forge.md.sha1.create().update(passFileBuffer.toString("binary")).digest().toHex();
|
||||||
|
|
||||||
async.each(list, function getHashAndArchive(file, callback) {
|
async.each(list, function getHashAndArchive(file, callback) {
|
||||||
if (/(manifest|signature|pass)/ig.test(file)) {
|
if (/(manifest|signature|pass)/ig.test(file)) {
|
||||||
@@ -298,17 +418,17 @@ function RequestHandler(request, response) {
|
|||||||
// adding the files to the zip - i'm not using .directory method because it adds also hidden files like .DS_Store on macOS
|
// adding the files to the zip - i'm not using .directory method because it adds also hidden files like .DS_Store on macOS
|
||||||
archive.file(`${passModelsDir}/${request.params.type}.pass/${file}`, { name: file });
|
archive.file(`${passModelsDir}/${request.params.type}.pass/${file}`, { name: file });
|
||||||
|
|
||||||
let hashFlow = crypto.createHash("sha1");
|
let hashFlow = forge.md.sha1.create();
|
||||||
|
|
||||||
fs.createReadStream(`${passModelsDir}/${request.params.type}.pass/${file}`)
|
fs.createReadStream(`${passModelsDir}/${request.params.type}.pass/${file}`)
|
||||||
.on("data", function(data) {
|
.on("data", function(data) {
|
||||||
hashFlow.update(data);
|
hashFlow.update(data.toString("binary"));
|
||||||
})
|
})
|
||||||
.on("error", function(e) {
|
.on("error", function(e) {
|
||||||
return callback(e);
|
return callback(e);
|
||||||
})
|
})
|
||||||
.on("end", function() {
|
.on("end", function() {
|
||||||
manifestRaw[file] = hashFlow.digest("hex").trim();
|
manifest[file] = hashFlow.digest().toHex().trim();
|
||||||
return callback();
|
return callback();
|
||||||
});
|
});
|
||||||
}, function end(error) {
|
}, function end(error) {
|
||||||
@@ -316,47 +436,47 @@ function RequestHandler(request, response) {
|
|||||||
throw new Error(`Unable to compile manifest. ${error}`);
|
throw new Error(`Unable to compile manifest. ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let uuid = UUIDGen();
|
// let uuid = UUIDGen();
|
||||||
|
|
||||||
generateManifest(manifestRaw, uuid)
|
// generateManifest(manifestRaw, uuid)
|
||||||
.then(function(manifestBuffer) {
|
// .then(function(manifestBuffer) {
|
||||||
|
|
||||||
archive.append(manifestBuffer, { name: "manifest.json" });
|
archive.append(Buffer.from(JSON.stringify(manifest), "utf8"), { name: "manifest.json" });
|
||||||
|
|
||||||
generateManifestSignature(uuid)
|
let signatureBuffer = generateManifestSignature(manifest);
|
||||||
.then(function(signatureBuffer) {
|
|
||||||
|
|
||||||
if (!fs.existsSync("output")) {
|
|
||||||
fs.mkdirSync("output");
|
|
||||||
}
|
|
||||||
|
|
||||||
archive.append(signatureBuffer, { name: "signature" });
|
console.log(signatureBuffer)
|
||||||
let outputWS = fs.createWriteStream(`${outputDir}/${request.params.type}.pkpass`);
|
|
||||||
|
|
||||||
archive.pipe(outputWS);
|
if (!fs.existsSync("output")) {
|
||||||
archive.finalize();
|
fs.mkdirSync("output");
|
||||||
|
}
|
||||||
|
|
||||||
outputWS.on("close", function() {
|
archive.append(signatureBuffer, { name: "signature" });
|
||||||
response.status(201).download(`${outputDir}/${request.params.type}.pkpass`, `${request.params.type}.pkpass`, {
|
let outputWS = fs.createWriteStream(`${outputDir}/${request.params.type}.pkpass`);
|
||||||
cacheControl: false,
|
|
||||||
headers: {
|
archive.pipe(outputWS);
|
||||||
"Content-type": "application/vnd.apple.pkpass",
|
archive.finalize();
|
||||||
"Content-length": fs.statSync(`${outputDir}/${request.params.type}.pkpass`).size
|
|
||||||
}
|
outputWS.on("close", function() {
|
||||||
});
|
response.status(201).download(`${outputDir}/${request.params.type}.pkpass`, `${request.params.type}.pkpass`, {
|
||||||
|
cacheControl: false,
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/vnd.apple.pkpass",
|
||||||
|
"Content-length": fs.statSync(`${outputDir}/${request.params.type}.pkpass`).size
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch(function(buffer) {
|
|
||||||
throw buffer.toString();
|
|
||||||
});
|
});
|
||||||
})
|
// })
|
||||||
.catch(function(error) {
|
// .catch(function(error) {
|
||||||
throw error;
|
// throw error;
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
|
throw err;
|
||||||
|
|
||||||
// 😊
|
// 😊
|
||||||
response.set("Content-Type", "application/json");
|
response.set("Content-Type", "application/json");
|
||||||
response.status(418).send({ ecode: 418, status: false, message: `Got error while parsing pass.json file: ${err}` });
|
response.status(418).send({ ecode: 418, status: false, message: `Got error while parsing pass.json file: ${err}` });
|
||||||
@@ -368,4 +488,24 @@ function RequestHandler(request, response) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { RequestHandler };
|
function init(configPath) {
|
||||||
|
if (Certificates.status) {
|
||||||
|
throw new Error("Initialization must be triggered only once.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configPath || fs.accessSync(path.resolve(__dirname, configPath)) !== undefined) {
|
||||||
|
throw new Error(`Cannot load configuration from 'path' (${configPath}). File not existing or missing path.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfiguration(configPath).then(function(config) {
|
||||||
|
Certificates.wwdr = config[0];
|
||||||
|
Certificates.signerCert = config[1];
|
||||||
|
Certificates.signerKey = config[2];
|
||||||
|
Certificates.status = true;
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
throw new Error(`Error: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init, RequestHandler };
|
||||||
|
|||||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -417,6 +417,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
||||||
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
|
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
|
||||||
},
|
},
|
||||||
|
"node-forge": {
|
||||||
|
"version": "0.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
|
||||||
|
"integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ=="
|
||||||
|
},
|
||||||
"normalize-path": {
|
"normalize-path": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^2.1.1",
|
"archiver": "^2.1.1",
|
||||||
"async": "^2.6.0",
|
"async": "^2.6.0",
|
||||||
"express": "^4.16.3"
|
"express": "^4.16.3",
|
||||||
|
"node-forge": "^0.7.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user