Merge branch 'issuer-rewrite'
This commit is contained in:
		
						commit
						f4b172af01
					
				@ -149,9 +149,10 @@ module.exports.create = function () {
 | 
				
			|||||||
  process.on('unhandledRejection', function (err) {
 | 
					  process.on('unhandledRejection', function (err) {
 | 
				
			||||||
    // this should always throw
 | 
					    // this should always throw
 | 
				
			||||||
    // (it means somewhere we're not using bluebird by accident)
 | 
					    // (it means somewhere we're not using bluebird by accident)
 | 
				
			||||||
    console.error('[caught] [unhandledRejection]');
 | 
					    console.error('[caught unhandledRejection]:', err.message || '');
 | 
				
			||||||
    console.error(Object.keys(err));
 | 
					    Object.keys(err).forEach(function (key) {
 | 
				
			||||||
    console.error(err);
 | 
					      console.log('\t'+key+': '+err[key]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    console.error(err.stack);
 | 
					    console.error(err.stack);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  process.on('rejectionHandled', function (msg) {
 | 
					  process.on('rejectionHandled', function (msg) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										495
									
								
								lib/apis.js
									
									
									
									
									
								
							
							
						
						
									
										495
									
								
								lib/apis.js
									
									
									
									
									
								
							@ -8,7 +8,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
  var express = require('express-lazy');
 | 
					  var express = require('express-lazy');
 | 
				
			||||||
  var fs = PromiseA.promisifyAll(require('fs'));
 | 
					  var fs = PromiseA.promisifyAll(require('fs'));
 | 
				
			||||||
  var path = require('path');
 | 
					  var path = require('path');
 | 
				
			||||||
  var localCache = { rests: {}, pkgs: {} };
 | 
					  var localCache = { rests: {}, pkgs: {}, assets: {} };
 | 
				
			||||||
  var promisableRequest = require('./common').promisableRequest;
 | 
					  var promisableRequest = require('./common').promisableRequest;
 | 
				
			||||||
  var rejectableRequest = require('./common').rejectableRequest;
 | 
					  var rejectableRequest = require('./common').rejectableRequest;
 | 
				
			||||||
  var crypto = require('crypto');
 | 
					  var crypto = require('crypto');
 | 
				
			||||||
@ -32,7 +32,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  */
 | 
					  */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function isThisClientAllowedToUseThisPkg(myConf, clientUrih, pkgId) {
 | 
					  function isThisClientAllowedToUseThisPkg(req, myConf, clientUrih, pkgId) {
 | 
				
			||||||
    var appApiGrantsPath = path.join(myConf.appApiGrantsPath, clientUrih);
 | 
					    var appApiGrantsPath = path.join(myConf.appApiGrantsPath, clientUrih);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return fs.readFileAsync(appApiGrantsPath, 'utf8').then(function (text) {
 | 
					    return fs.readFileAsync(appApiGrantsPath, 'utf8').then(function (text) {
 | 
				
			||||||
@ -51,12 +51,23 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (clientUrih === ('api.' + xconfx.setupDomain) && 'org.oauth3.consumer' === pkgId) {
 | 
					      console.log('#################################################');
 | 
				
			||||||
 | 
					      console.log('assets.' + xconfx.setupDomain);
 | 
				
			||||||
 | 
					      console.log('assets.' + clientUrih);
 | 
				
			||||||
 | 
					      console.log(req.clientAssetsUri);
 | 
				
			||||||
 | 
					      console.log(pkgId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (req.clientAssetsUri === ('assets.' + clientUrih) && -1 !== [ 'session', 'session@oauth3.org', 'azp@oauth3.org', 'issuer@oauth3.org' ].indexOf(pkgId)) {
 | 
				
			||||||
        // fallthrough
 | 
					        // fallthrough
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        return null;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (clientUrih === ('api.' + xconfx.setupDomain) && -1 !== ['org.oauth3.consumer', 'azp@oauth3.org', 'oauth3.org'].indexOf(pkgId)) {
 | 
				
			||||||
 | 
					        // fallthrough
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,7 +161,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  function accountRequired(req, res, next) {
 | 
					  function accountRequired(req, res, next) {
 | 
				
			||||||
    // if this already has auth, great
 | 
					    // if this already has auth, great
 | 
				
			||||||
    if (req.oauth3.ppid) {
 | 
					    if (req.oauth3.ppid && req.oauth3.accountIdx) {
 | 
				
			||||||
      next();
 | 
					      next();
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -211,57 +222,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
    rejectableRequest(req, res, promise, "[walnut@daplie.com] required account (not /public)");
 | 
					    rejectableRequest(req, res, promise, "[walnut@daplie.com] required account (not /public)");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function loadRestHelper(myConf, clientUrih, pkgId) {
 | 
					  function grantsRequired(grants) {
 | 
				
			||||||
    var pkgPath = path.join(myConf.restPath, pkgId);
 | 
					 | 
				
			||||||
    var pkgLinks = [];
 | 
					 | 
				
			||||||
    pkgLinks.push(pkgId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // TODO allow recursion, but catch cycles
 | 
					 | 
				
			||||||
    return fs.lstatAsync(pkgPath).then(function (stat) {
 | 
					 | 
				
			||||||
      if (!stat.isFile()) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return fs.readFileAsync(pkgPath, 'utf8').then(function (text) {
 | 
					 | 
				
			||||||
        pkgId = text.trim();
 | 
					 | 
				
			||||||
        pkgPath = path.join(myConf.restPath, pkgId);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }, function () {
 | 
					 | 
				
			||||||
      // ignore error
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }).then(function () {
 | 
					 | 
				
			||||||
      // TODO should not require package.json. Should work with files alone.
 | 
					 | 
				
			||||||
      return fs.readFileAsync(path.join(pkgPath, 'package.json'), 'utf8').then(function (text) {
 | 
					 | 
				
			||||||
        var pkg = JSON.parse(text);
 | 
					 | 
				
			||||||
        var pkgDeps = {};
 | 
					 | 
				
			||||||
        var myApp;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (pkg.walnut) {
 | 
					 | 
				
			||||||
          pkgPath = path.join(pkgPath, pkg.walnut);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Object.keys(apiDeps).forEach(function (key) {
 | 
					 | 
				
			||||||
          pkgDeps[key] = apiDeps[key];
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        Object.keys(apiFactories).forEach(function (key) {
 | 
					 | 
				
			||||||
          pkgDeps[key] = apiFactories[key];
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // TODO pull db stuff from package.json somehow and pass allowed data models as deps
 | 
					 | 
				
			||||||
        //
 | 
					 | 
				
			||||||
        // how can we tell which of these would be correct?
 | 
					 | 
				
			||||||
        // deps.memstore = apiFactories.memstoreFactory.create(pkgId);
 | 
					 | 
				
			||||||
        // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
 | 
					 | 
				
			||||||
        // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + pkgId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // let's go with this one for now and the api can choose to scope or not to scope
 | 
					 | 
				
			||||||
        pkgDeps.memstore = apiFactories.memstoreFactory.create(pkgId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        console.log('DEBUG pkgPath', pkgPath);
 | 
					 | 
				
			||||||
        myApp = express();
 | 
					 | 
				
			||||||
        myApp.handlePromise = promisableRequest;
 | 
					 | 
				
			||||||
        myApp.handleRejection = rejectableRequest;
 | 
					 | 
				
			||||||
        myApp.grantsRequired = function (grants) {
 | 
					 | 
				
			||||||
    if (!Array.isArray(grants)) {
 | 
					    if (!Array.isArray(grants)) {
 | 
				
			||||||
      throw new Error("Usage: app.grantsRequired([ 'name|altname|altname2', 'othergrant' ])");
 | 
					      throw new Error("Usage: app.grantsRequired([ 'name|altname|altname2', 'othergrant' ])");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -280,40 +241,91 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
        res.send({ error: { message: "You must be logged in", code: "E_NO_AUTHN" } });
 | 
					        res.send({ error: { message: "You must be logged in", code: "E_NO_AUTHN" } });
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            if ('string' !== typeof req.oauth3.token.scp) {
 | 
					      var scope = req.oauth3.token.scope || req.oauth3.token.scp || req.oauth3.token.grants;
 | 
				
			||||||
              res.send({ error: { message: "Token must contain a grants string in 'scp'", code: "E_NO_GRANTS" } });
 | 
					      if ('string' !== typeof scope) {
 | 
				
			||||||
 | 
					        res.send({ error: { message: "Token must contain a grants string in 'scope'", code: "E_NO_GRANTS" } });
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            tokenScopes = req.oauth3.token.scp.split(/[,\s]+/mg);
 | 
					      tokenScopes = scope.split(/[,\s]+/mg);
 | 
				
			||||||
      if (-1 !== tokenScopes.indexOf('*')) {
 | 
					      if (-1 !== tokenScopes.indexOf('*')) {
 | 
				
			||||||
        // has full account access
 | 
					        // has full account access
 | 
				
			||||||
        next();
 | 
					        next();
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // every grant in the array must be present
 | 
					      // every grant in the array must be present, though some grants can be satisfied
 | 
				
			||||||
            if (!grants.every(function (grant) {
 | 
					      // by multiple scopes.
 | 
				
			||||||
              var scopes = grant.split(/\|/g);
 | 
					      var missing = grants.filter(function (grant) {
 | 
				
			||||||
              return scopes.some(function (scp) {
 | 
					        return !grant.split('|').some(function (scp) {
 | 
				
			||||||
                return tokenScopes.some(function (s) {
 | 
					          return tokenScopes.indexOf(scp) !== -1;
 | 
				
			||||||
                  return scp === s;
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
            })) {
 | 
					      if (missing.length) {
 | 
				
			||||||
              res.send({ error: { message: "Token does not contain valid grants: '" + grants + "'", code: "E_NO_GRANTS" } });
 | 
					        res.send({ error: { message: "Token missing required grants: '" + missing.join(',') + "'", code: "E_NO_GRANTS" } });
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      next();
 | 
					      next();
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
        };
 | 
					  }
 | 
				
			||||||
 | 
					  function loadRestHelperApi(myConf, clientUrih, pkg, pkgId, pkgPath) {
 | 
				
			||||||
 | 
					    var pkgLinks = [];
 | 
				
			||||||
 | 
					    pkgLinks.push(pkgId);
 | 
				
			||||||
 | 
					    var pkgRestApi;
 | 
				
			||||||
 | 
					    var pkgDeps = {};
 | 
				
			||||||
 | 
					    var myApp;
 | 
				
			||||||
 | 
					    var pkgPathApi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var _getOauth3Controllers = pkgDeps.getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(
 | 
					    pkgPathApi = pkgPath;
 | 
				
			||||||
          { sqlite3Sock: xconfx.sqlite3Sock, ipcKey: xconfx.ipcKey }
 | 
					    if (pkg.walnut) {
 | 
				
			||||||
        ).getControllers;
 | 
					      pkgPathApi = path.join(pkgPath, pkg.walnut);
 | 
				
			||||||
        //require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
 | 
					    }
 | 
				
			||||||
        require('oauthcommon').inject(_getOauth3Controllers, myApp/*, pkgConf, pkgDeps*/);
 | 
					    pkgRestApi = require(pkgPathApi);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.keys(apiDeps).forEach(function (key) {
 | 
				
			||||||
 | 
					      pkgDeps[key] = apiDeps[key];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    Object.keys(apiFactories).forEach(function (key) {
 | 
				
			||||||
 | 
					      pkgDeps[key] = apiFactories[key];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO pull db stuff from package.json somehow and pass allowed data models as deps
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // how can we tell which of these would be correct?
 | 
				
			||||||
 | 
					    // deps.memstore = apiFactories.memstoreFactory.create(pkgId);
 | 
				
			||||||
 | 
					    // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
 | 
				
			||||||
 | 
					    // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + pkgId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // let's go with this one for now and the api can choose to scope or not to scope
 | 
				
			||||||
 | 
					    pkgDeps.memstore = apiFactories.memstoreFactory.create(pkgId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    myApp = express();
 | 
				
			||||||
 | 
					    myApp.handlePromise = promisableRequest;
 | 
				
			||||||
 | 
					    myApp.handleRejection = rejectableRequest;
 | 
				
			||||||
 | 
					    myApp.grantsRequired = grantsRequired;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function getSitePackageStoreProp(otherPkgId) {
 | 
				
			||||||
 | 
					      var restPath = path.join(myConf.restPath, otherPkgId);
 | 
				
			||||||
 | 
					      var apiPath = path.join(myConf.apiPath, otherPkgId);
 | 
				
			||||||
 | 
					      var dir;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // TODO usage package.json as a falback if the standard location is not used
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        dir = require(path.join(apiPath, 'models.js'));
 | 
				
			||||||
 | 
					      } catch(e) {
 | 
				
			||||||
 | 
					        dir = require(path.join(restPath, 'models.js'));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return getSiteStore(clientUrih, otherPkgId, dir);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function attachOauth3(req, res, next) {
 | 
				
			||||||
 | 
					      return getSitePackageStoreProp('issuer@oauth3.org').then(function (Models) {
 | 
				
			||||||
 | 
					        return require('./oauth3').attachOauth3(Models, req, res, next);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    myApp.use('/', attachOauth3);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO delete these caches when config changes
 | 
					    // TODO delete these caches when config changes
 | 
				
			||||||
    var _stripe;
 | 
					    var _stripe;
 | 
				
			||||||
@ -323,8 +335,12 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
    var _twilio;
 | 
					    var _twilio;
 | 
				
			||||||
    var _get_response;
 | 
					    var _get_response;
 | 
				
			||||||
    myApp.use('/', function preHandler(req, res, next) {
 | 
					    myApp.use('/', function preHandler(req, res, next) {
 | 
				
			||||||
 | 
					      //if (xconfx.debug) { console.log('[api.js] loading handler prereqs'); }
 | 
				
			||||||
      return getSiteConfig(clientUrih).then(function (siteConfig) {
 | 
					      return getSiteConfig(clientUrih).then(function (siteConfig) {
 | 
				
			||||||
            Object.defineProperty(req, 'getSiteMailer', {
 | 
					        //if (xconfx.debug) { console.log('[api.js] loaded handler site config'); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Use getSiteCapability('email@daplie.com') instead
 | 
				
			||||||
 | 
					        Object.defineProperty(req, 'getSiteMailer' /*deprecated*/, {
 | 
				
			||||||
          enumerable: true
 | 
					          enumerable: true
 | 
				
			||||||
        , configurable: false
 | 
					        , configurable: false
 | 
				
			||||||
        , writable: false
 | 
					        , writable: false
 | 
				
			||||||
@ -361,6 +377,13 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object.defineProperty(req, 'getSitePackageStore', {
 | 
				
			||||||
 | 
					          enumerable: true
 | 
				
			||||||
 | 
					        , configurable: false
 | 
				
			||||||
 | 
					        , writable: false
 | 
				
			||||||
 | 
					        , value: getSitePackageStoreProp
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Object.defineProperty(req, 'getSiteStore', {
 | 
					        Object.defineProperty(req, 'getSiteStore', {
 | 
				
			||||||
          enumerable: true
 | 
					          enumerable: true
 | 
				
			||||||
        , configurable: false
 | 
					        , configurable: false
 | 
				
			||||||
@ -502,7 +525,6 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
          // (realized later)
 | 
					          // (realized later)
 | 
				
			||||||
          // HAHA HAHA HAHAHAHAHA this is my own gist... so much more polite attribution
 | 
					          // HAHA HAHA HAHAHAHAHA this is my own gist... so much more polite attribution
 | 
				
			||||||
          var scmp = require('scmp')
 | 
					          var scmp = require('scmp')
 | 
				
			||||||
                , crypto = require('crypto')
 | 
					 | 
				
			||||||
            , mailgunExpirey = 15 * 60 * 1000
 | 
					            , mailgunExpirey = 15 * 60 * 1000
 | 
				
			||||||
            , mailgunHashType = 'sha256'
 | 
					            , mailgunHashType = 'sha256'
 | 
				
			||||||
            , mailgunSignatureEncoding = 'hex'
 | 
					            , mailgunSignatureEncoding = 'hex'
 | 
				
			||||||
@ -599,11 +621,64 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var settingsPromise = PromiseA.resolve();
 | 
				
			||||||
 | 
					        function manageSiteSettings(section) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          var submanager;
 | 
				
			||||||
 | 
					          var manager = {
 | 
				
			||||||
 | 
					            set: function (section, value) {
 | 
				
			||||||
 | 
					              if ('email@daplie.com' === section) {
 | 
				
			||||||
 | 
					                section = 'mailgun.org';
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              settingsPromise = settingsPromise.then(function () {
 | 
				
			||||||
 | 
					                return manager.get().then(function () {
 | 
				
			||||||
 | 
					                  siteConfig[section] = value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
 | 
				
			||||||
 | 
					                  return mkdirpAsync(siteConfigPath).then(function () {
 | 
				
			||||||
 | 
					                    return fs.writeFileAsync(path.join(siteConfigPath, 'config.json'), JSON.stringify(siteConfig), 'utf8');
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					              return settingsPromise;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          , get: function (section) {
 | 
				
			||||||
 | 
					              if ('email@daplie.com' === section) {
 | 
				
			||||||
 | 
					                section = 'mailgun.org';
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              settingsPromise = settingsPromise.then(function () {
 | 
				
			||||||
 | 
					                return getSiteConfig(clientUrih).then(function (_siteConfig) {
 | 
				
			||||||
 | 
					                  siteConfig = _siteConfig;
 | 
				
			||||||
 | 
					                  return PromiseA.resolve((_siteConfig || {})[section]);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					              return settingsPromise;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          submanager = manager;
 | 
				
			||||||
 | 
					          if (section) {
 | 
				
			||||||
 | 
					            submanager = {
 | 
				
			||||||
 | 
					              set: function (value) {
 | 
				
			||||||
 | 
					                return manager.set(section, value);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            , get: function () {
 | 
				
			||||||
 | 
					                return manager.get(section);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return apiDeps.Promise.resolve(submanager);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var caps = {
 | 
					        var caps = {
 | 
				
			||||||
          //
 | 
					          //
 | 
				
			||||||
          // Capabilities for APIs
 | 
					          // Capabilities for APIs
 | 
				
			||||||
          //
 | 
					          //
 | 
				
			||||||
              'email@daplie.com': mailgunMail     // whichever mailer
 | 
					          'settings.site@daplie.com': manageSiteSettings
 | 
				
			||||||
 | 
					        , 'email@daplie.com': mailgunMail     // whichever mailer
 | 
				
			||||||
        , 'mailer@daplie.com': mailgunMail    // whichever mailer
 | 
					        , 'mailer@daplie.com': mailgunMail    // whichever mailer
 | 
				
			||||||
        , 'mailgun@daplie.com': mailgunMail   // specifically mailgun
 | 
					        , 'mailgun@daplie.com': mailgunMail   // specifically mailgun
 | 
				
			||||||
        , 'tel@daplie.com': daplieTel         // whichever telephony service
 | 
					        , 'tel@daplie.com': daplieTel         // whichever telephony service
 | 
				
			||||||
@ -722,21 +797,218 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
    //
 | 
					    //
 | 
				
			||||||
    // TODO handle /accounts/:accountId
 | 
					    // TODO handle /accounts/:accountId
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
        return PromiseA.resolve(require(pkgPath).create({
 | 
					    return PromiseA.resolve(pkgRestApi.create({
 | 
				
			||||||
      etcpath: xconfx.etcpath
 | 
					      etcpath: xconfx.etcpath
 | 
				
			||||||
    }/*pkgConf*/, pkgDeps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
 | 
					    }/*pkgConf*/, pkgDeps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      //if (xconfx.debug) { console.log('[api.js] got handler'); }
 | 
				
			||||||
      myApp.use('/', function postHandler(req, res, next) {
 | 
					      myApp.use('/', function postHandler(req, res, next) {
 | 
				
			||||||
        req.url = req._walnutOriginalUrl;
 | 
					        req.url = req._walnutOriginalUrl;
 | 
				
			||||||
        next();
 | 
					        next();
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      localCache.pkgs[pkgId] = { pkgId: pkgId, pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
 | 
					      localCache.pkgs[pkgId] = { pkgId: pkgId, pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      pkgLinks.forEach(function (pkgLink) {
 | 
					      pkgLinks.forEach(function (pkgLink) {
 | 
				
			||||||
        localCache.pkgs[pkgLink] = localCache.pkgs[pkgId];
 | 
					        localCache.pkgs[pkgLink] = localCache.pkgs[pkgId];
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return localCache.pkgs[pkgId];
 | 
					      return localCache.pkgs[pkgId];
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  function loadRestHelperAssets(myConf, clientUrih, pkg, pkgId, pkgPath) {
 | 
				
			||||||
 | 
					    var myApp;
 | 
				
			||||||
 | 
					    var pkgDeps = {};
 | 
				
			||||||
 | 
					    var pkgRestAssets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      pkgRestAssets = require(path.join(pkgPath, 'assets.js'));
 | 
				
			||||||
 | 
					    } catch(e) {
 | 
				
			||||||
 | 
					      return PromiseA.reject(e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.keys(apiDeps).forEach(function (key) {
 | 
				
			||||||
 | 
					      pkgDeps[key] = apiDeps[key];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    Object.keys(apiFactories).forEach(function (key) {
 | 
				
			||||||
 | 
					      pkgDeps[key] = apiFactories[key];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO pull db stuff from package.json somehow and pass allowed data models as deps
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // how can we tell which of these would be correct?
 | 
				
			||||||
 | 
					    // deps.memstore = apiFactories.memstoreFactory.create(pkgId);
 | 
				
			||||||
 | 
					    // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
 | 
				
			||||||
 | 
					    // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + pkgId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // let's go with this one for now and the api can choose to scope or not to scope
 | 
				
			||||||
 | 
					    pkgDeps.memstore = apiFactories.memstoreFactory.create(pkgId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    myApp = express();
 | 
				
			||||||
 | 
					    myApp.handlePromise = promisableRequest;
 | 
				
			||||||
 | 
					    myApp.handleRejection = rejectableRequest;
 | 
				
			||||||
 | 
					    myApp.grantsRequired = grantsRequired;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function otherGetSitePackageStoreProp(otherPkgId) {
 | 
				
			||||||
 | 
					      var restPath = path.join(myConf.restPath, otherPkgId);
 | 
				
			||||||
 | 
					      var apiPath = path.join(myConf.apiPath, otherPkgId);
 | 
				
			||||||
 | 
					      var dir;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // TODO usage package.json as a falback if the standard location is not used
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        dir = require(path.join(apiPath, 'models.js'));
 | 
				
			||||||
 | 
					      } catch(e) {
 | 
				
			||||||
 | 
					        dir = require(path.join(restPath, 'models.js'));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return getSiteStore(clientUrih, otherPkgId, dir);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    myApp.use('/', function cookieAttachOauth3(req, res, next) {
 | 
				
			||||||
 | 
					      return otherGetSitePackageStoreProp('issuer@oauth3.org').then(function (Models) {
 | 
				
			||||||
 | 
					        return require('./oauth3').cookieOauth3(Models, req, res, next);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    myApp.use('/', function (req, res, next) {
 | 
				
			||||||
 | 
					      console.log('########################################### session ###############################');
 | 
				
			||||||
 | 
					      console.log('req.url', req.url);
 | 
				
			||||||
 | 
					      console.log('req.oauth3', req.oauth3);
 | 
				
			||||||
 | 
					      next();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    function otherAttachOauth3(req, res, next) {
 | 
				
			||||||
 | 
					      return otherGetSitePackageStoreProp('issuer@oauth3.org').then(function (Models) {
 | 
				
			||||||
 | 
					        return require('./oauth3').attachOauth3(Models, req, res, next);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    myApp.post('/assets/issuer@oauth3.org/session', otherAttachOauth3, function (req, res) {
 | 
				
			||||||
 | 
					      console.log('get the session');
 | 
				
			||||||
 | 
					      console.log(req.url);
 | 
				
			||||||
 | 
					      console.log("req.cookies:");
 | 
				
			||||||
 | 
					      console.log(req.cookies);
 | 
				
			||||||
 | 
					      console.log("req.oauth3:");
 | 
				
			||||||
 | 
					      console.log(req.oauth3);
 | 
				
			||||||
 | 
					      res.cookie('jwt', req.oauth3.encodedToken, { domain: req.clientAssetsUri, path: '/assets', httpOnly: true });
 | 
				
			||||||
 | 
					      //req.url;
 | 
				
			||||||
 | 
					      res.send({ success: true });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO delete these caches when config changes
 | 
				
			||||||
 | 
					    myApp.use('/', function preHandler(req, res, next) {
 | 
				
			||||||
 | 
					      //if (xconfx.debug) { console.log('[api.js] loading handler prereqs'); }
 | 
				
			||||||
 | 
					      return getSiteConfig(clientUrih).then(function (siteConfig) {
 | 
				
			||||||
 | 
					        //if (xconfx.debug) { console.log('[api.js] loaded handler site config'); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object.defineProperty(req, 'getSiteConfig', {
 | 
				
			||||||
 | 
					          enumerable: true
 | 
				
			||||||
 | 
					        , configurable: false
 | 
				
			||||||
 | 
					        , writable: false
 | 
				
			||||||
 | 
					        , value: function getSiteConfigProp(section) {
 | 
				
			||||||
 | 
					            return PromiseA.resolve((siteConfig || {})[section]);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object.defineProperty(req, 'getSitePackageConfig', {
 | 
				
			||||||
 | 
					          enumerable: true
 | 
				
			||||||
 | 
					        , configurable: false
 | 
				
			||||||
 | 
					        , writable: false
 | 
				
			||||||
 | 
					        , value: function getSitePackageConfigProp() {
 | 
				
			||||||
 | 
					            return getSitePackageConfig(clientUrih, pkgId);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object.defineProperty(req, 'getSiteStore', {
 | 
				
			||||||
 | 
					          enumerable: true
 | 
				
			||||||
 | 
					        , configurable: false
 | 
				
			||||||
 | 
					        , writable: false
 | 
				
			||||||
 | 
					        , value: function getSiteStoreProp() {
 | 
				
			||||||
 | 
					            var restPath = path.join(myConf.restPath, pkgId);
 | 
				
			||||||
 | 
					            var apiPath = path.join(myConf.apiPath, pkgId);
 | 
				
			||||||
 | 
					            var dir;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // TODO usage package.json as a falback if the standard location is not used
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              dir = require(path.join(apiPath, 'models.js'));
 | 
				
			||||||
 | 
					            } catch(e) {
 | 
				
			||||||
 | 
					              dir = require(path.join(restPath, 'models.js'));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return getSiteStore(clientUrih, pkgId, dir);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        req._walnutOriginalUrl = req.url;
 | 
				
			||||||
 | 
					        // "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello'
 | 
				
			||||||
 | 
					        req.url = req.url.replace(/\/(api|assets)\//, '').replace(/.*\/(api|assets)\//, '').replace(/([^\/]*\/+)/, '/');
 | 
				
			||||||
 | 
					        next();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    myApp.use('/public', function preHandler(req, res, next) {
 | 
				
			||||||
 | 
					      // TODO authenticate or use guest user
 | 
				
			||||||
 | 
					      req.isPublic = true;
 | 
				
			||||||
 | 
					      next();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    myApp.use('/accounts/:accountId', accountRequiredById);
 | 
				
			||||||
 | 
					    myApp.use('/acl', accountRequired);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // TODO handle /accounts/:accountId
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    function myAppWrapper(req, res, next) {
 | 
				
			||||||
 | 
					      return myApp(req, res, next);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Object.keys(myApp).forEach(function (key) {
 | 
				
			||||||
 | 
					      myAppWrapper[key] = myApp[key];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    myAppWrapper.use = function () { myApp.use.apply(myApp, arguments); };
 | 
				
			||||||
 | 
					    myAppWrapper.get = function () { myApp.get.apply(myApp, arguments); };
 | 
				
			||||||
 | 
					    myAppWrapper.post = function () { myApp.use(function (req, res, next) { next(); }); /*throw new Error("assets may not handle POST");*/ };
 | 
				
			||||||
 | 
					    myAppWrapper.put = function () { throw new Error("assets may not handle PUT"); };
 | 
				
			||||||
 | 
					    myAppWrapper.del = function () { throw new Error("assets may not handle DELETE"); };
 | 
				
			||||||
 | 
					    myAppWrapper.delete = function () { throw new Error("assets may not handle DELETE"); };
 | 
				
			||||||
 | 
					    return PromiseA.resolve(pkgRestAssets.create({
 | 
				
			||||||
 | 
					      etcpath: xconfx.etcpath
 | 
				
			||||||
 | 
					    }/*pkgConf*/, pkgDeps/*pkgDeps*/, myAppWrapper)).then(function (assetsHandler) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      //if (xconfx.debug) { console.log('[api.js] got handler'); }
 | 
				
			||||||
 | 
					      myApp.use('/', function postHandler(req, res, next) {
 | 
				
			||||||
 | 
					        req.url = req._walnutOriginalUrl;
 | 
				
			||||||
 | 
					        next();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return assetsHandler || myApp;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  function loadRestHelper(myConf, clientUrih, pkgId) {
 | 
				
			||||||
 | 
					    var pkgPath = path.join(myConf.restPath, pkgId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO allow recursion, but catch cycles
 | 
				
			||||||
 | 
					    return fs.lstatAsync(pkgPath).then(function (stat) {
 | 
				
			||||||
 | 
					      if (!stat.isFile()) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return fs.readFileAsync(pkgPath, 'utf8').then(function (text) {
 | 
				
			||||||
 | 
					        pkgId = text.trim();
 | 
				
			||||||
 | 
					        pkgPath = path.join(myConf.restPath, pkgId);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }, function () {
 | 
				
			||||||
 | 
					      // ignore error
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }).then(function () {
 | 
				
			||||||
 | 
					      // TODO should not require package.json. Should work with files alone.
 | 
				
			||||||
 | 
					      return fs.readFileAsync(path.join(pkgPath, 'package.json'), 'utf8').then(function (text) {
 | 
				
			||||||
 | 
					        var pkg = JSON.parse(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return loadRestHelperApi(myConf, clientUrih, pkg, pkgId, pkgPath).then(function (stuff) {
 | 
				
			||||||
 | 
					          return loadRestHelperAssets(myConf, clientUrih, pkg, pkgId, pkgPath).then(function (assetsHandler) {
 | 
				
			||||||
 | 
					            stuff.assetsHandler = assetsHandler;
 | 
				
			||||||
 | 
					            return stuff;
 | 
				
			||||||
 | 
					          }, function (err) {
 | 
				
			||||||
 | 
					            console.error('[lib/api.js] no assets handler:');
 | 
				
			||||||
 | 
					            console.error(err);
 | 
				
			||||||
 | 
					            return stuff;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -777,31 +1049,45 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return function (req, res, next) {
 | 
					  return function (req, res, next) {
 | 
				
			||||||
    cors(req, res, function () {
 | 
					    cors(req, res, function () {
 | 
				
			||||||
      if (xconfx.debug) { console.log('[api.js] post cors'); }
 | 
					      //if (xconfx.debug) { console.log('[api.js] after cors'); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Canonical client names
 | 
					      // Canonical client names
 | 
				
			||||||
      // example.com should use api.example.com/api for all requests
 | 
					      // example.com should use api.example.com/api for all requests
 | 
				
			||||||
      // sub.example.com/api should resolve to sub.example.com
 | 
					      // sub.example.com/api should resolve to sub.example.com
 | 
				
			||||||
      // example.com/subpath/api should resolve to example.com#subapp
 | 
					      // example.com/subapp/api should resolve to example.com#subapp
 | 
				
			||||||
      // sub.example.com/subpath/api should resolve to sub.example.com#subapp
 | 
					      // sub.example.com/subapp/api should resolve to sub.example.com#subapp
 | 
				
			||||||
      var clientUrih = req.hostname.replace(/^api\./, '') + req.url.replace(/\/api\/.*/, '/').replace(/\/+/g, '#').replace(/#$/, '');
 | 
					      var appUri = req.hostname.replace(/^(api|assets)\./, '') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
 | 
				
			||||||
      var clientApiUri = req.hostname + req.url.replace(/\/api\/.*/, '/').replace(/\/$/, '');
 | 
					      var clientUrih = appUri.replace(/\/+/g, '#').replace(/#$/, '');
 | 
				
			||||||
 | 
					      var clientApiUri = req.hostname.replace(/^(api|assets)\./, 'api.') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
 | 
				
			||||||
 | 
					      var clientAssetsUri = req.hostname.replace(/^(api|assets)\./, 'assets.') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
 | 
				
			||||||
 | 
					      //var clientAssetsUri = req.hostname.replace(/^(api|assets)\./, 'api.') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
 | 
				
			||||||
      // Canonical package names
 | 
					      // Canonical package names
 | 
				
			||||||
      // '/api/com.daplie.hello/hello' should resolve to 'com.daplie.hello'
 | 
					      // '/api/com.daplie.hello/hello' should resolve to 'com.daplie.hello'
 | 
				
			||||||
      // '/subapp/api/com.daplie.hello/hello' should also 'com.daplie.hello'
 | 
					      // '/subapp/api/com.daplie.hello/hello' should also 'com.daplie.hello'
 | 
				
			||||||
      // '/subapp/api/com.daplie.hello/' may exist... must be a small api
 | 
					      // '/subapp/api/com.daplie.hello/' may exist... must be a small api
 | 
				
			||||||
      var pkgId = req.url.replace(/.*\/api\//, '').replace(/^\//, '').replace(/\/.*/, '');
 | 
					      var pkgId = req.url.replace(/.*\/(api|assets)\//, '').replace(/^\//, '').replace(/\/.*/, '');
 | 
				
			||||||
      var now = Date.now();
 | 
					      var now = Date.now();
 | 
				
			||||||
      var hasBeenHandled = false;
 | 
					      var hasBeenHandled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Existing (Deprecated)
 | 
					      Object.defineProperty(req, 'clientUrl', {
 | 
				
			||||||
 | 
					        enumerable: true
 | 
				
			||||||
 | 
					      , configurable: false
 | 
				
			||||||
 | 
					      , writable: false
 | 
				
			||||||
 | 
					      , value: (req.headers.referer || ('https://' +  appUri)).replace(/\/$/, '').replace(/\?.*/, '')
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      Object.defineProperty(req, 'apiUrlPrefix', {
 | 
					      Object.defineProperty(req, 'apiUrlPrefix', {
 | 
				
			||||||
        enumerable: true
 | 
					        enumerable: true
 | 
				
			||||||
      , configurable: false
 | 
					      , configurable: false
 | 
				
			||||||
      , writable: false
 | 
					      , writable: false
 | 
				
			||||||
      , value: 'https://' + clientApiUri + '/api/' + pkgId
 | 
					      , value: 'https://' + clientApiUri + '/api/' + pkgId
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      Object.defineProperty(req, 'experienceId', {
 | 
					      Object.defineProperty(req, 'assetsUrlPrefix', {
 | 
				
			||||||
 | 
					        enumerable: true
 | 
				
			||||||
 | 
					      , configurable: false
 | 
				
			||||||
 | 
					      , writable: false
 | 
				
			||||||
 | 
					      , value: 'https://' + clientAssetsUri + '/assets/' + pkgId
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      Object.defineProperty(req, 'experienceId' /*deprecated*/, {
 | 
				
			||||||
        enumerable: true
 | 
					        enumerable: true
 | 
				
			||||||
      , configurable: false
 | 
					      , configurable: false
 | 
				
			||||||
      , writable: false
 | 
					      , writable: false
 | 
				
			||||||
@ -813,6 +1099,12 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
      , writable: false
 | 
					      , writable: false
 | 
				
			||||||
      , value: clientApiUri
 | 
					      , value: clientApiUri
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      Object.defineProperty(req, 'clientAssetsUri', {
 | 
				
			||||||
 | 
					        enumerable: true
 | 
				
			||||||
 | 
					      , configurable: false
 | 
				
			||||||
 | 
					      , writable: false
 | 
				
			||||||
 | 
					      , value: clientAssetsUri
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      Object.defineProperty(req, 'apiId', {
 | 
					      Object.defineProperty(req, 'apiId', {
 | 
				
			||||||
        enumerable: true
 | 
					        enumerable: true
 | 
				
			||||||
      , configurable: false
 | 
					      , configurable: false
 | 
				
			||||||
@ -820,7 +1112,6 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
      , value: pkgId
 | 
					      , value: pkgId
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // New
 | 
					 | 
				
			||||||
      Object.defineProperty(req, 'clientUrih', {
 | 
					      Object.defineProperty(req, 'clientUrih', {
 | 
				
			||||||
        enumerable: true
 | 
					        enumerable: true
 | 
				
			||||||
      , configurable: false
 | 
					      , configurable: false
 | 
				
			||||||
@ -838,37 +1129,61 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // TODO cache permission (although the FS is already cached, NBD)
 | 
					      // TODO cache permission (although the FS is already cached, NBD)
 | 
				
			||||||
      var promise = isThisClientAllowedToUseThisPkg(xconfx, clientUrih, pkgId).then(function (yes) {
 | 
					      var promise = isThisClientAllowedToUseThisPkg(req, xconfx, clientUrih, pkgId).then(function (yes) {
 | 
				
			||||||
 | 
					        //if (xconfx.debug) { console.log('[api.js] azp is allowed?', yes); }
 | 
				
			||||||
        if (!yes) {
 | 
					        if (!yes) {
 | 
				
			||||||
          notConfigured(req, res);
 | 
					          notConfigured(req, res);
 | 
				
			||||||
          return null;
 | 
					          return null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (localCache.rests[pkgId]) {
 | 
					        function handleWithHandler() {
 | 
				
			||||||
 | 
					          if (/\/assets\//.test(req.url) || /(^|\.)assets\./.test(req.hostname)) {
 | 
				
			||||||
 | 
					            if (localCache.assets[pkgId]) {
 | 
				
			||||||
 | 
					              if ('function' !== typeof localCache.assets[pkgId].handler) { console.log('localCache.assets[pkgId]'); console.log(localCache.assets[pkgId]); }
 | 
				
			||||||
 | 
					              localCache.assets[pkgId].handler(req, res, next);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              next();
 | 
				
			||||||
 | 
					              return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
            localCache.rests[pkgId].handler(req, res, next);
 | 
					            localCache.rests[pkgId].handler(req, res, next);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (localCache.rests[pkgId]) {
 | 
				
			||||||
 | 
					          if (handleWithHandler()) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          hasBeenHandled = true;
 | 
					          hasBeenHandled = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (now - localCache.rests[pkgId].createdAt > staleAfter) {
 | 
					          if (now - localCache.rests[pkgId].createdAt > staleAfter) {
 | 
				
			||||||
            localCache.rests[pkgId] = null;
 | 
					            localCache.rests[pkgId] = null;
 | 
				
			||||||
 | 
					            localCache.assets[pkgId] = null;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!localCache.rests[pkgId]) {
 | 
					        if (!localCache.rests[pkgId]) {
 | 
				
			||||||
          //return doesThisPkgExist
 | 
					          //return doesThisPkgExist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          //if (xconfx.debug) { console.log('[api.js] before rest handler'); }
 | 
				
			||||||
          return loadRestHandler(xconfx, clientUrih, pkgId).then(function (myHandler) {
 | 
					          return loadRestHandler(xconfx, clientUrih, pkgId).then(function (myHandler) {
 | 
				
			||||||
            if (!myHandler) {
 | 
					            if (!myHandler) {
 | 
				
			||||||
 | 
					              //if (xconfx.debug) { console.log('[api.js] not configured'); }
 | 
				
			||||||
              notConfigured(req, res);
 | 
					              notConfigured(req, res);
 | 
				
			||||||
              return;
 | 
					              return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now };
 | 
					            localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now };
 | 
				
			||||||
 | 
					            localCache.assets[pkgId] = { handler: myHandler.assetsHandler, createdAt: now };
 | 
				
			||||||
            if (!hasBeenHandled) {
 | 
					            if (!hasBeenHandled) {
 | 
				
			||||||
              myHandler.handler(req, res, next);
 | 
					              if (handleWithHandler()) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      rejectableRequest(req, res, promise, "[walnut@daplie.com] load api package");
 | 
					      rejectableRequest(req, res, promise, "[walnut@daplie.com] load api package");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,21 @@
 | 
				
			|||||||
'use strict';
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports.rejectableRequest = function rejectableRequest(req, res, promise, msg) {
 | 
					function rejectableRequest(req, res, promise, msg) {
 | 
				
			||||||
  return promise.error(function (err) {
 | 
					  return promise.error(function (err) {
 | 
				
			||||||
    res.error(err);
 | 
					    res.error(err);
 | 
				
			||||||
  }).catch(function (err) {
 | 
					  }).catch(function (err) {
 | 
				
			||||||
    console.error('[ERROR] \'' + msg + '\'');
 | 
					    console.error('[ERROR] \'' + msg + '\'');
 | 
				
			||||||
    console.error(err.message);
 | 
					    // The stack contains the message as well, so no need to log the message when we log the stack
 | 
				
			||||||
    console.error(err.stack);
 | 
					    console.error(err.stack || err.message || JSON.stringify(err));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.error(err);
 | 
					    res.error(err);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					}
 | 
				
			||||||
 | 
					module.exports.rejectableRequest = rejectableRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports.promisableRequest =
 | 
					module.exports.promisableRequest =
 | 
				
			||||||
module.exports.promiseRequest = function promiseRequest(req, res, promise, msg) {
 | 
					module.exports.promiseRequest = function promiseRequest(req, res, promise, msg) {
 | 
				
			||||||
  return promise.then(function (result) {
 | 
					  promise = promise.then(function (result) {
 | 
				
			||||||
    if (result._cache) {
 | 
					    if (result._cache) {
 | 
				
			||||||
      res.setHeader('Cache-Control', 'public, max-age=' + (result._cache / 1000));
 | 
					      res.setHeader('Cache-Control', 'public, max-age=' + (result._cache / 1000));
 | 
				
			||||||
      res.setHeader('Expires', new Date(Date.now() + result._cache).toUTCString());
 | 
					      res.setHeader('Expires', new Date(Date.now() + result._cache).toUTCString());
 | 
				
			||||||
@ -26,13 +27,7 @@ module.exports.promiseRequest = function promiseRequest(req, res, promise, msg)
 | 
				
			|||||||
      result = result._value;
 | 
					      result = result._value;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    res.send(result);
 | 
					    res.send(result);
 | 
				
			||||||
  }).error(function (err) {
 | 
					 | 
				
			||||||
    res.error(err);
 | 
					 | 
				
			||||||
  }).catch(function (err) {
 | 
					 | 
				
			||||||
    console.error('[ERROR] \'' + msg + '\'');
 | 
					 | 
				
			||||||
    console.error(err.message);
 | 
					 | 
				
			||||||
    console.error(err.stack);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    res.error(err);
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return rejectableRequest(req, res, promise, msg);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										26
									
								
								lib/main.js
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								lib/main.js
									
									
									
									
									
								
							@ -1,6 +1,6 @@
 | 
				
			|||||||
'use strict';
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi) {
 | 
					module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi, errorIfAssets) {
 | 
				
			||||||
  var PromiseA = require('bluebird');
 | 
					  var PromiseA = require('bluebird');
 | 
				
			||||||
  var path = require('path');
 | 
					  var path = require('path');
 | 
				
			||||||
  var fs = PromiseA.promisifyAll(require('fs'));
 | 
					  var fs = PromiseA.promisifyAll(require('fs'));
 | 
				
			||||||
@ -293,10 +293,27 @@ module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi
 | 
				
			|||||||
  // TODO handle assets.example.com/sub/assets/com.example.xyz/
 | 
					  // TODO handle assets.example.com/sub/assets/com.example.xyz/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  app.use('/api', require('connect-send-error').error());
 | 
					  app.use('/api', require('connect-send-error').error());
 | 
				
			||||||
 | 
					  app.use('/assets', require('connect-send-error').error());
 | 
				
			||||||
  app.use('/', function (req, res, next) {
 | 
					  app.use('/', function (req, res, next) {
 | 
				
			||||||
    // If this doesn't look like an API we can move along
 | 
					    // If this doesn't look like an API or assets we can move along
 | 
				
			||||||
    if (!/\/api(\/|$)/.test(req.url)) {
 | 
					
 | 
				
			||||||
      // /^api\./.test(req.hostname) &&
 | 
					    /*
 | 
				
			||||||
 | 
					    console.log('.');
 | 
				
			||||||
 | 
					    console.log('[main.js] req.url, req.hostname');
 | 
				
			||||||
 | 
					    console.log(req.url);
 | 
				
			||||||
 | 
					    console.log(req.hostname);
 | 
				
			||||||
 | 
					    console.log('.');
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!/\/(api|assets)(\/|$)/.test(req.url)) {
 | 
				
			||||||
 | 
					      //console.log('[main.js] api|assets');
 | 
				
			||||||
 | 
					      next();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // keep https://assets.example.com/assets but skip https://example.com/assets
 | 
				
			||||||
 | 
					    if (/\/assets(\/|$)/.test(req.url) && !/(^|\.)(api|assets)(\.)/.test(req.hostname) && !/^[0-9\.]+$/.test(req.hostname)) {
 | 
				
			||||||
 | 
					      //console.log('[main.js] skip');
 | 
				
			||||||
      next();
 | 
					      next();
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -325,6 +342,7 @@ module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi
 | 
				
			|||||||
    return;
 | 
					    return;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  app.use('/', errorIfApi);
 | 
					  app.use('/', errorIfApi);
 | 
				
			||||||
 | 
					  app.use('/', errorIfAssets);
 | 
				
			||||||
  app.use('/', serveStatic);
 | 
					  app.use('/', serveStatic);
 | 
				
			||||||
  app.use('/', serveApps);
 | 
					  app.use('/', serveApps);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										306
									
								
								lib/oauth3.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								lib/oauth3.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,306 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var PromiseA = require('bluebird');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function generateRescope(req, Models, decoded, fullPpid, ppid) {
 | 
				
			||||||
 | 
					  return function (/*sub*/) {
 | 
				
			||||||
 | 
					    // TODO: this function is supposed to convert PPIDs of different parties to some account
 | 
				
			||||||
 | 
					    // ID that allows application to keep track of permisions and what-not.
 | 
				
			||||||
 | 
					    console.log('[rescope] Attempting ', fullPpid);
 | 
				
			||||||
 | 
					    return Models.IssuerOauth3OrgGrants.find({ azpSub: fullPpid }).then(function (results) {
 | 
				
			||||||
 | 
					      if (results[0]) {
 | 
				
			||||||
 | 
					        console.log('[rescope] lukcy duck: got it on the 1st try');
 | 
				
			||||||
 | 
					        return PromiseA.resolve(results);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // XXX BUG XXX
 | 
				
			||||||
 | 
					      // should be able to distinguish between own ids and 3rd party via @whatever.com
 | 
				
			||||||
 | 
					      return Models.IssuerOauth3OrgGrants.find({ azpSub: ppid });
 | 
				
			||||||
 | 
					    }).then(function (results) {
 | 
				
			||||||
 | 
					      var result = results[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!result || !result.sub || !decoded.iss) {
 | 
				
			||||||
 | 
					        // XXX BUG XXX TODO swap this external ppid for an internal (and ask user to link with existing profile)
 | 
				
			||||||
 | 
					        //req.oauth3.accountIdx = fullPpid;
 | 
				
			||||||
 | 
					        throw new Error("internal / external ID swapping not yet implemented. TODO: "
 | 
				
			||||||
 | 
					          + "No profile found with that credential. Would you like to create a new profile or link to an existing profile?");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // XXX BUG XXX need to pass own url in to use as issuer for own tokens
 | 
				
			||||||
 | 
					      req.oauth3.accountIdx = result.sub + '@' + decoded.iss;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log('[rescope] result:');
 | 
				
			||||||
 | 
					      console.log(results);
 | 
				
			||||||
 | 
					      console.log(req.oauth3.accountIdx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return PromiseA.resolve(req.oauth3.accountIdx);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function extractAccessToken(req) {
 | 
				
			||||||
 | 
					  var token = null;
 | 
				
			||||||
 | 
					  var parts;
 | 
				
			||||||
 | 
					  var scheme;
 | 
				
			||||||
 | 
					  var credentials;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.headers && req.headers.authorization) {
 | 
				
			||||||
 | 
					    // Works for all of Authorization: Bearer {{ token }}, Token {{ token }}, JWT {{ token }}
 | 
				
			||||||
 | 
					    parts = req.headers.authorization.split(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (parts.length !== 2) {
 | 
				
			||||||
 | 
					      return PromiseA.reject(new Error("malformed Authorization header"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    scheme = parts[0];
 | 
				
			||||||
 | 
					    credentials = parts[1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (-1 !== ['token', 'bearer'].indexOf(scheme.toLowerCase())) {
 | 
				
			||||||
 | 
					      token = credentials;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.body && req.body.access_token) {
 | 
				
			||||||
 | 
					    if (token) { PromiseA.reject(new Error("token exists in header and body")); }
 | 
				
			||||||
 | 
					    token = req.body.access_token;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO disallow query with req.method === 'GET'
 | 
				
			||||||
 | 
					  // NOTE: the case of DDNS on routers requires a GET and access_token
 | 
				
			||||||
 | 
					  // (cookies should be used for protected static assets)
 | 
				
			||||||
 | 
					  if (req.query && req.query.access_token) {
 | 
				
			||||||
 | 
					    if (token) { PromiseA.reject(new Error("token already exists in either header or body and also in query")); }
 | 
				
			||||||
 | 
					    token = req.query.access_token;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /*
 | 
				
			||||||
 | 
					  err = new Error(challenge());
 | 
				
			||||||
 | 
					  err.code = 'E_BEARER_REALM';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!token) { return PromiseA.reject(err); }
 | 
				
			||||||
 | 
					  */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return PromiseA.resolve(token);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function verifyToken(token) {
 | 
				
			||||||
 | 
					  var jwt = require('jsonwebtoken');
 | 
				
			||||||
 | 
					  var decoded;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!token) {
 | 
				
			||||||
 | 
					    return PromiseA.reject({
 | 
				
			||||||
 | 
					      message: 'no token provided'
 | 
				
			||||||
 | 
					    , code: 'E_NO_TOKEN'
 | 
				
			||||||
 | 
					    , url: 'https://oauth3.org/docs/errors#E_NO_TOKEN'
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    decoded = jwt.decode(token, {complete: true});
 | 
				
			||||||
 | 
					  } catch (e) {}
 | 
				
			||||||
 | 
					  if (!decoded) {
 | 
				
			||||||
 | 
					    return PromiseA.reject({
 | 
				
			||||||
 | 
					      message: 'provided token not a JSON Web Token'
 | 
				
			||||||
 | 
					    , code: 'E_NOT_JWT'
 | 
				
			||||||
 | 
					    , url: 'https://oauth3.org/docs/errors#E_NOT_JWT'
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var sub = decoded.payload.sub || decoded.payload.ppid || decoded.payload.appScopedId;
 | 
				
			||||||
 | 
					  if (!sub) {
 | 
				
			||||||
 | 
					    return PromiseA.reject({
 | 
				
			||||||
 | 
					      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) {
 | 
				
			||||||
 | 
					    return PromiseA.reject({
 | 
				
			||||||
 | 
					      message: 'token missing kid'
 | 
				
			||||||
 | 
					    , code: 'E_MISSING_KID'
 | 
				
			||||||
 | 
					    , url: 'https://oauth3.org/docs/errors#E_MISSING_KID'
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!decoded.payload.iss) {
 | 
				
			||||||
 | 
					    return PromiseA.reject({
 | 
				
			||||||
 | 
					      message: 'token missing iss'
 | 
				
			||||||
 | 
					    , code: 'E_MISSING_ISS'
 | 
				
			||||||
 | 
					    , url: 'https://oauth3.org/docs/errors#E_MISSING_ISS'
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var OAUTH3 = require('oauth3.js');
 | 
				
			||||||
 | 
					  OAUTH3._hooks = require('oauth3.js/oauth3.node.storage.js');
 | 
				
			||||||
 | 
					  return OAUTH3.discover(decoded.payload.iss).then(function (directives) {
 | 
				
			||||||
 | 
					    var args = (directives || {}).retrieve_jwk;
 | 
				
			||||||
 | 
					    if (typeof args === 'string') {
 | 
				
			||||||
 | 
					      args = { url: args, method: 'GET' };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (typeof (args || {}).url !== 'string') {
 | 
				
			||||||
 | 
					      return PromiseA.reject({
 | 
				
			||||||
 | 
					        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 OAUTH3.request({
 | 
				
			||||||
 | 
					      url: OAUTH3.url.resolve(directives.api, url)
 | 
				
			||||||
 | 
					    , method: args.method
 | 
				
			||||||
 | 
					    , data: body
 | 
				
			||||||
 | 
					    }).catch(function (err) {
 | 
				
			||||||
 | 
					      return 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 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 PromiseA.reject(res.data.error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var opts = {};
 | 
				
			||||||
 | 
					    if (Array.isArray(res.data.alg)) {
 | 
				
			||||||
 | 
					      opts.algorithms = res.data.alg;
 | 
				
			||||||
 | 
					    } else if (typeof res.data.alg === 'string') {
 | 
				
			||||||
 | 
					      opts.algorithms = [res.data.alg];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      return jwt.verify(token, require('jwk-to-pem')(res.data), opts);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      return PromiseA.reject({
 | 
				
			||||||
 | 
					        message: 'token verification failed'
 | 
				
			||||||
 | 
					      , code: 'E_INVALID_TOKEN'
 | 
				
			||||||
 | 
					      , url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN'
 | 
				
			||||||
 | 
					      , subErr: err.toString()
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function deepFreeze(obj) {
 | 
				
			||||||
 | 
					  Object.keys(obj).forEach(function (key) {
 | 
				
			||||||
 | 
					    if (obj[key] && typeof obj[key] === 'object') {
 | 
				
			||||||
 | 
					      deepFreeze(obj[key]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  Object.freeze(obj);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cookieOauth3(Models, req, res, next) {
 | 
				
			||||||
 | 
					  req.oauth3 = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var token = req.cookies.jwt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  req.oauth3.encodedToken = token;
 | 
				
			||||||
 | 
					  req.oauth3.verifyAsync = function (jwt) {
 | 
				
			||||||
 | 
					    return verifyToken(jwt || token);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return verifyToken(token).then(function  (decoded) {
 | 
				
			||||||
 | 
					    req.oauth3.token = decoded;
 | 
				
			||||||
 | 
					    if (!decoded) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
 | 
				
			||||||
 | 
					    req.oauth3.ppid = ppid;
 | 
				
			||||||
 | 
					    req.oauth3.accountIdx = ppid+'@'+decoded.iss;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var hash = require('crypto').createHash('sha256').update(req.oauth3.accountIdx).digest('base64');
 | 
				
			||||||
 | 
					    hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
 | 
				
			||||||
 | 
					    req.oauth3.accountHash = hash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);
 | 
				
			||||||
 | 
					  }).then(function () {
 | 
				
			||||||
 | 
					    deepFreeze(req.oauth3);
 | 
				
			||||||
 | 
					    //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | 
				
			||||||
 | 
					    next();
 | 
				
			||||||
 | 
					  }, function (err) {
 | 
				
			||||||
 | 
					    if ('E_NO_TOKEN' === err.code) {
 | 
				
			||||||
 | 
					      next();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    console.error('[walnut] cookie lib/oauth3 error:');
 | 
				
			||||||
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    res.send(err);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function attachOauth3(Models, req, res, next) {
 | 
				
			||||||
 | 
					  req.oauth3 = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractAccessToken(req).then(function (token) {
 | 
				
			||||||
 | 
					    req.oauth3.encodedToken = token;
 | 
				
			||||||
 | 
					    req.oauth3.verifyAsync = function (jwt) {
 | 
				
			||||||
 | 
					      return verifyToken(jwt || token);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!token) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return verifyToken(token);
 | 
				
			||||||
 | 
					  }).then(function  (decoded) {
 | 
				
			||||||
 | 
					    req.oauth3.token = decoded;
 | 
				
			||||||
 | 
					    if (!decoded) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
 | 
				
			||||||
 | 
					    var fullPpid = ppid+'@'+decoded.iss;
 | 
				
			||||||
 | 
					    req.oauth3.ppid = ppid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO we can anonymize the relationship between our user as the other service's user
 | 
				
			||||||
 | 
					    // in our own database by hashing the remote service's ppid and using that as the lookup
 | 
				
			||||||
 | 
					    var hash = require('crypto').createHash('sha256').update(fullPpid).digest('base64');
 | 
				
			||||||
 | 
					    hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
 | 
				
			||||||
 | 
					    req.oauth3.accountHash = hash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('############### assigned req.oauth3:');
 | 
				
			||||||
 | 
					    console.log(req.oauth3);
 | 
				
			||||||
 | 
					  }).then(function () {
 | 
				
			||||||
 | 
					    //deepFreeze(req.oauth3);
 | 
				
			||||||
 | 
					    //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | 
				
			||||||
 | 
					    next();
 | 
				
			||||||
 | 
					  }, function (err) {
 | 
				
			||||||
 | 
					    console.error('[walnut] JWT lib/oauth3 error:');
 | 
				
			||||||
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    res.send(err);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.attachOauth3 = attachOauth3;
 | 
				
			||||||
 | 
					module.exports.cookieOauth3 = cookieOauth3;
 | 
				
			||||||
 | 
					module.exports.verifyToken = verifyToken;
 | 
				
			||||||
@ -55,19 +55,7 @@ function getApi(conf, pkgConf, pkgDeps, packagedApi) {
 | 
				
			|||||||
      packagedApi._api = require('express-lazy')();
 | 
					      packagedApi._api = require('express-lazy')();
 | 
				
			||||||
      packagedApi._api_app = myApp;
 | 
					      packagedApi._api_app = myApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      //require('./oauth3-auth').inject(conf, packagedApi._api, pkgConf, pkgDeps);
 | 
					      packagedApi._api.use('/', require('./oauth3').attachOauth3);
 | 
				
			||||||
      pkgDeps.getOauth3Controllers =
 | 
					 | 
				
			||||||
      packagedApi._getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(conf).getControllers;
 | 
					 | 
				
			||||||
      require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // DEBUG
 | 
					 | 
				
			||||||
      //
 | 
					 | 
				
			||||||
      /*
 | 
					 | 
				
			||||||
      packagedApi._api.use('/', function (req, res, next) {
 | 
					 | 
				
			||||||
        console.log('[DEBUG pkgApiApp]', req.method, req.hostname, req.url);
 | 
					 | 
				
			||||||
        next();
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      //*/
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // TODO fix backwards compat
 | 
					      // TODO fix backwards compat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -150,6 +150,21 @@ module.exports.create = function (webserver, xconfx, state) {
 | 
				
			|||||||
            models: models
 | 
					            models: models
 | 
				
			||||||
            // TODO don't let packages use this directly
 | 
					            // TODO don't let packages use this directly
 | 
				
			||||||
          , Promise: PromiseA
 | 
					          , Promise: PromiseA
 | 
				
			||||||
 | 
					          , dns: PromiseA.promisifyAll(require('dns'))
 | 
				
			||||||
 | 
					          , crypto: PromiseA.promisifyAll(require('crypto'))
 | 
				
			||||||
 | 
					          , fs: PromiseA.promisifyAll(require('fs'))
 | 
				
			||||||
 | 
					          , path: require('path')
 | 
				
			||||||
 | 
					          , validate: {
 | 
				
			||||||
 | 
					              isEmail: function (email) {
 | 
				
			||||||
 | 
					                return /@/.test(email) && !/\s+/.test(email);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            , email: function (email) {
 | 
				
			||||||
 | 
					                if (apiDeps.validate.isEmail(email)) {
 | 
				
			||||||
 | 
					                  return null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return new Error('invalid email address');
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
          var apiFactories = {
 | 
					          var apiFactories = {
 | 
				
			||||||
            memstoreFactory: { create: scopeMemstore }
 | 
					            memstoreFactory: { create: scopeMemstore }
 | 
				
			||||||
@ -180,7 +195,7 @@ module.exports.create = function (webserver, xconfx, state) {
 | 
				
			|||||||
          function setupMain() {
 | 
					          function setupMain() {
 | 
				
			||||||
            if (xconfx.debug) { console.log('[main] setup'); }
 | 
					            if (xconfx.debug) { console.log('[main] setup'); }
 | 
				
			||||||
            mainApp = express();
 | 
					            mainApp = express();
 | 
				
			||||||
            require('./main').create(mainApp, xconfx, apiFactories, apiDeps, errorIfApi).then(function () {
 | 
					            require('./main').create(mainApp, xconfx, apiFactories, apiDeps, errorIfApi, errorIfAssets).then(function () {
 | 
				
			||||||
              if (xconfx.debug) { console.log('[main] ready'); }
 | 
					              if (xconfx.debug) { console.log('[main] ready'); }
 | 
				
			||||||
              // TODO process.send({});
 | 
					              // TODO process.send({});
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -225,6 +240,24 @@ module.exports.create = function (webserver, xconfx, state) {
 | 
				
			|||||||
            next();
 | 
					            next();
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          function errorIfNotAssets(req, res, next) {
 | 
				
			||||||
 | 
					            var hostname = req.hostname || req.headers.host;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!/^assets\.[a-z0-9\-]+/.test(hostname)) {
 | 
				
			||||||
 | 
					              res.send({ error:
 | 
				
			||||||
 | 
					               { message: "['" + hostname + req.url + "'] protected asset access is restricted to proper 'asset'-prefixed lowercase subdomains."
 | 
				
			||||||
 | 
					                   + " The HTTP 'Host' header must exist and must begin with 'assets.' as in 'assets.example.com'."
 | 
				
			||||||
 | 
					                   + " For development you may test with assets.localhost.daplie.me (or any domain by modifying your /etc/hosts)"
 | 
				
			||||||
 | 
					               , code: 'E_NOT_API'
 | 
				
			||||||
 | 
					               , _hostname: hostname
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            next();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          function errorIfApi(req, res, next) {
 | 
					          function errorIfApi(req, res, next) {
 | 
				
			||||||
            if (!/^api\./.test(req.headers.host)) {
 | 
					            if (!/^api\./.test(req.headers.host)) {
 | 
				
			||||||
              next();
 | 
					              next();
 | 
				
			||||||
@ -240,7 +273,25 @@ module.exports.create = function (webserver, xconfx, state) {
 | 
				
			|||||||
              return;
 | 
					              return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            res.send({ error: { code: 'E_NO_IMPL', message: "not implemented" } });
 | 
					            res.send({ error: { code: 'E_NO_IMPL', message: "API not implemented" } });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          function errorIfAssets(req, res, next) {
 | 
				
			||||||
 | 
					            if (!/^assets\./.test(req.headers.host)) {
 | 
				
			||||||
 | 
					              next();
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // has api. hostname prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // doesn't have /api url prefix
 | 
				
			||||||
 | 
					            if (!/^\/assets\//.test(req.url)) {
 | 
				
			||||||
 | 
					              console.log('[walnut/worker assets] req.url', req.url);
 | 
				
			||||||
 | 
					              res.send({ error: { message: "missing /assets/ url prefix" } });
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            res.send({ error: { code: 'E_NO_IMPL', message: "assets handler not implemented" } });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          app.disable('x-powered-by');
 | 
					          app.disable('x-powered-by');
 | 
				
			||||||
@ -258,8 +309,11 @@ module.exports.create = function (webserver, xconfx, state) {
 | 
				
			|||||||
          }));
 | 
					          }));
 | 
				
			||||||
          app.use('/api', recase);
 | 
					          app.use('/api', recase);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          var cookieParser = require('cookie-parser'); // signing is done in JWT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
 | 
					          app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
 | 
				
			||||||
          app.use('/api', errorIfNotApi);
 | 
					          app.use('/api', errorIfNotApi);
 | 
				
			||||||
 | 
					          app.use('/assets', /*errorIfNotAssets,*/ cookieParser()); // serializer { path: '/assets', httpOnly: true, sameSite: true/*, domain: assets.example.com*/ }
 | 
				
			||||||
          app.use('/', function (req, res) {
 | 
					          app.use('/', function (req, res) {
 | 
				
			||||||
            if (!(req.encrypted || req.secure)) {
 | 
					            if (!(req.encrypted || req.secure)) {
 | 
				
			||||||
              // did not come from https
 | 
					              // did not come from https
 | 
				
			||||||
 | 
				
			|||||||
@ -52,6 +52,8 @@
 | 
				
			|||||||
    "express": "4.x",
 | 
					    "express": "4.x",
 | 
				
			||||||
    "express-lazy": "^1.1.1",
 | 
					    "express-lazy": "^1.1.1",
 | 
				
			||||||
    "express-session": "^1.11.3",
 | 
					    "express-session": "^1.11.3",
 | 
				
			||||||
 | 
					    "jsonwebtoken": "^7.4.1",
 | 
				
			||||||
 | 
					    "jwk-to-pem": "^1.2.6",
 | 
				
			||||||
    "mailchimp-api-v3": "^1.7.0",
 | 
					    "mailchimp-api-v3": "^1.7.0",
 | 
				
			||||||
    "mandrill-api": "^1.0.45",
 | 
					    "mandrill-api": "^1.0.45",
 | 
				
			||||||
    "masterquest-sqlite3": "git+https://git.daplie.com/node/masterquest-sqlite3.git",
 | 
					    "masterquest-sqlite3": "git+https://git.daplie.com/node/masterquest-sqlite3.git",
 | 
				
			||||||
@ -59,7 +61,7 @@
 | 
				
			|||||||
    "multiparty": "^4.1.3",
 | 
					    "multiparty": "^4.1.3",
 | 
				
			||||||
    "nodemailer": "^1.4.0",
 | 
					    "nodemailer": "^1.4.0",
 | 
				
			||||||
    "nodemailer-mailgun-transport": "1.x",
 | 
					    "nodemailer-mailgun-transport": "1.x",
 | 
				
			||||||
    "oauthcommon": "git+https://git.daplie.com/node/oauthcommon.git",
 | 
					    "oauth3.js": "git+https://git.daplie.com/OAuth3/oauth3.js.git",
 | 
				
			||||||
    "request": "^2.81.0",
 | 
					    "request": "^2.81.0",
 | 
				
			||||||
    "serve-static": "1.x",
 | 
					    "serve-static": "1.x",
 | 
				
			||||||
    "sqlite3-cluster": "git+https://git.daplie.com/coolaj86/sqlite3-cluster.git#v2",
 | 
					    "sqlite3-cluster": "git+https://git.daplie.com/coolaj86/sqlite3-cluster.git#v2",
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user