1210 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1210 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
 | 
						|
module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | 
						|
  var PromiseA = apiDeps.Promise;
 | 
						|
  var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
 | 
						|
  var request = PromiseA.promisify(require('request'));
 | 
						|
  //var express = require('express');
 | 
						|
  var express = require('express-lazy');
 | 
						|
  var fs = PromiseA.promisifyAll(require('fs'));
 | 
						|
  var path = require('path');
 | 
						|
  var localCache = { rests: {}, pkgs: {}, assets: {} };
 | 
						|
  var promisableRequest = require('./common').promisableRequest;
 | 
						|
  var rejectableRequest = require('./common').rejectableRequest;
 | 
						|
  var crypto = require('crypto');
 | 
						|
 | 
						|
  // TODO xconfx.apispath
 | 
						|
  xconfx.restPath = path.join(__dirname, '..', '..', 'packages', 'rest');
 | 
						|
  xconfx.apiPath = path.join(__dirname, '..', '..', 'packages', 'api');
 | 
						|
  xconfx.appApiGrantsPath = path.join(__dirname, '..', '..', 'packages', 'client-api-grants');
 | 
						|
  xconfx.appConfigPath = path.join(__dirname, '..', '..', 'var');
 | 
						|
 | 
						|
  function notConfigured(req, res) {
 | 
						|
    var msg = "api package '" + req.pkgId + "' not configured for client uri '" + req.experienceId + "'"
 | 
						|
      + ". To configure it place a new line '" + req.pkgId + "' in the file '/srv/walnut/packages/client-api-grants/" + req.experienceId + "'"
 | 
						|
      ;
 | 
						|
 | 
						|
    res.send({ error: { message: msg } });
 | 
						|
  }
 | 
						|
 | 
						|
  /*
 | 
						|
  function isThisPkgInstalled(myConf, pkgId) {
 | 
						|
  }
 | 
						|
  */
 | 
						|
 | 
						|
  function isThisClientAllowedToUseThisPkg(req, myConf, clientUrih, pkgId) {
 | 
						|
    var appApiGrantsPath = path.join(myConf.appApiGrantsPath, clientUrih);
 | 
						|
 | 
						|
    return fs.readFileAsync(appApiGrantsPath, 'utf8').then(function (text) {
 | 
						|
      return text.trim().split(/\n/);
 | 
						|
    }, function (err) {
 | 
						|
      if ('ENOENT' !== err.code) {
 | 
						|
        console.error(err);
 | 
						|
      }
 | 
						|
      return [];
 | 
						|
    }).then(function (apis) {
 | 
						|
      if (apis.some(function (api) {
 | 
						|
        if (api === pkgId) {
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
      })) {
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
 | 
						|
      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
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
 | 
						|
      if (clientUrih === ('api.' + xconfx.setupDomain) && -1 !== ['org.oauth3.consumer', 'azp@oauth3.org', 'oauth3.org'].indexOf(pkgId)) {
 | 
						|
        // fallthrough
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
 | 
						|
      return null;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function getSitePackageConfig(clientUrih, pkgId) {
 | 
						|
    var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
 | 
						|
    return mkdirpAsync(siteConfigPath).then(function () {
 | 
						|
      return fs.readFileAsync(path.join(siteConfigPath, pkgId + '.json'), 'utf8').then(function (text) {
 | 
						|
        return JSON.parse(text);
 | 
						|
      }).then(function (data) { return data; }, function (/*err*/) { return {}; });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function getSiteConfig(clientUrih) {
 | 
						|
    // TODO test if the requesting package has permission to the root-level site config
 | 
						|
    var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
 | 
						|
    return mkdirpAsync(siteConfigPath).then(function () {
 | 
						|
      return fs.readFileAsync(path.join(siteConfigPath, 'config.json'), 'utf8').then(function (text) {
 | 
						|
        return JSON.parse(text);
 | 
						|
      }).then(function (data) { return data; }, function (/*err*/) { return {}; });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  var modelsCache = {};
 | 
						|
  function getSiteStore(clientUrih, pkgId, dir) {
 | 
						|
    if (!modelsCache[clientUrih]) {
 | 
						|
      modelsCache[clientUrih] = apiFactories.systemSqlFactory.create({
 | 
						|
        init: true
 | 
						|
      , dbname: clientUrih // '#' is a valid file name character
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    // DB scopes:
 | 
						|
    // system (global)
 | 
						|
    // experience (per domain)
 | 
						|
    // api (per api)
 | 
						|
    // account (per user account)
 | 
						|
    // client (per 3rd party client)
 | 
						|
 | 
						|
    // scope Experience to db
 | 
						|
    // scope Api by table
 | 
						|
    // scope Account and Client by column
 | 
						|
    return modelsCache[clientUrih].then(function (db) {
 | 
						|
      var wrap = require('masterquest-sqlite3');
 | 
						|
 | 
						|
      return wrap.wrap(db, dir).then(function (models) {
 | 
						|
        //modelsCache[clientUrih] = PromiseA.resolve(models);
 | 
						|
        return models;
 | 
						|
      });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function accountRequiredById(req, res, next) {
 | 
						|
    var promise = req.oauth3.verifyAsync().then(function (/*result*/) {
 | 
						|
      var tok = req.oauth3.token;
 | 
						|
      var accountId = req.params.accountId || '__NO_ID_GIVEN__';
 | 
						|
      var ppid;
 | 
						|
      var iss = tok.iss;
 | 
						|
 | 
						|
      if (tok.sub && tok.sub.split(/,/g).filter(function (ppid) {
 | 
						|
        return ppid === accountId;
 | 
						|
      }).length) {
 | 
						|
        ppid = accountId;
 | 
						|
      }
 | 
						|
 | 
						|
      // Deprecated backwards compat. To be removed.
 | 
						|
      if (tok.axs && tok.axs.filter(function (acc) {
 | 
						|
        return acc.id === accountId || acc.appScopedId === accountId;
 | 
						|
      }).length) {
 | 
						|
        ppid = accountId;
 | 
						|
      }
 | 
						|
 | 
						|
      if (tok.acx && accountId === (tok.acx.appScopedId || tok.acx.id || tok.acx)) {
 | 
						|
        ppid = accountId;
 | 
						|
      }
 | 
						|
 | 
						|
      if (!ppid) {
 | 
						|
        return PromiseA.reject(new Error("missing accountId '" + accountId + "' in access token"));
 | 
						|
      }
 | 
						|
 | 
						|
      return req.oauth3.rescope().then(function (accountIdx) {
 | 
						|
        req.oauth3.accountIdx = accountIdx;
 | 
						|
        req.oauth3.ppid = ppid;
 | 
						|
        //console.log('[walnut@daplie.com] accountIdx:', accountIdx);
 | 
						|
        //console.log('[walnut@daplie.com] ppid:', ppid);
 | 
						|
 | 
						|
        next();
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    rejectableRequest(req, res, promise, "[walnut@daplie.com] attach account by id");
 | 
						|
  }
 | 
						|
 | 
						|
  function accountRequired(req, res, next) {
 | 
						|
    console.log('[accountRequired] [enter]');
 | 
						|
 | 
						|
    var myIss = req.experienceId;
 | 
						|
    var isPpid;
 | 
						|
 | 
						|
    // if this already has auth, great
 | 
						|
    if (req.oauth3.ppid && req.oauth3.accountIdx) {
 | 
						|
      // except that if it's a ppid, we have to internally exchange it for the real token
 | 
						|
      isPpid = (myIss === req.oauth3.iss && myIss !== req.oauth3.azp);
 | 
						|
      if (!isPpid) {
 | 
						|
        console.log('[accountRequired] has token already');
 | 
						|
        console.log(req.oauth3);
 | 
						|
        console.log('');
 | 
						|
        next();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!req.oauth3.encodedToken) {
 | 
						|
      // being public does not disallow authentication
 | 
						|
      if (req.isPublic) {
 | 
						|
        next();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      rejectableRequest(
 | 
						|
        req
 | 
						|
      , res
 | 
						|
      , PromiseA.reject(new Error("this secure resource requires an access token"))
 | 
						|
      , "[walnut@daplie.com] required account (not /public)"
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // verify the auth if it's here
 | 
						|
    var promise = req.oauth3.verifyAsync().then(function (/*result*/) {
 | 
						|
      var tok = req.oauth3.token;
 | 
						|
      var ppid;
 | 
						|
      var err;
 | 
						|
      var iss = tok.iss;
 | 
						|
 | 
						|
      if (tok.sub) {
 | 
						|
        if (tok.sub.split(/,/g).length > 1) {
 | 
						|
          err = new Error("more than one 'sub' specified in token");
 | 
						|
          return PromiseA.reject(err);
 | 
						|
        }
 | 
						|
        ppid = tok.sub;
 | 
						|
      }
 | 
						|
 | 
						|
      if (!ppid) {
 | 
						|
        return PromiseA.reject(new Error("could not determine accountId from access token"));
 | 
						|
      }
 | 
						|
 | 
						|
      return req.oauth3.rescope().then(function (accountIdx) {
 | 
						|
        console.log('[accountRequired] req.oauth3');
 | 
						|
        console.log(accountIdx);
 | 
						|
 | 
						|
        var sub = accountIdx.split('@')[0];
 | 
						|
        var iss = accountIdx.split('@')[1];
 | 
						|
        var id = sub + '@' + iss;
 | 
						|
 | 
						|
        req.oauth3.profile = {
 | 
						|
          id: id
 | 
						|
        , sub: sub
 | 
						|
        , iss: iss
 | 
						|
        };
 | 
						|
        req.oauth3.id = id;
 | 
						|
        req.oauth3.sub = sub;
 | 
						|
        req.oauth3.iss = iss;
 | 
						|
 | 
						|
        req.oauth3.accountIdx = accountIdx;
 | 
						|
        req.oauth3.ppid = ppid;
 | 
						|
 | 
						|
        next();
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    rejectableRequest(req, res, promise, "[walnut@daplie.com] required account (not /public)");
 | 
						|
  }
 | 
						|
 | 
						|
  function grantsRequired(grants) {
 | 
						|
    if (!Array.isArray(grants)) {
 | 
						|
      throw new Error("Usage: app.grantsRequired([ 'name|altname|altname2', 'othergrant' ])");
 | 
						|
    }
 | 
						|
 | 
						|
    if (!grants.length) {
 | 
						|
      return function (req, res, next) {
 | 
						|
        next();
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    return function (req, res, next) {
 | 
						|
      var tokenScopes;
 | 
						|
 | 
						|
      if (!(req.oauth3 || req.oauth3.token)) {
 | 
						|
        // TODO some error generator for standard messages
 | 
						|
        res.send({ error: { message: "You must be logged in", code: "E_NO_AUTHN" } });
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      var scope = req.oauth3.token.scope || req.oauth3.token.scp || req.oauth3.token.grants;
 | 
						|
      if ('string' !== typeof scope) {
 | 
						|
        res.send({ error: { message: "Token must contain a grants string in 'scope'", code: "E_NO_GRANTS" } });
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      tokenScopes = scope.split(/[,\s]+/mg);
 | 
						|
      if (-1 !== tokenScopes.indexOf('*')) {
 | 
						|
        // has full account access
 | 
						|
        next();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // every grant in the array must be present, though some grants can be satisfied
 | 
						|
      // by multiple scopes.
 | 
						|
      var missing = grants.filter(function (grant) {
 | 
						|
        return !grant.split('|').some(function (scp) {
 | 
						|
          return tokenScopes.indexOf(scp) !== -1;
 | 
						|
        });
 | 
						|
      });
 | 
						|
      if (missing.length) {
 | 
						|
        res.send({ error: { message: "Token missing required grants: '" + missing.join(',') + "'", code: "E_NO_GRANTS" } });
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      next();
 | 
						|
    };
 | 
						|
  }
 | 
						|
  function loadRestHelperApi(myConf, clientUrih, pkg, pkgId, pkgPath) {
 | 
						|
    var pkgLinks = [];
 | 
						|
    pkgLinks.push(pkgId);
 | 
						|
    var pkgRestApi;
 | 
						|
    var pkgDeps = {};
 | 
						|
    var myApp;
 | 
						|
    var pkgPathApi;
 | 
						|
 | 
						|
    pkgPathApi = pkgPath;
 | 
						|
    if (pkg.walnut) {
 | 
						|
      pkgPathApi = path.join(pkgPath, pkg.walnut);
 | 
						|
    }
 | 
						|
    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
 | 
						|
    var _stripe;
 | 
						|
    var _stripe_test;
 | 
						|
    var _mandrill;
 | 
						|
    var _mailchimp;
 | 
						|
    var _twilio;
 | 
						|
    var _get_response;
 | 
						|
    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'); }
 | 
						|
 | 
						|
        // Use getSiteCapability('email@daplie.com') instead
 | 
						|
        Object.defineProperty(req, 'getSiteMailer' /*deprecated*/, {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , writable: false
 | 
						|
        , value: function getSiteMailerProp() {
 | 
						|
            var nodemailer = require('nodemailer');
 | 
						|
            var transport = require('nodemailer-mailgun-transport');
 | 
						|
            //var mailconf = require('../../../com.daplie.mailer/config.mailgun');
 | 
						|
            var mailconf = siteConfig['mailgun.org'];
 | 
						|
            var mailer = PromiseA.promisifyAll(nodemailer.createTransport(transport(mailconf)));
 | 
						|
 | 
						|
            return mailer;
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        Object.defineProperty(req, 'getSiteConfig', {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , writable: false
 | 
						|
        , value: function getSiteConfigProp(section) {
 | 
						|
            // deprecated
 | 
						|
            if ('com.daplie.tel' === section) {
 | 
						|
              section = 'tel@daplie.com';
 | 
						|
            }
 | 
						|
            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, 'getSitePackageStore', {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , writable: false
 | 
						|
        , value: getSitePackageStoreProp
 | 
						|
        });
 | 
						|
 | 
						|
        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);
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        /*
 | 
						|
        Object.defineProperty(req, 'getSitePayments', {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , writable: false
 | 
						|
        , value: function getSitePaymentsProp() {
 | 
						|
          }
 | 
						|
        });
 | 
						|
        */
 | 
						|
        // TODO allow third-party clients stripe ids destination
 | 
						|
        // https://stripe.com/docs/connect/payments-fees
 | 
						|
        Object.defineProperty(req, 'Stripe', {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , get: function () {
 | 
						|
            _stripe = _stripe || require('stripe')(siteConfig['stripe.com'].live.secret);
 | 
						|
            return _stripe;
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        Object.defineProperty(req, 'StripeTest', {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , get: function () {
 | 
						|
            _stripe_test = _stripe_test || require('stripe')(siteConfig['stripe.com'].test.secret);
 | 
						|
            return _stripe_test;
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        Object.defineProperty(req, 'Mandrill', {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , get: function () {
 | 
						|
            if (!_mandrill) {
 | 
						|
              var Mandrill = require('mandrill-api/mandrill');
 | 
						|
              _mandrill = new Mandrill.Mandrill(siteConfig['mandrill.com'].apiKey);
 | 
						|
              _mandrill.messages.sendTemplateAsync = function (opts) {
 | 
						|
                return new PromiseA(function (resolve, reject) {
 | 
						|
                  _mandrill.messages.sendTemplate(opts, resolve, reject);
 | 
						|
                });
 | 
						|
              };
 | 
						|
            }
 | 
						|
            return _mandrill;
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        Object.defineProperty(req, 'Mailchimp', {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , get: function () {
 | 
						|
            var Mailchimp = require('mailchimp-api-v3');
 | 
						|
            _mailchimp = _mailchimp || new Mailchimp(siteConfig['mailchimp.com'].apiKey);
 | 
						|
            return _mailchimp;
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        Object.defineProperty(req, 'GetResponse', {
 | 
						|
          enumerable: true
 | 
						|
        , configurable: false
 | 
						|
        , get: function () {
 | 
						|
            if (_get_response) {
 | 
						|
              return _get_response;
 | 
						|
            }
 | 
						|
            _get_response = {
 | 
						|
              saveSubscriber: function (email, opts) {
 | 
						|
                var config = siteConfig['getresponse@daplie.com'];
 | 
						|
                var customFields = [];
 | 
						|
                Object.keys(config.customFields).forEach(function (name) {
 | 
						|
                  if (typeof opts[name] !== 'undefined') {
 | 
						|
                    customFields.push({
 | 
						|
                      customFieldId: config.customFields[name]
 | 
						|
                    , value: [ String(opts[name]) ]
 | 
						|
                    });
 | 
						|
                  }
 | 
						|
                });
 | 
						|
 | 
						|
                return request({
 | 
						|
                  method: 'POST'
 | 
						|
                , url: 'https://api.getresponse.com/v3/contacts'
 | 
						|
                , headers: { 'X-Auth-Token': 'api-key ' + config.apiKey }
 | 
						|
                , json: true
 | 
						|
                , body: {
 | 
						|
                    name: opts.name
 | 
						|
                  , email: email
 | 
						|
                  , ipAddress: opts.ipAddress
 | 
						|
                  , campaign: { campaignId: config.campaignId }
 | 
						|
                  , customFieldValues: customFields
 | 
						|
                  }
 | 
						|
                }).then(function (resp) {
 | 
						|
                  if (resp.statusCode === 202) {
 | 
						|
                    return;
 | 
						|
                  }
 | 
						|
                  return PromiseA.reject(resp.body.message);
 | 
						|
                });
 | 
						|
              }
 | 
						|
            };
 | 
						|
 | 
						|
            return _get_response;
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        var Twilio = require('twilio');
 | 
						|
        function twilioTel(/*opts*/) {
 | 
						|
          if (_twilio) {
 | 
						|
            return apiDeps.Promise.resolve(_twilio);
 | 
						|
          }
 | 
						|
 | 
						|
          _twilio = new Twilio.RestClient(
 | 
						|
            siteConfig['twilio.com'].live.id
 | 
						|
          , siteConfig['twilio.com'].live.auth
 | 
						|
          );
 | 
						|
          return apiDeps.Promise.resolve(_twilio);
 | 
						|
        }
 | 
						|
 | 
						|
        // TODO shared memory db
 | 
						|
        var mailgunTokens = {};
 | 
						|
        function validateMailgun(apiKey, timestamp, token, signature) {
 | 
						|
          // https://gist.github.com/coolaj86/81a3b61353d2f0a2552c
 | 
						|
          // (realized later)
 | 
						|
          // HAHA HAHA HAHAHAHAHA this is my own gist... so much more polite attribution
 | 
						|
          var scmp = require('scmp')
 | 
						|
            , mailgunExpirey = 15 * 60 * 1000
 | 
						|
            , mailgunHashType = 'sha256'
 | 
						|
            , mailgunSignatureEncoding = 'hex'
 | 
						|
            ;
 | 
						|
          var actual
 | 
						|
            , adjustedTimestamp = parseInt(timestamp, 10) * 1000
 | 
						|
            , fresh = (Math.abs(Date.now() - adjustedTimestamp) < mailgunExpirey)
 | 
						|
            ;
 | 
						|
 | 
						|
          if (!fresh) {
 | 
						|
            console.error('[mailgun] Stale Timestamp: this may be an attack');
 | 
						|
            console.error('[mailgun] However, this is most likely your fault\n');
 | 
						|
            console.error('[mailgun] run `ntpdate ntp.ubuntu.com` and check your system clock\n');
 | 
						|
            console.error('[mailgun] System Time: ' + new Date().toString());
 | 
						|
            console.error('[mailgun] Mailgun Time: ' + new Date(adjustedTimestamp).toString(), timestamp);
 | 
						|
            console.error('[mailgun] Delta: ' + (Date.now() - adjustedTimestamp));
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
 | 
						|
          if (mailgunTokens[token]) {
 | 
						|
            console.error('[mailgun] Replay Attack');
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
 | 
						|
          mailgunTokens[token] = true;
 | 
						|
 | 
						|
          setTimeout(function () {
 | 
						|
            delete mailgunTokens[token];
 | 
						|
          }, mailgunExpirey + (5 * 1000));
 | 
						|
 | 
						|
          actual = crypto.createHmac(mailgunHashType, apiKey)
 | 
						|
            .update(new Buffer(timestamp + token, 'utf8'))
 | 
						|
            .digest(mailgunSignatureEncoding)
 | 
						|
            ;
 | 
						|
          return scmp(signature, actual);
 | 
						|
        }
 | 
						|
 | 
						|
        function mailgunMail(/*opts*/) {
 | 
						|
          return apiDeps.Promise.resolve(req.getSiteMailer());
 | 
						|
        }
 | 
						|
        function getResponseList() {
 | 
						|
          return apiDeps.Promise.resolve(req.GetResponse);
 | 
						|
        }
 | 
						|
 | 
						|
        // Twilio Parameters are often 26 long
 | 
						|
        var bodyParserTwilio = require('body-parser').urlencoded({ limit: '4kb', parameterLimit: 100, extended: false });
 | 
						|
        // Mailgun has something like 50 parameters
 | 
						|
        var bodyParserMailgun = require('body-parser').urlencoded({ limit: '1024kb', parameterLimit: 500, extended: false });
 | 
						|
        function bodyMultiParserMailgun (req, res, next) {
 | 
						|
          var multiparty = require('multiparty');
 | 
						|
          var form = new multiparty.Form();
 | 
						|
 | 
						|
          form.parse(req, function (err, fields/*, files*/) {
 | 
						|
            if (err) {
 | 
						|
              console.error('Error');
 | 
						|
              console.error(err);
 | 
						|
              res.end("Couldn't parse form");
 | 
						|
              return;
 | 
						|
            }
 | 
						|
 | 
						|
            var body;
 | 
						|
            req.body = req.body || {};
 | 
						|
            Object.keys(fields).forEach(function (key) {
 | 
						|
              // TODO what if there were two of something?
 | 
						|
              // (even though there won't be)
 | 
						|
              req.body[key] = fields[key][0];
 | 
						|
            });
 | 
						|
            body = req.body;
 | 
						|
 | 
						|
            next();
 | 
						|
          });
 | 
						|
        }
 | 
						|
 | 
						|
        function daplieTel() {
 | 
						|
          return twilioTel().then(function (twilio) {
 | 
						|
            function sms(opts) {
 | 
						|
              // opts = { to, from, body }
 | 
						|
              return new apiDeps.Promise(function (resolve, reject) {
 | 
						|
                twilio.sendSms(opts, function (err, resp) {
 | 
						|
                  if (err) {
 | 
						|
                    reject(err);
 | 
						|
                    return;
 | 
						|
                  }
 | 
						|
 | 
						|
                  resolve(resp);
 | 
						|
                });
 | 
						|
              });
 | 
						|
            }
 | 
						|
 | 
						|
            return {
 | 
						|
              sms: sms
 | 
						|
            , mms: function () { throw new Error('MMS Not Implemented'); }
 | 
						|
            };
 | 
						|
          });
 | 
						|
        }
 | 
						|
 | 
						|
        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 = {
 | 
						|
          //
 | 
						|
          // Capabilities for APIs
 | 
						|
          //
 | 
						|
          'settings.site@daplie.com': manageSiteSettings
 | 
						|
        , 'email@daplie.com': mailgunMail     // whichever mailer
 | 
						|
        , 'mailer@daplie.com': mailgunMail    // whichever mailer
 | 
						|
        , 'mailgun@daplie.com': mailgunMail   // specifically mailgun
 | 
						|
        , 'tel@daplie.com': daplieTel         // whichever telephony service
 | 
						|
        , 'twilio@daplie.com': twilioTel      // specifically twilio
 | 
						|
        , 'com.daplie.tel.twilio': twilioTel  // deprecated alias
 | 
						|
 | 
						|
        , 'getresponse@daplie.com': getResponseList
 | 
						|
          //
 | 
						|
          // Webhook Parsers
 | 
						|
          //
 | 
						|
        //, 'mailgun.urlencoded@daplie.com': function (req, res, next) { ... }
 | 
						|
        , 'mailgun.parsers@daplie.com': function (req, res, next) {
 | 
						|
            var chunks = [];
 | 
						|
 | 
						|
            req.on('data', function (chunk) {
 | 
						|
              chunks.push(chunk);
 | 
						|
            });
 | 
						|
            req.on('end', function () {
 | 
						|
            });
 | 
						|
 | 
						|
            function verify() {
 | 
						|
              var body = req.body;
 | 
						|
              var mailconf = siteConfig['mailgun.org'];
 | 
						|
 | 
						|
              if (!body.timestamp) {
 | 
						|
                console.log('mailgun parser req.headers');
 | 
						|
                console.log(req.headers);
 | 
						|
                chunks.forEach(function (datum) {
 | 
						|
                  console.log('Length:', datum.length);
 | 
						|
                  //console.log(datum.toString('utf8'));
 | 
						|
                });
 | 
						|
                console.log('weird body');
 | 
						|
                console.log(body);
 | 
						|
              }
 | 
						|
 | 
						|
              if (!validateMailgun(mailconf.apiKey, body.timestamp, body.token, body.signature)) {
 | 
						|
                console.error('Request came, but not from Mailgun');
 | 
						|
                console.error(req.url);
 | 
						|
                console.error(req.headers);
 | 
						|
                res.send({ error: { message: 'Invalid signature. Are you even Mailgun?' } });
 | 
						|
                return;
 | 
						|
              }
 | 
						|
 | 
						|
              next();
 | 
						|
            }
 | 
						|
 | 
						|
            if (/urlencoded/.test(req.headers['content-type'])) {
 | 
						|
              console.log('urlencoded');
 | 
						|
              bodyParserMailgun(req, res, verify);
 | 
						|
            }
 | 
						|
            else if (/multipart/.test(req.headers['content-type'])) {
 | 
						|
              console.log('multipart');
 | 
						|
              bodyMultiParserMailgun(req, res, verify);
 | 
						|
            }
 | 
						|
            else {
 | 
						|
              console.log('no parser');
 | 
						|
              next();
 | 
						|
            }
 | 
						|
          }
 | 
						|
        , 'twilio.urlencoded@daplie.com': function (req, res, next) {
 | 
						|
            // TODO null for res and Promise instead of next?
 | 
						|
            return bodyParserTwilio(req, res, function () {
 | 
						|
              var signature = req.headers['x-twilio-signature'];
 | 
						|
              var auth = siteConfig['twilio.com'].live.auth;
 | 
						|
              var fullUrl = 'https://' + req.headers.host + req._walnutOriginalUrl;
 | 
						|
              var validSig = Twilio.validateRequest(auth, signature, fullUrl, req.body);
 | 
						|
              /*
 | 
						|
              console.log('Twilio Signature Check');
 | 
						|
              console.log('auth', auth);
 | 
						|
              console.log('sig', signature);
 | 
						|
              console.log('fullUrl', fullUrl);
 | 
						|
              console.log(req.body);
 | 
						|
              console.log('valid', validSig);
 | 
						|
              */
 | 
						|
              if (!validSig) {
 | 
						|
                res.statusCode = 401;
 | 
						|
                res.setHeader('Content-Type', 'text/xml');
 | 
						|
                res.end('<Error>Invalid signature. Are you even Twilio?</Error>');
 | 
						|
                return;
 | 
						|
              }
 | 
						|
              // TODO session via db req.body.CallId req.body.smsId
 | 
						|
              next();
 | 
						|
            });
 | 
						|
          }
 | 
						|
        };
 | 
						|
        req.getSiteCapability = function (capname, opts, b, c) {
 | 
						|
          if (caps[capname]) {
 | 
						|
            return caps[capname](opts, b, c);
 | 
						|
          }
 | 
						|
          if (siteConfig[capname]) {
 | 
						|
            var service = siteConfig[capname].service || siteConfig[capname];
 | 
						|
            if (caps[service]) {
 | 
						|
              return caps[service](opts, b, c);
 | 
						|
            }
 | 
						|
          }
 | 
						|
          return apiDeps.Promise.reject(
 | 
						|
            new Error("['" + req.clientApiUri + '/' + pkgId + "'] "
 | 
						|
              + "capability '" + capname + "' not implemented")
 | 
						|
          );
 | 
						|
        };
 | 
						|
 | 
						|
        req._walnutOriginalUrl = req.url;
 | 
						|
        // "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello'
 | 
						|
        req.url = req.url.replace(/\/api\//, '').replace(/.*\/api\//, '').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
 | 
						|
    //
 | 
						|
    return PromiseA.resolve(pkgRestApi.create({
 | 
						|
      etcpath: xconfx.etcpath
 | 
						|
    }/*pkgConf*/, pkgDeps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
 | 
						|
 | 
						|
      //if (xconfx.debug) { console.log('[api.js] got handler'); }
 | 
						|
      myApp.use('/', function postHandler(req, res, next) {
 | 
						|
        req.url = req._walnutOriginalUrl;
 | 
						|
        next();
 | 
						|
      });
 | 
						|
 | 
						|
      localCache.pkgs[pkgId] = { pkgId: pkgId, pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
 | 
						|
 | 
						|
      pkgLinks.forEach(function (pkgLink) {
 | 
						|
        localCache.pkgs[pkgLink] = 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;
 | 
						|
          });
 | 
						|
        });
 | 
						|
      });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  // Read packages/apis/sub.sld.tld (forward dns) to find list of apis as tld.sld.sub (reverse dns)
 | 
						|
  // TODO packages/allowed_apis/sub.sld.tld (?)
 | 
						|
  // TODO auto-register org.oauth3.consumer for primaryDomain (and all sites?)
 | 
						|
  function loadRestHandler(myConf, clientUrih, pkgId) {
 | 
						|
    return PromiseA.resolve().then(function () {
 | 
						|
      if (!localCache.pkgs[pkgId]) {
 | 
						|
        return loadRestHelper(myConf, clientUrih, pkgId);
 | 
						|
      }
 | 
						|
 | 
						|
      return localCache.pkgs[pkgId];
 | 
						|
      // TODO expire require cache
 | 
						|
      /*
 | 
						|
      if (Date.now() - localCache.pkgs[pkgId].createdAt < (5 * 60 * 1000)) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      */
 | 
						|
    }, function (/*err*/) {
 | 
						|
      // TODO what kind of errors might we want to handle?
 | 
						|
      return null;
 | 
						|
    }).then(function (restPkg) {
 | 
						|
      return restPkg;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  var CORS = require('connect-cors');
 | 
						|
  var cors = CORS({ credentials: true, headers: [
 | 
						|
    'X-Requested-With'
 | 
						|
  , 'X-HTTP-Method-Override'
 | 
						|
  , 'Content-Type'
 | 
						|
  , 'Accept'
 | 
						|
  , 'Authorization'
 | 
						|
  ], methods: [ "GET", "POST", "PATCH", "PUT", "DELETE" ] });
 | 
						|
  var staleAfter = (5 * 60 * 1000);
 | 
						|
 | 
						|
  return function (req, res, next) {
 | 
						|
    cors(req, res, function () {
 | 
						|
      //if (xconfx.debug) { console.log('[api.js] after cors'); }
 | 
						|
 | 
						|
      // Canonical client names
 | 
						|
      // example.com should use api.example.com/api for all requests
 | 
						|
      // sub.example.com/api should resolve to sub.example.com
 | 
						|
      // example.com/subapp/api should resolve to example.com#subapp
 | 
						|
      // sub.example.com/subapp/api should resolve to sub.example.com#subapp
 | 
						|
      var appUri = req.hostname.replace(/^(api|assets)\./, '') + req.url.replace(/\/(api|assets)\/.*/, '/').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
 | 
						|
      // '/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/' may exist... must be a small api
 | 
						|
      var pkgId = req.url.replace(/.*\/(api|assets)\//, '').replace(/^\//, '').replace(/\/.*/, '');
 | 
						|
      var now = Date.now();
 | 
						|
      var hasBeenHandled = false;
 | 
						|
 | 
						|
      Object.defineProperty(req, 'clientUrl', {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
      , value: (req.headers.referer || ('https://' +  appUri)).replace(/\/$/, '').replace(/\?.*/, '')
 | 
						|
      });
 | 
						|
      Object.defineProperty(req, 'apiUrlPrefix', {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
      , value: 'https://' + clientApiUri + '/api/' + pkgId
 | 
						|
      });
 | 
						|
      Object.defineProperty(req, 'assetsUrlPrefix', {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
      , value: 'https://' + clientAssetsUri + '/assets/' + pkgId
 | 
						|
      });
 | 
						|
      Object.defineProperty(req, 'experienceId' /*deprecated*/, {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
      , value: clientUrih
 | 
						|
      });
 | 
						|
      Object.defineProperty(req, 'clientApiUri', {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
      , value: clientApiUri
 | 
						|
      });
 | 
						|
      Object.defineProperty(req, 'clientAssetsUri', {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
      , value: clientAssetsUri
 | 
						|
      });
 | 
						|
      Object.defineProperty(req, 'apiId', {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
      , value: pkgId
 | 
						|
      });
 | 
						|
 | 
						|
      Object.defineProperty(req, 'clientUrih', {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
        // TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app
 | 
						|
        // (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
 | 
						|
        // NOTE: probably best to alias the name logically
 | 
						|
      , value: clientUrih
 | 
						|
      });
 | 
						|
      Object.defineProperty(req, 'pkgId', {
 | 
						|
        enumerable: true
 | 
						|
      , configurable: false
 | 
						|
      , writable: false
 | 
						|
      , value: pkgId
 | 
						|
      });
 | 
						|
 | 
						|
      // TODO cache permission (although the FS is already cached, NBD)
 | 
						|
      var promise = isThisClientAllowedToUseThisPkg(req, xconfx, clientUrih, pkgId).then(function (yes) {
 | 
						|
        //if (xconfx.debug) { console.log('[api.js] azp is allowed?', yes); }
 | 
						|
        if (!yes) {
 | 
						|
          notConfigured(req, res);
 | 
						|
          return null;
 | 
						|
        }
 | 
						|
 | 
						|
        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);
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (localCache.rests[pkgId]) {
 | 
						|
          if (handleWithHandler()) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          hasBeenHandled = true;
 | 
						|
 | 
						|
          if (now - localCache.rests[pkgId].createdAt > staleAfter) {
 | 
						|
            localCache.rests[pkgId] = null;
 | 
						|
            localCache.assets[pkgId] = null;
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (!localCache.rests[pkgId]) {
 | 
						|
          //return doesThisPkgExist
 | 
						|
 | 
						|
          //if (xconfx.debug) { console.log('[api.js] before rest handler'); }
 | 
						|
          return loadRestHandler(xconfx, clientUrih, pkgId).then(function (myHandler) {
 | 
						|
            if (!myHandler) {
 | 
						|
              //if (xconfx.debug) { console.log('[api.js] not configured'); }
 | 
						|
              notConfigured(req, res);
 | 
						|
              return;
 | 
						|
            }
 | 
						|
 | 
						|
            localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now };
 | 
						|
            localCache.assets[pkgId] = { handler: myHandler.assetsHandler, createdAt: now };
 | 
						|
            if (!hasBeenHandled) {
 | 
						|
              if (handleWithHandler()) {
 | 
						|
                return;
 | 
						|
              }
 | 
						|
            }
 | 
						|
          });
 | 
						|
        }
 | 
						|
      });
 | 
						|
 | 
						|
      rejectableRequest(req, res, promise, "[walnut@daplie.com] load api package");
 | 
						|
    });
 | 
						|
  };
 | 
						|
};
 |