Server uses TLS, sign all requests to cloudapi, and enable SSO.
This commit is contained in:
parent
9abb845eec
commit
6aea7758ee
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
cfg/key.pem
|
||||||
|
cfg/cert.pem
|
||||||
|
45
README.md
45
README.md
@ -0,0 +1,45 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Generate keys
|
||||||
|
|
||||||
|
From within the config/ directory:
|
||||||
|
|
||||||
|
openssl genrsa -out key.pem
|
||||||
|
openssl req -new -key key.pem -out csr.pem
|
||||||
|
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
|
||||||
|
rm csr.pem
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Ensure the config file in config/ matches your details.
|
||||||
|
|
||||||
|
# Running the server
|
||||||
|
|
||||||
|
node bin/server.js config/prod.json
|
||||||
|
|
||||||
|
# Endpoints
|
||||||
|
|
||||||
|
## GET /static/*
|
||||||
|
|
||||||
|
This is where all the front-end code goes. All files will be served as-is as
|
||||||
|
found in that directory. The default is static/index.html. There is no
|
||||||
|
authentication; all files are public.
|
||||||
|
|
||||||
|
## GET /login
|
||||||
|
|
||||||
|
Call this endpoint to begin the login cycle. It will redirect you to the SSO
|
||||||
|
login page.
|
||||||
|
|
||||||
|
## GET /token
|
||||||
|
|
||||||
|
Upon successful login, the SSO login page will redirect to this endpoint. This
|
||||||
|
endpoint will return a 204, along with a X-Auth-Token header that must be saved
|
||||||
|
by the front-end code. All subsequent calls should provide this X-Auth-Token
|
||||||
|
header.
|
||||||
|
|
||||||
|
## Other
|
||||||
|
|
||||||
|
All other calls will be passed through to cloudapi. For these calls to succeed,
|
||||||
|
they MUST provide the X-Auth-Token header that the /token endpoint returns.
|
151
bin/server.js
151
bin/server.js
@ -1,25 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
// Copyright 2021 Spearhead Systems S.R.L.
|
||||||
|
|
||||||
const mod_restify = require('restify');
|
const mod_restify = require('restify');
|
||||||
const mod_cueball = require('cueball');
|
const mod_cueball = require('cueball');
|
||||||
|
const mod_crypto = require('crypto');
|
||||||
|
const mod_fs = require('fs');
|
||||||
|
const mod_sdcauth = require('smartdc-auth');
|
||||||
|
|
||||||
|
|
||||||
const cloudapi_client = mod_restify.createStringClient({
|
// Globals that are assigned to by main(). They are used by proxy() and login().
|
||||||
url: 'https://eu-ro-1.api.spearhead.cloud',
|
let CONFIG = {};
|
||||||
agent: new mod_cueball.HttpsAgent({
|
let PRIVATE_KEY = '';
|
||||||
spares: 4, maximum: 10,
|
let CLOUDAPI = {};
|
||||||
recovery: {
|
let SIGNER = {};
|
||||||
default: {
|
|
||||||
timeout: 2000,
|
|
||||||
retries: 5,
|
|
||||||
delay: 250,
|
|
||||||
maxDelay: 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Take any HTTP request that has a token, sign that request with an
|
||||||
|
// HTTP-Signature header, and pass it along to cloudapi. Return any response
|
||||||
|
// from cloudapi to our client caller. Effectively this function is a proxy
|
||||||
|
// that solely signs the request as it passes through.
|
||||||
function proxy(req, res, cb) {
|
function proxy(req, res, cb) {
|
||||||
|
// return data from cloudapi to the client caller
|
||||||
function proxyReturn(err, _, res2, data) {
|
function proxyReturn(err, _, res2, data) {
|
||||||
if (err && !res2.statusCode) {
|
if (err && !res2.statusCode) {
|
||||||
res.send(500);
|
res.send(500);
|
||||||
@ -34,38 +35,137 @@ function proxy(req, res, cb) {
|
|||||||
return cb();
|
return cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check the X-Auth-Token is present
|
||||||
|
if (req.header('X-Auth-Token') == undefined) {
|
||||||
|
res.send({"Error": "X-Auth-Token header missing"});
|
||||||
|
res.send(401);
|
||||||
|
return cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign the request before forwarding to cloudapi
|
||||||
|
let headers = req.headers;
|
||||||
|
var rs = mod_sdcauth.requestSigner({ sign: SIGNER });
|
||||||
|
headers.date = rs.writeDateHeader();
|
||||||
|
rs.writeTarget(req.method, req.url);
|
||||||
|
|
||||||
|
rs.sign(function signedCb(err, authz) {
|
||||||
|
if (err) {
|
||||||
|
return (cb(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.authorization = authz;
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
path: req.url,
|
path: req.url,
|
||||||
headers: req.headers
|
headers: headers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// make the call to cloudapi
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case 'GET': cloudapi_client.get(opts, proxyReturn); break;
|
case 'GET': CLOUDAPI.get(opts, proxyReturn); break;
|
||||||
case 'DEL': cloudapi_client.del(opts, proxyReturn); break;
|
case 'DEL': CLOUDAPI.del(opts, proxyReturn); break;
|
||||||
case 'HEAD': cloudapi_client.head(opts, proxyReturn); break;
|
case 'HEAD': CLOUDAPI.head(opts, proxyReturn); break;
|
||||||
case 'POST': cloudapi_client.post(opts, req.body, proxyReturn); break;
|
case 'POST': CLOUDAPI.post(opts, req.body, proxyReturn); break;
|
||||||
case 'PUT': cloudapi_client.del(opts, req.body, proxyReturn); break;
|
case 'PUT': CLOUDAPI.del(opts, req.body, proxyReturn); break;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Redirect to SSO with (signed) details that the SSO will need to generate a
|
||||||
|
// secure token. Once the user successfully logs in, the token is returned
|
||||||
|
// through an SSO redirect to token() below.
|
||||||
function login(req, res, cb) {
|
function login(req, res, cb) {
|
||||||
res.send('login');
|
const query = {
|
||||||
|
permissions: '{"cloudapi":["/my/*"]}',
|
||||||
|
returnto: CONFIG.urls.local + '/token',
|
||||||
|
now: new Date().toUTCString(),
|
||||||
|
keyid: '/' + CONFIG.key.user + '/keys/' + CONFIG.key.id,
|
||||||
|
nonce: mod_crypto.randomBytes(15).toString('base64')
|
||||||
|
};
|
||||||
|
|
||||||
|
// the query args MUST be sorted for SSO to validate
|
||||||
|
const querystr = Object.keys(query).sort().map(function encode(key) {
|
||||||
|
return key + '=' + encodeURIComponent(query[key]);
|
||||||
|
}).join('&');
|
||||||
|
|
||||||
|
let url = CONFIG.urls.sso + '/login?' + querystr;
|
||||||
|
|
||||||
|
const signer = mod_crypto.createSign('sha256');
|
||||||
|
signer.update(encodeURIComponent(url));
|
||||||
|
const signature = signer.sign(PRIVATE_KEY, 'base64');
|
||||||
|
url += '&sig=' + encodeURIComponent(signature);
|
||||||
|
|
||||||
|
res.redirect(url, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Once a user successfully logs in, they are redirected to here. We convert
|
||||||
|
// the token that was returned to use as query arg into an X-Auth-Token header
|
||||||
|
// that is returned to the client caller. This header must be provided by the
|
||||||
|
// client from now on in order to communicate with Cloudapi.
|
||||||
|
function token(req, res, cb) {
|
||||||
|
const token = decodeURIComponent(req.query().split('=')[1]);
|
||||||
|
res.header('X-Auth-Token', token);
|
||||||
|
res.send(204);
|
||||||
return cb();
|
return cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Start up HTTP server and pool of cloudapi clients.
|
||||||
|
//
|
||||||
|
// Read from config file, establish crypto singer needed for requests to
|
||||||
|
// cloudapi, prepare pool of HTTP clients for communication to cloudapi, and
|
||||||
|
// start up HTTP server.
|
||||||
function main() {
|
function main() {
|
||||||
const server = mod_restify.createServer();
|
// load config and private key
|
||||||
|
const configStr = mod_fs.readFileSync(process.argv[2]);
|
||||||
|
CONFIG = JSON.parse(configStr);
|
||||||
|
PRIVATE_KEY = mod_fs.readFileSync(CONFIG.key.path);
|
||||||
|
|
||||||
|
// signer is used for signing requests made to cloudapi with HTTP-Signature
|
||||||
|
SIGNER = mod_sdcauth.privateKeySigner({
|
||||||
|
key: PRIVATE_KEY,
|
||||||
|
keyId: CONFIG.key.id,
|
||||||
|
user: CONFIG.key.user
|
||||||
|
});
|
||||||
|
|
||||||
|
// enable pool of clients to cloudapi
|
||||||
|
CLOUDAPI = mod_restify.createStringClient({
|
||||||
|
url: CONFIG.urls.cloudapi,
|
||||||
|
agent: new mod_cueball.HttpsAgent({
|
||||||
|
spares: 4,
|
||||||
|
maximum: 10,
|
||||||
|
recovery: {
|
||||||
|
default: {
|
||||||
|
timeout: 2000,
|
||||||
|
retries: 5,
|
||||||
|
delay: 250,
|
||||||
|
maxDelay: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// prepare HTTP server
|
||||||
|
const options = {
|
||||||
|
key: mod_fs.readFileSync(CONFIG.server.key),
|
||||||
|
cert: mod_fs.readFileSync(CONFIG.server.cert)
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = mod_restify.createServer(options);
|
||||||
server.use(mod_restify.authorizationParser());
|
server.use(mod_restify.authorizationParser());
|
||||||
server.use(mod_restify.bodyParser());
|
server.use(mod_restify.bodyParser());
|
||||||
|
|
||||||
|
// where to server static content from
|
||||||
server.get(/^\/static.*/, mod_restify.plugins.serveStatic({
|
server.get(/^\/static.*/, mod_restify.plugins.serveStatic({
|
||||||
directory: 'static',
|
directory: 'static',
|
||||||
default: 'index.html'
|
default: 'index.html'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.post('/login', login);
|
// route HTTP requests to proper functions
|
||||||
|
server.get('/login', login);
|
||||||
|
server.get('/token', token);
|
||||||
|
|
||||||
server.get(/^/, proxy);
|
server.get(/^/, proxy);
|
||||||
server.put(/^/, proxy);
|
server.put(/^/, proxy);
|
||||||
@ -73,7 +173,8 @@ function main() {
|
|||||||
server.post(/^/, proxy);
|
server.post(/^/, proxy);
|
||||||
server.head(/^/, proxy);
|
server.head(/^/, proxy);
|
||||||
|
|
||||||
server.listen(8080, function () {
|
// enable HTTP server
|
||||||
|
server.listen(CONFIG.server.port, function listening() {
|
||||||
console.log('%s listening at %s', server.name, server.url);
|
console.log('%s listening at %s', server.name, server.url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
19
cfg/example.json
Normal file
19
cfg/example.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"//port": "if using a non-default port, make sure urls.local matches",
|
||||||
|
"port": 443,
|
||||||
|
"key": "cfg/key.pem",
|
||||||
|
"cert": "cfg/cert.pem"
|
||||||
|
},
|
||||||
|
"urls": {
|
||||||
|
"local": "https://localhost",
|
||||||
|
"sso": "https://sso.spearhead.cloud",
|
||||||
|
"cloudapi": "https://eu-ro-1.api.spearhead.cloud"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"user": "developer_name",
|
||||||
|
"//id": "replace id below with the signature of the path'd key",
|
||||||
|
"id": "0b:bf:00:e7:95:7b:e8:13:54:7f:37:00:04:07:64:04",
|
||||||
|
"path": "/path/to/developer_key"
|
||||||
|
}
|
||||||
|
}
|
136
package-lock.json
generated
136
package-lock.json
generated
@ -102,6 +102,11 @@
|
|||||||
"safe-json-stringify": "1.2.0"
|
"safe-json-stringify": "1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"clone": {
|
||||||
|
"version": "0.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-0.1.5.tgz",
|
||||||
|
"integrity": "sha1-RvKRQ9B2bWY9vX+At1IKFXg9IEI="
|
||||||
|
},
|
||||||
"cmdutil": {
|
"cmdutil": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cmdutil/-/cmdutil-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cmdutil/-/cmdutil-1.1.0.tgz",
|
||||||
@ -538,6 +543,27 @@
|
|||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
||||||
},
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "2.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||||
|
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||||
|
"requires": {
|
||||||
|
"core-util-is": "1.0.2",
|
||||||
|
"inherits": "2.0.4",
|
||||||
|
"isarray": "1.0.0",
|
||||||
|
"process-nextick-args": "2.0.1",
|
||||||
|
"safe-buffer": "5.1.2",
|
||||||
|
"string_decoder": "1.1.1",
|
||||||
|
"util-deprecate": "1.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"restify": {
|
"restify": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/restify/-/restify-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/restify/-/restify-4.3.4.tgz",
|
||||||
@ -748,6 +774,102 @@
|
|||||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||||
"integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo="
|
"integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo="
|
||||||
},
|
},
|
||||||
|
"smartdc-auth": {
|
||||||
|
"version": "2.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/smartdc-auth/-/smartdc-auth-2.5.7.tgz",
|
||||||
|
"integrity": "sha1-QtRXEOeR3rkt+RMmyO7RvVqELLY=",
|
||||||
|
"requires": {
|
||||||
|
"assert-plus": "1.0.0",
|
||||||
|
"bunyan": "1.8.12",
|
||||||
|
"clone": "0.1.5",
|
||||||
|
"dashdash": "1.10.1",
|
||||||
|
"http-signature": "1.3.5",
|
||||||
|
"once": "1.3.0",
|
||||||
|
"sshpk": "1.16.1",
|
||||||
|
"sshpk-agent": "1.8.1",
|
||||||
|
"vasync": "1.4.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bunyan": {
|
||||||
|
"version": "1.8.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
|
||||||
|
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
|
||||||
|
"requires": {
|
||||||
|
"dtrace-provider": "0.8.8",
|
||||||
|
"moment": "2.29.1",
|
||||||
|
"mv": "2.1.1",
|
||||||
|
"safe-json-stringify": "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dashdash": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.10.1.tgz",
|
||||||
|
"integrity": "sha1-Cr8a+JqPUSmoHxjCs1sh3yJiL2A=",
|
||||||
|
"requires": {
|
||||||
|
"assert-plus": "0.1.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"assert-plus": {
|
||||||
|
"version": "0.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
|
||||||
|
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extsprintf": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-TVi4Fazlvr/E6/A8+YsKdgSpm4Y="
|
||||||
|
},
|
||||||
|
"json-schema": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz",
|
||||||
|
"integrity": "sha1-UDVPGfYDkXxpX3C4Wvp3w7DyNQY="
|
||||||
|
},
|
||||||
|
"jsprim": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz",
|
||||||
|
"integrity": "sha1-zRNGbqJIDb2DlqVw1H0x3aR2+LE=",
|
||||||
|
"requires": {
|
||||||
|
"extsprintf": "1.0.0",
|
||||||
|
"json-schema": "0.2.2",
|
||||||
|
"verror": "1.3.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"verror": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz",
|
||||||
|
"integrity": "sha1-impKw6jHdLb2h/7OSb3/14VS4s0=",
|
||||||
|
"requires": {
|
||||||
|
"extsprintf": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"once": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.3.0.tgz",
|
||||||
|
"integrity": "sha1-FRr4a/wfCMS58H0GqyUP/L61ZYE="
|
||||||
|
},
|
||||||
|
"vasync": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.4.3.tgz",
|
||||||
|
"integrity": "sha1-yG1S4rcWE9Ke7fFZ8xNdvnSc7jc=",
|
||||||
|
"requires": {
|
||||||
|
"jsprim": "0.3.0",
|
||||||
|
"verror": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"verror": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-KktOsUogcFHnWm+U7lExW/FzobA=",
|
||||||
|
"requires": {
|
||||||
|
"extsprintf": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sshpk": {
|
"sshpk": {
|
||||||
"version": "1.16.1",
|
"version": "1.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
|
||||||
@ -764,6 +886,20 @@
|
|||||||
"tweetnacl": "0.14.5"
|
"tweetnacl": "0.14.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sshpk-agent": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sshpk-agent/-/sshpk-agent-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-YzAzemVrXEf1OeZUpveXLeYUT5VVw/I5gxLeyzq1aMS3pRvFvCeaGliNFjKR3VKtGXRqF9WamqKwYadIG6vStQ==",
|
||||||
|
"requires": {
|
||||||
|
"assert-plus": "1.0.0",
|
||||||
|
"dashdash": "1.14.1",
|
||||||
|
"getpass": "0.1.7",
|
||||||
|
"mooremachine": "2.3.0",
|
||||||
|
"readable-stream": "2.3.7",
|
||||||
|
"sshpk": "1.16.1",
|
||||||
|
"verror": "1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"cueball": "2.10.1",
|
"cueball": "2.10.1",
|
||||||
"http-signature": "1.3.5",
|
"http-signature": "1.3.5",
|
||||||
"restify": "4.3.4",
|
"restify": "4.3.4",
|
||||||
"vasync": "2.2.0"
|
"vasync": "2.2.0",
|
||||||
|
"smartdc-auth": "2.5.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user