Comparing version 1.0.2 to 2.2.0
@@ -1,295 +0,488 @@ | ||
var https = require('https') | ||
, querystring = require('querystring') | ||
, crypto = require('crypto') | ||
/* | ||
* OpenTok server-side SDK | ||
*/ | ||
// tokbox constants: | ||
, TOKEN_SENTINEL = "T1==" | ||
, API_HOST = "api.opentok.com" | ||
, SESSION_API_ENDPOINT = "/hl/session/create" | ||
, GET_MANIFEST = "/archive/getmanifest/" | ||
, GET_URL = "/hl/archive/url/"; | ||
// Dependencies | ||
var net = require('net'), | ||
querystring = require('querystring'), | ||
crypto = require('crypto'), | ||
_ = require('lodash'), | ||
Client = require('./client'), | ||
Session = require('./session'), | ||
archiving = require('./archiving'), | ||
package = require ('../package.json') | ||
errors = require('./errors'); | ||
var RoleConstants = exports.RoleConstants = { | ||
SUBSCRIBER: "subscriber", | ||
PUBLISHER: "publisher", | ||
MODERATOR: "moderator" | ||
} | ||
// Internal Constants | ||
var TOKEN_SENTINEL = "T1=="; | ||
// JS constants | ||
var JSObject = "object" | ||
, JSFunction = "function" | ||
, JSString = "string" | ||
, JSNumber = "number" | ||
/** | ||
* Contains methods for creating OpenTok sessions, generating tokens, and working with archives. | ||
* <p> | ||
* To create a new OpenTok object, call the OpenTok constructor with your OpenTok API key | ||
* and the API secret from <a href="https://dashboard.tokbox.com">the OpenTok dashboard</a>. | ||
* Do not publicly share your API secret. You will use it with the OpenTok constructor | ||
* (only on your web server) to create OpenTok sessions. | ||
* <p> | ||
* Be sure to include the entire OpenTok Node.js SDK on your web server. | ||
* | ||
* @class OpenTok | ||
* | ||
* @param apiKey {String} Your OpenTok API key. (See the | ||
* <a href="https://dashboard.tokbox.com">OpenTok dashboard</a> page.) | ||
* @param apiSecret {String} Your OpenTok API secret. (See the | ||
* <a href="https://dashboard.tokbox.com">OpenTok dashboard</a> page.) | ||
*/ | ||
var OpenTok = function(apiKey, apiSecret, env) { | ||
// we're loose about calling this constructor with `new`, we got your back | ||
if (!(this instanceof OpenTok)) return new OpenTok(apiKey, apiSecret, env); | ||
// OpenTokSession constructor (only used internally) | ||
var OpenTokSession = function(sessionId){ | ||
this.sessionId = sessionId | ||
} | ||
// OpenTokSDK constructor | ||
var OpenTokSDK = exports.OpenTokSDK = function(partnerId, partnerSecret){ | ||
if(partnerId && partnerSecret){ | ||
this.partnerId = partnerId; | ||
this.partnerSecret = partnerSecret; | ||
this.api_url = API_HOST; | ||
}else{ | ||
return new Error("Invalid Key or Secret"); | ||
// validate arguments: apiKey := Number|String, apiSecret := String | ||
if (!(_.isNumber(apiKey) || _.isString(apiKey)) || !_.isString(apiSecret)) { | ||
return new Error('Invalid arguments when initializing OpenTok:'); | ||
} | ||
} | ||
OpenTokSDK.OpenTokArchive = function(sdkObject){ | ||
var self = this; | ||
this.resources = []; | ||
this.addVideoResource = function(res){ | ||
self.resources.push(res); | ||
}; | ||
this.downloadArchiveURL=function(vid, callback){ | ||
var options = { | ||
host: sdkObject.api_url, | ||
path: GET_URL+sdkObject.archiveId+"/"+vid, | ||
method: 'GET', | ||
headers: { | ||
'x-tb-token-auth':sdkObject.token | ||
} | ||
}; | ||
var req = https.request(options, function(res){ | ||
var chunks = ''; | ||
res.setEncoding('utf8') | ||
res.on('data', function(chunk){ | ||
chunks += chunk | ||
}) | ||
res.on('end', function(){ | ||
callback(chunks); | ||
}) | ||
}) | ||
req.end() | ||
}; | ||
}; | ||
OpenTokSDK.OpenTokArchiveVideoResource = function(vid,length,name){ | ||
this.vid = vid; | ||
this.length = length; | ||
this.name = name; | ||
this.getId = function(){ | ||
return vid; | ||
}; | ||
}; | ||
OpenTokSDK.prototype.getArchiveManifest = function(archiveId, token, callback){ | ||
this.get_archive_manifest( archiveId, token, callback ); | ||
} | ||
// apiKey argument can be a Number, but we will internally store it as a String | ||
if (_.isNumber(apiKey)) apiKey = apiKey.toString(); | ||
OpenTokSDK.prototype.get_archive_manifest = function(archiveId, token, callback){ | ||
this.token = token; | ||
this.archiveId = archiveId; | ||
var self = this; | ||
var parseResponse = function(chunks){ | ||
var response = new OpenTokSDK.OpenTokArchive(self); | ||
var start = chunks.match('<resources>') | ||
, end = chunks.match('</resources>') | ||
var videoTags = null | ||
if(start && end){ | ||
videoTags = chunks.substr(start.index + 12, (end.index - start.index - 12)) | ||
attr = videoTags.split('"'); | ||
if(attr.length>5){ | ||
vid = attr[1] | ||
length = attr[3] | ||
name = attr[5] | ||
resource = new OpenTokSDK.OpenTokArchiveVideoResource(vid, length, name); | ||
response.addVideoResource(resource); | ||
} | ||
} | ||
callback(response); | ||
this.client = new Client({ apiKey: apiKey, apiSecret: apiSecret }); | ||
this.apiKey = apiKey; | ||
this.apiSecret = apiSecret; | ||
// TODO: this is a pretty obvious seam, the integration could be more smooth | ||
var archiveConfig = { | ||
apiEndpoint: 'https://api.opentok.com', | ||
apiKey: apiKey, | ||
apiSecret: apiSecret | ||
}; | ||
var options = { | ||
host: this.api_url, | ||
path: GET_MANIFEST+archiveId, | ||
method: 'GET', | ||
headers: { | ||
'x-tb-token-auth':token | ||
} | ||
// env can be either an object with a bunch of DI options, or a simple string for the apiUrl | ||
if (_.isString(env)) { | ||
this.client.config({ apiUrl: env }); | ||
archiveConfig.apiEndpoint = env; | ||
} | ||
var req = https.request(options, function(res){ | ||
var chunks = ''; | ||
res.setEncoding('utf8') | ||
res.on('data', function(chunk){ | ||
chunks += chunk | ||
}) | ||
res.on('end', function(){ | ||
parseResponse(chunks); | ||
}) | ||
}) | ||
req.end() | ||
/** | ||
* Starts archiving an OpenTok 2.0 session. | ||
* <p> | ||
* Clients must be actively connected to the OpenTok session for you to successfully start | ||
* recording an archive. | ||
* <p> | ||
* You can only record one archive at a time for a given session. You can only record archives | ||
* of sessions that uses the OpenTok Media Router; you cannot archive peer-to-peer sessions. | ||
* | ||
* @param sessionId The session ID of the OpenTok session to archive. | ||
* | ||
* @param options {Object} An optional options object with one property — <code>name</code> | ||
* (a String). This is the name of the archive. You can use this name to identify the archive. | ||
* It is a property of the Archive object, and it is a property of archive-related events in the | ||
* OpenTok client libraries. | ||
* | ||
* @param callback {Function} The function to call upon completing the operation. Two arguments | ||
* are passed to the function: | ||
* | ||
* <ul> | ||
* | ||
* <li> | ||
* <code>error</code> — An error object (if the call to the method fails). | ||
* </li> | ||
* | ||
* <li> | ||
* <code>archive</code> — The Archive object. This object includes properties defining | ||
* the archive, including the archive ID. | ||
* </li> | ||
* | ||
* </ul> | ||
* | ||
* @method #startArchive | ||
* @memberof OpenTok | ||
*/ | ||
this.startArchive = archiving.startArchive.bind(null, archiveConfig); | ||
/** | ||
* Stops an OpenTok archive that is being recorded. | ||
* <p> | ||
* Archives automatically stop recording after 90 minutes or when all clients have disconnected | ||
* from the session being archived. | ||
* | ||
* @param archiveId {String} The archive ID of the archive you want to stop recording. | ||
* @return The Archive object corresponding to the archive being STOPPED. | ||
* | ||
* @param callback {Function} The function to call upon completing the operation. Two arguments | ||
* are passed to the function: | ||
* | ||
* <ul> | ||
* | ||
* <li> | ||
* <code>error</code> — An error object (if the call to the method fails). | ||
* </li> | ||
* | ||
* <li> | ||
* <code>archive</code> — The Archive object. | ||
* </li> | ||
* | ||
* </ul> | ||
* | ||
* @method #stopArchive | ||
* @memberof OpenTok | ||
*/ | ||
this.stopArchive = archiving.stopArchive.bind(null, archiveConfig); | ||
/** | ||
* Gets an {@link Archive} object for the given archive ID. | ||
* | ||
* @param archiveId The archive ID. | ||
* | ||
* @return The {@link Archive} object. | ||
* | ||
* @method #getArchive | ||
* @memberof OpenTok | ||
*/ | ||
this.getArchive = archiving.getArchive.bind(null, archiveConfig); | ||
/** | ||
* Deletes an OpenTok archive. | ||
* <p> | ||
* You can only delete an archive which has a status of "available" or "uploaded". Deleting an | ||
* archive removes its record from the list of archives. For an "available" archive, it also | ||
* removes the archive file, making it unavailable for download. | ||
* | ||
* @param {String} archiveId The archive ID of the archive you want to delete. | ||
* | ||
* @param callback {Function} The function to call upon completing the operation. On successfully | ||
* deleting the archive, the function is called with no arguments passed in. On failure, an error | ||
* object is passed into the function. | ||
* | ||
* @method #deleteArchive | ||
* @memberof OpenTok | ||
*/ | ||
this.deleteArchive = archiving.deleteArchive.bind(null, archiveConfig); | ||
/** | ||
* Retrieves a List of {@link Archive} objects, representing archives that are both | ||
* both completed and in-progress, for your API key. | ||
* | ||
* @param options {Object} An options parameter with two properties: | ||
* | ||
* <ul> | ||
* | ||
* <li> | ||
* <code>count</code> — The index offset of the first archive. 0 is offset of the most | ||
* recently started archive. 1 is the offset of the archive that started prior to the most | ||
* recent archive. This limit is 1000 archives. | ||
* </li> | ||
* | ||
* <li> | ||
* <code>offset</code> — The offset for the first archive to list (starting with the | ||
* first archive recorded as offset 0). | ||
* </li> | ||
* | ||
* </ul> | ||
* | ||
* <p>If you don't pass in an <code>options</code> argument, the method returns up to 1000 archives | ||
* starting with the first archive recorded. | ||
* | ||
* @param callback {Function} The function to call upon completing the operation. Two arguments | ||
* are passed to the function: | ||
* | ||
* <ul> | ||
* | ||
* <li> | ||
* <code>error</code> — An error object (if the call to the method fails). | ||
* </li> | ||
* | ||
* <li> | ||
* <code>archives</code> — An array of Archive objects. | ||
* </li> | ||
* | ||
* </ul> | ||
* | ||
* @method #listArchives | ||
* @memberof OpenTok | ||
*/ | ||
this.listArchives = archiving.listArchives.bind(null, archiveConfig); | ||
} | ||
OpenTokSDK.prototype.generate_token = function(ops){ | ||
ops = ops || {}; | ||
/** | ||
* Creates a new OpenTok session and returns the session ID, which uniquely identifies | ||
* the session. | ||
* <p> | ||
* For example, when using the OpenTok.js library, use the session ID when calling the | ||
* <a href="http://tokbox.com/opentok/libraries/client/js/reference/OT.html#initSession"> | ||
* OT.initSession()</a> method (to initialize an OpenTok session). | ||
* <p> | ||
* OpenTok sessions do not expire. However, authentication tokens do expire (see the | ||
* generateToken(String, TokenOptions) method). Also note that sessions cannot | ||
* explicitly be destroyed. | ||
* <p> | ||
* A session ID string can be up to 255 characters long. | ||
* | ||
* You can also create a session using the | ||
* <a href="http://www.tokbox.com/opentok/api/#session_id_production">OpenTok REST API</a> | ||
* or the <a href="https://dashboard.tokbox.com/projects">OpenTok dashboard</a>. | ||
* | ||
* @return A session ID for the new session. For example, when using the OpenTok.js library, use | ||
* this session ID when calling the <code>OT.initSession()</code> method. | ||
* | ||
* @param {Object} options | ||
* This object defines options for the session, including the following properties (both of which | ||
* are optional): | ||
* | ||
* <ul> | ||
* | ||
* <li><code>location</code> (String) — | ||
* An IP address that the OpenTok servers will use to situate the session in the global | ||
* OpenTok network. If you do not set a location hint, the OpenTok servers will be based on | ||
* the first client connecting to the session. | ||
* </li> | ||
* | ||
* <li><code>mediaMode</code> (String) — | ||
* Determines whether the session will transmit streams using the OpenTok Media Router | ||
* (<code>"routed"</code>) or not (<code>"relayed"</code>). By default, sessions use | ||
* the OpenTok Media Router. | ||
* <p> | ||
* The <a href="http://tokbox.com/#multiparty" target="_top"> OpenTok Media Router</a> | ||
* provides the following benefits: | ||
* | ||
* <ul> | ||
* <li>The OpenTok Media Router can decrease bandwidth usage in multiparty sessions. | ||
* (When the <code>mediaMode</code> parameter is set to <code>"relayed"</code>, | ||
* each client must send a separate audio-video stream to each client subscribing to | ||
* it.)</li> | ||
* <li>The OpenTok Media Router can improve the quality of the user experience through | ||
* <a href="http://tokbox.com/#iqc" target="_top">Intelligent Quality Control</a>. With | ||
* Intelligent Quality Control, if a client's connectivity degrades to a degree that | ||
* it does not support video for a stream it's subscribing to, the video is dropped on | ||
* that client (without affecting other clients), and the client receives audio only. | ||
* If the client's connectivity improves, the video returns.</li> | ||
* <li>The OpenTok Media Router supports the | ||
* <a href="http://tokbox.com/platform/archiving" target="_top">archiving</a> | ||
* feature, which lets you record, save, and retrieve OpenTok sessions.</li> | ||
* </ul> | ||
* | ||
* <p> | ||
* With the <code>mediaMode</code> parameter set to <code>"relayed"</code>, the session | ||
* will attempt to transmit streams directly between clients. If clients cannot connect due to | ||
* firewall restrictions, the session uses the OpenTok TURN server to relay audio-video | ||
* streams. | ||
* <p> | ||
* You will be billed for streamed minutes if you use the OpenTok Media Router or if the | ||
* session uses the OpenTok TURN server to relay streams. For information on pricing, see the | ||
* <a href="http://www.tokbox.com/pricing" target="_top">OpenTok pricing page</a>. | ||
* | ||
* @param {Function} callback | ||
* The function that is called when the operation completes. This function is passed two arguments: | ||
* | ||
* <ul> | ||
* <li> | ||
* <code>sessionId</code> — On sucess, this parameter is set to the session ID of | ||
* the session. Otherwise it is set to null. | ||
* </li> | ||
* <li> | ||
* <code>error</code> — On failiure, this parameter is set to an error object. | ||
* Check the error message for details. | ||
* </li> | ||
* </ul> | ||
*/ | ||
OpenTok.prototype.createSession = function(opts, callback) { | ||
var backupOpts; | ||
// At some point in this packages existence, three different forms of Session ID were used | ||
// Fallback to default (last session created using this OpenTokSDK instance) | ||
var sessionId = ops.session_id || ops.sessionId || ops.session || this.sessionId; | ||
if( !sessionId || sessionId == "" ){ | ||
throw new Error("Null or empty session ID is not valid"); | ||
if (_.isFunction(opts)) { | ||
// shift arguments if the opts is left out | ||
callback = opts; | ||
opts = {}; | ||
} else if (!_.isFunction(callback)) { | ||
// one of the args has to be a function, or we bail | ||
return new Error('Invalid arguments when calling createSession, must provide a callback'); | ||
} | ||
// validate partner id | ||
var subSessionId = sessionId.substring(2); | ||
subSessionId = subSessionId.replace(/-/g, "+").replace(/_/g, "/"); | ||
var decodedSessionId = new Buffer(subSessionId, "base64").toString("ascii").split("~"); | ||
for(var i = 0; i < decodedSessionId.length; i++){ | ||
if (decodedSessionId && decodedSessionId.length > 1){ | ||
break | ||
} | ||
subSessionId = subSessionId + "="; | ||
// whitelist the keys allowed | ||
_.pick(_.defaults(opts, { "mediaMode" : "routed" }), "mediaMode", "location"); | ||
if ( opts.mediaMode !== "routed" && opts.mediaMode !== "relayed" ) { | ||
opts.mediaMode = "routed"; | ||
} | ||
if (decodedSessionId[1] != this.partnerId){ | ||
throw new Error("An invalid session ID was passed"); | ||
if ( "location" in opts && !net.isIPv4(opts.location) ) { | ||
return process.nextTick(function() { callback(new Error('Invalid arguments when calling createSession, ' + | ||
'location must be an IPv4 address'))}); | ||
} | ||
// rename mediaMode -> p2p.preference | ||
backupOpts = _.clone(opts); | ||
var mediaModeToParam = { | ||
"routed" : "disabled", | ||
"relayed" : "enabled" | ||
}; | ||
opts["p2p.preference"] = mediaModeToParam[opts["mediaMode"]]; | ||
delete opts["mediaMode"]; | ||
var createTime = OpenTokSDK.prototype._getUTCDate() | ||
, sig | ||
, tokenString | ||
, tokenParams | ||
, tokenBuffer | ||
, dataString | ||
, dataParams = { | ||
session_id: sessionId, | ||
create_time: createTime, | ||
nonce: Math.floor(Math.random()*999999), | ||
role: RoleConstants.PUBLISHER // will be overriden below if passed in | ||
}; | ||
this.client.createSession(opts, function(err, json) { | ||
if (err) return callback(new Error('Failed to createSession. '+err)); | ||
callback(null, new Session(this, json.sessions.Session.session_id, backupOpts)); | ||
}); | ||
}; | ||
// pass through any other tokbox parameters: | ||
for(var op in ops){ | ||
if(ops.hasOwnProperty(op)){ | ||
dataParams[op] = ops[op] | ||
} | ||
} | ||
/** | ||
* Creates a token for connecting to an OpenTok session. In order to authenticate a user | ||
* connecting to an OpenTok session, the client passes a token when connecting to the session. | ||
* <p> | ||
* For testing, you can also use the <a href="https://dashboard.tokbox.com/projects">OpenTok | ||
* dashboard</a> page to generate test tokens. | ||
* | ||
* @param sessionId The session ID corresponding to the session to which the user will connect. | ||
* | ||
* @param options An object that defines options for the token (each of which is optional): | ||
* | ||
* <ul> | ||
* <li><code>role</code> (String) — The role for the token. Each role defines a set of | ||
* permissions granted to the token: | ||
* | ||
* <ul> | ||
* <li> <code>'subscriber'</code> — A subscriber can only subscribe to streams.</li> | ||
* | ||
* <li> <code>'publisher'</code> — A publisher can publish streams, subscribe to | ||
* streams, and signal. (This is the default value if you do not specify a role.)</li> | ||
* | ||
* <li> <code>'moderator'</code> — In addition to the privileges granted to a | ||
* publisher, in clients using the OpenTok.js 2.2 library, a moderator can call the | ||
* <code>forceUnpublish()</code> and <code>forceDisconnect()</code> method of the | ||
* Session object.</li> | ||
* </ul> | ||
* | ||
* </li> | ||
* | ||
* <li><code>expireTime</code> (Number) — The expiration time for the token, in seconds | ||
* since the UNIX epoch. The maximum expiration time is 30 days after the creation time. | ||
* The default expiration time of 24 hours after the token creation time. | ||
* </li> | ||
* | ||
* <li><code>data</code> (String) — A string containing connection metadata describing the | ||
* end-user.For example, you can pass the user ID, name, or other data describing the end-user. | ||
* The length of the string is limited to 1000 characters. This data cannot be updated once it | ||
* is set. | ||
* </li> | ||
* </ul> | ||
* | ||
* @return The token string. | ||
*/ | ||
OpenTok.prototype.generateToken = function(sessionId, opts) { | ||
var decoded, tokenData; | ||
dataString = querystring.stringify(dataParams) | ||
sig = this._signString(dataString, this.partnerSecret) | ||
tokenParams = ["partner_id=",this.partnerId,"&sig=",sig,":",dataString].join("") | ||
tokenBuffer = new Buffer(tokenParams,"utf8"); | ||
return TOKEN_SENTINEL + tokenBuffer.toString('base64'); | ||
} | ||
if (!opts) opts = {}; | ||
if ( !_.isString(sessionId) ) return null; | ||
OpenTokSDK.prototype.generateToken = function(ops){ | ||
return this.generate_token(ops); | ||
} | ||
// validate the sessionId belongs to the apiKey of this OpenTok instance | ||
decoded = decodeSessionId(sessionId); | ||
if ( !decoded || decoded.apiKey !== this.apiKey) return null; | ||
OpenTokSDK.prototype.create_session = function(ipPassthru, properties, callback){ | ||
var sessionId | ||
, params = { | ||
partner_id: this.partnerId, | ||
}; | ||
// combine defaults, opts, and whitelisted property names to create tokenData | ||
if (_.isNumber(opts.expireTime) || _.isString(opts.expireTime)) opts.expire_time = opts.expireTime; | ||
if (opts.data) opts.connection_data = opts.data; | ||
tokenData = _.pick(_.defaults(opts, { | ||
session_id: sessionId, | ||
create_time: Math.round(new Date().getTime() / 1000), | ||
expire_time: Math.round(new Date().getTime() / 1000) + (60*60*24), // 1 day | ||
nonce: Math.random(), | ||
role: 'publisher' | ||
}), 'session_id', 'create_time', 'nonce', 'role', 'expire_time', 'connection_data'); | ||
// No user specified parameter | ||
if( typeof(ipPassthru) == JSFunction ){ | ||
callback = ipPassthru; | ||
ipPassthru = null; | ||
properties = null; | ||
// validate tokenData | ||
if (!_.contains(['publisher', 'subscriber', 'moderator'], tokenData.role)) return null; | ||
if (!_.isNumber(tokenData.expire_time)) return null; | ||
if (tokenData.connection_data && | ||
(tokenData.connection_data.length > 1024 || !_.isString(tokenData.connection_data))) { | ||
return null; | ||
} | ||
// location is passed in only | ||
if( typeof(ipPassthru) == JSString && typeof(properties) == JSFunction ){ | ||
callback = properties; | ||
properties = null; | ||
} | ||
// property is passed in only | ||
if( typeof(ipPassthru) == JSObject && typeof(properties) == JSFunction ){ | ||
callback = properties; | ||
properties = ipPassthru; | ||
ipPassthru = null; | ||
} | ||
// property and location passed in, do nothing | ||
for(var p in properties){ | ||
params[p] = properties[p] | ||
} | ||
var self = this; | ||
sessionId = this._doRequest(params, function(err, chunks){ | ||
if (err) return this._handleError({ action: 'createSession', location: ipPassthru, props: params, cause: err}, callback); | ||
return encodeToken(tokenData, this.apiKey, this.apiSecret); | ||
} | ||
var start = chunks.match('<session_id>') | ||
, end = chunks.match('</session_id>') | ||
, sessionId; | ||
if(start && end){ | ||
self.sessionId = chunks.substr(start.index + 12, (end.index - start.index - 12)) | ||
} | ||
callback(null, self.sessionId) | ||
}); | ||
/* | ||
* decodes a sessionId into the metadata that it contains | ||
* @param {string} sessionId | ||
* @returns {?SessionInfo} sessionInfo | ||
*/ | ||
function decodeSessionId(sessionId) { | ||
var fields, sessionInfo; | ||
// remove sentinal (e.g. '1_', '2_') | ||
sessionId = sessionId.substring(2); | ||
// replace invalid base64 chars | ||
sessionId = sessionId.replace(/-/g, "+").replace(/_/g, "/"); | ||
// base64 decode | ||
sessionId = new Buffer(sessionId, "base64").toString("ascii"); | ||
// separate fields | ||
fields = sessionId.split("~"); | ||
return { | ||
apiKey: fields[1], | ||
location: fields[2], | ||
create_time: new Date(fields[3]) | ||
}; | ||
} | ||
OpenTokSDK.prototype.createSession = function(ipPassthru, properties, callback){ | ||
this.create_session(ipPassthru, properties, callback); | ||
/* | ||
* encodes token data into a valid token | ||
* @param {Object} data | ||
* @param {string} apiKey | ||
* @param {string} apiSecret | ||
* @returns {string} token | ||
*/ | ||
function encodeToken(data, apiKey, apiSecret) { | ||
var dataString = querystring.stringify(data), | ||
sig = signString(dataString, apiSecret), | ||
decoded = new Buffer("partner_id="+apiKey+"&sig="+sig+":"+dataString, 'utf8'); | ||
return TOKEN_SENTINEL + decoded.toString('base64'); | ||
} | ||
OpenTokSDK.prototype._signString = function(string, secret){ | ||
var hmac = crypto.createHmac('sha1',secret) | ||
hmac.update(string) | ||
return hmac.digest('hex') | ||
/* | ||
* sign a string | ||
* @param {string} unsigned | ||
* @param {string} key | ||
* @returns {string} signed | ||
*/ | ||
function signString(unsigned, key) { | ||
var hmac = crypto.createHmac('sha1', key); | ||
hmac.update(unsigned); | ||
return hmac.digest('hex'); | ||
} | ||
OpenTokSDK.prototype._doRequest = function(params, callback){ | ||
var dataString = querystring.stringify(params); | ||
/* | ||
* handles the result of a session creation | ||
* @callback OpenTok~createSessionCallback | ||
* @param {?Error} err | ||
* @param {string} sessionId | ||
*/ | ||
var options = { | ||
host: this.api_url, | ||
path: SESSION_API_ENDPOINT, | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
'Content-Length': dataString.length, | ||
'X-TB-PARTNER-AUTH': this.partnerId + ":" + this.partnerSecret | ||
} | ||
} | ||
/* | ||
* handles the result of a REST request | ||
* @callback OpenTok~doRequestCallback | ||
* @param {?Error} err | ||
* @param {string} responseXml | ||
*/ | ||
var req = https.request(options, function(res){ | ||
var chunks = ''; | ||
/* | ||
* is interested in an error, can be a super-type of OpenTok~createSessionCallback | ||
* @callback OpenTok~doRequestCallback | ||
* @param {?Error} err | ||
* @param {...*} arguments | ||
*/ | ||
res.setEncoding('utf8') | ||
/** | ||
* @typedef SessionInfo | ||
* @type {Object} | ||
* @property {string} apiKey The API Key that created the session | ||
* @property {number} location The location hint used when creating the session | ||
* @property {Date] create_time The time at which the session was created | ||
*/ | ||
res.on('data', function(chunk){ | ||
chunks += chunk | ||
}) | ||
res.on('end', function(){ | ||
callback(null, chunks); | ||
}) | ||
}) | ||
req.write(dataString) | ||
req.on('error', function(e) { | ||
callback(e); | ||
}); | ||
req.end() | ||
} | ||
/* | ||
* Sends errors to callback functions in pretty, readable messages | ||
* External Interface | ||
*/ | ||
OpenTokSDK.prototype._handleError = function(details, cb) { | ||
var message; | ||
// Construct message according the the action (or method) that is triggering the error | ||
if (details.action === 'createSession') { | ||
message = 'Failed to create new OpenTok Session using location: ' + details.location + ', properties: ' + JSON.stringify(details.props) + '.'; | ||
} | ||
module.exports = OpenTok; | ||
// When there is an underlying error that caused this one, give some details about it | ||
if (details.cause) { | ||
message += 'This error was caused by another error: "' + details.cause.message + '".'; | ||
for(var key in errors) { | ||
if(errors.hasOwnProperty(key)) { | ||
OpenTok[key] = errors[key]; | ||
} | ||
return cb(new Error(message)); | ||
}; | ||
OpenTokSDK.prototype._getUTCDate = function(){ | ||
var D= new Date(); | ||
return Date.UTC(D.getUTCFullYear(), D.getUTCMonth(), D.getUTCDate(), D.getUTCHours(), | ||
D.getUTCMinutes(), D.getUTCSeconds()).toString().substr(0,10) | ||
} | ||
{ | ||
"name": "opentok", | ||
"description": "OpenTokSDK for node.js", | ||
"version": "1.0.2", | ||
"homepage": "https://github.com/opentok/opentok", | ||
"repository": "https://github.com/opentok/opentok.git", | ||
"description": "OpenTok server-side SDK", | ||
"version": "2.2.0", | ||
"homepage": "http://opentok.github.io/opentok-node", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/opentok/opentok-node.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/opentok/opentok-node/issues", | ||
"email": "support@tokbox.com" | ||
}, | ||
"license": "MIT", | ||
"contributors": [ | ||
@@ -15,19 +23,33 @@ { | ||
"name": "Song Zheng", | ||
"email": "song@tokbox.com" | ||
"email": "song@tokbox.com", | ||
"url": "http://songz.me" | ||
}, | ||
{ | ||
"name": "Ankur Oberoi", | ||
"email": "aoberoi@gmail.com" | ||
"email": "aoberoi@gmail.com", | ||
"url": "http://aoberoi.me" | ||
} | ||
], | ||
"main": "./lib/opentok", | ||
"directories": { | ||
"lib": "lib" | ||
"main": "lib/opentok.js", | ||
"scripts": { | ||
"test": "grunt" | ||
}, | ||
"engines": { | ||
"node": "*" | ||
"node": ">=0.10.0" | ||
}, | ||
"devDependencies": { | ||
"jasmine-node" : "1.0.x" | ||
"async": "^0.2.10", | ||
"chai": "^1.9.1", | ||
"grunt": "^0.4.4", | ||
"grunt-jasmine-node": "^0.2.1", | ||
"grunt-jsdoc": "^0.5.4", | ||
"grunt-mocha-test": "^0.10.0", | ||
"nock": "^0.27.2" | ||
}, | ||
"dependencies": { | ||
"xmljson": "~0.2.0", | ||
"underscore": "~1.5.2", | ||
"request": "^2.34.0", | ||
"lodash": "^2.4.1" | ||
} | ||
} |
293
README.md
@@ -1,197 +0,222 @@ | ||
<div align="center"> | ||
<a href="http://tokbox.com/"> | ||
<img src="https://swww.tokbox.com/img/img_www_platform_devices.jpg" style="margin: 0 auto;" alt="Open Tok" /> | ||
</a> | ||
</div> | ||
# OpenTok Node SDK | ||
# OpenTokSDK for Node.js | ||
[![Build Status](https://travis-ci.org/opentok/opentok-node.png)](https://travis-ci.org/opentok/opentok-node) | ||
OpenTok is a free set of APIs from TokBox that enables websites to weave live group video communication into their online experience. With OpenTok you have the freedom and flexibility to create the most engaging web experience for your users. OpenTok is currently available as a JavaScript and ActionScript 3.0 library. Check out <http://www.tokbox.com/> and <http://www.tokbox.com/opentok/tools/js/gettingstarted> for more information. | ||
The OpenTok Node SDK lets you generate | ||
[sessions](http://www.tokbox.com/opentok/tutorials/create-session/) and | ||
[tokens](http://www.tokbox.com/opentok/tutorials/create-token/) for | ||
[OpenTok](http://www.tokbox.com/) applications, and | ||
[archive](http://www.tokbox.com/platform/archiving) OpenTok 2.0 sessions. | ||
This is the OpenTok NodeJS Module. | ||
If you are updating from a previous version of this SDK, see | ||
[Important changes in v2.2](#important-changes-in-v22). | ||
* [Installation](#installation) | ||
* [How to use](#how-to-use) | ||
* [Require the opentok module](#require-the-opentok-module) | ||
* [API key and API secret](#api-key-and-api-secret) | ||
* [OpenTokSDK](#opentoksdk) | ||
* [Creating Sessions](#creating-sessions) | ||
* [Generating Token](#generating-token) | ||
* [Downloading Archive Videos](#downloading-archive-videos) | ||
* [Get Archive Manifest](#get-archive-manifest) | ||
* [Get video ID](#get-video-id) | ||
* [Get Download URL](#get-download-url) | ||
* [Example](#example) | ||
* [Want to contribute?](#want-to-contribute) | ||
# Installation using npm (recommended): | ||
npm helps manage dependencies for node projects. Find more info here: <http://npmjs.org> | ||
## Installation | ||
Run this command to install the package and adding it to your `package.json`: | ||
To install using npm, add OpenTok to `package.json` and run `npm install`: | ||
```javascript | ||
"dependencies" : { | ||
"opentok" : "1.0.x", | ||
... | ||
} | ||
``` | ||
$ npm install opentok --save | ||
``` | ||
To install as a regular npm package just type `npm install opentok` | ||
# Usage | ||
## How to use | ||
## Initializing | ||
### Require the `opentok` module | ||
Import the module to get a constructor function for an OpenTok object, then call it with `new` to | ||
initantiate it with your own API Key and API Secret. | ||
Add the following code to the top of any file using the `opentok` module: | ||
```javascript | ||
var OpenTok = require('opentok'), | ||
opentok = new OpenTok(apiKey, apiSecret); | ||
``` | ||
var OpenTok = require('opentok'); | ||
## Creating Sessions | ||
### API key and API secret | ||
To create an OpenTok Session, use the `opentok.createSession(properties, callback)` method. The | ||
`properties` parameter is an optional object used to specify whether the session uses the OpenTok | ||
Media Router and to specify a location hint. The callback has the signature | ||
`function(error, session)`. The `session` returned in the callback is an instance of Session. | ||
Session objects have a `sessionId` property that is useful to be saved to a persistent store | ||
(such as a database). | ||
Create an account at [TokBox](http://tokbox.com), and sign into your [Dashboard](https://dashboard.tokbox.com) for an API Key and Secret. | ||
### OpenTokSDK | ||
In order to use any of the server side functions, you must first create an `OpenTokSDK` object with your developer credentials. | ||
You must pass in your *API key* and *API secret*. | ||
```javascript | ||
var key = ''; // Replace with your API key | ||
var secret = ''; // Replace with your API secret | ||
var opentok = new OpenTok.OpenTokSDK(key, secret); | ||
``` | ||
// Just a plain Session | ||
opentok.createSession(function(err, session) { | ||
if (err) return console.log(err); | ||
### Creating Sessions | ||
Use your `OpenTokSDK` object to create a `session_id`. | ||
`createSession` takes 2-3 parameters: | ||
// save the sessionId | ||
db.save('session', session.sessionId, done); | ||
}); | ||
#### Parameters | ||
| Name | Description | Type | Optional | | ||
| ---------- | ------------------------------------------------ | ------ | -------- | | ||
| location | Give OpenTok a hint of where the clients connecting will be located by specifiying an IP (e.g. '127.0.0.1') | string | yes | | ||
| properties | Additional session options | object | yes | | ||
| properties['p2p.preference'] | set to 'enabled' or 'disabled' | string | yes | | ||
| callback | This is a function that handles the server response after session has been created. The result sessionId is a string. | fn(err, sessionId) | no | | ||
// A The session will attempt to transmit streams directly between clients. | ||
// If clients cannot connect, the session uses the OpenTok TURN server: | ||
opentok.createSession({mediaMode:"relayed"}, function(err, session) { | ||
if (err) return console.log(err); | ||
// save the sessionId | ||
db.save('session', session.sessionId, done); | ||
}); | ||
Example: P2P disabled (default) | ||
// A Session with a location hint | ||
opentok.createSession({location:'12.34.56.78'}, function(err, session) { | ||
if (err) return console.log(err); | ||
```javascript | ||
var location = '127.0.0.1'; // use an IP or 'localhost' | ||
var sessionId = ''; | ||
opentok.createSession(location, function(err, sessionId){ | ||
if (err) return throw new Error("session creation failed"); | ||
// Do things with sessionId | ||
// save the sessionId | ||
db.save('session', session.sessionId, done); | ||
}); | ||
``` | ||
## Generating Tokens | ||
Example: P2P enabled | ||
Once a Session is created, you can start generating Tokens for clients to use when connecting to it. | ||
You can generate a token by calling the `opentok.generateToken(sessionId, options)` method. Another | ||
way is to call the `session.generateToken(options)` method of a Session object. The `options` | ||
parameter is an optional object used to set the role, expire time, and connection data of the Token. | ||
```javascript | ||
var location = '127.0.0.1'; // use an IP of 'localhost' | ||
var sessionId = ''; | ||
opentok.createSession(location, {'p2p.preference':'enabled'}, function(err, sessionId){ | ||
if (err) return throw new Error("session creation failed"); | ||
// Do things with sessionId | ||
// Generate a Token from just a sessionId (fetched from a database) | ||
token = opentok.generateToken(sessionId); | ||
// Genrate a Token from a session object (returned from createSession) | ||
token = session.generateToken(); | ||
// Set some options in a Token | ||
token = session.generateToken({ | ||
role : 'moderator', | ||
expireTime : (new Date().getTime() / 1000)+(7 * 24 * 60 * 60), // in one week | ||
data : 'name=Johnny' | ||
}); | ||
``` | ||
### Generating Token | ||
With the generated session_id and an OpenTokSDK object, you can start generating tokens for each user. | ||
`generateToken` takes in an object with 1-4 properties, and RETURNS a token as a string: | ||
## Working with archives | ||
#### Parameters | ||
You can start the recording of an OpenTok Session using the `opentok.startArchive(sessionId, | ||
options, callback)` method. The `options` parameter is an optional object used to set the name of | ||
the Archive. The callback has the signature `function(err, archive)`. The `archive` returned in | ||
the callback is an instance of `Archive`. Note that you can only start an archive on a Session with | ||
connected clients. | ||
| Name | Description | Type | Optional | | ||
| --------------- | -------------------------------------------------------------------------------------------------------------------- |:------:|:--------:| | ||
| session_id | This token is tied to the session it is generated with | string | no | | ||
| role | opentok.RoleConstants.{SUBSCRIBER|PUBLISHER|MODERATOR}. Publisher role used when omitted. | string | yes | | ||
| expire_time | Time when token will expire in unix timestamp. | int | yes | | ||
| connection_data | Stores static metadata to pass to other users connected to the session. (eg. names, user id, etc) | string | yes | | ||
```javascript | ||
opentok.startArchive(sessionId, { name: 'Important Presentation' }, function(err, archive) { | ||
if (err) return console.log(err); | ||
Example: | ||
<pre> | ||
var token = opentok.generateToken({session_id:session_id, role:OpenTok.RoleConstants.PUBLISHER, connection_data:"userId:42"}); | ||
</pre> | ||
// The id property is useful to save off into a database | ||
console.log("new archive:" + archive.id); | ||
}); | ||
``` | ||
### Downloading Archive Videos | ||
To Download archived video, you must have an Archive ID (from the client), and a moderator token. For more information see <http://www.tokbox.com/opentok/api/tools/documentation/api/server_side_libraries.html#download_archive>. | ||
You can stop the recording of a started Archive using the `opentok.stopArchive(archiveId, callback)` | ||
method. You can also do this using the `archive.stop(callback)` method an `Archive` instance. The | ||
callback has a signature `function(err, archive)`. The `archive` returned in the callback is an | ||
instance of `Archive`. | ||
#### Quick Overview of the javascript library: <http://www.tokbox.com/opentok/api/tools/js/documentation/api/Session.html#createArchive> | ||
1. Create an event listener on `archiveCreated` event: `session.addEventListener('archiveCreated', archiveCreatedHandler);` | ||
2. Create an archive: `archive = session.createArchive(...);` | ||
3. When archive is successfully created `archiveCreatedHandler` would be triggered. An Archive object containing `archiveId` property is passed into your function. Save this in your database, this archiveId is what you use to reference the archive for playbacks and download videos | ||
4. After your archive has been created, you can start recording videos into it by calling `session.startRecording(archive)` | ||
Optionally, you can also use the standalone archiving, which means that each archive would have only 1 video: <http://www.tokbox.com/opentok/api/tools/js/documentation/api/RecorderManager.html> | ||
```javascript | ||
opentok.stopArchive(archiveId, function(err, archive) { | ||
if (err) return console.log(err); | ||
### Get Archive Manifest | ||
With your **moderator token** and a OpenTokSDK object, you can generate a OpenTokArchive object, which contains information for all videos in the Archive | ||
`OpenTokSDK.getArchiveManifest()` takes in 3 parameters: **archiveId** and **moderator token**, and a callback function | ||
console.log("Stopped archive:" + archive.id); | ||
}); | ||
archive.stop(function(err, archive) { | ||
if (err) return console.log(err); | ||
}); | ||
``` | ||
#### Parameters | ||
| Name | Description | Type | Optional | | ||
| --------------- | ------------------------------------------------------------ | ------------- | -------- | | ||
| archive_id | Get this from the client that created the archive. | string | no | | ||
| token | Get this from the client or the generate_token method. | string | no | | ||
| handler | This function is triggered after it receives the Archive Manifest. The parameter is an `OpenTokArchive` object. The *resources* property of this object is array of `OpenTokArchiveVideoResource` objects, and each `OpenTokArchiveVideoResource` object represents a video in the archive. | fn(tbarchive) | no | | ||
To get an `Archive` instance (and all the information about it) from an `archiveId`, use the | ||
`opentok.getArchive(archiveId, callback)` method. The callback has a function signature | ||
`function(err, archive)`. You can inspect the properties of the archive for more details. | ||
```javascript | ||
opentok.getArchive(archiveId, function(err, archive) { | ||
if (err) return console.log(err); | ||
Example: (opentok is an OpentokSDK object) | ||
console.log(archive); | ||
}); | ||
``` | ||
To delete an Archive, you can call the `opentok.deleteArchive(archiveId, callback)` method or the | ||
`delete(callback)` method of an `Archive` instance. The callback has a signature `function(err)`. | ||
```javascript | ||
var token = 'moderator_token'; | ||
var archiveId = '5f74aee5-ab3f-421b-b124-ed2a698ee939'; // Obtained from Javascript Library | ||
// Delete an Archive from an archiveId (fetched from database) | ||
opentok.deleteArchive(archiveId, function(err) { | ||
if (err) console.log(err); | ||
}); | ||
opentok.getArchiveManifest(archiveId, token, function(tbarchive){ | ||
var otArchive = tbarchive; | ||
// Delete an Archive from an Archive instance (returned from archives.create, archives.find) | ||
archive.delete(function(err) { | ||
if (err) console.log(err); | ||
}); | ||
``` | ||
### Get video ID | ||
`OpenTokArchive.resources` is an array of `OpenTokArchiveVideoResource` objects. OpenTokArchiveVideoResource has a `getId()` method that returns the videoId as a string. | ||
You can also get a list of all the Archives you've created (up to 1000) with your API Key. This is | ||
done using the `opentok.listArchives(options, callback)` method. The parameter `options` is an | ||
optional object used to specify an `offset` and `count` to help you paginate through the results. | ||
The callback has a signature `function(err, archives, totalCount)`. The `archives` returned from | ||
the callback is an array of `Archive` instances. The `totalCount` returned from the callback is | ||
the total number of archives your API Key has generated. | ||
Example: | ||
```javascript | ||
opentok.getArchiveManifest(archiveId, token, function(tbarchive){ | ||
var vidID = tbarchive.resources[0].getId(); | ||
opentok.listArchives({offset:100, count:50}, function(error, archives, totalCount) { | ||
if (error) return console.log("error:", error); | ||
console.log(totalCount + " archives"); | ||
for (var i = 0; i < archives.length; i++) { | ||
console.log(archives[i].id); | ||
} | ||
}); | ||
``` | ||
### Get Download URL | ||
`OpenTokArchive` objects have a `downloadArchiveURL(video_id, handler)` method that will return a URL string for downloading the video in the archive. Video files are FLV format. | ||
# Samples | ||
There are two sample applications included in this repository. To get going as fast as possible, clone the whole | ||
repository and follow the Walkthroughs: | ||
#### Parameters | ||
* [HelloWorld](sample/HelloWorld/README.md) | ||
* [Archiving](sample/Archiving/README.md) | ||
| Name | Description | Type | Optional | | ||
| --------------- |------------------------------------------------------------- | ------------- | -------- | | ||
| video_id | The Video ID returned from OpenTokArchiveVideoResource.getId() | string | no | | ||
| handler | This function is triggered after it receives the URL for video. The result is a URL string. | [fn(url)] | no | | ||
# Documentation | ||
Example: | ||
Reference documentation is available at <http://www.tokbox.com/opentok/libraries/server/node/reference/index.html> and in the | ||
docs directory of the SDK. | ||
```javascript | ||
var url = ''; | ||
otArchive.downloadArchiveURL(vidID, function(resp){ | ||
url = resp; | ||
}); | ||
``` | ||
# Requirements | ||
You need an OpenTok API key and API secret, which you can obtain at <https://dashboard.tokbox.com>. | ||
## Example | ||
The OpenTok Node SDK requires node 0.10 or higher. | ||
Check out the basic working example in examples/app.js | ||
# Release Notes | ||
### When done, update package.json version number and publish npm | ||
See the [Releases](https://github.com/opentok/opentok-node/releases) page for details | ||
about each release. | ||
npm publish | ||
## Important changes in v2.2 | ||
## Want to contribute? | ||
### To run test suite: | ||
jasmine-node --coffee spec/ | ||
This version of the SDK includes support for working with OpenTok 2.0 archives. (This API does not | ||
work with OpenTok 1.0 archives.) | ||
The `create_session()` has changed to take one parameter: an `options` object that has `location` | ||
and `mediaMode` properties. The `mediaMode` property replaces the `properties.p2p.preference` | ||
parameter in the previous version of the SDK. | ||
The `generateToken()` has changed to take two parameters: the session ID and an `options` object that has `role`, `expireTime` and `data` properties. | ||
See the reference documentation | ||
<http://www.tokbox.com/opentok/libraries/server/node/reference/index.html> and in the | ||
docs directory of the SDK. | ||
# Development and Contributing | ||
Interested in contributing? We :heart: pull requests! See the [Development](DEVELOPING.md) and | ||
[Contribution](CONTRIBUTING.md) guidelines. | ||
# Support | ||
See <http://tokbox.com/opentok/support/> for all our support options. | ||
Find a bug? File it on the [Issues](https://github.com/opentok/opentok-node/issues) page. Hint: | ||
test cases are really helpful! |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
295747
52
2369
0
223
2
4
7
1
6
+ Addedlodash@^2.4.1
+ Addedrequest@^2.34.0
+ Addedunderscore@~1.5.2
+ Addedxmljson@~0.2.0
+ Addedajv@6.12.6(transitive)
+ Addedasn1@0.2.6(transitive)
+ Addedassert-plus@1.0.0(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedaws-sign2@0.7.0(transitive)
+ Addedaws4@1.13.2(transitive)
+ Addedbcrypt-pbkdf@1.0.2(transitive)
+ Addedcaseless@0.12.0(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcore-util-is@1.0.2(transitive)
+ Addeddashdash@1.14.1(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedecc-jsbn@0.1.2(transitive)
+ Addedextend@3.0.2(transitive)
+ Addedextsprintf@1.3.0(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedforever-agent@0.6.1(transitive)
+ Addedform-data@2.3.3(transitive)
+ Addedgetpass@0.1.7(transitive)
+ Addedhar-schema@2.0.0(transitive)
+ Addedhar-validator@5.1.5(transitive)
+ Addedhttp-signature@1.2.0(transitive)
+ Addedis-typedarray@1.0.0(transitive)
+ Addedisstream@0.1.2(transitive)
+ Addedjsbn@0.1.1(transitive)
+ Addedjson-schema@0.4.0(transitive)
+ Addedjson-schema-traverse@0.4.1(transitive)
+ Addedjson-stringify-safe@5.0.1(transitive)
+ Addedjsprim@1.4.2(transitive)
+ Addedlodash@2.4.2(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedoauth-sign@0.9.0(transitive)
+ Addedperformance-now@2.1.0(transitive)
+ Addedpsl@1.10.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedqs@6.5.3(transitive)
+ Addedrequest@2.88.2(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsax@0.5.8(transitive)
+ Addedsshpk@1.18.0(transitive)
+ Addedtough-cookie@2.5.0(transitive)
+ Addedtunnel-agent@0.6.0(transitive)
+ Addedtweetnacl@0.14.5(transitive)
+ Addedunderscore@1.5.2(transitive)
+ Addeduri-js@4.4.1(transitive)
+ Addeduuid@3.4.0(transitive)
+ Addedverror@1.10.0(transitive)
+ Addedxml2js@0.2.8(transitive)
+ Addedxmlbuilder@0.4.3(transitive)
+ Addedxmljson@0.2.0(transitive)