Compare commits
14 Commits
Author | SHA1 | Date |
---|---|---|
AJ ONeal | 667fbbab3a | |
AJ ONeal | df6b77daf5 | |
AJ ONeal | d962557b02 | |
AJ ONeal | 04aee13896 | |
AJ ONeal | 8e2e09f582 | |
AJ ONeal | c99e2923b2 | |
AJ ONeal | 34a44561aa | |
AJ ONeal | a7c99a952a | |
AJ ONeal | 83927315f8 | |
AJ ONeal | ccbca7f661 | |
AJ ONeal | 22fd3ff917 | |
AJ ONeal | 0c13b2abcf | |
AJ ONeal | f4445586a5 | |
AJ ONeal | 69ce9bf95f |
|
@ -0,0 +1 @@
|
|||
oauth3.org/
|
Before Width: | Height: | Size: 43 B After Width: | Height: | Size: 43 B |
Before Width: | Height: | Size: 43 B After Width: | Height: | Size: 43 B |
|
@ -0,0 +1 @@
|
|||
index.json
|
151
oauth3.core.js
151
oauth3.core.js
|
@ -1,4 +1,4 @@
|
|||
/ * global Promise */
|
||||
/* global Promise */
|
||||
;(function (exports) {
|
||||
'use strict';
|
||||
|
||||
|
@ -17,6 +17,13 @@
|
|||
err.code = params.error.code || params.error;
|
||||
return err;
|
||||
}
|
||||
, create: function (opts) {
|
||||
var err = new Error(opts.message);
|
||||
err.code = opts.code;
|
||||
err.uri = opts.uri || opts.url;
|
||||
err.subErr = opts.subErr;
|
||||
return err;
|
||||
}
|
||||
}
|
||||
, _binStr: {
|
||||
bufferToBinStr: function (buf) {
|
||||
|
@ -205,6 +212,75 @@
|
|||
}
|
||||
return str;
|
||||
}
|
||||
, jwk: {
|
||||
get: function (decoded) {
|
||||
return OAUTH3.discover(decoded.payload.iss).then(function (directives) {
|
||||
var urlObj = OAUTH3.urls.jwk(directives, decoded);
|
||||
return OAUTH3.request(urlObj).catch(function (err) {
|
||||
return OAUTH3.PromiseA.reject({
|
||||
message: 'failed to retrieve public key from token issuer'
|
||||
, code: 'E_NO_PUB_KEY'
|
||||
, url: 'https://oauth3.org/docs/errors#E_NO_PUB_KEY'
|
||||
, subErr: err.toString()
|
||||
});
|
||||
});
|
||||
}, function (err) {
|
||||
return OAUTH3.PromiseA.reject({
|
||||
message: 'token issuer is not a valid OAuth3 provider'
|
||||
, code: 'E_INVALID_ISS'
|
||||
, url: 'https://oauth3.org/docs/errors#E_INVALID_ISS'
|
||||
, subErr: err.toString()
|
||||
});
|
||||
}).then(function (res) {
|
||||
if (res.data.error) {
|
||||
return OAUTH3.PromiseA.reject(res.data.error);
|
||||
}
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
, verifyToken: function (token) {
|
||||
var decoded;
|
||||
|
||||
if (!token) {
|
||||
return OAUTH3.PromiseA.reject({
|
||||
message: 'no token provided'
|
||||
, code: 'E_NO_TOKEN'
|
||||
, url: 'https://oauth3.org/docs/errors#E_NO_TOKEN'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
decoded = OAUTH3.jwt.decode(token, {complete: true});
|
||||
} catch (e) {}
|
||||
if (!decoded) {
|
||||
return OAUTH3.PromiseA.reject({
|
||||
message: 'provided token not a JSON Web Token'
|
||||
, code: 'E_NOT_JWT'
|
||||
, url: 'https://oauth3.org/docs/errors#E_NOT_JWT'
|
||||
});
|
||||
}
|
||||
|
||||
return OAUTH3.jwk.get(decoded).then(function (jwk) {
|
||||
var opts = {};
|
||||
if (Array.isArray(jwk.alg)) {
|
||||
opts.algorithms = jwk.alg;
|
||||
} else if (typeof jwk.alg === 'string') {
|
||||
opts.algorithms = [ jwk.alg ];
|
||||
}
|
||||
|
||||
try {
|
||||
return OAUTH3.jwt.verify(token, jwk, opts);
|
||||
} catch (err) {
|
||||
return OAUTH3.PromiseA.reject({
|
||||
message: 'token verification failed'
|
||||
, code: 'E_INVALID_TOKEN'
|
||||
, url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN'
|
||||
, subErr: err.toString()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
, jwt: {
|
||||
// decode only (no verification)
|
||||
decode: function (token, opts) {
|
||||
|
@ -228,7 +304,7 @@
|
|||
, payload: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1]))
|
||||
};
|
||||
}
|
||||
, verify: function (token, jwk) {
|
||||
, verify: function (token, jwk/*, opts*/) {
|
||||
if (!OAUTH3.crypto) {
|
||||
return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable"));
|
||||
}
|
||||
|
@ -237,9 +313,15 @@
|
|||
var parts = token.split(/\./g);
|
||||
var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.'));
|
||||
var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]);
|
||||
var decoded = OAUTH3.jwt.decode(token, { complete: true });
|
||||
|
||||
// TODO disallow none and hmac algorithms
|
||||
// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
||||
if (!decoded.header.alg || 'none' === decoded.header.alg.toString() || /^HS/i.test(decoded.header.alg.toString())) {
|
||||
throw new Error("token algorithm '" + decoded.header.alg + "' is not accepted");
|
||||
}
|
||||
return OAUTH3.crypto.core.verify(jwk, data, signature).then(function () {
|
||||
return OAUTH3.jwt.decode(token);
|
||||
return decoded;
|
||||
});
|
||||
}
|
||||
, sign: function (payload, jwk) {
|
||||
|
@ -335,6 +417,69 @@
|
|||
opts._pathname = ".well-known/oauth3/scopes.json";
|
||||
return OAUTH3.urls.rpc(providerUri, opts);
|
||||
}
|
||||
, jwk: function (directives, decoded) {
|
||||
var sub = decoded.payload.sub || decoded.payload.ppid || decoded.payload.appScopedId;
|
||||
if (!sub) {
|
||||
throw OAUTH3.error.create({
|
||||
message: 'token missing sub'
|
||||
, code: 'E_MISSING_SUB'
|
||||
, url: 'https://oauth3.org/docs/errors#E_MISSING_SUB'
|
||||
});
|
||||
}
|
||||
var kid = decoded.header.kid || decoded.payload.kid;
|
||||
if (!kid) {
|
||||
throw OAUTH3.error.create({
|
||||
message: 'token missing kid'
|
||||
, code: 'E_MISSING_KID'
|
||||
, url: 'https://oauth3.org/docs/errors#E_MISSING_KID'
|
||||
});
|
||||
}
|
||||
if (!decoded.payload.iss) {
|
||||
throw OAUTH3.error.create({
|
||||
message: 'token missing iss'
|
||||
, code: 'E_MISSING_ISS'
|
||||
, url: 'https://oauth3.org/docs/errors#E_MISSING_ISS'
|
||||
});
|
||||
}
|
||||
|
||||
var args = (directives || {}).retrieve_jwk;
|
||||
if (typeof args === 'string') {
|
||||
args = { url: args, method: 'GET' };
|
||||
}
|
||||
if (typeof (args || {}).url !== 'string') {
|
||||
throw OAUTH3.error.create({
|
||||
message: 'token issuer does not support retrieving JWKs'
|
||||
, code: 'E_INVALID_ISS'
|
||||
, url: 'https://oauth3.org/docs/errors#E_INVALID_ISS'
|
||||
});
|
||||
}
|
||||
|
||||
var params = {
|
||||
sub: sub
|
||||
, kid: kid
|
||||
};
|
||||
var url = args.url;
|
||||
var body;
|
||||
Object.keys(params).forEach(function (key) {
|
||||
if (url.indexOf(':'+key) !== -1) {
|
||||
url = url.replace(':'+key, params[key]);
|
||||
delete params[key];
|
||||
}
|
||||
});
|
||||
if (Object.keys(params).length > 0) {
|
||||
if ('GET' === (args.method || 'GET').toUpperCase()) {
|
||||
url += '?' + OAUTH3.query.stringify(params);
|
||||
} else {
|
||||
body = params;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: OAUTH3.url.resolve(directives.api, url)
|
||||
, method: args.method
|
||||
, data: body
|
||||
};
|
||||
}
|
||||
, implicitGrant: function (directive, opts) {
|
||||
//
|
||||
// Example Implicit Grant Request
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
var PromiseA;
|
||||
try {
|
||||
PromiseA = require('bluebird');
|
||||
} catch(e) {
|
||||
PromiseA = global.Promise;
|
||||
}
|
||||
var promisify = PromiseA.promisify || require('util').promisify;
|
||||
|
||||
//var OAUTH3 = require('./oauth3.core.js').OAUTH3;
|
||||
var OAUTH3 = require('./oauth3.issuer.js').OAUTH3;
|
||||
// used for OAUTH3.urls.resourcePasswordOwner
|
||||
// used for OAUTH3.authn.loginMeta
|
||||
// used for OAUTH3.authn.otp
|
||||
// used for OAUTH3.authn.resourcePasswordOwner
|
||||
var PromiseA = require('bluebird');
|
||||
var requestAsync = PromiseA.promisify(require('request'));
|
||||
|
||||
var requestAsync = promisify(require('@coolaj86/urequest'));
|
||||
var crypto = require('crypto');
|
||||
|
||||
OAUTH3.crypto = OAUTH3.crypto || {};
|
||||
OAUTH3.crypto.core = require('./oauth3.node.crypto.js');
|
||||
OAUTH3.PromiseA = PromiseA;
|
||||
OAUTH3._discoverHelper = function(providerUri, opts) {
|
||||
return OAUTH3._node.discover(providerUri, opts);
|
||||
|
@ -28,7 +38,6 @@ OAUTH3._base64.atob = function (base64) {
|
|||
OAUTH3._base64.btoa = function (text) {
|
||||
return new Buffer(text, 'utf8').toString('base64');
|
||||
};
|
||||
OAUTH3._defaultStorage = require('./oauth3.node.storage');
|
||||
|
||||
OAUTH3._node = {};
|
||||
OAUTH3._node.discover = function(providerUri/*, opts*/) {
|
||||
|
@ -97,5 +106,13 @@ OAUTH3._node.randomState = function () {
|
|||
return crypto.randomBytes(16).toString('hex');
|
||||
};
|
||||
OAUTH3.randomState = OAUTH3._node.randomState;
|
||||
OAUTH3._nodeCreate = OAUTH3.create;
|
||||
OAUTH3.create = function (loc, opts) {
|
||||
if (!loc) {
|
||||
loc = {};
|
||||
}
|
||||
OAUTH3._defaultStorage = require('./oauth3.node.storage').create(loc, opts);
|
||||
return OAUTH3._nodeCreate.apply(OAUTH3, arguments);
|
||||
};
|
||||
|
||||
module.exports = OAUTH3;
|
||||
|
|
|
@ -1,137 +1,159 @@
|
|||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var fs = PromiseA.promisifyAll(require('fs'));
|
||||
var PromiseA;
|
||||
try {
|
||||
PromiseA = require('bluebird');
|
||||
} catch(e) {
|
||||
PromiseA = global.Promise;
|
||||
}
|
||||
var promisify = PromiseA.promisify || require('util').promisify;
|
||||
var fs = {
|
||||
existsSync: require('fs').existsSync
|
||||
, mkdirSync: require('fs').mkdirSync
|
||||
, readdirAsync: promisify(require('fs').readdir)
|
||||
, writeFileAsync: promisify(require('fs').writeFile)
|
||||
, unlinkAsync: promisify(require('fs').unlink)
|
||||
};
|
||||
var path = require('path');
|
||||
//var oauth3dir = process.cwd();
|
||||
var oauth3dir = path.join(require('os').homedir(), '.oauth3', 'v1');
|
||||
var sessionsdir = path.join(oauth3dir, 'sessions');
|
||||
var directivesdir = path.join(oauth3dir, 'directives');
|
||||
var metadir = path.join(oauth3dir, 'meta');
|
||||
|
||||
// We can reasonably assume the existence of the home directory, but we can't assume
|
||||
// that there will already be a `.oauth3` directory or anything inside of it.
|
||||
if (!fs.existsSync(path.join(oauth3dir, '..'))) {
|
||||
fs.mkdirSync(path.join(oauth3dir, '..'));
|
||||
}
|
||||
if (!fs.existsSync(oauth3dir)) {
|
||||
fs.mkdirSync(oauth3dir);
|
||||
}
|
||||
if (!fs.existsSync(directivesdir)) {
|
||||
fs.mkdirSync(directivesdir);
|
||||
}
|
||||
if (!fs.existsSync(sessionsdir)) {
|
||||
fs.mkdirSync(sessionsdir);
|
||||
}
|
||||
if (!fs.existsSync(metadir)) {
|
||||
fs.mkdirSync(metadir);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
directives: {
|
||||
all: function () {
|
||||
return fs.readdirAsync(directivesdir).then(function (nodes) {
|
||||
return nodes.map(function (node) {
|
||||
try {
|
||||
return require(path.join(directivesdir, node));
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
});
|
||||
create: function (loc/*, opts*/) {
|
||||
var oauth3dir;
|
||||
if (!loc.pathname) {
|
||||
loc.pathname = require('os').hostname();
|
||||
oauth3dir = path.join(loc.pathname, '.config/oauth3.org');
|
||||
} else {
|
||||
oauth3dir = path.join(loc.pathname, 'oauth3.org', 'v1');
|
||||
}
|
||||
, get: function (providerUri) {
|
||||
// TODO make safe
|
||||
try {
|
||||
return require(path.join(directivesdir, providerUri + '.json'));
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
, set: function (providerUri, directives) {
|
||||
return fs.writeFileAsync(
|
||||
path.join(directivesdir, providerUri + '.json')
|
||||
, JSON.stringify(directives, null, 2)
|
||||
).then(function () {
|
||||
return directives;
|
||||
});
|
||||
}
|
||||
, clear: function () {
|
||||
return fs.readdirAsync(directivesdir).then(function (nodes) {
|
||||
return PromiseA.all(nodes.map(function (node) {
|
||||
return fs.unlinkAsync(path.join(directivesdir, node)).then(function () { }, function () { });
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
var sessionsdir = path.join(oauth3dir, 'sessions');
|
||||
var directivesdir = path.join(oauth3dir, 'directives');
|
||||
var metadir = path.join(oauth3dir, 'meta');
|
||||
|
||||
, sessions: {
|
||||
all: function (providerUri) {
|
||||
return fs.readdirAsync(sessionsdir).then(function (nodes) {
|
||||
return nodes.map(function (node) {
|
||||
var result = require(path.join(sessionsdir, node));
|
||||
if (result.link) {
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean).filter(function (result) {
|
||||
if (!providerUri || providerUri === result.issuer) {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
});
|
||||
// We can reasonably assume the existence of the home directory, but we can't assume
|
||||
// that there will already be a `.oauth3` directory or anything inside of it.
|
||||
if (!fs.existsSync(path.join(oauth3dir, '..'))) {
|
||||
fs.mkdirSync(path.join(oauth3dir, '..'));
|
||||
}
|
||||
, get: function (providerUri, id) {
|
||||
var result;
|
||||
try {
|
||||
if (!fs.existsSync(oauth3dir)) {
|
||||
fs.mkdirSync(oauth3dir);
|
||||
}
|
||||
if (!fs.existsSync(directivesdir)) {
|
||||
fs.mkdirSync(directivesdir);
|
||||
}
|
||||
if (!fs.existsSync(sessionsdir)) {
|
||||
fs.mkdirSync(sessionsdir);
|
||||
}
|
||||
if (!fs.existsSync(metadir)) {
|
||||
fs.mkdirSync(metadir);
|
||||
}
|
||||
|
||||
var OAUTH3 = {};
|
||||
OAUTH3.directives = {
|
||||
all: function () {
|
||||
return fs.readdirAsync(directivesdir).then(function (nodes) {
|
||||
return nodes.map(function (node) {
|
||||
try {
|
||||
return require(path.join(directivesdir, node));
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
});
|
||||
}
|
||||
, get: function (providerUri) {
|
||||
// TODO make safe
|
||||
try {
|
||||
return require(path.join(directivesdir, providerUri + '.json'));
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
, set: function (providerUri, directives) {
|
||||
return fs.writeFileAsync(
|
||||
path.join(directivesdir, providerUri + '.json')
|
||||
, JSON.stringify(directives, null, 2)
|
||||
).then(function () {
|
||||
return directives;
|
||||
});
|
||||
}
|
||||
, clear: function () {
|
||||
return fs.readdirAsync(directivesdir).then(function (nodes) {
|
||||
return PromiseA.all(nodes.map(function (node) {
|
||||
return fs.unlinkAsync(path.join(directivesdir, node)).then(function () { }, function () { });
|
||||
}));
|
||||
});
|
||||
}
|
||||
};
|
||||
OAUTH3.sessions = {
|
||||
all: function (providerUri) {
|
||||
return fs.readdirAsync(sessionsdir).then(function (nodes) {
|
||||
return nodes.map(function (node) {
|
||||
var result = require(path.join(sessionsdir, node));
|
||||
if (result.link) {
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean).filter(function (result) {
|
||||
if (!providerUri || providerUri === result.issuer) {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
, get: function (providerUri, id) {
|
||||
var result;
|
||||
try {
|
||||
if (id) {
|
||||
return PromiseA.resolve(require(path.join(sessionsdir, providerUri + '.' + id + '.json')));
|
||||
}
|
||||
else {
|
||||
result = require(path.join(sessionsdir, providerUri + '.json'));
|
||||
// TODO make safer
|
||||
if (result.link && '/' !== result.link[0] && !/\.\./.test(result.link)) {
|
||||
result = require(path.join(sessionsdir, result.link));
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
return PromiseA.resolve(null);
|
||||
}
|
||||
return PromiseA.resolve(result);
|
||||
}
|
||||
, set: function (providerUri, session, id) {
|
||||
var p;
|
||||
|
||||
if (id) {
|
||||
return PromiseA.resolve(require(path.join(sessionsdir, providerUri + '.' + id + '.json')));
|
||||
p = fs.writeFileAsync(path.join(sessionsdir, providerUri + '.' + id + '.json'), JSON.stringify(session, null, 2));
|
||||
}
|
||||
else {
|
||||
result = require(path.join(sessionsdir, providerUri + '.json'));
|
||||
// TODO make safer
|
||||
if (result.link && '/' !== result.link[0] && !/\.\./.test(result.link)) {
|
||||
result = require(path.join(sessionsdir, result.link));
|
||||
}
|
||||
p = fs.writeFileAsync(path.join(sessionsdir, providerUri + '.json'), JSON.stringify(session, null, 2));
|
||||
}
|
||||
} catch(e) {
|
||||
return PromiseA.resolve(null);
|
||||
return p.then(function () {
|
||||
return session;
|
||||
});
|
||||
}
|
||||
return PromiseA.resolve(result);
|
||||
}
|
||||
, set: function (providerUri, session, id) {
|
||||
var p;
|
||||
, clear: function () {
|
||||
return fs.readdirAsync(sessionsdir).then(function (nodes) {
|
||||
return PromiseA.all(nodes.map(function (node) {
|
||||
return fs.unlinkAsync(path.join(sessionsdir, node));
|
||||
}));
|
||||
});
|
||||
}
|
||||
};
|
||||
OAUTH3.meta = {
|
||||
get: function (key) {
|
||||
// TODO make safe
|
||||
try {
|
||||
return PromiseA.resolve(require(path.join(metadir, key + '.json')));
|
||||
} catch(e) {
|
||||
return PromiseA.resolve(null);
|
||||
}
|
||||
}
|
||||
, set: function (key, value) {
|
||||
return fs.writeFileAsync(path.join(metadir, key + '.json'), JSON.stringify(value, null, 2)).then(function () {
|
||||
return value;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (id) {
|
||||
p = fs.writeFileAsync(path.join(sessionsdir, providerUri + '.' + id + '.json'), JSON.stringify(session, null, 2));
|
||||
}
|
||||
else {
|
||||
p = fs.writeFileAsync(path.join(sessionsdir, providerUri + '.json'), JSON.stringify(session, null, 2));
|
||||
}
|
||||
return p.then(function () {
|
||||
return session;
|
||||
});
|
||||
}
|
||||
, clear: function () {
|
||||
return fs.readdirAsync(sessionsdir).then(function (nodes) {
|
||||
return PromiseA.all(nodes.map(function (node) {
|
||||
return fs.unlinkAsync(path.join(sessionsdir, node));
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
, meta: {
|
||||
get: function (key) {
|
||||
// TODO make safe
|
||||
try {
|
||||
return PromiseA.resolve(require(path.join(metadir, key + '.json')));
|
||||
} catch(e) {
|
||||
return PromiseA.resolve(null);
|
||||
}
|
||||
}
|
||||
, set: function (key, value) {
|
||||
return fs.writeFileAsync(path.join(metadir, key + '.json'), JSON.stringify(value, null, 2)).then(function () {
|
||||
return value;
|
||||
});
|
||||
}
|
||||
return OAUTH3;
|
||||
}
|
||||
};
|
||||
|
|
12
package.json
12
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "oauth3.js",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.5",
|
||||
"description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.",
|
||||
"main": "oauth3.node.js",
|
||||
"scripts": {
|
||||
|
@ -32,23 +32,25 @@
|
|||
"sign"
|
||||
],
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.0",
|
||||
"@coolaj86/urequest": "^1.3.6",
|
||||
"elliptic": "^6.4.0",
|
||||
"request": "^2.81.0",
|
||||
"terminal-forms.js": "git+https://git.oauth3.org/OAuth3/terminal-forms.js.git#v1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^14.1.0",
|
||||
"browserify-aes": "^1.0.6",
|
||||
"create-hash": "^1.1.2",
|
||||
"pbkdf2": "^3.0.9",
|
||||
"browserify": "^14.1.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-cli": "^1.2.2",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-streamify": "^1.0.2",
|
||||
"gulp-uglify": "^2.1.0",
|
||||
"pbkdf2": "^3.0.9",
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
},
|
||||
"trulyOptionalDependencies": {
|
||||
"bluebird": "^3.5.0"
|
||||
},
|
||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "(MIT OR Apache-2.0)"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue