├── .gitignore ├── config-example.js ├── package.json ├── example.js ├── lib ├── tools.js ├── recurly.js └── transparent.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | config.js 2 | test.js 3 | -------------------------------------------------------------------------------- /config-example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | API_USERNAME: '', 3 | API_PASSWORD: '', 4 | PRIVATE_KEY: '', 5 | SUBDOMAIN: '', 6 | ENVIRONMENT: 'sandbox', 7 | DEBUG: false 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "node-recurly" 2 | , "description" : "Library for accessing the api for the Recurly recurring billing service." 3 | , "keywords" : [ "recurly", "e-commerce", "recurring billing" ] 4 | , "version" : "0.1.1" 5 | , "homepage" : "https://github.com/robrighter/node-recurly" 6 | , "author" : "Rob Righter (http://github.com/robrighter)" 7 | , "contributors" : 8 | [ "Rob Righter (http://github.com/robrighter)" ] 9 | , "repository" : 10 | { "type" : "git" 11 | , "url" : "git://github.com/robrighter/node-recurly.git" 12 | } 13 | , "bugs" : 14 | { "web" : "https://github.com/robrighter/node-recurly/issues" } 15 | , "directories" : { "lib" : "./lib" } 16 | , "main" : "./lib/recurly.js" 17 | , "dependencies" : { 18 | "xml2js": ">= 0.1.5" 19 | } 20 | , "engines": { "node": ">= 0.4.1 < 0.7.0" } 21 | } -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var Recurly = require('./lib/recurly'); 3 | var callback = function(result){ 4 | console.log(sys.inspect(result)); 5 | }; 6 | 7 | recurly = new Recurly(require('./config')); 8 | // recurly.charges.chargeAccount('robrighter@gmail.com',{ 9 | // amount_in_cents: '850', 10 | // description: 'testing the charge' 11 | // },callback); 12 | 13 | //recurly.charges.listAll('robrighter@gmail.com',callback); 14 | 15 | // recurly.coupons.redeemOnAccount('robrighter@gmail.com', { 16 | // coupon_code: '50PERCENTOFF' 17 | // },callback); 18 | 19 | //recurly.coupons.removeFromAccount('robrighter@gmail.com',callback); 20 | 21 | //recurly.coupons.getAssociatedWithAccount('robrighter@gmail.com',callback); 22 | 23 | // recurly.credits.creditAccount('robrighter@gmail.com',{ 24 | // amount_in_cents: 550, 25 | // description: 'Cutting you a break 3' 26 | // }, callback); 27 | 28 | //recurly.credits.listAll('robrighter@gmail.com',callback); 29 | 30 | //recurly.invoices.getAssociatedWithAccount('robrighter@gmail.com',callback); 31 | //recurly.invoices.get('7aba9e26feae42c1acb078fea1024c6f',callback); 32 | //recurly.invoices.invoiceAccount('robrighter@gmail.com',callback); 33 | 34 | //recurly.subscriptions.getAssociatedWithAccount('robrighter@gmail.com',callback); 35 | 36 | //recurly.subscriptionPlans.listAll(callback); 37 | //recurly.subscriptions.refund('robrighter@gmail.com',callback,'partial'); 38 | 39 | // recurly.subscriptions.create('robrighter@gmail.com',{ 40 | // plan_code: 'test-plan', 41 | // quantity: 1, 42 | // account: { 43 | // billing_info: { 44 | // first_name: 'berty', 45 | // last_name: 'tester', 46 | // address1: '123 my street', 47 | // address2: '', 48 | // city: 'Chattanooga', 49 | // state: 'TN', 50 | // zip: '37408', 51 | // country: 'US' 52 | // } 53 | // } 54 | // },callback); 55 | 56 | // recurly.transactions.createImmediateOneTimeTransaction('robrighter@gmail.com',{ 57 | // account:{ 58 | // account_code: 'robrighter' 59 | // }, 60 | // amount_in_cents: 600, 61 | // description: 'just testing things out' 62 | // },callback) 63 | 64 | var data = { 65 | 'account[account_code]': 'demo-1301435036', 66 | 'account[username]': 'username123', 67 | 'redirect_url': 'http://localhost/subscribe.php', 68 | 'subscription[plan_code]': 'test-plan' 69 | } 70 | 71 | console.log(recurly.transparent.subscribeUrl()); 72 | 73 | console.log(recurly.transparent.hidden_field(data)); 74 | 75 | recurly.transparent.getResults('dfd82a741b3e5f15e32439fb66f7696046138105',//confirm 76 | '31c6f6c96f3045cdbc126934295e889b',//result 77 | '422',//status 78 | 'subscription',//type 79 | callback) -------------------------------------------------------------------------------- /lib/tools.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | var https = require('https'); 4 | var xml2js = require('xml2js'); 5 | 6 | module.exports = function(config){ 7 | 8 | var that = this; 9 | ///////////////////////////////////////// 10 | // REQUESTOR // 11 | ///////////////////////////////////////// 12 | 13 | this.request = function(endpoint, method, callback, data){ 14 | if(data){ 15 | data = '\n' + data; 16 | } 17 | var options = { 18 | host: config.RECURLY_HOST, 19 | port: 443, 20 | path: endpoint, 21 | method: method, 22 | headers: { 23 | Authorization: "Basic "+(new Buffer(config.API_USERNAME+":"+config.API_PASSWORD)).toString('base64'), 24 | Accept: 'application/xml', 25 | 'Content-Length' : (data) ? data.length : 0 26 | } 27 | }; 28 | 29 | if(method.toLowerCase() == 'post' || method.toLowerCase() == 'put' ){ 30 | options.headers['Content-Type'] = 'application/xml'; 31 | that.debug(data); 32 | } 33 | that.debug(options); 34 | var req = https.request(options, function(res) { 35 | 36 | var responsedata = ''; 37 | res.on('data', function(d) { 38 | responsedata+=d; 39 | }); 40 | res.on('end', function(){ 41 | responsedata = that.trim(responsedata); 42 | that.debug('Response is: ' + res.statusCode); 43 | that.debug(responsedata); 44 | try{ 45 | var toreturn = {status: 'ok', data: '' }; 46 | if((res.statusCode == 404) || (res.statusCode == 422) || (res.statusCode == 500) || (res.statusCode == 412)){ 47 | toreturn.status = 'error'; 48 | that.parseXML(responsedata, function(result){ 49 | toreturn.data = result; 50 | callback(toreturn); 51 | }); 52 | } 53 | else if(res.statusCode >= 400){ 54 | toreturn.status = 'error'; 55 | toreturn.data = res.statusCode; 56 | toreturn.additional = responsedata; 57 | callback(toreturn); 58 | } 59 | else{ 60 | if(responsedata != ''){ 61 | that.parseXML(responsedata, function(result){ 62 | toreturn.data = result; 63 | callback(toreturn); 64 | }); 65 | } 66 | else{ 67 | callback({status: 'ok', description: res.statusCode }); 68 | } 69 | } 70 | return; 71 | } 72 | catch(e){ 73 | throw e 74 | callback({status: 'error', description: e }); 75 | } 76 | }); 77 | }); 78 | if(data){ 79 | req.write(data); 80 | } 81 | req.end(); 82 | req.on('error', function(e) { 83 | callback({status: 'error', description: e }); 84 | }); 85 | } 86 | 87 | 88 | ///////////////////////////////////// 89 | // UTILS // 90 | ///////////////////////////////////// 91 | 92 | this.debug = function(s){ 93 | if(config.DEBUG){ 94 | console.log(s); 95 | } 96 | } 97 | 98 | this.js2xml = function js2xml(js, wraptag){ 99 | if(js instanceof Object){ 100 | return js2xml(Object.keys(js).map(function(key){return js2xml(js[key], key);}).join('\n'), wraptag); 101 | }else{return ((wraptag)?'<'+ wraptag+'>' : '' ) + js + ((wraptag)?'' : '' );} 102 | } 103 | 104 | this.parseXML = function(xml, callback){ 105 | var parser = new xml2js.Parser(); 106 | parser.addListener('end', function(result) { 107 | callback(result); 108 | }); 109 | parser.parseString(xml); 110 | } 111 | 112 | this.trim = function(str) { 113 | str = str.replace(/^\s+/, ''); 114 | for (var i = str.length - 1; i >= 0; i--) { 115 | if (/\S/.test(str.charAt(i))) { 116 | str = str.substring(0, i + 1); 117 | break; 118 | } 119 | } 120 | return str; 121 | } 122 | 123 | this.htmlEscape = function(html) { 124 | return String(html) 125 | .replace(/&/g, '&') 126 | .replace(/"/g, '"') 127 | .replace(//g, '>'); 129 | }; 130 | 131 | this.urlEncode = function(toencode){ 132 | return escape(toencode).replace(/\//g,'%2F').replace(/\+/g, '%2B'); 133 | } 134 | 135 | this.traverse = function traverse(obj,func, parent) { 136 | for (i in obj){ 137 | func.apply(this,[i,obj[i],parent]); 138 | if (obj[i] instanceof Object && !(obj[i] instanceof Array)) { 139 | traverse(obj[i],func, i); 140 | } 141 | } 142 | } 143 | 144 | this.getPropertyRecursive = function(obj, property){ 145 | var acc = []; 146 | this.traverse(obj, function(key, value, parent){ 147 | if(key === property){ 148 | acc.push({parent: parent, value: value}); 149 | } 150 | }); 151 | return acc; 152 | } 153 | 154 | } 155 | 156 | })(); 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node-Recurly 2 | =============== 3 | 4 | node-recurly is a node.js library for using the recurly recurring billing service. This library is intended to follow very closely the recurly documentation found at: 5 | http://docs.recurly.com/ 6 | 7 | Installation 8 | =============== 9 | 10 | npm install node-recurly 11 | 12 | add a config file to your project that has contents similar to: 13 | 14 | module.exports = { 15 | API_USERNAME: 'secret', 16 | API_PASSWORD: 'secret', 17 | PRIVATE_KEY: 'secret', 18 | SUBDOMAIN: '[your_account]', 19 | ENVIRONMENT: 'sandbox', 20 | DEBUG: false 21 | }; 22 | 23 | 24 | Usage 25 | =============== 26 | 27 | var Recurly = require('node-recurly'); 28 | var recurly = new Recurly(require('./config')); 29 | 30 | After that, just call the methods below: 31 | 32 | 33 | Accounts 34 | =============== 35 | http://docs.recurly.com/api/accounts 36 | 37 | recurly.accounts.create(details, callback) 38 | 39 | recurly.accounts.update(accountcode, details, callback) 40 | 41 | 42 | recurly.accounts.get(accountcode, callback) 43 | 44 | 45 | recurly.accounts.close(accountcode, callback) 46 | 47 | 48 | recurly.accounts.listAll(callback, filter) 49 | 50 | Billing Information 51 | =============== 52 | http://docs.recurly.com/transparent-post/billing-info 53 | 54 | recurly.billingInfo.update(accountcode, details, callback) 55 | 56 | 57 | recurly.billingInfo.get(accountcode, callback) 58 | 59 | 60 | recurly.billingInfo.delete(accountcode, callback) 61 | 62 | 63 | Charges 64 | =============== 65 | http://docs.recurly.com/api/charges 66 | 67 | recurly.charges.listAll(accountcode, callback, filter) 68 | 69 | 70 | recurly.charges.chargeAccount(accountcode, details, callback) 71 | 72 | 73 | Coupons 74 | =============== 75 | http://docs.recurly.com/api/coupons 76 | 77 | recurly.coupons.getAssociatedWithAccount(accountcode, callback) 78 | 79 | 80 | 81 | NOTE: Redeem coupon with subscription not added here since it is a duplication of the subscription creation method 82 | 83 | recurly.coupons.redeemOnAccount(accountcode, details, callback) 84 | 85 | 86 | recurly.coupons.removeFromAccount(accountcode, callback) 87 | 88 | 89 | Charges 90 | =============== 91 | http://docs.recurly.com/api/credits 92 | 93 | recurly.credits.listAll(accountcode, callback) 94 | 95 | 96 | recurly.credits.creditAccount(accountcode, details, callback) 97 | 98 | 99 | Invoices 100 | =============== 101 | http://docs.recurly.com/api/invoices 102 | 103 | recurly.invoices.getAssociatedWithAccount(accountcode, callback) 104 | 105 | 106 | recurly.invoices.get(invoiceid, callback) 107 | 108 | 109 | recurly.invoices.invoiceAccount(accountcode, callback) 110 | 111 | 112 | 113 | Subscriptions 114 | =============== 115 | http://docs.recurly.com/api/subscriptions 116 | 117 | recurly.subscriptions.getAssociatedWithAccount(accountcode, callback) 118 | 119 | 120 | 121 | **NOTE Certain uses of this method will have implications on PCI compliance because this 122 | function requires access to and transmission of customer credit card information. 123 | 124 | recurly.subscriptions.create(accountcode, details, callback) 125 | 126 | 127 | details requires a timeframe(now, renew) 128 | 129 | recurly.subscriptions.update(accountcode, details, callback) 130 | 131 | 132 | refundtype can be 'partial', 'full' or 'none' 133 | 134 | recurly.subscriptionsrefund(accountcode, callback, refundtype) 135 | 136 | 137 | 138 | Subscription Plans 139 | =============== 140 | http://docs.recurly.com/api/subscription-plans 141 | 142 | recurly.subscriptionPlans.listAll(callback) 143 | 144 | 145 | recurly.subscriptionPlans.get(plancode, callback) 146 | 147 | 148 | 149 | Create, Update, and Delete are not implemented because the reculy documentation indicates them as advanced cases 150 | 151 | 152 | Transactions 153 | =============== 154 | http://docs.recurly.com/api/transactions 155 | 156 | recurly.transactions.listAll(accountcode, callback, filter) 157 | 158 | 159 | recurly.transactions.getAssociatedWithAccount(accountcode, callback) 160 | 161 | 162 | recurly.transactions.get(transactionid, callback) 163 | 164 | 165 | recurly.transactions.void(transactionid, callback) 166 | 167 | 168 | recurly.transactions.refund(transactionid, callback, amount) 169 | 170 | NOTE Certain uses of this method will have implications on PCI compliance because this 171 | function requires access to and transmission of customer credit card information. 172 | 173 | recurly.transactions.createImmediateOneTimeTransaction(accountcode, details, callback) 174 | 175 | Transparent Post 176 | ================== 177 | http://docs.recurly.com/transparent-post/basics 178 | 179 | recurly.transparent.billingInfoUrl 180 | 181 | recurly.transparent.subscribeUrl 182 | 183 | recurly.transparent.transactionUrl 184 | 185 | recurly.transparent.hidden_field(data) 186 | 187 | recurly.transparent.getResults(confirm, result, status, type, callback){ 188 | 189 | recurly.transparent..getFormValuesFromResult(result, type) -------------------------------------------------------------------------------- /lib/recurly.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var https = require('https'); 3 | var xml2js = require('xml2js'); 4 | var Transparent = require('./transparent'); 5 | var Tools = require('./tools'); 6 | module.exports = function(config){ 7 | 8 | //add additional info to the config object 9 | config.RECURLY_HOST = 'api-' + config.ENVIRONMENT + '.recurly.com' 10 | config.RECURLY_BASE_URL = 'https://' + config.RECURLY_HOST; 11 | 12 | var t = new Tools(config); 13 | var transparent = new Transparent(config); 14 | 15 | //http://docs.recurly.com/api/accounts 16 | this.accounts = { 17 | create: function(details, callback){ 18 | t.request('/accounts', 'POST', callback, t.js2xml(details,'account')); 19 | }, 20 | update: function(accountcode, details, callback){ 21 | t.request('/accounts/'+accountcode, 'PUT', callback, t.js2xml(details,'account')); 22 | }, 23 | get: function(accountcode, callback){ 24 | t.request('/accounts/'+accountcode, 'GET', callback); 25 | }, 26 | close: function(accountcode, callback){ 27 | t.request('/accounts/'+accountcode, 'DELETE', callback); 28 | }, 29 | listAll: function(callback, filter){ 30 | t.request('/accounts' + ((filter)? '?show='+filter : '' ), 'GET', callback); 31 | } 32 | } 33 | 34 | //http://docs.recurly.com/transparent-post/billing-info 35 | this.billingInfo = { 36 | update: function(accountcode, details, callback){ 37 | t.request('/accounts/'+accountcode+'/billing_info', 'PUT', callback, t.js2xml(details,'billing_info')); 38 | }, 39 | get: function(accountcode, callback){ 40 | t.request('/accounts/'+accountcode+'/billing_info', 'GET', callback); 41 | }, 42 | delete: function(accountcode, callback){ 43 | t.request('/accounts/'+accountcode+'/billing_info', 'DELETE', callback); 44 | } 45 | } 46 | 47 | //http://docs.recurly.com/api/charges 48 | this.charges = { 49 | listAll: function(accountcode, callback, filter){ 50 | t.request('/accounts/' + accountcode +'/charges'+ ((filter)? '?show='+filter : '' ), 'GET', callback); 51 | }, 52 | chargeAccount: function(accountcode, details, callback){ 53 | t.request('/accounts/' + accountcode + '/charges', 'POST', callback, t.js2xml(details,'charge')); 54 | } 55 | } 56 | 57 | //http://docs.recurly.com/api/coupons 58 | this.coupons = { 59 | getAssociatedWithAccount: function(accountcode, callback){ 60 | t.request('/accounts/' + accountcode +'/coupon' , 'GET', callback); 61 | }, 62 | //NOTE: Redeem coupon with subscription not added here since it is a duplication of the subscription creation method 63 | redeemOnAccount: function(accountcode, details, callback){ 64 | t.request('/accounts/' + accountcode + '/coupon', 'POST', callback, t.js2xml(details,'coupon')); 65 | }, 66 | removeFromAccount: function(accountcode, callback){ 67 | t.request('/accounts/'+accountcode+'/coupon', 'DELETE', callback); 68 | } 69 | 70 | } 71 | 72 | //http://docs.recurly.com/api/credits 73 | this.credits = { 74 | listAll: function(accountcode, callback){ 75 | t.request('/accounts/' + accountcode +'/credits', 'GET', callback); 76 | }, 77 | creditAccount: function(accountcode, details, callback){ 78 | t.request('/accounts/' + accountcode + '/credits', 'POST', callback, t.js2xml(details,'credit')); 79 | } 80 | } 81 | 82 | //http://docs.recurly.com/api/invoices 83 | this.invoices = { 84 | getAssociatedWithAccount: function(accountcode, callback){ 85 | t.request('/accounts/' + accountcode +'/invoices', 'GET', callback); 86 | }, 87 | get: function(invoiceid, callback){ 88 | t.request('/invoices/' + invoiceid, 'GET', callback); 89 | }, 90 | invoiceAccount: function(accountcode, callback){ 91 | t.request('/accounts/' + accountcode + '/invoices', 'POST', callback); 92 | } 93 | } 94 | 95 | //http://docs.recurly.com/api/subscriptions 96 | this.subscriptions = { 97 | getAssociatedWithAccount: function(accountcode, callback){ 98 | t.request('/accounts/' + accountcode +'/subscription', 'GET', callback); 99 | }, 100 | //**NOTE Certain uses of this method will have implications on PCI compliance because this 101 | /// function requires access to and transmission of customer credit card information. 102 | create: function(accountcode, details, callback){ 103 | t.request('/accounts/' + accountcode + '/subscription', 'POST', callback, t.js2xml(details,'subscription')); 104 | }, 105 | update: function(accountcode, details, callback) { 106 | t.request('/accounts/' + accountcode +'/subscription', 'PUT', callback, t.js2xml(details,'subscription')); 107 | }, 108 | reactivate: function(accountcode, details, callback) { 109 | t.request('/accounts/' + accountcode +'/subscription/reactivate', 'POST', callback, t.js2xml(details,'subscription')); 110 | }, 111 | // refundtype can be 'partial', 'full' or 'none' 112 | refund: function(accountcode, callback, refundtype){ 113 | t.request('/accounts/' + accountcode +'/subscription'+ ((refundtype)? '?refund='+refundtype : '' ), 'DELETE', callback); 114 | } 115 | } 116 | 117 | //http://docs.recurly.com/api/subscription-plans 118 | this.subscriptionPlans = { 119 | listAll: function(callback){ 120 | t.request('/company/plans', 'GET', callback); 121 | }, 122 | get: function(plancode, callback){ 123 | t.request('/company/plans/' + plancode, 'GET', callback); 124 | } 125 | //Create, Update, and Delete are not implemented because the reculy documentation indicates them as advanced cases 126 | } 127 | 128 | //http://docs.recurly.com/api/transactions 129 | this.transactions = { 130 | listAll: function(accountcode, callback, filter){ 131 | t.request('/transactions'+ ((filter)? '?show='+filter : '' ), 'GET', callback); 132 | }, 133 | getAssociatedWithAccount: function(accountcode, callback){ 134 | t.request('/accounts/' + accountcode +'/transactions', 'GET', callback); 135 | }, 136 | get: function(transactionid, callback){ 137 | t.request('/transactions/' + transactionid, 'GET', callback); 138 | }, 139 | void: function(transactionid, callback){ 140 | t.request('/transactions/' + transactionid+ '?action=void', 'DELETE', callback); 141 | }, 142 | refund: function(transactionid, callback, amount){ 143 | t.request('/transactions/' + transactionid+ '?action=refund&amount='+amount, 'DELETE', callback); 144 | }, 145 | //**NOTE Certain uses of this method will have implications on PCI compliance because this 146 | /// function requires access to and transmission of customer credit card information. 147 | createImmediateOneTimeTransaction: function(accountcode, details, callback){ 148 | t.request('/transactions', 'POST', callback, t.js2xml(details,'transaction')); 149 | } 150 | } 151 | 152 | //https://github.com/recurly/recurly-client-ruby/blob/master/lib/recurly/transparent.rb 153 | this.transparent = transparent; 154 | 155 | 156 | }//end class 157 | })(); 158 | -------------------------------------------------------------------------------- /lib/transparent.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | var qs = require('querystring'); 4 | var crypto = require('crypto'); 5 | var Tools = require('./tools'); 6 | 7 | module.exports = function(config){ 8 | 9 | var t = new Tools(config); 10 | 11 | var TRANSPARENT_POST_BASE_URL = config.RECURLY_BASE_URL + '/transparent/' + config.SUBDOMAIN; 12 | var BILLING_INFO_URL = TRANSPARENT_POST_BASE_URL + '/billing_info'; 13 | var SUBSCRIBE_URL = TRANSPARENT_POST_BASE_URL + '/subscription'; 14 | var TRANSACTION_URL = TRANSPARENT_POST_BASE_URL + '/transaction'; 15 | 16 | t.debug('============================'); 17 | t.debug(TRANSPARENT_POST_BASE_URL); 18 | t.debug(BILLING_INFO_URL); 19 | t.debug(SUBSCRIBE_URL); 20 | t.debug(TRANSACTION_URL); 21 | t.debug('============================'); 22 | 23 | this.billingInfoUrl = function(){return BILLING_INFO_URL}; 24 | this.subscribeUrl = function(){return SUBSCRIBE_URL}; 25 | this.transactionUrl = function(){return TRANSACTION_URL}; 26 | 27 | this.hidden_field = function(data){ 28 | return ''; 29 | } 30 | 31 | this.getResults = function(confirm, result, status, type, callback){ 32 | validateQueryString(confirm, type, status, result) 33 | t.request('/transparent/results/' + result, 'GET', callback); 34 | } 35 | 36 | this.getFormValuesFromResult = function getFormValuesFromResult(result, type){ 37 | var fields = {}; 38 | var errors = []; 39 | t.traverse(result.data,function(key, value, parent){ 40 | var shouldprint = false; 41 | var toprint = '' 42 | if(value instanceof Object){ 43 | if(Object.keys(value).length === 0){ 44 | shouldprint = true; 45 | toprint = '' 46 | } 47 | if(Object.hasOwnProperty('@') || Object.hasOwnProperty('#')){ 48 | shouldprint = true; 49 | toprint = value; 50 | } 51 | if(value instanceof Array){ 52 | shouldprint = true; 53 | toprint = value; 54 | } 55 | } 56 | else if(!(value instanceof Object)){ 57 | shouldprint = true; 58 | toprint = value; 59 | if(key === 'error'){ 60 | errors.push( { field: '_general', reason: value } ); 61 | shouldprint = false; 62 | } 63 | } 64 | if(key === "@" || key === '#'){ 65 | shouldprint = false; 66 | } 67 | if(parent === "@" || parent === '#'){ 68 | shouldprint = false; 69 | } 70 | 71 | if(!parent){ 72 | switch(type){ 73 | case 'subscribe': 74 | parent = 'account'; 75 | break; 76 | case 'billing_info': 77 | parent = 'billing_info'; 78 | } 79 | } 80 | 81 | if(key === 'errors'){ 82 | shouldprint = false; 83 | errors = errors.concat(processErrors(value, parent)); 84 | } 85 | 86 | if(shouldprint){ 87 | var toadd = {}; 88 | try{ 89 | fields[parent+'['+key+']'] = toprint.replace(/'/g, '''); 90 | } 91 | catch(e){ 92 | t.debug('GET FIELDS: could not process: ' + parent+'['+key+'] : ' + toprint ); 93 | } 94 | } 95 | }); 96 | errors = handleFuzzyLogicSpecialCases(errors); 97 | return {fields: fields, errors: errors}; 98 | } 99 | 100 | function processErrors(errors, parent){ 101 | var acc = []; 102 | var processSingleError = function(e){ 103 | try{ 104 | acc.push({ 105 | field: parent+'['+e['@'].field+']', 106 | reason: e['#'].replace(/'/g, ''') 107 | }); 108 | } 109 | catch(err){ 110 | t.debug('Could not process listed error: ' + e); 111 | } 112 | }; 113 | errors.forEach(function(item){ 114 | if(item instanceof Array){ 115 | //loop through the error list 116 | item.forEach(processSingleError) 117 | } 118 | else{ 119 | //its a single error so grab it out 120 | try{ 121 | processSingleError(item); 122 | } 123 | catch(err){ 124 | t.debug('Could not process single error: ' + item); 125 | } 126 | } 127 | }); 128 | return acc; 129 | } 130 | 131 | function encoded_data(data){ 132 | verify_required_fields(data); 133 | var query_string = make_query_string(data); 134 | var validation_string = hash(query_string); 135 | return validation_string + "|" + query_string; 136 | } 137 | 138 | function verify_required_fields(params){ 139 | if(!params.hasOwnProperty('redirect_url')){ 140 | throw "Missing required parameter: redirect_url"; 141 | } 142 | if(!params.hasOwnProperty('account[account_code]')){ 143 | throw "Missing required parameter: account[account_code]"; 144 | } 145 | } 146 | 147 | function make_query_string(params){ 148 | params.time = makeDate(); 149 | return buildQueryStringFromSortedObject(makeSortedObject(params, true)); 150 | } 151 | 152 | function makeDate(){ 153 | var d = new Date(); 154 | var addleadingzero = function(n){ return (n<10)?'0'+n:''+n }; 155 | return d.getUTCFullYear() + '-' + 156 | addleadingzero(d.getUTCMonth()+1) + '-' + 157 | addleadingzero(d.getUTCDate()) + 'T' + 158 | addleadingzero(d.getUTCHours()) + ':' + 159 | addleadingzero(d.getUTCMinutes()) + ':' + 160 | addleadingzero(d.getUTCSeconds()) + 'Z'; 161 | } 162 | 163 | function hash(data) { 164 | //get the sha1 of the private key in binary 165 | var shakey = crypto.createHash('sha1'); 166 | shakey.update(config.PRIVATE_KEY); 167 | shakey = shakey.digest('binary'); 168 | //now make an hmac and return it as hex 169 | var hmac = crypto.createHmac('sha1', shakey); 170 | hmac.update(data); 171 | return hmac.digest('hex'); 172 | //php: 03021207ad681f2ea9b9e1fc20ac7ae460d8d988 <== Yes this sign is identical to the php version 173 | //node: 03021207ad681f2ea9b9e1fc20ac7ae460d8d988 174 | } 175 | 176 | function buildQueryStringFromSortedObject(params){ 177 | return params.map(function(p){ 178 | return escape(p.key) + "=" + t.urlEncode(p.value); 179 | }).join('&'); 180 | } 181 | 182 | function makeSortedObject(obj, casesensitive){ 183 | return Object.keys(obj).map(function(key){ 184 | return {key: key, value: obj[key]}; 185 | }).sort(function(a,b){ 186 | return (casesensitive? a.key : a.key.toLowerCase()) > (casesensitive? b.key : b.key.toLowerCase()); 187 | }); 188 | } 189 | 190 | //Used for validating return params from Recurly 191 | function validateQueryString(confirm, type, status, result_key) 192 | { 193 | var values = { 194 | result: result_key, 195 | status: status, 196 | type: type 197 | } 198 | var query_values = buildQueryStringFromSortedObject(makeSortedObject(values, true)); 199 | hashed_values = hash(query_values); 200 | 201 | if(hashed_values !== confirm) { 202 | throw "Error: Forged query string"; 203 | } 204 | return true; 205 | } 206 | 207 | function handleFuzzyLogicSpecialCases(errors){ 208 | var toreturn = [] 209 | errors.forEach(function(e){ 210 | switch(e.field){ 211 | case 'billing_info[verification_value]': 212 | toreturn.push(copyWithNewName('billing_info[credit_card][verification_value]', e)); 213 | toreturn.push(copyWithNewName('credit_card[verification_value]', e)); 214 | break; 215 | case 'credit_card[number]': 216 | toreturn.push(copyWithNewName('billing_info[credit_card][number]', e)); 217 | toreturn.push(e); 218 | break; 219 | default: 220 | toreturn.push(e); 221 | break; 222 | } 223 | }); 224 | return toreturn; 225 | } 226 | 227 | function copyWithNewName(name, error){ 228 | return {field: name,reason: error.reason}; 229 | } 230 | 231 | }//END CLASS 232 | 233 | })(); --------------------------------------------------------------------------------