├── .gitignore ├── .npmignore ├── test ├── receipts │ ├── facebook.app_token │ ├── amazon.secret │ ├── google_pub │ │ ├── iap-live │ │ └── iap-sandbox │ ├── amazon │ ├── facebook │ ├── unity_amazon │ ├── google │ ├── unity_google │ ├── windows │ ├── unity_apple │ └── apple ├── facebook.js ├── windows.js └── amazon.js ├── logo ├── 75x75.png ├── 120x120.png ├── 250X250.png ├── in-app-purchase-logo.png ├── License ├── in-app-purchase-logo.svg └── 75x75.svg ├── scripts ├── lint │ ├── index.js │ └── linter │ │ └── index.js └── detect_js_change ├── lib ├── amazonManager.js ├── verbose.js ├── responseData.js ├── util.js ├── request.js ├── async.js ├── roku.js ├── windows.js ├── amazon.js ├── googleAPI.js ├── amazon2.js ├── facebook.js └── apple.js ├── lint ├── constants.js ├── .github ├── dependabot.yml └── workflows │ ├── nodejs.yml │ └── codeql.yml ├── bin └── lint ├── .editorconfig ├── LICENSE ├── Makefile ├── package.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | issues/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | logo 3 | .editorconfig 4 | .github 5 | lint 6 | scripts 7 | -------------------------------------------------------------------------------- /test/receipts/facebook.app_token: -------------------------------------------------------------------------------- 1 | 483436115748302|d25da68caa30b617e2b4e3990cb91e4e -------------------------------------------------------------------------------- /logo/75x75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlatIO/in-app-purchase/HEAD/logo/75x75.png -------------------------------------------------------------------------------- /logo/120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlatIO/in-app-purchase/HEAD/logo/120x120.png -------------------------------------------------------------------------------- /logo/250X250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlatIO/in-app-purchase/HEAD/logo/250X250.png -------------------------------------------------------------------------------- /logo/in-app-purchase-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlatIO/in-app-purchase/HEAD/logo/in-app-purchase-logo.png -------------------------------------------------------------------------------- /test/receipts/amazon.secret: -------------------------------------------------------------------------------- 1 | 2:3qmi91l7QDyb2bvg-vt3t9KVxRjsgTwXMbWn_Wl2NQRJojLJyH55sKxxrTaYW0l-:8lPDDCnc_Y0_qEi9OxnAjg== 2 | -------------------------------------------------------------------------------- /scripts/lint/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var linter = require('./linter'); 4 | 5 | module.exports = function __lintIndex(path, packagePath, ignorelist, cb) { 6 | linter.start(path, packagePath, ignorelist, function __lintIndexOnStart(error) { 7 | if (error) { 8 | return cb(error); 9 | } 10 | cb(); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/amazonManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var amazon = require('./amazon'); 4 | var amazon2 = require('./amazon2'); 5 | 6 | module.exports.create = function (_config) { 7 | if (_config && _config.amazonAPIVersion === 2) { 8 | amazon2.readConfig(_config); 9 | return amazon2; 10 | } else { 11 | amazon.readConfig(_config); 12 | return amazon; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const lint = require('./scripts/lint'); 4 | lint(__dirname + '/lib/', __dirname + '/package.json', [], function (error) { 5 | if (error) { 6 | process.exit(1); 7 | } 8 | lint(__dirname + '/index.js', __dirname + '/package.json', [], function (error) { 9 | if (error) { 10 | process.exit(1); 11 | } 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/receipts/google_pub/iap-live: -------------------------------------------------------------------------------- 1 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgVv152mMs3kn+uvEfU1gbhf0/CcAMhInr3wvS+E4SEaDS/SLtvzeC/XQdoHRLUnOW5Q40074eL7sL0WvKG4fEpnTD4aP85HwkoYNPzW7o2h7TwpxH46pmsCZ4p3rPRNBaNObe/6aKZ9IxrTsy/pUhN5X3sD3wAbXLsCYqbNrJY1VHaoqCWFucvn7VW6GXLLojWFZnprMlTtsdHMwGeHrkJfUOOE1ArQJaVbSCzDjpnuOGh3kZGRCklLXtKk3CTzzrNFT9ONR0BftrB3VviId28HcakZ7bv4DkiRGSTz6+SmYS9UMA6xUoBW3jj7+fNWUMeZ3ds1bFiMtApr7ykw73wIDAQAB 2 | -------------------------------------------------------------------------------- /test/receipts/google_pub/iap-sandbox: -------------------------------------------------------------------------------- 1 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgVv152mMs3kn+uvEfU1gbhf0/CcAMhInr3wvS+E4SEaDS/SLtvzeC/XQdoHRLUnOW5Q40074eL7sL0WvKG4fEpnTD4aP85HwkoYNPzW7o2h7TwpxH46pmsCZ4p3rPRNBaNObe/6aKZ9IxrTsy/pUhN5X3sD3wAbXLsCYqbNrJY1VHaoqCWFucvn7VW6GXLLojWFZnprMlTtsdHMwGeHrkJfUOOE1ArQJaVbSCzDjpnuOGh3kZGRCklLXtKk3CTzzrNFT9ONR0BftrB3VviId28HcakZ7bv4DkiRGSTz6+SmYS9UMA6xUoBW3jj7+fNWUMeZ3ds1bFiMtApr7ykw73wIDAQAB 2 | -------------------------------------------------------------------------------- /test/receipts/amazon: -------------------------------------------------------------------------------- 1 | {"userId":"rqHhP8Rqdzi9r9R2hjwaD0heMRE0Ls-IlfuHKp6xhW4=","receiptId":"2:Cgegdpydx7a69L_jspvKWh1aVRllaEYd7KWAwr2hsINmo78oPmdQH0fml6rN5eWyzHqa98V5Q6r7DsP7Ehzt8y4Qwc4mI41adS10_xP_GdYvh-0nqXk6Kjkb2zzCTXNof7Him87DRfRw_RcQ2T7VUX0H0hpo-oc_5FxZ27Yqp77twtCLDEMT4stNDAO_RQjXBAp9X7YMxnVkBnpD0GpnvJO_MKQfNWqJElYTC4i6YbpPNQ1YWdEre7HCWTF_Iuxgb5rXB34toV9w8wx2aKQnpwa_j-ZjkEh7VHmZ-vo9mIjrMuEG4H_cKpXM-HnZgplk:ewP5BKbKstGWDzcvcGSrRw=="} 2 | -------------------------------------------------------------------------------- /test/receipts/facebook: -------------------------------------------------------------------------------- 1 | fXQoyoD4lMqQ8Ilo0bFg9dvbL00D4hzzi8ON-mw7l0g.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImFtb3VudCI6IjEuOTkiLCJhcHBfaWQiOiI0ODM0MzYxMTU3NDgzMDIiLCJjdXJyZW5jeSI6IlVTRCIsImlzc3VlZF9hdCI6MTU2NTE4Njk0NCwicGF5bWVudF9pZCI6IjE2MjE3MDI1NTQ2MjcwMDEiLCJwcm9kdWN0X2lkIjoidGVzdF9wcm9kdWN0IiwicHVyY2hhc2VfdGltZSI6MTU2NTE4Njk0MywicHVyY2hhc2VfdG9rZW4iOiIyMzY1NjU1NzEzNTQyNjU5IiwicXVhbnRpdHkiOiIxIiwicmVxdWVzdF9pZCI6IiIsInN0YXR1cyI6ImNvbXBsZXRlZCJ9 -------------------------------------------------------------------------------- /scripts/detect_js_change: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Do we have any javascript files changed in git 4 | files=$(git diff-index --name-only HEAD | grep ".js$"); 5 | 6 | # Loop at least once to find if there's any javascript file 7 | for file in ${files}; do 8 | if [ -f ${file} ]; then 9 | # At least a javascript file changed 10 | echo 1; 11 | exit 0; 12 | fi 13 | done 14 | 15 | # No javascript file changed 16 | 17 | echo 0; 18 | exit 0; 19 | -------------------------------------------------------------------------------- /test/receipts/unity_amazon: -------------------------------------------------------------------------------- 1 | {"Store":"AmazonApps","Payload":"{\"userId\":\"rqHhP8Rqdzi9r9R2hjwaD0heMRE0Ls-IlfuHKp6xhW4=\",\"receiptId\":\"2:Cgegdpydx7a69L_jspvKWh1aVRllaEYd7KWAwr2hsINmo78oPmdQH0fml6rN5eWyzHqa98V5Q6r7DsP7Ehzt8y4Qwc4mI41adS10_xP_GdYvh-0nqXk6Kjkb2zzCTXNof7Him87DRfRw_RcQ2T7VUX0H0hpo-oc_5FxZ27Yqp77twtCLDEMT4stNDAO_RQjXBAp9X7YMxnVkBnpD0GpnvJO_MKQfNWqJElYTC4i6YbpPNQ1YWdEre7HCWTF_Iuxgb5rXB34toV9w8wx2aKQnpwa_j-ZjkEh7VHmZ-vo9mIjrMuEG4H_cKpXM-HnZgplk:ewP5BKbKstGWDzcvcGSrRw==\"}"} 2 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // services 4 | exports.SERVICES = { 5 | UNITY: 'unity', 6 | APPLE: 'apple', 7 | GOOGLE: 'google', 8 | WINDOWS: 'windows', 9 | AMAZON: 'amazon', 10 | ROKU: 'roku', 11 | FACEBOOK: 'facebook' 12 | }; 13 | 14 | exports.UNITY = { 15 | APPLE: 'AppleAppStore', 16 | GOOGLE: 'GooglePlay', 17 | AMAZON: 'AmazonApps' 18 | }; 19 | 20 | // validation 21 | exports.VALIDATION = { 22 | SUCCESS: 0, 23 | FAILURE: 1, 24 | POSSIBLE_HACK: 2 25 | }; 26 | -------------------------------------------------------------------------------- /lib/verbose.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | var enabled = false; 6 | 7 | module.exports.setup = function (config) { 8 | enabled = (config && config.verbose === true) ? true : false; 9 | }; 10 | 11 | module.exports.log = function () { 12 | if (!enabled) { 13 | return; 14 | } 15 | var logs = []; 16 | logs.push('[' + Date.now() + '][VERBOSE]'); 17 | for (var i in arguments) { 18 | logs.push(arguments[i]); 19 | } 20 | console.log.apply(console, logs); 21 | }; 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: eslint 10 | versions: 11 | - 7.21.0 12 | - 7.23.0 13 | - dependency-name: mocha 14 | versions: 15 | - 8.3.1 16 | - dependency-name: lodash 17 | versions: 18 | - 4.17.20 19 | - dependency-name: eslint-utils 20 | versions: 21 | - 1.4.3 22 | - dependency-name: acorn 23 | versions: 24 | - 5.7.4 25 | -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const async = require('../lib/async'); 4 | const lint = require('../scripts/lint'); 5 | 6 | var root = process.cwd(); 7 | var list = []; 8 | 9 | process.chdir(root); 10 | for (var i = 2, len = process.argv.length; i < len; i++) { 11 | list.push(root + '/' + process.argv[i]); 12 | } 13 | async.forEachSeries(list, function (path, next) { 14 | lint(path, '', [], function (error) { 15 | if (error) { 16 | return next(error); 17 | } 18 | next(); 19 | }); 20 | }, function (error) { 21 | if (error) { 22 | return process.exit(1); 23 | } 24 | process.exit(0); 25 | }); 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [constants.js] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | # Matches multiple files with brace expansion notation 16 | # Set default charset 17 | [*.{js,jsx}] 18 | charset = utf-8 19 | indent_style = space 20 | indent_size = 4 21 | 22 | # Matches the exact file .travis.yml 23 | [.travis.yml] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | [package.json] 28 | indent_style = space 29 | indent_size = 1 30 | -------------------------------------------------------------------------------- /lib/responseData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.parse = function (response) { 4 | var res = {}; 5 | for (var key in response) { 6 | var val = response[key]; 7 | var name = toCamelCase(key); 8 | if (!isNaN(val) && typeof val !== 'boolean') { 9 | res[name] = parseFloat(val); 10 | } else { 11 | res[name] = val; 12 | } 13 | } 14 | return res; 15 | }; 16 | 17 | function toCamelCase(str) { 18 | var list = str.split('_'); 19 | var res = ''; 20 | for (var i = 0, len = list.length; i < len; i++) { 21 | res += (i === 0 ? list[i][0] : list[i][0].toUpperCase()) + list[i].substring(1); 22 | } 23 | return res; 24 | } 25 | -------------------------------------------------------------------------------- /test/receipts/google: -------------------------------------------------------------------------------- 1 | {"data":{"packageName":"com.topdox.android.trivialdrivesample2","productId":"topdox_android_monthly_subscription","purchaseTime":1456139019030,"purchaseState":0,"purchaseToken":"edgcacfhmkpekcilnihgdjkb.AO-J1OxnZr_-c4xGioV-wbb9YI4w7gtRzY87CRLsa6CrHuP_nF97WNzHaBjbqCyZeYYf_sZByLD1DKxkMOFlpIsiOJnSeHxu5XIwa303DbJwFQ7Lo-sM6dgY4-4DCEqk61C9qgUx0GsLaOMZJF0zMC0mRS9K8Z2P3-uSDQpUv0qorTGt7xQC42s","autoRenewing":true},"signature":"DefUPOQ2/c3LwySfk+fdczZefijWQ+eZKzOOM3bIH2+Dz+XgNUtoUe4A4logwZKKkduIJthAxuKbf5JeCspTQI8yLCBYRU0LBv4vjINNRpjY/vCeXUaFeQd5Sd1iw186pw7vvsUoSdrIdVlf2BaARJ5M2hO8SmeZRBFxaZOlN5Ud8rRNxFQOkMXxDtwY+6ihYViLDKjY4Ej1wi7pFTPPRz9R7I9APGT9UJQ/M47DWqd3bZMlZ84TPSntRXb/Qf0QUswS9fV36pQCFKwFfIXEmnF1hQfIxMTRyyMKOw7SqPT8xazexDGy9mcxaOzskeC0OZL2E4jfTnQSoMY/woCIKg=="} 2 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [16.x, 18.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | env: 26 | CI: true 27 | NODE_ENV: "test" 28 | 29 | - name: Run Tests 30 | run: npm test 31 | env: 32 | NODE_ENV: "test" 33 | CI: true 34 | -------------------------------------------------------------------------------- /test/receipts/unity_google: -------------------------------------------------------------------------------- 1 | {"Store":"GooglePlay","Payload":{"json":{"packageName":"com.topdox.android.trivialdrivesample2","productId":"topdox_android_monthly_subscription","purchaseTime":1456139019030,"purchaseState":0,"purchaseToken":"edgcacfhmkpekcilnihgdjkb.AO-J1OxnZr_-c4xGioV-wbb9YI4w7gtRzY87CRLsa6CrHuP_nF97WNzHaBjbqCyZeYYf_sZByLD1DKxkMOFlpIsiOJnSeHxu5XIwa303DbJwFQ7Lo-sM6dgY4-4DCEqk61C9qgUx0GsLaOMZJF0zMC0mRS9K8Z2P3-uSDQpUv0qorTGt7xQC42s","autoRenewing":true},"signature":"DefUPOQ2/c3LwySfk+fdczZefijWQ+eZKzOOM3bIH2+Dz+XgNUtoUe4A4logwZKKkduIJthAxuKbf5JeCspTQI8yLCBYRU0LBv4vjINNRpjY/vCeXUaFeQd5Sd1iw186pw7vvsUoSdrIdVlf2BaARJ5M2hO8SmeZRBFxaZOlN5Ud8rRNxFQOkMXxDtwY+6ihYViLDKjY4Ej1wi7pFTPPRz9R7I9APGT9UJQ/M47DWqd3bZMlZ84TPSntRXb/Qf0QUswS9fV36pQCFKwFfIXEmnF1hQfIxMTRyyMKOw7SqPT8xazexDGy9mcxaOzskeC0OZL2E4jfTnQSoMY/woCIKg=="}} 2 | -------------------------------------------------------------------------------- /logo/License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Baran Pirinçal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | === License Statement 2 | 3 | ## Node.js framework gracenode ## 4 | 5 | Copyright (c) 2010, 2011, 2012, 2013, 2014 Nobuyori Takahashi 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | module.exports.walkDir = function __libWalkDir(path, cb) { 6 | var res = []; 7 | var pending = 0; 8 | var eachWalk = function __libEachWalk(error, results) { 9 | if (error) { 10 | return cb(error); 11 | } 12 | res = res.concat(results); 13 | pending--; 14 | if (!pending) { 15 | return cb(null, res); 16 | } 17 | }; 18 | fs.lstat(path, function __libWalkOnStat(error, stat) { 19 | if (error) { 20 | return cb(error); 21 | } 22 | if (!stat.isDirectory()) { 23 | res.push({ file: path, stat: stat }); 24 | return cb(null, res); 25 | } 26 | fs.readdir(path, function __libWalkOnReaddir(error, list) { 27 | if (error) { 28 | return cb(error); 29 | } 30 | pending += list.length; 31 | if (!pending) { 32 | return cb(null, res); 33 | } 34 | for (var i = 0, len = list.length; i < len; i++) { 35 | var file = list[i]; 36 | var slash = path.substring(path.length - 1) !== '/' ? '/' : ''; 37 | var filePath = path + slash + file; 38 | module.exports.walkDir(filePath, eachWalk); 39 | } 40 | }); 41 | }); 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /test/receipts/windows: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | cdiU06eD8X/w1aGCHeaGCG9w/kWZ8I099rw4mmPpvdU= 14 | 15 | 16 | SjRIxS/2r2P6ZdgaR9bwUSa6ZItYYFpKLJZrnAa3zkMylbiWjh9oZGGng2p6/gtBHC2dSTZlLbqnysJjl7mQp/A3wKaIkzjyRXv3kxoVaSV0pkqiPt04cIfFTP0JZkE5QD/vYxiWjeyGp1dThEM2RV811sRWvmEs/hHhVxb32e8xCLtpALYx3a9lW51zRJJN0eNdPAvNoiCJlnogAoTToUQLHs72I1dECnSbeNPXiG7klpy5boKKMCZfnVXXkneWvVFtAA1h2sB7ll40LEHO4oYN6VzD+uKd76QOgGmsu9iGVyRvvmMtahvtL1/pxoxsTRedhKq6zrzCfT8qfh3C1w== 17 | 18 | 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint 2 | lint: 3 | ./lint 4 | 5 | .PHONY: test 6 | test: 7 | npx mocha test/apple.js -R spec -b --timeout=5000 --path=false 8 | npx mocha test/google.js -R spec -b --path=false --pk=false 9 | npx mocha test/amazon.js -R spec -b --timeout=5000 --sharedKey=false --path=false 10 | npx mocha test/windows.js -R spec -b --timeout=5000 --path=false 11 | #npx mocha test/facebook.js -R spec -b --timeout=5000 --appAccessToken=false --path=false 12 | 13 | .PHONY: aptest 14 | aptest: 15 | npx mocha test/apple.js -R spec -b --timeout=5000 --path=false 16 | 17 | .PHONY: gotest 18 | gotest: 19 | npx mocha test/google.js -R spec -b --path=false --pk=false 20 | 21 | .PHONY: amtest 22 | amtest: 23 | npx mocha test/amazon.js -R spec -b --timeout=5000 --sharedKey=false --path=false 24 | 25 | .PHONY: witest 26 | witest: 27 | npx mocha test/windows.js -R spec -b --timeout=5000 --path=false 28 | 29 | .PHONY: fatest 30 | witest: 31 | npx mocha test/facebook.js -R spec -b --timeout=5000 --appAccessToken=false --path=false 32 | 33 | .PHONY: test-apple 34 | test-apple: 35 | npx mocha test/apple.js -R spec -b --timeout=5000 --path=$(path) 36 | 37 | .PHONY: test-google 38 | test-google: 39 | npx mocha test/google.js -R spec -b --path=$(path) --pk=$(pk) 40 | 41 | .PHONY: test-windows 42 | test-windows: 43 | npx mocha test/windows.js -R spec -b --timeout=5000 --path=$(path) 44 | 45 | .PHONY: test-amazon 46 | test-amazon: 47 | npx mocha test/amazon.js -R spec -b --timeout=5000 --sharedKey=$(sharedKey) --path=$(path) 48 | 49 | .PHONY: test-facebook 50 | test-facebook: 51 | npx mocha test/facebook.js -R spec -b --timeout=5000 --appAccessToken=$(appAccessToken) --path=$(path) 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flat/in-app-purchase", 3 | "description": "In-App-Purchase validation and subscription management for iOS, Android, Amazon, and Windows", 4 | "version": "1.13.5", 5 | "author": { 6 | "name": "Nobuyori Takahashi", 7 | "email": "voltrue2@yahoo.com" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/FlatIO/in-app-purchase.git" 12 | }, 13 | "dependencies": { 14 | "fb": "^2.0.0", 15 | "jwt-simple": "^0.5.6", 16 | "request": "2.88.2", 17 | "urlsafe-base64": "^1.0.0", 18 | "xml-crypto": "3.0.1", 19 | "@xmldom/xmldom": "0.8.5" 20 | }, 21 | "devDependencies": { 22 | "mocha": "10.1.0", 23 | "eslint": "8.27.0" 24 | }, 25 | "scripts": { 26 | "lint": "make lint", 27 | "test": "make lint && make test" 28 | }, 29 | "#engine": "node >= 0.10.20", 30 | "contributors": [], 31 | "keywords": [ 32 | "Apple", 33 | "iOS", 34 | "Android", 35 | "Windows", 36 | "Amazon", 37 | "Roku", 38 | "Purchase", 39 | "Subscription", 40 | "App", 41 | "in-app-purchase", 42 | "in-app-billing", 43 | "Validation", 44 | "IAP" 45 | ], 46 | "eslintConfig": { 47 | "parserOptions": { 48 | "ecmaVersion": 6, 49 | "sourceType": "module", 50 | "jsx": true 51 | }, 52 | "env": { 53 | "node": true, 54 | "es6": true, 55 | "amd": true, 56 | "mocha": true 57 | }, 58 | "rules": { 59 | "max-depth": 5, 60 | "no-undef": "error", 61 | "no-unused-vars": "error", 62 | "no-cond-assign": "error", 63 | "no-console": "warn", 64 | "no-dupe-args": "error", 65 | "no-dupe-keys": "error", 66 | "no-empty": "error", 67 | "no-duplicate-case": "error", 68 | "no-func-assign": "error", 69 | "no-inner-declarations": "warn", 70 | "no-irregular-whitespace": "error", 71 | "no-obj-calls": "error", 72 | "no-sparse-arrays": "error", 73 | "no-unreachable": "error", 74 | "no-unsafe-negation": "error", 75 | "use-isnan": "error", 76 | "valid-typeof": "error", 77 | "dot-notation": "warn", 78 | "guard-for-in": "off", 79 | "no-eval": "error", 80 | "no-global-assign": "error", 81 | "no-implicit-globals": "error", 82 | "no-implied-eval": "error", 83 | "no-loop-func": "error", 84 | "no-octal": "off", 85 | "no-octal-escape": "off", 86 | "no-proto": "error", 87 | "no-redeclare": "error", 88 | "no-restricted-properties": "error", 89 | "eqeqeq": "error", 90 | "quotes": [ 91 | "error", 92 | "single" 93 | ], 94 | "curly": "error", 95 | "camelcase": "error", 96 | "validthis": "off", 97 | "bitwise": "off", 98 | "semi": "error" 99 | } 100 | }, 101 | "license": "MIT" 102 | } 103 | -------------------------------------------------------------------------------- /test/receipts/unity_apple: -------------------------------------------------------------------------------- 1 | {"Store":"AppleAppStore","Payload":"ewoJInNpZ25hdHVyZSIgPSAiQXBNVUJDODZBbHpOaWtWNVl0clpBTWlKUWJLOEVkZVhrNjNrV0JBWHpsQzhkWEd1anE0N1puSVlLb0ZFMW9OL0ZTOGNYbEZmcDlZWHQ5aU1CZEwyNTBsUlJtaU5HYnloaXRyeVlWQVFvcmkzMlc5YVIwVDhML2FZVkJkZlcrT3kvUXlQWkVtb05LeGhudDJXTlNVRG9VaFo4Wis0cFA3MHBlNWtVUWxiZElWaEFBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NHVVVrVTNaV0FTMU1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEE1TURZeE5USXlNRFUxTmxvWERURTBNRFl4TkRJeU1EVTFObG93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNclJqRjJjdDRJclNkaVRDaGFJMGc4cHd2L2NtSHM4cC9Sd1YvcnQvOTFYS1ZoTmw0WElCaW1LalFRTmZnSHNEczZ5anUrK0RyS0pFN3VLc3BoTWRkS1lmRkU1ckdYc0FkQkVqQndSSXhleFRldngzSExFRkdBdDFtb0t4NTA5ZGh4dGlJZERnSnYyWWFWczQ5QjB1SnZOZHk2U01xTk5MSHNETHpEUzlvWkhBZ01CQUFHamNqQndNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVOaDNvNHAyQzBnRVl0VEpyRHRkREM1RllRem93RGdZRFZSMFBBUUgvQkFRREFnZUFNQjBHQTFVZERnUVdCQlNwZzRQeUdVakZQaEpYQ0JUTXphTittVjhrOVRBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQUVhU2JQanRtTjRDL0lCM1FFcEszMlJ4YWNDRFhkVlhBZVZSZVM1RmFaeGMrdDg4cFFQOTNCaUF4dmRXLzNlVFNNR1k1RmJlQVlMM2V0cVA1Z204d3JGb2pYMGlreVZSU3RRKy9BUTBLRWp0cUIwN2tMczlRVWU4Y3pSOFVHZmRNMUV1bVYvVWd2RGQ0TndOWXhMUU1nNFdUUWZna1FRVnk4R1had1ZIZ2JFL1VDNlk3MDUzcEdYQms1MU5QTTN3b3hoZDNnU1JMdlhqK2xvSHNTdGNURXFlOXBCRHBtRzUrc2s0dHcrR0szR01lRU41LytlMVFUOW5wL0tsMW5qK2FCdzdDMHhzeTBiRm5hQWQxY1NTNnhkb3J5L0NVdk02Z3RLc21uT09kcVRlc2JwMGJzOHNuNldxczBDOWRnY3hSSHVPTVoydG04bnBMVW03YXJnT1N6UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREV5TFRBMExUTXdJREE0T2pBMU9qVTFJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkltOXlhV2RwYm1Gc0xYUnlZVzV6WVdOMGFXOXVMV2xrSWlBOUlDSXhNREF3TURBd01EUTJNVGM0T0RFM0lqc0tDU0ppZG5KeklpQTlJQ0l5TURFeU1EUXlOeUk3Q2draWRISmhibk5oWTNScGIyNHRhV1FpSUQwZ0lqRXdNREF3TURBd05EWXhOemc0TVRjaU93b0pJbkYxWVc1MGFYUjVJaUE5SUNJeElqc0tDU0p2Y21sbmFXNWhiQzF3ZFhKamFHRnpaUzFrWVhSbExXMXpJaUE5SUNJeE16TTFOems0TXpVMU9EWTRJanNLQ1NKd2NtOWtkV04wTFdsa0lpQTlJQ0pqYjIwdWJXbHVaRzF2WW1Gd2NDNWtiM2R1Ykc5aFpDSTdDZ2tpYVhSbGJTMXBaQ0lnUFNBaU5USXhNVEk1T0RFeUlqc0tDU0ppYVdRaUlEMGdJbU52YlM1dGFXNWtiVzlpWVhCd0xrMXBibVJOYjJJaU93b0pJbkIxY21Ob1lYTmxMV1JoZEdVdGJYTWlJRDBnSWpFek16VTNPVGd6TlRVNE5qZ2lPd29KSW5CMWNtTm9ZWE5sTFdSaGRHVWlJRDBnSWpJd01USXRNRFF0TXpBZ01UVTZNRFU2TlRVZ1JYUmpMMGROVkNJN0Nna2ljSFZ5WTJoaGMyVXRaR0YwWlMxd2MzUWlJRDBnSWpJd01USXRNRFF0TXpBZ01EZzZNRFU2TlRVZ1FXMWxjbWxqWVM5TWIzTmZRVzVuWld4bGN5STdDZ2tpYjNKcFoybHVZV3d0Y0hWeVkyaGhjMlV0WkdGMFpTSWdQU0FpTWpBeE1pMHdOQzB6TUNBeE5Ub3dOVG8xTlNCRmRHTXZSMDFVSWpzS2ZRPT0iOwoJImVudmlyb25tZW50IiA9ICJTYW5kYm94IjsKCSJwb2QiID0gIjEwMCI7Cgkic2lnbmluZy1zdGF0dXMiID0gIjAiOwp9"} 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '35 12 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /test/receipts/apple: -------------------------------------------------------------------------------- 1 | ewoJInNpZ25hdHVyZSIgPSAiQXBNVUJDODZBbHpOaWtWNVl0clpBTWlKUWJLOEVk 2 | ZVhrNjNrV0JBWHpsQzhkWEd1anE0N1puSVlLb0ZFMW9OL0ZTOGNYbEZmcDlZWHQ5 3 | aU1CZEwyNTBsUlJtaU5HYnloaXRyeVlWQVFvcmkzMlc5YVIwVDhML2FZVkJkZlcr 4 | T3kvUXlQWkVtb05LeGhudDJXTlNVRG9VaFo4Wis0cFA3MHBlNWtVUWxiZElWaEFB 5 | QURWekNDQTFNd2dnSTdvQU1DQVFJQ0NHVVVrVTNaV0FTMU1BMEdDU3FHU0liM0RR 6 | RUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pT 7 | QkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlF 8 | RjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIz 9 | SmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEE1TURZeE5U 10 | SXlNRFUxTmxvWERURTBNRFl4TkRJeU1EVTFObG93WkRFak1DRUdBMVVFQXd3YVVI 11 | VnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVr 12 | RndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NX 13 | NWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZ 14 | MEFNSUdKQW9HQkFNclJqRjJjdDRJclNkaVRDaGFJMGc4cHd2L2NtSHM4cC9Sd1Yv 15 | cnQvOTFYS1ZoTmw0WElCaW1LalFRTmZnSHNEczZ5anUrK0RyS0pFN3VLc3BoTWRk 16 | S1lmRkU1ckdYc0FkQkVqQndSSXhleFRldngzSExFRkdBdDFtb0t4NTA5ZGh4dGlJ 17 | ZERnSnYyWWFWczQ5QjB1SnZOZHk2U01xTk5MSHNETHpEUzlvWkhBZ01CQUFHamNq 18 | QndNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVOaDNvNHAyQzBn 19 | RVl0VEpyRHRkREM1RllRem93RGdZRFZSMFBBUUgvQkFRREFnZUFNQjBHQTFVZERn 20 | UVdCQlNwZzRQeUdVakZQaEpYQ0JUTXphTittVjhrOVRBUUJnb3Foa2lHOTJOa0Jn 21 | VUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQUVhU2JQanRtTjRDL0lC 22 | M1FFcEszMlJ4YWNDRFhkVlhBZVZSZVM1RmFaeGMrdDg4cFFQOTNCaUF4dmRXLzNl 23 | VFNNR1k1RmJlQVlMM2V0cVA1Z204d3JGb2pYMGlreVZSU3RRKy9BUTBLRWp0cUIw 24 | N2tMczlRVWU4Y3pSOFVHZmRNMUV1bVYvVWd2RGQ0TndOWXhMUU1nNFdUUWZna1FR 25 | Vnk4R1had1ZIZ2JFL1VDNlk3MDUzcEdYQms1MU5QTTN3b3hoZDNnU1JMdlhqK2xv 26 | SHNTdGNURXFlOXBCRHBtRzUrc2s0dHcrR0szR01lRU41LytlMVFUOW5wL0tsMW5q 27 | K2FCdzdDMHhzeTBiRm5hQWQxY1NTNnhkb3J5L0NVdk02Z3RLc21uT09kcVRlc2Jw 28 | MGJzOHNuNldxczBDOWRnY3hSSHVPTVoydG04bnBMVW03YXJnT1N6UT09IjsKCSJw 29 | dXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdS 30 | aGRHVXRjSE4wSWlBOUlDSXlNREV5TFRBMExUTXdJREE0T2pBMU9qVTFJRUZ0WlhK 31 | cFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkltOXlhV2RwYm1Gc0xYUnlZVzV6WVdO 32 | MGFXOXVMV2xrSWlBOUlDSXhNREF3TURBd01EUTJNVGM0T0RFM0lqc0tDU0ppZG5K 33 | eklpQTlJQ0l5TURFeU1EUXlOeUk3Q2draWRISmhibk5oWTNScGIyNHRhV1FpSUQw 34 | Z0lqRXdNREF3TURBd05EWXhOemc0TVRjaU93b0pJbkYxWVc1MGFYUjVJaUE5SUNJ 35 | eElqc0tDU0p2Y21sbmFXNWhiQzF3ZFhKamFHRnpaUzFrWVhSbExXMXpJaUE5SUNJ 36 | eE16TTFOems0TXpVMU9EWTRJanNLQ1NKd2NtOWtkV04wTFdsa0lpQTlJQ0pqYjIw 37 | dWJXbHVaRzF2WW1Gd2NDNWtiM2R1Ykc5aFpDSTdDZ2tpYVhSbGJTMXBaQ0lnUFNB 38 | aU5USXhNVEk1T0RFeUlqc0tDU0ppYVdRaUlEMGdJbU52YlM1dGFXNWtiVzlpWVhC 39 | d0xrMXBibVJOYjJJaU93b0pJbkIxY21Ob1lYTmxMV1JoZEdVdGJYTWlJRDBnSWpF 40 | ek16VTNPVGd6TlRVNE5qZ2lPd29KSW5CMWNtTm9ZWE5sTFdSaGRHVWlJRDBnSWpJ 41 | d01USXRNRFF0TXpBZ01UVTZNRFU2TlRVZ1JYUmpMMGROVkNJN0Nna2ljSFZ5WTJo 42 | aGMyVXRaR0YwWlMxd2MzUWlJRDBnSWpJd01USXRNRFF0TXpBZ01EZzZNRFU2TlRV 43 | Z1FXMWxjbWxqWVM5TWIzTmZRVzVuWld4bGN5STdDZ2tpYjNKcFoybHVZV3d0Y0hW 44 | eVkyaGhjMlV0WkdGMFpTSWdQU0FpTWpBeE1pMHdOQzB6TUNBeE5Ub3dOVG8xTlNC 45 | RmRHTXZSMDFVSWpzS2ZRPT0iOwoJImVudmlyb25tZW50IiA9ICJTYW5kYm94IjsK 46 | CSJwb2QiID0gIjEwMCI7Cgkic2lnbmluZy1zdGF0dXMiID0gIjAiOwp9 47 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var https = require('https'); 5 | 6 | module.exports = function (options, cb) { 7 | send(options.method, options, cb); 8 | }; 9 | 10 | module.exports.post = function (options, cb) { 11 | send('POST', options, cb); 12 | }; 13 | 14 | module.exports.put = function (options, cb) { 15 | send('PUT', options, cb); 16 | }; 17 | 18 | module.exports.del = function (options, cb) { 19 | send('DELETE', options, cb); 20 | }; 21 | 22 | module.exports.patch = function (options, cb) { 23 | send('PATCH', options, cb); 24 | }; 25 | 26 | module.exports.get = function (options, cb) { 27 | send('GET', options, cb); 28 | }; 29 | 30 | module.exports.head = function (options, cb) { 31 | send('HEAD', options, cb); 32 | }; 33 | 34 | function send(method, options, cb) { 35 | 36 | if (typeof options === 'string') { 37 | options = { 38 | url: options, 39 | body: null 40 | }; 41 | } 42 | 43 | var bodyData = qstring(options.body, options || {}); 44 | var opts = createOptions(method, bodyData, options || {}); 45 | opts.headers = addHeaders(opts.headers, options || {}); 46 | var proto = opts.port === 443 ? https : http; 47 | var req = proto.request(opts, function (res) { 48 | res.setEncoding('utf8'); 49 | var data = ''; 50 | res.on('data', function (chunk) { 51 | data += chunk; 52 | }); 53 | res.on('end', function () { 54 | try { 55 | cb(null, res, JSON.parse(data)); 56 | } catch (e) { 57 | cb(null, res, data); 58 | } 59 | }); 60 | }); 61 | req.on('error', cb); 62 | 63 | if (bodyData) { 64 | req.write(bodyData); 65 | } 66 | 67 | req.end(); 68 | } 69 | 70 | function qstring(body, options) { 71 | 72 | if (!body) { 73 | return ''; 74 | } 75 | 76 | if (options.json) { 77 | return JSON.stringify(body); 78 | } 79 | 80 | var q = []; 81 | for (var name in body) { 82 | q.push( 83 | encodeURIComponent(name) + 84 | '=' + 85 | encodeURIComponent(body[name]) 86 | ); 87 | } 88 | return q.join('&'); 89 | } 90 | 91 | function createOptions(method, data, options) { 92 | var url = options.url; 93 | var proto = url.substring(0, url.indexOf('://') + 3); 94 | var noProto = url.replace(proto, ''); 95 | var host = noProto.substring(0, noProto.indexOf('/')); 96 | var path = noProto.substring(noProto.indexOf('/')); 97 | var port = options.port || (proto === 'http://' ? 80 : 443); 98 | var ctype = options.json ? 'application/json' : 'application/x-www-form-urlencoded'; 99 | var opts = { 100 | host: host, 101 | port: port, 102 | path: path, 103 | method: method, 104 | headers: { 105 | 'Content-Type': ctype, 106 | 'Content-Length': Buffer.byteLength(data) 107 | } 108 | }; 109 | return opts; 110 | } 111 | 112 | function addHeaders(headers, options) { 113 | var headersToAdd = options.headers; 114 | if (!headersToAdd) { 115 | return headers; 116 | } 117 | for (var name in headersToAdd) { 118 | headers[name] = headersToAdd[name]; 119 | } 120 | return headers; 121 | } 122 | -------------------------------------------------------------------------------- /lib/async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | async library is not v8 crankshft friendly at all 5 | and we wanto support node.js that does not support Promise 6 | */ 7 | 8 | module.exports = { 9 | eachSeries: eachSeries, 10 | forEachSeries: eachSeries, 11 | // eachSeries/forEachSeries w/ extra parameters 12 | loopSeries: loopSeries, 13 | each: eachSeries, 14 | forEach: eachSeries, 15 | series: series, 16 | promiseAll: promiseAll 17 | }; 18 | 19 | function promiseAll(promiseList) { 20 | if (!promiseList || !Array.isArray(promiseList) || !promiseList.length) { 21 | return new Promise(_onEmptyPromiseList); 22 | } 23 | var promise = promiseList[0](); 24 | if (promise instanceof Promise === false) { 25 | return new Promise(_onInvalidPromiseInList.bind(null, { index: 0 })); 26 | } 27 | for (var i = 1, len = promiseList.length; i < len; i++) { 28 | promise = promise.then(promiseList[i]); 29 | if (promise instanceof Promise === false) { 30 | return new Promise(_onInvalidPromiseInList.bind(null, { index: i })); 31 | } 32 | } 33 | return promise; 34 | } 35 | 36 | function _onEmptyPromiseList(resolve) { 37 | resolve(); 38 | } 39 | 40 | function _onInvalidPromiseInList(bind, resolve, reject) { 41 | reject(new Error('Not promise in list at', bind.index)); 42 | } 43 | 44 | function eachSeries(list, each, cb, _index) { 45 | if (!list || !list.length) { 46 | return _finish(cb); 47 | } 48 | var item = list[0]; 49 | if (item === undefined) { 50 | return _finish(cb); 51 | } 52 | if (!_index) { 53 | _index = 0; 54 | } 55 | each(item, _onEachSeries.bind(null, { 56 | list: list.slice(1), 57 | each: each, 58 | cb: cb, 59 | index: _index 60 | }), _index); 61 | } 62 | 63 | function _onEachSeries(bind, error) { 64 | if (error) { 65 | return _finish(bind.cb, error); 66 | } 67 | eachSeries(bind.list, bind.each, bind.cb, bind.index + 1); 68 | } 69 | 70 | function loopSeries(list, params, each, cb, _index) { 71 | if (!list || !list.length) { 72 | return _finish(cb); 73 | } 74 | var item = list[0]; 75 | if (item === undefined) { 76 | return _finish(cb); 77 | } 78 | if (!_index) { 79 | _index = 0; 80 | } 81 | each(item, params, _onLoopSeries.bind(null, { 82 | list: list.slice(1), 83 | params: params, 84 | each: each, 85 | cb: cb, 86 | index: _index 87 | }), _index); 88 | } 89 | 90 | function _onLoopSeries(bind, error) { 91 | if (error) { 92 | return _finish(bind.cb, error); 93 | } 94 | loopSeries( 95 | bind.list, 96 | bind.params, 97 | bind.each, 98 | bind.cb, 99 | bind.index + 1 100 | ); 101 | } 102 | 103 | function series(list, cb) { 104 | if (!list || !list.length) { 105 | return _finish(cb); 106 | } 107 | var item = list[0]; 108 | if (item === undefined) { 109 | return _finish(cb); 110 | } 111 | if (typeof item !== 'function') { 112 | return _finish(cb, new Error('FoundNonFunctionInList')); 113 | } 114 | item(_onSeries.bind(null, { 115 | list: list.slice(1), 116 | cb: cb 117 | })); 118 | } 119 | 120 | function _onSeries(bind, error) { 121 | if (error) { 122 | return _finish(bind.cb, error); 123 | } 124 | series(bind.list, bind.cb); 125 | } 126 | 127 | function _finish(cb, error) { 128 | if (typeof cb === 'function') { 129 | cb(error); 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /lib/roku.js: -------------------------------------------------------------------------------- 1 | var constants = require('../constants'); 2 | var request = require('request'); 3 | var responseData = require('./responseData'); 4 | var verbose = require('./verbose'); 5 | 6 | var API_KEY = '{apiKey}'; 7 | var TRANSACTION_ID = '{transactionId}'; 8 | 9 | var VALIDATION_PATH = 'https://apipub.roku.com/listen/transaction-service.svc/validate-transaction/' + API_KEY + '/' + TRANSACTION_ID; 10 | 11 | var NAME = ''; 12 | 13 | var config; 14 | 15 | module.exports.readConfig = function (configIn) { 16 | config = configIn || {}; 17 | // apply configurations 18 | if (config.requestDefaults) { 19 | request = request.defaults(config.requestDefaults); 20 | } 21 | verbose.setup(config); 22 | }; 23 | 24 | /* 25 | https://sdkdocs.roku.com/display/sdkdoc/Web+Service+API#WebServiceAPI-ValidateTransaction 26 | receipt: 27 | */ 28 | module.exports.validatePurchase = function (apiKey, receipt, cb) { 29 | var path; 30 | var characters = receipt.match(/\w/g) || ''; 31 | var dashes = receipt.match(/-/g) || ''; 32 | if (characters.length !== 32) { 33 | return cb(new Error('Receipt must contain 32 digits')); 34 | } else if (dashes.length !== 4) { 35 | return cb(new Error('Receipt must contain 4 dashes')); 36 | } 37 | 38 | // override rokuApiKey from config to allow dynamically fed secret to validate 39 | if (apiKey) { 40 | verbose.log(NAME, 'Use dynamically rokuApiKey:', apiKey); 41 | path = VALIDATION_PATH.replace(API_KEY, apiKey); 42 | } else { 43 | path = VALIDATION_PATH.replace(API_KEY, config.rokuApiKey); 44 | } 45 | path = path.replace(TRANSACTION_ID, receipt); 46 | verbose.log(NAME, 'Validate:', path, receipt); 47 | send(path, function (error, res) { 48 | if (error) { 49 | verbose.log(NAME, 'Validation failed:', error.message); 50 | return cb(error); 51 | } 52 | verbose.log(NAME, 'Validation successful:', res); 53 | cb(null, res); 54 | }); 55 | }; 56 | 57 | module.exports.getPurchaseData = function (purchase, options) { 58 | if (!purchase || !purchase.transactionId) { 59 | return null; 60 | } 61 | 62 | var now = Date.now(); 63 | 64 | if (options && options.ignoreExpired && purchase.expirationDate <= now) { 65 | return []; 66 | } 67 | 68 | var obj = responseData.parse(purchase); 69 | // obj.transactionId = purchase.transactionId; 70 | // obj.productId = purchase.productId; 71 | // obj.quantity = 1; 72 | // obj.purchaseDate = purchase.startDate || now; 73 | // obj.expirationDate = purchase.expirationDate || 0; 74 | return [ obj ]; 75 | }; 76 | 77 | function send(path, cb) { 78 | var options = { 79 | uri: path, 80 | json: true 81 | }; 82 | request.get(options, function (error, response, body) { 83 | if (error) { 84 | return cb(error, { 85 | status: constants.VALIDATION.FAILURE, 86 | message: error.message || 'Unknown' 87 | }); 88 | } 89 | if (response.statusCode !== 200) { 90 | return cb(error, { 91 | status: constants.VALIDATION.FAILURE, 92 | message: body 93 | }); 94 | } 95 | 96 | var res = body; 97 | if (res.errorMessage) { 98 | return cb(new Error(res.errorMessage), { 99 | status: constants.VALIDATION.FAILURE, 100 | message: res.errorMessage 101 | }); 102 | } 103 | 104 | // parse non-standard date properties (i.e. /Date(1483242628000-0800)/) 105 | // in order to extract value by milliseconds 106 | if (res.expirationDate) { 107 | res.expirationDate = new Date(parseInt(res.expirationDate.substr(6), 10)).getTime(); 108 | } 109 | res.originalPurchaseDate = new Date(parseInt(res.originalPurchaseDate.substr(6), 10)).getTime(); 110 | res.purchaseDate = new Date(parseInt(res.purchaseDate.substr(6), 10)).getTime(); 111 | 112 | res.status = 0; 113 | res.service = constants.SERVICES.ROKU; 114 | cb(null, res); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /scripts/lint/linter/index.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const pkg = require('../../../package.json'); 4 | const util = require('../../../lib/util'); 5 | const Linter = require('eslint').Linter; 6 | const linter = new Linter(); 7 | 8 | const MAX_SOURCE_LEN = 100; 9 | const HIDDEN = '/.'; 10 | const JS = '.js'; 11 | const MOD_PATH = 'node_modules/'; 12 | const GREY = '0;90'; 13 | const DARK_BLUE = '0;34'; 14 | const GREEN = '0;32'; 15 | const BROWN = '0;33'; 16 | const RED = '0;31'; 17 | 18 | var conf = pkg.eslintConfig; 19 | 20 | module.exports = { 21 | start: start 22 | }; 23 | 24 | function getRootPath() { 25 | return '../../../'; 26 | } 27 | 28 | function start(path, _packagePath, ignores, cb) { 29 | var packagePath = getPackagePath(_packagePath); 30 | try { 31 | conf = require(packagePath + '/package.json').eslintConfig; 32 | process.stdout.write(color( 33 | 'Lint loading ' + packagePath + 34 | '/package.json' + ' as configuration', GREY) + '\n' 35 | ); 36 | } catch (err) { 37 | process.stdout.write(color( 38 | 'Lint loading ' + __dirname + 39 | '/../../../package.json as configuration', GREY) + '\n' 40 | ); 41 | } 42 | util.walkDir(path, function (error, list) { 43 | if (error) { 44 | return cb(error); 45 | } 46 | var failed = lint(path, ignores, list); 47 | cb(failed); 48 | }); 49 | } 50 | 51 | function getPackagePath(path, lastTry) { 52 | if (!path) { 53 | return process.cwd(); 54 | } 55 | try { 56 | fs.statSync(path); 57 | } catch (err) { 58 | if (lastTry) { 59 | return process.cwd(); 60 | } 61 | return getPackagePath(getRootPath() + path, true); 62 | } 63 | return path; 64 | } 65 | 66 | function lint(path, ignores, list) { 67 | for (var i = 0, len = list.length; i < len; i++) { 68 | var error = _onEachLint(path, ignores, list[i]); 69 | if (error) { 70 | return error; 71 | } 72 | } 73 | return null; 74 | } 75 | 76 | function _onEachLint(path, ignores, item) { 77 | var pathFragment = item.file.replace(path, ''); 78 | if (pathFragment.indexOf(MOD_PATH) === 0) { 79 | return; 80 | } 81 | if (item.file.indexOf(HIDDEN) !== -1) { 82 | return; 83 | } 84 | if (!isJs(item.file)) { 85 | return; 86 | } 87 | if (ignores && ignores.length) { 88 | for (var i = 0, len = ignores.length; i < len; i++) { 89 | if (pathFragment.indexOf(ignores[i]) !== 0) { 90 | continue; 91 | } 92 | var skip = color( 93 | 'Lint [ ', GREY) + 94 | color('Ignore', DARK_BLUE) + 95 | color(' ] ' + item.file, GREY 96 | ); 97 | process.stdout.write(skip + '\n'); 98 | return; 99 | } 100 | } 101 | return _exec(item.file); 102 | } 103 | 104 | function _exec(file) { 105 | try { 106 | return _onExec(file, fs.readFileSync(file, 'utf8')); 107 | } catch (error) { 108 | // failed to open the file to lint... 109 | return error; 110 | } 111 | } 112 | 113 | function _onExec(file, data) { 114 | var msg = linter.verify(data, conf); 115 | if (!msg.length) { 116 | // no error! 117 | var good = color('Lint [ ', GREY) + color('OK', GREEN) + color(' ] ' + file, GREY); 118 | process.stdout.write(good + '\n'); 119 | return; 120 | } 121 | // if there are errors... 122 | var error = print(file, msg); 123 | if (error) { 124 | return new Error('lint error: ' + file); 125 | } 126 | return; 127 | } 128 | 129 | function isJs(path) { 130 | var index = path.lastIndexOf('.'); 131 | var ext = path.substring(index); 132 | return ext === JS; 133 | } 134 | 135 | function print(file, msg) { 136 | var error = false; 137 | for (var i = 0, len = msg.length; i < len; i++) { 138 | var item = msg[i]; 139 | if (item.severity === 2) { 140 | error = true; 141 | } 142 | process.stdout.write( 143 | color('Lint [', GREY) + 144 | getSeverity(item.severity) + 145 | color('] ' + file + ' Line:' + item.line + ' Column:' + item.column, GREY) + 146 | '\n' + 147 | color('[', GREY) + 148 | getType(item.severity, item.nodeType) + 149 | getMessage(item.severity, item.message) + 150 | color(']', GREY) + 151 | '\n' + 152 | '{' + color(item.ruleId, GREY) + '} ' + 153 | getSource(color(item.source, GREY)) + '\n' 154 | ); 155 | } 156 | return error; 157 | } 158 | 159 | function getSeverity(severity) { 160 | switch (severity) { 161 | case 1: 162 | return color('Warning', BROWN); 163 | case 2: 164 | return color('Error', RED); 165 | default: 166 | return color('Info', DARK_BLUE); 167 | } 168 | } 169 | 170 | function getType(severity, type) { 171 | type = '<' + type + '>'; 172 | switch (severity) { 173 | case 1: 174 | return color(type, BROWN); 175 | case 2: 176 | return color(type, RED); 177 | default: 178 | return color(type, DARK_BLUE); 179 | } 180 | } 181 | 182 | function getMessage(severity, msg) { 183 | switch (severity) { 184 | case 1: 185 | return color(msg, BROWN); 186 | case 2: 187 | return color(msg, RED); 188 | default: 189 | return color(msg, DARK_BLUE); 190 | } 191 | } 192 | 193 | function getSource(source) { 194 | if (source.length > MAX_SOURCE_LEN) { 195 | return source.substring(0, MAX_SOURCE_LEN) + '...'; 196 | } 197 | return source; 198 | } 199 | 200 | function color(val, code) { 201 | return '\x1b[' + code + 'm' + val + '\x1b[0m'; 202 | } 203 | 204 | -------------------------------------------------------------------------------- /lib/windows.js: -------------------------------------------------------------------------------- 1 | var verbose = require('./verbose'); 2 | var constants = require('../constants'); 3 | var xmlCrypto = require('xml-crypto'); 4 | var Parser = require('@xmldom/xmldom').DOMParser; 5 | var request = require('request'); 6 | var responseData = require('./responseData'); 7 | var url = 'https://lic.apps.microsoft.com/licensing/certificateserver/?cid='; 8 | var sigXPath = '//*//*[local-name(.)=\'Signature\' and namespace-uri(.)=\'http://www.w3.org/2000/09/xmldsig#\']'; 9 | 10 | var NAME = ''; 11 | 12 | module.exports.readConfig = function (configIn) { 13 | if (!configIn) { 14 | // no required config 15 | return; 16 | } 17 | verbose.setup(configIn); 18 | // Apply any default settings to Request. 19 | if ('requestDefaults' in configIn) { 20 | request = request.defaults(configIn.requestDefaults); 21 | } 22 | }; 23 | 24 | // receipt is an XML string... oh microsoft... why? 25 | module.exports.validatePurchase = function (receipt, cb) { 26 | var certId; 27 | var options = { 28 | ignoreWhiteSpace: true, 29 | errorHandler: { 30 | fatalError: handleError, 31 | error: handleError 32 | } 33 | }; 34 | var handleError = function (error) { 35 | if (typeof error === 'string') { 36 | error = new Error(error); 37 | } 38 | cb(error); 39 | }; 40 | verbose.log(NAME, 'Validate:', receipt); 41 | try { 42 | var doc = new Parser(options).parseFromString(receipt); 43 | certId = doc.firstChild.getAttribute('CertificateId'); 44 | } catch (e) { 45 | verbose.log(NAME, 'Failed:', e); 46 | return cb(new Error('failed to validate purchase: ' + e.message), { status: constants.VALIDATION.FAILURE, message: e.message }); 47 | } 48 | if (!certId) { 49 | verbose.log(NAME, 'Failed: Invalid certificate ID'); 50 | return cb(new Error('failed to find certificate ID'), { status: constants.VALIDATION.FAILURE, message: 'Invalid certificate ID' }); 51 | } 52 | verbose.log(NAME, 'Get public key from:', url + certId); 53 | send(url + certId, function (error, body) { 54 | if (error) { 55 | verbose.log(NAME, 'Failed to get public key:', (url + certId), error); 56 | return cb(error); 57 | } 58 | var data; 59 | try { 60 | var publicKey = body; 61 | var canonicalXML = removeWhiteSpace(doc.firstChild).toString(); 62 | var signature = xmlCrypto.xpath(doc, sigXPath); 63 | var sig = new xmlCrypto.SignedXml(); 64 | sig.keyInfoProvider = new Cert(publicKey); 65 | sig.loadSignature(signature.toString()); 66 | if (sig.checkSignature(canonicalXML)) { 67 | // create purchase data 68 | var items = doc.getElementsByTagName('ProductReceipt'); 69 | var purchases = []; 70 | for (var i = 0, len = items.length; i < len; i++) { 71 | var item = items[i]; 72 | purchases.push({ 73 | transactionId: item.getAttribute('Id'), 74 | productId: item.getAttribute('ProductId'), 75 | purchaseDate: item.getAttribute('PurchaseDate'), 76 | expirationDate: item.getAttribute('ExpirationDate'), 77 | productType: item.getAttribute('ProductType'), 78 | appId: item.getAttribute('AppId') 79 | }); 80 | } 81 | // successful validation 82 | data = { 83 | service: constants.SERVICES.WINDOWS, 84 | status: constants.VALIDATION.SUCCESS, 85 | purchases: purchases 86 | }; 87 | } 88 | } catch (e) { 89 | verbose.log(NAME, 'Failed to validated:', e); 90 | return cb(new Error('failed to validate purchase: ' + e.message), { status: constants.VALIDATION.FAILURE, message: e.message }); 91 | } 92 | // done 93 | verbose.log(NAME, 'Validation success:', data); 94 | cb(null, data); 95 | }); 96 | }; 97 | 98 | module.exports.getPurchaseData = function (purchase, options) { 99 | if (!purchase || !purchase.purchases || !purchase.purchases.length) { 100 | return null; 101 | } 102 | var data = []; 103 | for (var i = 0, len = purchase.purchases.length; i < len; i++) { 104 | var item = purchase.purchases[i]; 105 | var exp = new Date(item.expirationDate).getTime(); 106 | 107 | if (options && options.ignoreExpired && exp && Date.now() - exp >= 0) { 108 | // we are told to ignore expired item and it has been expired 109 | continue; 110 | } 111 | 112 | var parsed = responseData.parse(item); 113 | parsed.purchaseDate = new Date(item.purchaseDate).getTime(); 114 | parsed.expirationDate = exp; 115 | parsed.quantity = 1; 116 | data.push(parsed); 117 | } 118 | return data; 119 | }; 120 | 121 | function send(url, cb) { 122 | var options = { 123 | encoding: null, 124 | url: url 125 | }; 126 | request.get(options, function (error, res, body) { 127 | if (error) { 128 | return cb(error, { status: res.status, message: body }); 129 | } 130 | if (!body) { 131 | return cb(new Error('invalid response from the service'), { status: res.status, message: 'Unknown' }); 132 | } 133 | cb(null, body.toString('utf8')); 134 | }); 135 | } 136 | 137 | function removeWhiteSpace(node) { 138 | var rootNode = node; 139 | while (node) { 140 | const nextSibling = node.nextSibling; 141 | if (!node.tagName && (node.nextSibling || node.previousSibling)) { 142 | node.parentNode.removeChild(node); 143 | } 144 | if (node.firstChild) { 145 | removeWhiteSpace(node.firstChild); 146 | } 147 | node = nextSibling; 148 | } 149 | return rootNode; 150 | } 151 | 152 | function Cert(pubKey) { 153 | this._pubKey = pubKey; 154 | } 155 | 156 | Cert.prototype.getKeyInfo = function () { 157 | return ''; 158 | }; 159 | 160 | Cert.prototype.getKey = function () { 161 | return this._pubKey; 162 | }; 163 | -------------------------------------------------------------------------------- /lib/amazon.js: -------------------------------------------------------------------------------- 1 | var constants = require('../constants'); 2 | var request = require('request'); 3 | var fs = require('fs'); 4 | var responseData = require('./responseData'); 5 | var verbose = require('./verbose'); 6 | 7 | var VER = '2.0'; 8 | var SECRET = '{developerSecret}'; 9 | var UID = '{userId}'; 10 | var PTOKEN = '{purchaseToken}'; 11 | var ERRORS = { 12 | VALIDATION: { 13 | 400: 'The transaction represented by this Purchase Token is no longer valid.', 14 | 404: 'Unknown operation exception.', 15 | 496: 'Invalid sharedSecret', 16 | 497: 'Invalid User ID', 17 | 498: 'Invalid Purchase Token', 18 | 499: 'The Purchase Token was created with credentials that have expired, use renew to generate a valid purchase token.', 19 | 500: 'There was an Internal Server Error' 20 | }, 21 | RENEW: { 22 | 400: 'Bad Request', 23 | 404: 'Unknown operation exception.', 24 | 496: 'Invalid sharedSecret', 25 | 497: 'Invalid User ID', 26 | 498: 'Invalid Purchase Token', 27 | 500: 'There is an Internal Server Error' 28 | } 29 | }; 30 | 31 | var VALIDATION_PATH = 'https://appstore-sdk.amazon.com/version/' + 32 | VER + '/verify/developer/' + SECRET + '/user/' + UID + '/purchaseToken/' + PTOKEN; 33 | var RENEW_PATH = 'https://appstore-sdk.amazon.com/version/' + 34 | VER + '/renew/developer/' + SECRET + '/user/' + UID + '/purchaseToken/' + PTOKEN; 35 | 36 | var S_VAL_PATH = VALIDATION_PATH; 37 | var S_R_PATH = RENEW_PATH; 38 | 39 | var NAME = ''; 40 | 41 | var config; 42 | 43 | module.exports.readConfig = function (configIn) { 44 | config = configIn; 45 | // Apply any default settings to Request. 46 | if ('requestDefaults' in configIn) { 47 | request = request.defaults(configIn.requestDefaults); 48 | } 49 | verbose.setup(config); 50 | }; 51 | 52 | module.exports.setup = function (cb) { 53 | if (!config || !config.secret) { 54 | return cb(); 55 | } 56 | fs.exists(config.secret, function (exists) { 57 | var secret = ''; 58 | if (!exists) { 59 | // use the string value literally 60 | secret = config.secret; 61 | VALIDATION_PATH = VALIDATION_PATH.replace(SECRET, config.secret); 62 | RENEW_PATH = RENEW_PATH.replace(SECRET, config.secret); 63 | verbose.log(NAME, 'Secret:', config.secret); 64 | return cb(); 65 | } 66 | // assume it as a file path 67 | fs.readFile(config.secret, 'UTF-8', function (error, val) { 68 | if (error) { 69 | return cb(error); 70 | } 71 | secret = val.replace(/(\r|\n)/g, ''); 72 | VALIDATION_PATH = VALIDATION_PATH.replace(SECRET, secret); 73 | RENEW_PATH = RENEW_PATH.replace(SECRET, secret); 74 | verbose.log(NAME, 'Secret:', secret); 75 | cb(); 76 | }); 77 | }); 78 | }; 79 | 80 | /* 81 | receipt: { 82 | userId: 83 | receiptId: 84 | } 85 | */ 86 | module.exports.validatePurchase = function (dSecret, receipt, cb) { 87 | var rpath = RENEW_PATH; 88 | var path; 89 | 90 | // override secret with dSecret to allow dynamically fed secret to validate 91 | if (dSecret) { 92 | verbose.log(NAME, 'Use dynamically fed secret:', dSecret); 93 | rpath = S_R_PATH.replace(SECRET, dSecret); 94 | var vpath = S_VAL_PATH.replace(SECRET, dSecret); 95 | path = vpath.replace(UID, receipt.userId); 96 | } else { 97 | path = VALIDATION_PATH.replace(UID, receipt.userId); 98 | } 99 | path = path.replace(PTOKEN, receipt.receiptId); 100 | verbose.log(NAME, 'Validate:', path, receipt); 101 | send(path, ERRORS.VALIDATION, function (error, res) { 102 | if (error) { 103 | if (res === 499) { 104 | // must be renewed and re-tried 105 | var renew = rpath.replace(UID, receipt.userId); 106 | renew = renew.replace(PTOKEN, receipt.receiptId); 107 | verbose.log(NAME, 'Purchase must be renewed (' + res + '):', renew); 108 | send(renew, ERRORS.RENEW, function (error, renewed) { 109 | if (error) { 110 | var renewedErrorRes = { 111 | status: renewed, 112 | message: ERRORS.RENEW[renewed] || 'Unkown' 113 | }; 114 | verbose.log(NAME, 'Failed to renew purchase:', renewedErrorRes); 115 | return cb(error, renewedErrorRes); 116 | } 117 | var renewedReceipt = { 118 | receiptId: renewed.purchaseToken, 119 | userId: receipt.userId 120 | }; 121 | verbose.log(NAME, 'Purchase renewed:', renewedReceipt); 122 | module.exports.validatePurchase(renewedReceipt, cb); 123 | }); 124 | return; 125 | } 126 | 127 | var errorRes = { 128 | status: res, 129 | message: ERRORS.VALIDATION[res] || 'Unknown' 130 | }; 131 | verbose.log(NAME, 'Validation failed:', errorRes); 132 | return cb(error, errorRes); 133 | } 134 | verbose.log(NAME, 'Validation successful:', res); 135 | cb(null, res); 136 | }); 137 | }; 138 | 139 | module.exports.getPurchaseData = function (purchase, options) { 140 | if (!purchase || !purchase.purchaseToken) { 141 | return null; 142 | } 143 | 144 | var now = Date.now(); 145 | 146 | if (options && options.ignoreExpired && purchase.expirationDate <= now) { 147 | return []; 148 | } 149 | 150 | var obj = responseData.parse(purchase); 151 | obj.transactionId = purchase.purchaseToken; 152 | obj.productId = purchase.sku; 153 | obj.purchaseData = purchase.itemType; 154 | obj.quantity = 1; 155 | obj.purchaseDate = purchase.startDate || now; 156 | obj.expirationDate = purchase.endDate || 0; 157 | return [ obj ]; 158 | }; 159 | 160 | function send(path, errorMap, cb) { 161 | request.get(path, function (error, response, body) { 162 | var errorMsg = errorMap[response.statusCode]; 163 | if (errorMsg) { 164 | return cb(new Error(errorMsg + ': ' + path), response.statusCode); 165 | } 166 | if (error) { 167 | error.message += ': ' + path; 168 | return cb(error); 169 | } 170 | var res = JSON.parse(body); 171 | res.status = 0; 172 | res.service = constants.SERVICES.AMAZON; 173 | cb(null, res); 174 | }); 175 | } 176 | -------------------------------------------------------------------------------- /lib/googleAPI.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Uses Google API and the receipt only requies purchaseToken string to validate 5 | * 6 | */ 7 | 8 | var jwt = require('jwt-simple'); 9 | var util = require('util'); 10 | var request = require('request'); 11 | var constants = require('../constants'); 12 | var verbose = require('./verbose'); 13 | 14 | var NAME = 'GOOGLE API'; 15 | var GET_TOKEN = 'https://accounts.google.com/o/oauth2/token'; 16 | var SCOPE = 'https://www.googleapis.com/auth/androidpublisher'; 17 | var PRODUCT_VAL = 'https://www.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s?access_token=%s'; 18 | var SUBSCR_VAL = 'https://www.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptions/%s/tokens/%s?access_token=%s'; 19 | 20 | // This comes from Google Developer account 21 | var conf = { 22 | clientEmail: null, 23 | privateKey: null 24 | }; 25 | 26 | module.exports = { 27 | config: config, 28 | validatePurchase: validatePurchase 29 | }; 30 | 31 | function config(_conf) { 32 | if (!_conf.clientEmail) { 33 | throw new Error('Google API requires client email'); 34 | } 35 | if (!_conf.privateKey) { 36 | throw new Error('Google API requires private key'); 37 | } 38 | conf.clientEmail = _conf.clientEmail; 39 | conf.privateKey = _conf.privateKey; 40 | } 41 | 42 | /** 43 | * googleServiceAccount is optional 44 | * googleServiceAccount { 45 | clientEmail: , 46 | privateKey: 47 | } 48 | * receipt { 49 | packageName: , 50 | productId: 51 | purchaseToken: , 52 | subscription: 53 | } 54 | */ 55 | function validatePurchase(_googleServiceAccount, receipt, cb) { 56 | verbose.log(NAME, 'Validate this', receipt); 57 | if (!receipt.packageName) { 58 | return cb(new Error('Missing Package Name'), { 59 | status: constants.VALIDATION.FAILURE, 60 | message: 'Missing Package Name', 61 | data: receipt 62 | }); 63 | } else if (!receipt.productId) { 64 | return cb(new Error('Missing Product ID'), { 65 | status: constants.VALIDATION.FAILURE, 66 | message: 'Missing Product ID', 67 | data: receipt 68 | }); 69 | } else if (!receipt.purchaseToken) { 70 | return cb(new Error('Missing Purchase Token'), { 71 | status: constants.VALIDATION.FAILURE, 72 | message: 'Missing Purchase Token', 73 | data: receipt 74 | }); 75 | } 76 | var googleServiceAccount = conf; 77 | if (_googleServiceAccount && _googleServiceAccount.clientEmail && _googleServiceAccount.privateKey) { 78 | verbose.log(NAME, 'Using one time key data:', _googleServiceAccount); 79 | googleServiceAccount = _googleServiceAccount; 80 | } 81 | _getToken(googleServiceAccount.clientEmail, googleServiceAccount.privateKey, function (error, token) { 82 | if (error) { 83 | return cb(error, { 84 | status: constants.VALIDATION.FAILURE, 85 | message: error.message 86 | }); 87 | } 88 | var url = _getValidationUrl(receipt, token); 89 | verbose.log(NAME, 'Validation URL:', url); 90 | var params = { 91 | method: 'GET', 92 | url: url, 93 | json: true 94 | }; 95 | request(params, function (error, res, body) { 96 | if (error) { 97 | return cb(error, { status: constants.VALIDATION.FAILURE, message: body }); 98 | } 99 | if (res.statusCode === 410) { 100 | // https://stackoverflow.com/questions/45688494/google-android-publisher-api-responds-with-410-purchasetokennolongervalid-erro 101 | verbose.log(NAME, 'Receipt is no longer valid'); 102 | return cb(new Error('ReceiptNoLongerValid'), { 103 | status: constants.VALIDATION.FAILURE, 104 | message: body 105 | }); 106 | } 107 | if (res.statusCode > 399) { 108 | verbose.log(NAME, 'Validation failed:', res.statusCode, body); 109 | var msg; 110 | try { 111 | msg = JSON.stringify(body, null, 2); 112 | } catch (e) { 113 | msg = body; 114 | } 115 | return cb(new Error('Status:' + res.statusCode + ' - ' + msg), { 116 | status: constants.VALIDATION.FAILURE, 117 | message: body, 118 | data: receipt 119 | }); 120 | } 121 | // we need service 122 | var resp = { 123 | service: constants.SERVICES.GOOGLE, 124 | status: constants.VALIDATION.SUCCESS, 125 | packageName: receipt.packageName, 126 | productId: receipt.productId, 127 | purchaseToken: receipt.purchaseToken 128 | }; 129 | for (var name in body) { 130 | resp[name] = body[name]; 131 | } 132 | cb(null, resp); 133 | }); 134 | }); 135 | } 136 | 137 | function _getToken(clientEmail, privateKey, cb) { 138 | var now = Math.floor(Date.now() / 1000); 139 | var token = jwt.encode({ 140 | iss: clientEmail, 141 | scope: SCOPE, 142 | aud: GET_TOKEN, 143 | exp: now + 3600, 144 | iat: now 145 | }, privateKey, 'RS256'); 146 | var params = { 147 | method: 'POST', 148 | url: GET_TOKEN, 149 | body: 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=' + token, 150 | headers: { 151 | 'content-type': 'application/x-www-form-urlencoded' 152 | }, 153 | json: true 154 | }; 155 | verbose.log(NAME, 'Get token with', clientEmail, '\n', privateKey); 156 | request(params, function (error, res, body) { 157 | if (error) { 158 | return cb(error); 159 | } 160 | if (res.statusCode > 399) { 161 | return cb(new Error('Failed to get token: ' + body)); 162 | } 163 | cb(null, body.access_token); 164 | }); 165 | } 166 | 167 | // receipt: { purchaseToken, subscription } 168 | function _getValidationUrl(receipt, token) { 169 | var url = ''; 170 | switch (receipt.subscription) { 171 | case true: 172 | url = SUBSCR_VAL; 173 | break; 174 | case false: 175 | default: 176 | url = PRODUCT_VAL; 177 | break; 178 | } 179 | return util.format( 180 | url, 181 | encodeURIComponent(receipt.packageName), 182 | encodeURIComponent(receipt.productId), 183 | encodeURIComponent(receipt.purchaseToken), 184 | encodeURIComponent(token) 185 | ); 186 | } 187 | 188 | -------------------------------------------------------------------------------- /lib/amazon2.js: -------------------------------------------------------------------------------- 1 | var constants = require('../constants'); 2 | var request = require('request'); 3 | var fs = require('fs'); 4 | var verbose = require('./verbose'); 5 | 6 | // for some reason amazon v2.0 RVS is 1.0.... 7 | var VER = '1.0'; 8 | var SECRET = '{sharedSecret}'; 9 | var UID = '{userId}'; 10 | var RID = '{receiptId}'; 11 | var ERRORS = { 12 | VALIDATION: { 13 | 400: 'The transaction represented by this Purchase Token is no longer valid.', 14 | 404: 'Unknown operation exception.', 15 | 496: 'Invalid sharedSecret', 16 | 497: 'Invalid User ID', 17 | 498: 'Invalid Purchase Token', 18 | 499: 'The Purchase Token was created with credentials that have expired, use renew to generate a valid purchase token.', 19 | 500: 'There was an Internal Server Error' 20 | }, 21 | RENEW: { 22 | 400: 'Bad Request', 23 | 404: 'Unknown operation exception.', 24 | 496: 'Invalid sharedSecret', 25 | 497: 'Invalid User ID', 26 | 498: 'Invalid Purchase Token', 27 | 500: 'There is an Internal Server Error' 28 | } 29 | }; 30 | 31 | var VALIDATION_HOST = 'https://appstore-sdk.amazon.com'; 32 | var VALIDATION_PATH = '/version/' + 33 | VER + '/verifyReceiptId/developer/' + SECRET + '/user/' + UID + '/receiptId/' + RID; 34 | var RENEW_PATH = '/version/' + 35 | VER + '/renew/developer/' + SECRET + '/user/' + UID + '/receiptId/' + RID; 36 | 37 | var S_VAL_PATH = VALIDATION_PATH; 38 | var S_R_PATH = RENEW_PATH; 39 | 40 | var NAME = ''; 41 | 42 | var config; 43 | var validationHost = VALIDATION_HOST; 44 | 45 | module.exports.readConfig = function (configIn) { 46 | config = configIn; 47 | // apply configurations 48 | if (configIn.requestDefaults) { 49 | request = request.defaults(configIn.requestDefaults); 50 | } 51 | // we allow the user to set an alternative validation host for testing 52 | if (configIn.amazonValidationHost) { 53 | // changes the host permanently... 54 | VALIDATION_HOST = configIn.amazonValidationHost; 55 | validationHost = VALIDATION_HOST; 56 | } 57 | verbose.setup(config); 58 | }; 59 | 60 | module.exports.setup = function (cb) { 61 | if (!config || !config.secret) { 62 | return cb(); 63 | } 64 | fs.exists(config.secret, function (exists) { 65 | var secret = ''; 66 | if (!exists) { 67 | // use the string value literally 68 | secret = config.secret; 69 | VALIDATION_PATH = VALIDATION_PATH.replace(SECRET, config.secret); 70 | RENEW_PATH = RENEW_PATH.replace(SECRET, config.secret); 71 | verbose.log(NAME, 'Secret:', config.secret); 72 | return cb(); 73 | } 74 | // assume it as a file path 75 | fs.readFile(config.secret, 'UTF-8', function (error, val) { 76 | if (error) { 77 | return cb(error); 78 | } 79 | secret = val.replace(/(\r|\n)/g, ''); 80 | VALIDATION_PATH = VALIDATION_PATH.replace(SECRET, secret); 81 | RENEW_PATH = RENEW_PATH.replace(SECRET, secret); 82 | verbose.log(NAME, 'Secret:', secret); 83 | cb(); 84 | }); 85 | }); 86 | }; 87 | 88 | // test use ONLY 89 | module.exports.setValidationHost = function (_validationHost) { 90 | validationHost = _validationHost; 91 | return true; 92 | }; 93 | 94 | // test use ONLY... 95 | module.exports.resetValidationHost = function () { 96 | validationHost = VALIDATION_HOST; 97 | return true; 98 | }; 99 | 100 | /* 101 | receipt: { 102 | userId: 103 | receiptId: 104 | } 105 | */ 106 | module.exports.validatePurchase = function (dSecret, receipt, cb) { 107 | var rpath = validationHost + RENEW_PATH; 108 | var path; 109 | 110 | // override secret with dSecret to allow dynamically fed secret to validate 111 | if (dSecret) { 112 | verbose.log(NAME, 'Use dynamically fed secret:', dSecret); 113 | rpath = S_R_PATH.replace(SECRET, dSecret); 114 | var vpath = S_VAL_PATH.replace(SECRET, dSecret); 115 | path = validationHost + vpath.replace(UID, receipt.userId); 116 | } else { 117 | path = validationHost + VALIDATION_PATH.replace(UID, receipt.userId); 118 | } 119 | path = path.replace(RID, receipt.receiptId); 120 | verbose.log(NAME, 'Validate:', path, receipt); 121 | send(path, ERRORS.VALIDATION, function (error, res) { 122 | if (error) { 123 | if (res === 499) { 124 | // must be renewed and re-tried 125 | var renew = rpath.replace(UID, receipt.userId); 126 | renew = renew.replace(RID, receipt.receiptId); 127 | verbose.log(NAME, 'Purchase must be renewed (' + res + '):', renew); 128 | send(renew, ERRORS.RENEW, function (error, renewed) { 129 | if (error) { 130 | var renewedErrorRes = { 131 | status: renewed, 132 | message: ERRORS.RENEW[renewed] || 'Unknown' 133 | }; 134 | verbose.log(NAME, 'Failed to renew purchase:', renewedErrorRes); 135 | return cb(error, renewedErrorRes); 136 | } 137 | var renewedReceipt = { 138 | receiptId: renewed.receiptId, 139 | userId: receipt.userId 140 | }; 141 | verbose.log(NAME, 'Purchase renewed:', renewedReceipt); 142 | module.exports.validatePurchase(renewedReceipt, cb); 143 | }); 144 | return; 145 | } 146 | 147 | var errorRes = { 148 | sandbox: _isSandbox(path), 149 | status: res, 150 | message: ERRORS.VALIDATION[res] || 'Unknown' 151 | }; 152 | verbose.log(NAME, 'Validation failed:', errorRes); 153 | return cb(error, errorRes); 154 | } 155 | verbose.log(NAME, 'Validation successful:', res); 156 | res.sandbox = _isSandbox(path); 157 | cb(null, res); 158 | }); 159 | }; 160 | 161 | function _isSandbox(path) { 162 | return !path.startsWith(`${VALIDATION_HOST}/`); 163 | } 164 | 165 | module.exports.getPurchaseData = function (purchase, options) { 166 | if (!purchase || !purchase.receiptId) { 167 | return null; 168 | } 169 | 170 | var now = Date.now(); 171 | 172 | if (options && options.ignoreExpired && purchase.cancelDate && purchase.cancelDate <= now) { 173 | return []; 174 | } 175 | 176 | var obj = {}; 177 | obj.transactionId = purchase.receiptId; 178 | obj.productId = purchase.productId; 179 | obj.purchaseData = purchase.productType; 180 | obj.quantity = 1; 181 | obj.purchaseDate = purchase.purchaseDate || now; 182 | obj.expirationDate = purchase.cancelDate || 0; 183 | return [ obj ]; 184 | }; 185 | 186 | function send(path, errorMap, cb) { 187 | request.get(path, function (error, response, body) { 188 | if (response) { 189 | var errorMsg = errorMap[response.statusCode]; 190 | if (errorMsg) { 191 | return cb(new Error(errorMsg + ': ' + path), response.statusCode); 192 | } 193 | } 194 | if (error) { 195 | error.message += ': ' + path; 196 | return cb(error); 197 | } 198 | var res = null; 199 | try { 200 | res = JSON.parse(body); 201 | res.status = 0; 202 | res.service = constants.SERVICES.AMAZON; 203 | } 204 | catch (exc) { 205 | exc.message += ': ' + path + ' [status: ' + response.statusCode + ']'; 206 | return cb(exc); 207 | } 208 | cb(null, res); 209 | }); 210 | } 211 | -------------------------------------------------------------------------------- /lib/facebook.js: -------------------------------------------------------------------------------- 1 | var verbose = require('./verbose'); 2 | var constants = require('../constants'); 3 | var responseData = require('./responseData'); 4 | var request = require('request'); 5 | var crypto = require('crypto'); 6 | var urlbase64 = require('urlsafe-base64'); 7 | var FB = require('fb'); 8 | 9 | var fb = new FB.Facebook({ version: 'v3.3' }); 10 | 11 | var config = null; 12 | 13 | function isValidConfigKey(key) { 14 | return key.match(/^facebook/); 15 | } 16 | 17 | module.exports.readConfig = function (configIn) { 18 | if (!configIn) { 19 | // no facebook iap or password not required 20 | return; 21 | } 22 | 23 | // set up verbose logging 24 | verbose.setup(configIn); 25 | 26 | config = {}; 27 | var configValueSet = false; 28 | // Apply any default settings to Request. 29 | if ('requestDefaults' in configIn) { 30 | request = request.defaults(configIn.requestDefaults); 31 | } 32 | Object.keys(configIn).forEach(function (key) { 33 | if (isValidConfigKey(key)) { 34 | config[key] = configIn[key]; 35 | configValueSet = true; 36 | } 37 | }); 38 | if (configIn.facebookAppSecret && configIn.facebookAppId) { 39 | fb.setAccessToken(configIn.facebookAppId + '|' + configIn.facebookAppSecret); 40 | } 41 | 42 | if (!configValueSet) { 43 | config = null; 44 | } 45 | }; 46 | 47 | module.exports.setup = function (cb) { 48 | if (!config || !config.facebookAppSecret || !config.facebookAppId) { 49 | if (process.env.FACEBOOK_APP_SECRET) { 50 | config = config || {}; 51 | config.facebookAppSecret = process.env.FACEBOOK_APP_SECRET; 52 | } 53 | if (process.env.FACEBOOK_APP_ID) { 54 | config = config || {}; 55 | config.facebookAppId = process.env.FACEBOOK_APP_ID; 56 | } 57 | } 58 | 59 | return cb(); 60 | }; 61 | 62 | module.exports.validatePurchase = function (oneTimeAppAccessToken, receipt, cb) { 63 | var appId = config.facebookAppId; 64 | var appSecret = config.facebookAppSecret; 65 | 66 | if (oneTimeAppAccessToken) { 67 | verbose.log(' Using dynamic app access token:', oneTimeAppAccessToken); 68 | var splitedToken = oneTimeAppAccessToken.split('|'); 69 | appId = splitedToken[0]; 70 | appSecret = splitedToken[1]; 71 | } 72 | 73 | // see "Order Fulfillment" to understand what following steps are at https://developers.facebook.com/docs/games_payments/fulfillment#orderfulfillment 74 | verbose.log(' Validate signed_request: ', receipt); 75 | var signAndRequest = receipt.split('.'); 76 | var encodedSign = signAndRequest[0]; 77 | var encodedPurchase = signAndRequest[1]; 78 | 79 | if (signAndRequest.length !== 2) { 80 | verbose.log(' Receipt involves unrelated data'); 81 | cb(new Error('failed to validate purchase: involve unrelated data'), { 82 | status: constants.VALIDATION.FAILURE, 83 | message: 'involve unrelated data' 84 | }); 85 | return; 86 | } 87 | 88 | // because urlbase64.decode executes decoding after triming encodedSign, check whether it involves non-urlbase64 character 89 | // encodedPurchase is not the case because crypto.createHmac makes non-urlbase64 characters into consideration. 90 | if (!urlbase64.validate(encodedSign)) { 91 | verbose.log(' Sign is not urlsafe-base64 encoded'); 92 | cb(new Error('failed to validate purchase: signature is not a valid urlsafe-base64 form'), { 93 | status: constants.VALIDATION.FAILURE, 94 | message: 'signature is not a valid urlsafe-base64 form' 95 | }); 96 | return; 97 | } 98 | 99 | var sign = ''; 100 | var purchase = ''; 101 | var purchaseObj = ''; 102 | try { 103 | sign = urlbase64.decode(encodedSign); 104 | purchase = urlbase64.decode(encodedPurchase); 105 | purchaseObj = JSON.parse(purchase); 106 | } catch (e) { 107 | verbose.log(' Failed to parse receipt: ', e); 108 | cb(new Error('failed to validate purchase: receipt is malformed'), { 109 | status: constants.VALIDATION.FAILURE, 110 | message: 'receipt is malformed' 111 | }); 112 | return; 113 | } 114 | 115 | if (purchaseObj.algorithm !== 'HMAC-SHA256') { 116 | verbose.log(' Receipt sign algorithm is not HMAC-SHA256'); 117 | cb(new Error('failed to validate purchase: invalid algorithm'), { 118 | status: constants.VALIDATION.FAILURE, 119 | message: 'invalid algorithm' 120 | }); 121 | return; 122 | } 123 | if (purchaseObj.status !== 'completed') { 124 | verbose.log(' Receipt status is not completed'); 125 | cb(new Error('failed to validate purchase: payments not completed: ' + purchaseObj.status), { 126 | status: constants.VALIDATION.FAILURE, 127 | message: 'payments not completed: ' + purchaseObj.status 128 | }); 129 | return; 130 | } 131 | 132 | var hmac = crypto.createHmac('sha256', appSecret).update(encodedPurchase).digest(); 133 | 134 | if (!sign.equals(hmac)) { 135 | verbose.log(' Sign is invalid'); 136 | cb(new Error('failed to validate purchase: invalid sign'), { 137 | status: constants.VALIDATION.FAILURE, 138 | message: 'invalid sign' 139 | }); 140 | return; 141 | } 142 | 143 | var requestParams = { 144 | fields: 'id,user,actions,application,created_time,country,items,payout_foreign_exchange_rate,phone_support_eligible,refundable_amount,tax,tax_country,test', 145 | access_token: appId + '|' + appSecret, // eslint-disable-line camelcase 146 | }; 147 | fb.api(purchaseObj.payment_id + '?', 'get', requestParams, function (paymentRes) { 148 | if (!paymentRes || paymentRes.error) { 149 | verbose.log(' Failed to call graph api: ', paymentRes); 150 | paymentRes = paymentRes || {}; 151 | cb(new Error('failed to validate purchase: graph api call error: ' + JSON.stringify(paymentRes.error)), { 152 | status: constants.VALIDATION.FAILURE, 153 | message: 'graph api call error: ' + JSON.stringify(paymentRes.error) 154 | }); 155 | return; 156 | } 157 | var quantityNum = Number(purchaseObj.quantity); 158 | if (purchaseObj.amount !== paymentRes.actions[0].amount || 159 | purchaseObj.currency !== paymentRes.actions[0].currency || 160 | purchaseObj.status !== paymentRes.actions[0].status || 161 | paymentRes.actions[0].type !== 'charge' || 162 | quantityNum !== paymentRes.items[0].quantity) { 163 | verbose.log(' Receipt info does not match with info from facebook'); 164 | cb(new Error('failed to validate purchase: payment information not matching'), { 165 | status: constants.VALIDATION.FAILURE, 166 | message: 'payment information not matching' 167 | }); 168 | return; 169 | } 170 | purchaseObj.service = constants.SERVICES.FACEBOOK; 171 | purchaseObj.status = constants.VALIDATION.SUCCESS; 172 | verbose.log(' Validation success: ', purchaseObj); 173 | cb(null, purchaseObj); 174 | return; 175 | }); 176 | }; 177 | 178 | module.exports.getPurchaseData = function (purchase) { 179 | if (!purchase) { 180 | return null; 181 | } 182 | var data = []; 183 | var purchaseInfo = responseData.parse(purchase); 184 | // purchase(singed_request) already has product_id and quantity 185 | purchaseInfo.transactionId = purchase.payment_id; 186 | purchaseInfo.purchaseDate = purchase.purchase_time * 1000; 187 | data.push(purchaseInfo); 188 | return data; 189 | }; 190 | -------------------------------------------------------------------------------- /test/facebook.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | var fixedPath = process.cwd() + '/test/receipts/facebook'; 4 | var fixedAppAccessTokenPath = process.cwd() + '/test/receipts/facebook.app_token'; 5 | 6 | describe('#### Facebook ####', function () { 7 | 8 | var appAccessTokenPath = process.argv[process.argv.length - 2].replace('--appAccessToken=', ''); 9 | var receiptPath = process.argv[process.argv.length - 1].replace('--path=', ''); 10 | 11 | if (appAccessTokenPath === 'false') { 12 | appAccessTokenPath = fixedAppAccessTokenPath; 13 | } 14 | if (receiptPath === 'false') { 15 | receiptPath = fixedPath; 16 | } 17 | 18 | var receipt = null; 19 | var appId = null; 20 | var appSecret = null; 21 | before(function (done) { 22 | fs.readFile(receiptPath, 'UTF-8', function (error, data) { 23 | assert.equal(error, null); 24 | receipt = data.toString(); 25 | fs.readFile(appAccessTokenPath, 'UTF-8', function (error, data) { 26 | assert.equal(error, null); 27 | var splitedToken = data.toString().split('|'); 28 | appId = splitedToken[0]; 29 | appSecret = splitedToken[1]; 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | it('Can NOT validate facebook in-app-purchase with incorrect receipt w/ auto-service detection', function (done) { 36 | var iap = require('../'); 37 | var fakeReceipt = 'MDAwMDA.e30K'; 38 | iap.config({ 39 | verbose: true, 40 | facebookAppSecret: appSecret, 41 | facebookAppId: appId 42 | }); 43 | iap.setup(function (error) { 44 | assert.equal(error, null); 45 | iap.validate(fakeReceipt, function (error, response) { 46 | assert(error); 47 | assert.equal(iap.isValidated(response), false); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | 53 | it('Can NOT validate facebook in-app-purchase with incorrect receipt', function (done) { 54 | var iap = require('../'); 55 | var fakeReceipt = 'MDAwMDA.e30K'; 56 | iap.config({ 57 | verbose: true, 58 | facebookAppSecret: appSecret, 59 | facebookAppId: appId 60 | }); 61 | iap.setup(function (error) { 62 | assert.equal(error, null); 63 | iap.validate(iap.FACEBOOK, fakeReceipt, function (error, response) { 64 | assert(error); 65 | assert.equal(iap.isValidated(response), false); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | 71 | it('Can validate facebook in-app-purchase w/ Promise & auto-service detection', function (done) { 72 | 73 | if (!Promise) { 74 | return done(); 75 | } 76 | 77 | var iap = require('../'); 78 | iap.config({ 79 | verbose: true, 80 | facebookAppSecret: appSecret, 81 | facebookAppId: appId 82 | }); 83 | var promise = iap.setup(); 84 | promise.then(function () { 85 | var val = iap.validate(receipt); 86 | val.then(function (response) { 87 | assert.equal(iap.isValidated(response), true); 88 | var pdata = iap.getPurchaseData(response); 89 | for (var i = 0, len = pdata.length; i < len; i++) { 90 | assert(pdata[i].productId); 91 | assert(pdata[i].purchaseDate); 92 | assert(pdata[i].quantity); 93 | } 94 | done(); 95 | }).catch(function (error) { 96 | done(error); 97 | }); 98 | }).catch(function (error) { 99 | done(error); 100 | }); 101 | }); 102 | 103 | it('Can validate facebook in-app-purchase w/ auto-service detection', function (done) { 104 | var iap = require('../'); 105 | iap.config({ 106 | verbose: true, 107 | facebookAppSecret: appSecret, 108 | facebookAppId: appId 109 | }); 110 | iap.setup(function (error) { 111 | assert.equal(error, null); 112 | iap.validate(receipt, function (error, response) { 113 | assert.equal(error, null); 114 | assert.equal(iap.isValidated(response), true); 115 | var pdata = iap.getPurchaseData(response); 116 | for (var i = 0, len = pdata.length; i < len; i++) { 117 | assert(pdata[i].productId); 118 | assert(pdata[i].purchaseDate); 119 | assert(pdata[i].quantity); 120 | } 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | it('Can validate facebook in-app-purchase', function (done) { 127 | var iap = require('../'); 128 | iap.config({ 129 | verbose: true, 130 | facebookAppSecret: appSecret, 131 | facebookAppId: appId 132 | }); 133 | iap.setup(function (error) { 134 | assert.equal(error, null); 135 | iap.validate(iap.FACEBOOK, receipt, function (error, response) { 136 | assert.equal(error, null); 137 | assert.equal(iap.isValidated(response), true); 138 | var pdata = iap.getPurchaseData(response); 139 | for (var i = 0, len = pdata.length; i < len; i++) { 140 | assert(pdata[i].productId); 141 | assert(pdata[i].purchaseDate); 142 | assert(pdata[i].quantity); 143 | } 144 | done(); 145 | }); 146 | }); 147 | }); 148 | 149 | it('Can get an error response', function (done) { 150 | var iap = require('../'); 151 | var fakeReceipt = "MDAwMDA.e30K"; 152 | iap.config({ 153 | verbose: true, 154 | facebookAppSecret: appSecret, 155 | facebookAppId: appId 156 | }); 157 | iap.setup(function (error) { 158 | assert.equal(error, null); 159 | iap.validate(iap.FACEBOOK, fakeReceipt, function (error, response) { 160 | assert(error); 161 | assert(response); 162 | assert(response.status); 163 | assert(response.message); 164 | assert.equal(iap.isValidated(response), false); 165 | done(); 166 | }); 167 | }); 168 | }); 169 | 170 | it('Can validate facebook in-app-purchase using .validateOnce()', function (done) { 171 | var iap = require('../'); 172 | iap.config({ 173 | verbose: true, 174 | facebookAppSecret: '', 175 | facebookAppId: '' 176 | }); 177 | iap.setup(function (error) { 178 | assert.equal(error, null); 179 | iap.validateOnce(iap.FACEBOOK, appId + '|' + appSecret, receipt, function (error, response) { 180 | assert.equal(error, null); 181 | assert.equal(iap.isValidated(response), true); 182 | var pdata = iap.getPurchaseData(response); 183 | for (var i = 0, len = pdata.length; i < len; i++) { 184 | assert(pdata[i].productId); 185 | assert(pdata[i].purchaseDate); 186 | assert(pdata[i].quantity); 187 | } 188 | done(); 189 | }); 190 | }); 191 | }); 192 | 193 | it('Can validate facebook in-app-purchase using .validateOnce() w/ auto-service detection', function (done) { 194 | var iap = require('../'); 195 | iap.config({ 196 | verbose: true, 197 | facebookAppSecret: '', 198 | facebookAppId: '' 199 | }); 200 | iap.setup(function (error) { 201 | assert.equal(error, null); 202 | iap.validateOnce(receipt, appId + '|' + appSecret, function (error, response) { 203 | assert.equal(error, null); 204 | assert.equal(iap.isValidated(response), true); 205 | var pdata = iap.getPurchaseData(response); 206 | for (var i = 0, len = pdata.length; i < len; i++) { 207 | assert(pdata[i].productId); 208 | assert(pdata[i].purchaseDate); 209 | assert(pdata[i].quantity); 210 | } 211 | done(); 212 | }); 213 | }); 214 | }); 215 | 216 | }); 217 | -------------------------------------------------------------------------------- /test/windows.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | var fixedPath = process.cwd() + '/test/receipts/windows'; 4 | 5 | describe('#### Windows ####', function () { 6 | 7 | it('Can validate windows in-app-purchase w/o waiting for .setup()', function (done) { 8 | 9 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 10 | 11 | if (path === 'false') { 12 | path = fixedPath; 13 | } 14 | 15 | var iap = require('../'); 16 | iap.config({ verbose: true }); 17 | iap.setup(); 18 | fs.readFile(path, function (error, data) { 19 | assert.equal(error, undefined); 20 | var receipt = data.toString(); 21 | iap.validate(iap.WINDOWS, receipt, function (error, response) { 22 | assert.equal(error, undefined); 23 | assert.equal(iap.isValidated(response), true); 24 | var data = iap.getPurchaseData(response); 25 | for (var i = 0, len = data.length; i < len; i++) { 26 | assert(data[i].productId); 27 | assert(data[i].purchaseDate); 28 | assert(data[i].expirationDate); 29 | assert(data[i].quantity); 30 | } 31 | done(); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | it('Can validate windows in-app-purchase w/ Promise & auto-service detection', function (done) { 38 | 39 | if (!Promise) { 40 | return done(); 41 | } 42 | 43 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 44 | 45 | if (path === 'false') { 46 | path = fixedPath; 47 | } 48 | 49 | var iap = require('../'); 50 | var receipt = fs.readFileSync(path, 'utf8'); 51 | var promise = iap.setup(); 52 | promise.then(function () { 53 | var val = iap.validate(receipt); 54 | val.then(function (response) { 55 | assert.equal(iap.isValidated(response), true); 56 | var data = iap.getPurchaseData(response); 57 | for (var i = 0, len = data.length; i < len; i++) { 58 | assert(data[i].productId); 59 | assert(data[i].purchaseDate); 60 | assert(data[i].expirationDate); 61 | assert(data[i].quantity); 62 | } 63 | done(); 64 | }).catch(function (error) { 65 | throw error; 66 | }); 67 | }).catch(function (error) { 68 | throw error; 69 | }); 70 | 71 | }); 72 | 73 | it('Can validate windows in-app-purchase w/ auto-service detection', function (done) { 74 | 75 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 76 | 77 | if (path === 'false') { 78 | path = fixedPath; 79 | } 80 | 81 | var iap = require('../'); 82 | iap.config({ verbose: true }); 83 | iap.setup(function (error) { 84 | assert.equal(error, undefined); 85 | fs.readFile(path, function (error, data) { 86 | assert.equal(error, undefined); 87 | var receipt = data.toString(); 88 | iap.validate(receipt, function (error, response) { 89 | assert.equal(error, undefined); 90 | assert.equal(iap.isValidated(response), true); 91 | var data = iap.getPurchaseData(response); 92 | for (var i = 0, len = data.length; i < len; i++) { 93 | assert(data[i].productId); 94 | assert(data[i].purchaseDate); 95 | assert(data[i].expirationDate); 96 | assert(data[i].quantity); 97 | } 98 | done(); 99 | }); 100 | }); 101 | }); 102 | 103 | }); 104 | 105 | it('Can validate windows in-app-purchase', function (done) { 106 | 107 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 108 | 109 | if (path === 'false') { 110 | path = fixedPath; 111 | } 112 | 113 | var iap = require('../'); 114 | iap.config({ verbose: true }); 115 | iap.setup(function (error) { 116 | assert.equal(error, undefined); 117 | fs.readFile(path, function (error, data) { 118 | assert.equal(error, undefined); 119 | var receipt = data.toString(); 120 | iap.validate(iap.WINDOWS, receipt, function (error, response) { 121 | assert.equal(error, undefined); 122 | assert.equal(iap.isValidated(response), true); 123 | var data = iap.getPurchaseData(response); 124 | for (var i = 0, len = data.length; i < len; i++) { 125 | assert(data[i].productId); 126 | assert(data[i].purchaseDate); 127 | assert(data[i].expirationDate); 128 | assert(data[i].quantity); 129 | } 130 | done(); 131 | }); 132 | }); 133 | }); 134 | 135 | }); 136 | 137 | it('Can validate windows in-app-purchase using .validateOnce() w/ auto-service detection', function (done) { 138 | 139 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 140 | 141 | if (path === 'false') { 142 | path = fixedPath; 143 | } 144 | 145 | var iap = require('../'); 146 | iap.config({ verbose: true }); 147 | iap.setup(function (error) { 148 | assert.equal(error, undefined); 149 | fs.readFile(path, function (error, data) { 150 | assert.equal(error, undefined); 151 | var receipt = data.toString(); 152 | iap.validateOnce(receipt, null, function (error, response) { 153 | assert.equal(error, undefined); 154 | assert.equal(iap.isValidated(response), true); 155 | var data = iap.getPurchaseData(response); 156 | for (var i = 0, len = data.length; i < len; i++) { 157 | assert(data[i].productId); 158 | assert(data[i].purchaseDate); 159 | assert(data[i].expirationDate); 160 | assert(data[i].quantity); 161 | } 162 | done(); 163 | }); 164 | }); 165 | }); 166 | 167 | }); 168 | 169 | it('Can validate windows in-app-purchase using .validateOnce()', function (done) { 170 | 171 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 172 | 173 | if (path === 'false') { 174 | path = fixedPath; 175 | } 176 | 177 | var iap = require('../'); 178 | iap.config({ verbose: true }); 179 | iap.setup(function (error) { 180 | assert.equal(error, undefined); 181 | fs.readFile(path, function (error, data) { 182 | assert.equal(error, undefined); 183 | var receipt = data.toString(); 184 | iap.validateOnce(iap.WINDOWS, null, receipt, function (error, response) { 185 | assert.equal(error, undefined); 186 | assert.equal(iap.isValidated(response), true); 187 | var data = iap.getPurchaseData(response); 188 | for (var i = 0, len = data.length; i < len; i++) { 189 | assert(data[i].productId); 190 | assert(data[i].purchaseDate); 191 | assert(data[i].expirationDate); 192 | assert(data[i].quantity); 193 | } 194 | done(); 195 | }); 196 | }); 197 | }); 198 | 199 | }); 200 | 201 | it('Can validate windows in-app-purchase and ignores expired item', function (done) { 202 | 203 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 204 | 205 | if (path === 'false') { 206 | path = fixedPath; 207 | } 208 | 209 | var iap = require('../'); 210 | iap.config({ verbose: true }); 211 | iap.setup(function (error) { 212 | assert.equal(error, undefined); 213 | fs.readFile(path, function (error, data) { 214 | assert.equal(error, undefined); 215 | var receipt = data.toString(); 216 | iap.validate(iap.WINDOWS, receipt, function (error, response) { 217 | assert.equal(error, undefined); 218 | assert.equal(iap.isValidated(response), true); 219 | var data = iap.getPurchaseData(response, { ignoreExpired: true }); 220 | assert.equal(data.length, 0); 221 | done(); 222 | }); 223 | }); 224 | }); 225 | 226 | }); 227 | 228 | it('Can NOT validate windows in-app-purchase with incorrect receipt w/ auto-service detection', function (done) { 229 | 230 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 231 | 232 | if (path === 'false') { 233 | path = fixedPath; 234 | } 235 | 236 | var iap = require('../'); 237 | iap.config({ verbose: true }); 238 | iap.setup(function (error) { 239 | assert.equal(error, undefined); 240 | iap.validate(iap.WINDOWS, 'fake-receipt', function (error, response) { 241 | assert(error); 242 | assert.equal(iap.isValidated(response), false); 243 | done(); 244 | }); 245 | }); 246 | 247 | }); 248 | 249 | it('Can NOT validate windows in-app-purchase with incorrect receipt', function (done) { 250 | 251 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 252 | 253 | if (path === 'false') { 254 | path = fixedPath; 255 | } 256 | 257 | var iap = require('../'); 258 | iap.config({ verbose: true }); 259 | iap.setup(function (error) { 260 | assert.equal(error, undefined); 261 | iap.validate(iap.WINDOWS, 'fake-receipt', function (error, response) { 262 | assert(error); 263 | assert.equal(iap.isValidated(response), false); 264 | done(); 265 | }); 266 | }); 267 | 268 | }); 269 | 270 | it('Can get an error response', function (done) { 271 | 272 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 273 | 274 | if (path === 'false') { 275 | path = fixedPath; 276 | } 277 | 278 | var iap = require('../'); 279 | iap.config({ verbose: true }); 280 | iap.setup(function (error) { 281 | assert.equal(error, undefined); 282 | iap.validate(iap.WINDOWS, 'fake-receipt', function (error, response) { 283 | assert(error); 284 | assert(response.status); 285 | assert(response.message); 286 | assert.equal(iap.isValidated(response), false); 287 | done(); 288 | }); 289 | }); 290 | 291 | }); 292 | 293 | }); 294 | -------------------------------------------------------------------------------- /test/amazon.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | var fixedPath = process.cwd() + '/test/receipts/amazon'; 4 | var fixedKeyPath = process.cwd() + '/test/receipts/amazon.secret'; 5 | 6 | describe('#### Amazon ####', function () { 7 | 8 | var sharedKey = process.argv[process.argv.length - 2].replace('--sharedKey=', ''); 9 | var path = process.argv[process.argv.length - 1].replace('--path=', ''); 10 | var iap = require('../'); 11 | 12 | if (sharedKey === 'false') { 13 | sharedKey = fixedKeyPath; 14 | } 15 | if (path === 'false') { 16 | path = fixedPath; 17 | } 18 | 19 | it('Can NOT validate amazon in-app-purchase with incorrect receipt w/ auto-service detection', function (done) { 20 | var fakeReceipt = { userId: null, receiptId: 'fake-receipt' }; 21 | iap.config({ 22 | verbose: true, 23 | secret: sharedKey 24 | }); 25 | iap.setup(function (error) { 26 | iap.validate(fakeReceipt, function (error, response) { 27 | assert(error); 28 | assert.equal(iap.isValidated(response), false); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Can NOT validate amazon in-app-purchase with incorrect receipt', function (done) { 35 | var fakeReceipt = { userId: null, receiptId: 'fake-receipt' }; 36 | iap.config({ 37 | verbose: true, 38 | secret: sharedKey 39 | }); 40 | iap.setup(function (error) { 41 | iap.validate(iap.AMAZON, fakeReceipt, function (error, response) { 42 | assert(error); 43 | assert.equal(iap.isValidated(response), false); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | it('Can validate amazon in-app-purchase w/ Promise & auto-service detection', function (done) { 50 | 51 | if (!Promise) { 52 | return done(); 53 | } 54 | 55 | fs.readFile(path, 'UTF-8', function (error, data) { 56 | assert.equal(error, null); 57 | iap.config({ 58 | verbose: true, 59 | secret: sharedKey 60 | }); 61 | var promise = iap.setup(); 62 | promise.then(function () { 63 | var receipt = JSON.parse(data.toString()); 64 | var val = iap.validate(receipt); 65 | val.then(function (response) { 66 | assert.equal(iap.isValidated(response), true); 67 | var pdata = iap.getPurchaseData(response); 68 | for (var i = 0, len = pdata.length; i < len; i++) { 69 | assert(pdata[i].productId); 70 | assert(pdata[i].purchaseDate); 71 | assert(pdata[i].quantity); 72 | } 73 | done(); 74 | }).catch(function (error) { 75 | throw error; 76 | }); 77 | }).catch(function (error) { 78 | throw error; 79 | }); 80 | }); 81 | }); 82 | 83 | it('Can validate Unity amazon in-app-purchase w/ autho-service detection', function(done) { 84 | var path = process.cwd() + '/test/receipts/unity_amazon'; 85 | 86 | fs.readFile(path, 'UTF-8', function (error, data) { 87 | assert.equal(error, null); 88 | iap.config({ 89 | verbose: true, 90 | secret: sharedKey 91 | }); 92 | iap.setup(function (error) { 93 | assert.equal(error, null); 94 | var receipt = JSON.parse(data.toString()); 95 | iap.validate(receipt, function (error, response) { 96 | assert.equal(error, null); 97 | assert.equal(iap.isValidated(response), true); 98 | var pdata = iap.getPurchaseData(response); 99 | for (var i = 0, len = pdata.length; i < len; i++) { 100 | assert(pdata[i].productId); 101 | assert(pdata[i].purchaseDate); 102 | assert(pdata[i].quantity); 103 | } 104 | done(); 105 | }); 106 | }); 107 | }); 108 | }); 109 | 110 | it('Can validate amazon in-app-purchase w/ auto-service detection', function (done) { 111 | fs.readFile(path, 'UTF-8', function (error, data) { 112 | assert.equal(error, null); 113 | iap.config({ 114 | verbose: true, 115 | secret: sharedKey 116 | }); 117 | iap.setup(function (error) { 118 | assert.equal(error, null); 119 | var receipt = JSON.parse(data.toString()); 120 | iap.validate(receipt, function (error, response) { 121 | assert.equal(error, null); 122 | assert.equal(iap.isValidated(response), true); 123 | var pdata = iap.getPurchaseData(response); 124 | for (var i = 0, len = pdata.length; i < len; i++) { 125 | assert(pdata[i].productId); 126 | assert(pdata[i].purchaseDate); 127 | assert(pdata[i].quantity); 128 | } 129 | done(); 130 | }); 131 | }); 132 | }); 133 | }); 134 | 135 | it('Can validate amazon in-app-purchase', function (done) { 136 | fs.readFile(path, 'UTF-8', function (error, data) { 137 | assert.equal(error, null); 138 | iap.config({ 139 | verbose: true, 140 | secret: sharedKey 141 | }); 142 | iap.setup(function (error) { 143 | assert.equal(error, null); 144 | var receipt = JSON.parse(data.toString()); 145 | iap.validate(iap.AMAZON, receipt, function (error, response) { 146 | assert.equal(error, null); 147 | assert.equal(iap.isValidated(response), true); 148 | var pdata = iap.getPurchaseData(response); 149 | for (var i = 0, len = pdata.length; i < len; i++) { 150 | assert(pdata[i].productId); 151 | assert(pdata[i].purchaseDate); 152 | assert(pdata[i].quantity); 153 | } 154 | done(); 155 | }); 156 | }); 157 | }); 158 | }); 159 | 160 | it('Can get an error response', function (done) { 161 | var fakeReceipt = { userId: null, receiptId: 'fake-receipt' }; 162 | iap.config({ 163 | verbose: true, 164 | secret: sharedKey 165 | }); 166 | iap.setup(function (error) { 167 | iap.validate(iap.AMAZON, fakeReceipt, function (error, response) { 168 | assert(error); 169 | assert(response); 170 | assert(response.status); 171 | assert(response.message); 172 | assert.equal(iap.isValidated(response), false); 173 | done(); 174 | }); 175 | }); 176 | }); 177 | 178 | it('Can validate amazon in-app-purchase with dynamically fed secret', function (done) { 179 | fs.readFile(path, 'UTF-8', function (error, data) { 180 | assert.equal(error, null); 181 | iap.config({ 182 | verbose: true, 183 | secret: null 184 | }); 185 | iap.setup(function (error) { 186 | assert.equal(error, null); 187 | var receipt = JSON.parse(data.toString()); 188 | fs.readFile(sharedKey, 'UTF-8', function (error, secret) { 189 | assert.equal(error, null); 190 | secret = secret.replace(/(\r|\n)/g, ''); 191 | iap.validateOnce(iap.AMAZON, secret, receipt, function (error, response) { 192 | assert.equal(error, null); 193 | assert.equal(iap.isValidated(response), true); 194 | var pdata = iap.getPurchaseData(response); 195 | for (var i = 0, len = pdata.length; i < len; i++) { 196 | assert(pdata[i].productId); 197 | assert(pdata[i].purchaseDate); 198 | assert(pdata[i].quantity); 199 | } 200 | done(); 201 | }); 202 | }); 203 | }); 204 | }); 205 | }); 206 | 207 | it('Can change validation host w/ Amazon API version 2', function (done) { 208 | fs.readFile(path, 'UTF-8', function (error, data) { 209 | assert.equal(error, null); 210 | iap.config({ 211 | verbose: true, 212 | secret: sharedKey 213 | }); 214 | iap.config({ amazonAPIVersion: 2 }); 215 | iap.setup(function (error) { 216 | assert.equal(error, null); 217 | var set = iap.setAmazonValidationHost('fooooooo'); 218 | assert.equal(set, true); 219 | var receipt = JSON.parse(data.toString()); 220 | iap.validate(iap.AMAZON, receipt, function (error) { 221 | assert.notEqual(error, null); 222 | assert.notEqual(error.message.indexOf('fooooooo'), -1); 223 | done(); 224 | }); 225 | }); 226 | }); 227 | }); 228 | 229 | it('Can reset validation host w/ Amazon API version 2', function (done) { 230 | fs.readFile(path, 'UTF-8', function (error, data) { 231 | assert.equal(error, null); 232 | iap.config({ 233 | verbose: true, 234 | secret: sharedKey 235 | }); 236 | iap.config({ amazonAPIVersion: 2 }); 237 | iap.setup(function (error) { 238 | assert.equal(error, null); 239 | var set = iap.resetAmazonValidationHost(); 240 | assert.equal(set, true); 241 | var receipt = JSON.parse(data.toString()); 242 | iap.validate(iap.AMAZON, receipt, function (error) { 243 | assert.notEqual(error, null); 244 | assert.notEqual(error.message.indexOf('https://appstore-sdk.amazon.com'), -1); 245 | done(); 246 | }); 247 | }); 248 | }); 249 | }); 250 | 251 | }); 252 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('./lib/async'); 4 | 5 | var apple = require('./lib/apple'); 6 | var google = require('./lib/google'); 7 | var windows = require('./lib/windows'); 8 | var amazonManager = require('./lib/amazonManager'); 9 | var facebook = require('./lib/facebook'); 10 | var roku = require('./lib/roku'); 11 | var constants = require('./constants'); 12 | var verbose = require('./lib/verbose'); 13 | 14 | var IS_WINDOWS = '<\/Receipt>'; 15 | 16 | var amazon; 17 | 18 | function handlePromisedFunctionCb(resolve, reject) { 19 | return function _handlePromisedCallback(error, response) { 20 | if (error) { 21 | var errorData = { error: error, status: null, message: null }; 22 | if (response !== null && typeof response === 'object') { 23 | errorData.status = response.status; 24 | errorData.message = response.message; 25 | } 26 | return reject(JSON.stringify(errorData), response); 27 | } 28 | return resolve(response); 29 | }; 30 | } 31 | 32 | module.exports.UNITY = constants.SERVICES.UNITY; 33 | module.exports.APPLE = constants.SERVICES.APPLE; 34 | module.exports.GOOGLE = constants.SERVICES.GOOGLE; 35 | module.exports.WINDOWS = constants.SERVICES.WINDOWS; 36 | module.exports.AMAZON = constants.SERVICES.AMAZON; 37 | module.exports.FACEBOOK = constants.SERVICES.FACEBOOK; 38 | module.exports.ROKU = constants.SERVICES.ROKU; 39 | 40 | module.exports.config = function (configIn) { 41 | apple.readConfig(configIn); 42 | google.readConfig(configIn); 43 | windows.readConfig(configIn); 44 | amazon = amazonManager.create(configIn); 45 | facebook.readConfig(configIn); 46 | roku.readConfig(configIn); 47 | verbose.setup(configIn); 48 | }; 49 | 50 | module.exports.setup = function (cb) { 51 | if (!cb && Promise) { 52 | return new Promise(function (resolve, reject) { 53 | module.exports.setup(handlePromisedFunctionCb(resolve, reject)); 54 | }); 55 | } 56 | async.series([ 57 | function (next) { 58 | apple.setup(next); 59 | }, 60 | function (next) { 61 | google.setup(next); 62 | }, 63 | function (next) { 64 | amazon.setup(next); 65 | }, 66 | function (next) { 67 | facebook.setup(next); 68 | }, 69 | ], cb); 70 | }; 71 | 72 | module.exports.getService = function (receipt) { 73 | if (!receipt) { 74 | throw new Error('Receipt was null or undefined'); 75 | } 76 | if (receipt.indexOf && receipt.indexOf(IS_WINDOWS) !== -1) { 77 | return module.exports.WINDOWS; 78 | } 79 | if (typeof receipt === 'object') { 80 | // receipt could be either Google, Amazon, or Unity (Apple or Google or Amazon) 81 | if (isUnityReceipt(receipt)) { 82 | return module.exports.UNITY; 83 | } 84 | if (receipt.signature) { 85 | return module.exports.GOOGLE; 86 | } else if (receipt.purchaseToken) { 87 | return module.exports.GOOGLE; 88 | } else { 89 | return module.exports.AMAZON; 90 | } 91 | } 92 | if (typeof receipt === 'string') { 93 | var characters = receipt.match(/\w/g) || ''; 94 | var dashes = receipt.match(/-/g) || ''; 95 | if (characters.length === 32 && dashes.length === 4) { 96 | return module.exports.ROKU; 97 | } 98 | } 99 | try { 100 | // receipt could be either Google, Amazon, or Unity (Apple or Google or Amazon) 101 | var parsed = JSON.parse(receipt); 102 | if (isUnityReceipt(parsed)) { 103 | return module.exports.UNITY; 104 | } 105 | if (parsed.signature) { 106 | return module.exports.GOOGLE; 107 | } else if (parsed.purchaseToken) { 108 | return module.exports.GOOGLE; 109 | } else { 110 | return module.exports.AMAZON; 111 | } 112 | } catch (error) { 113 | var dotSplitedReceipt = receipt.split('.'); 114 | if (dotSplitedReceipt.length === 2) { 115 | return module.exports.FACEBOOK; 116 | } 117 | return module.exports.APPLE; 118 | } 119 | }; 120 | 121 | module.exports.validate = function (service, receipt, cb) { 122 | if (receipt === undefined && cb === undefined) { 123 | // we are given 1 argument as: const promise = .validate(receipt) 124 | receipt = service; 125 | service = module.exports.getService(receipt); 126 | } 127 | if (cb === undefined && typeof receipt === 'function') { 128 | // we are given 2 arguments as: .validate(receipt, cb) 129 | cb = receipt; 130 | receipt = service; 131 | service = module.exports.getService(receipt); 132 | } 133 | if (!cb && Promise) { 134 | return new Promise(function (resolve, reject) { 135 | module.exports.validate( 136 | service, 137 | receipt, 138 | handlePromisedFunctionCb(resolve, reject) 139 | ); 140 | }); 141 | } 142 | 143 | if (service === module.exports.UNITY) { 144 | service = getServiceFromUnityReceipt(receipt); 145 | receipt = parseUnityReceipt(receipt); 146 | } 147 | 148 | switch (service) { 149 | case module.exports.APPLE: 150 | apple.validatePurchase(null, receipt, cb); 151 | break; 152 | case module.exports.GOOGLE: 153 | google.validatePurchase(null, receipt, cb); 154 | break; 155 | case module.exports.WINDOWS: 156 | windows.validatePurchase(receipt, cb); 157 | break; 158 | case module.exports.AMAZON: 159 | amazon.validatePurchase(null, receipt, cb); 160 | break; 161 | case module.exports.FACEBOOK: 162 | facebook.validatePurchase(null, receipt, cb); 163 | break; 164 | case module.exports.ROKU: 165 | roku.validatePurchase(null, receipt, cb); 166 | break; 167 | default: 168 | return cb(new Error('invalid service given: ' + service)); 169 | } 170 | }; 171 | 172 | module.exports.validateOnce = function (service, secretOrPubKey, receipt, cb) { 173 | if (receipt === undefined && cb === undefined) { 174 | // we are given 2 arguments as: const promise = .validateOnce(receipt, secretOrPubKey) 175 | receipt = service; 176 | service = module.exports.getService(receipt); 177 | } 178 | if (cb === undefined && typeof receipt === 'function') { 179 | // we are given 3 arguemnts as: .validateOnce(receipt, secretPubKey, cb) 180 | cb = receipt; 181 | receipt = service; 182 | service = module.exports.getService(receipt); 183 | } 184 | 185 | if (!cb && Promise) { 186 | return new Promise(function (resolve, reject) { 187 | module.exports.validateOnce( 188 | service, 189 | secretOrPubKey, 190 | receipt, 191 | handlePromisedFunctionCb(resolve, reject) 192 | ); 193 | }); 194 | } 195 | 196 | if (service === module.exports.UNITY) { 197 | service = getServiceFromUnityReceipt(receipt); 198 | receipt = parseUnityReceipt(receipt); 199 | } 200 | 201 | if (!secretOrPubKey && service !== module.exports.APPLE && service !== module.exports.WINDOWS) { 202 | verbose.log('<.validateOnce>', service, receipt); 203 | return cb(new Error('missing secret or public key for dynamic validation:' + service)); 204 | } 205 | 206 | switch (service) { 207 | case module.exports.APPLE: 208 | apple.validatePurchase(secretOrPubKey, receipt, cb); 209 | break; 210 | case module.exports.GOOGLE: 211 | google.validatePurchase(secretOrPubKey, receipt, cb); 212 | break; 213 | case module.exports.WINDOWS: 214 | windows.validatePurchase(receipt, cb); 215 | break; 216 | case module.exports.AMAZON: 217 | amazon.validatePurchase(secretOrPubKey, receipt, cb); 218 | break; 219 | case module.exports.FACEBOOK: 220 | facebook.validatePurchase(secretOrPubKey, receipt, cb); 221 | break; 222 | case module.exports.ROKU: 223 | roku.validatePurchase(secretOrPubKey, receipt, cb); 224 | break; 225 | default: 226 | verbose.log('<.validateOnce>', secretOrPubKey, receipt); 227 | return cb(new Error('invalid service given: ' + service)); 228 | } 229 | }; 230 | 231 | module.exports.isValidated = function (response) { 232 | if (response && response.status === constants.VALIDATION.SUCCESS) { 233 | return true; 234 | } 235 | return false; 236 | }; 237 | 238 | module.exports.isExpired = function (purchasedItem) { 239 | if (!purchasedItem || !purchasedItem.transactionId) { 240 | throw new Error('invalid purchased item given:\n' + JSON.stringify(purchasedItem)); 241 | } 242 | if (purchasedItem.cancellationDate) { 243 | // it has been cancelled 244 | return true; 245 | } 246 | if (!purchasedItem.expirationDate) { 247 | // there is no exipiration date with this item 248 | return false; 249 | } 250 | if (Date.now() - purchasedItem.expirationDate >= 0) { 251 | return true; 252 | } 253 | // has not exipired yet 254 | return false; 255 | }; 256 | 257 | module.exports.isCanceled = function (purchasedItem) { 258 | if (!purchasedItem || !purchasedItem.transactionId) { 259 | throw new Error('invalid purchased item given:\n' + JSON.stringify(purchasedItem)); 260 | } 261 | if (purchasedItem.cancellationDate) { 262 | // it has been cancelled 263 | return true; 264 | } 265 | return false; 266 | }; 267 | 268 | module.exports.getPurchaseData = function (purchaseData, options) { 269 | if (!purchaseData || !purchaseData.service) { 270 | return null; 271 | } 272 | switch (purchaseData.service) { 273 | case module.exports.APPLE: 274 | return apple.getPurchaseData(purchaseData, options); 275 | case module.exports.GOOGLE: 276 | return google.getPurchaseData(purchaseData, options); 277 | case module.exports.WINDOWS: 278 | return windows.getPurchaseData(purchaseData, options); 279 | case module.exports.AMAZON: 280 | return amazon.getPurchaseData(purchaseData, options); 281 | case module.exports.FACEBOOK: 282 | return facebook.getPurchaseData(purchaseData, options); 283 | case module.exports.ROKU: 284 | return roku.getPurchaseData(purchaseData, options); 285 | default: 286 | return null; 287 | } 288 | }; 289 | 290 | module.exports.refreshGoogleToken = function (cb) { 291 | if (!cb && Promise) { 292 | return new Promise(function (resolve, reject) { 293 | module.exports.refreshGoogleToken(handlePromisedFunctionCb(resolve, reject)); 294 | }); 295 | } 296 | google.refreshToken(cb); 297 | 298 | }; 299 | 300 | module.exports.setAmazonValidationHost = function (vhost) { 301 | if (amazon.setValidationHost) { 302 | return amazon.setValidationHost(vhost); 303 | } 304 | return false; 305 | }; 306 | 307 | module.exports.resetAmazonValidationHost = function () { 308 | if (amazon.resetValidationHost) { 309 | return amazon.resetValidationHost(); 310 | } 311 | return false; 312 | }; 313 | 314 | function isUnityReceipt(receipt) { 315 | if (receipt.Store) { 316 | if ( 317 | receipt.Store === constants.UNITY.GOOGLE || 318 | receipt.Store === constants.UNITY.APPLE || 319 | receipt.Store === constants.UNITY.AMAZON 320 | ) { 321 | return true; 322 | } 323 | } 324 | return false; 325 | } 326 | 327 | function getServiceFromUnityReceipt(receipt) { 328 | if (typeof receipt !== 'object') { 329 | // at this point we have already established the fact that receipt is a valid JSON string 330 | receipt = JSON.parse(receipt); 331 | } 332 | switch (receipt.Store) { 333 | case constants.UNITY.GOOGLE: 334 | return module.exports.GOOGLE; 335 | case constants.UNITY.APPLE: 336 | return module.exports.APPLE; 337 | case constants.UNITY.AMAZON: 338 | return module.exports.AMAZON; 339 | } 340 | // invalid Store value 341 | return null; 342 | } 343 | 344 | function parseUnityReceipt(receipt) { 345 | verbose.log('Parse Unity receipt as ' + receipt.Store); 346 | if (typeof receipt !== 'object') { 347 | // at this point we have already established the fact that receipt is a valid JSON string 348 | receipt = JSON.parse(receipt); 349 | } 350 | switch (receipt.Store) { 351 | case constants.UNITY.GOOGLE: 352 | if (typeof receipt.Payload === 'string') { 353 | try { 354 | receipt.Payload = JSON.parse(receipt.Payload); 355 | } catch (error) { 356 | throw error; 357 | } 358 | } 359 | var payloadContent = typeof receipt.Payload.json !== 'object' ? JSON.parse(receipt.Payload.json) : receipt.Payload.json; 360 | return { 361 | data: receipt.Payload.json, 362 | signature: receipt.Payload.signature, 363 | // add field necessary to use google service account 364 | packageName: payloadContent.packageName, 365 | productId: payloadContent.productId, 366 | purchaseToken: payloadContent.purchaseToken, 367 | subscription: (receipt.Subscription !== undefined && receipt.Subscription) 368 | }; 369 | case constants.UNITY.AMAZON: 370 | if (typeof receipt.Payload === 'string') { 371 | try { 372 | receipt.Payload = JSON.parse(receipt.Payload); 373 | } catch (error) { 374 | throw error; 375 | } 376 | } 377 | return receipt.Payload; 378 | case constants.UNITY.APPLE: 379 | return receipt.Payload; 380 | } 381 | } 382 | 383 | // test use only 384 | module.exports.reset = function () { 385 | // resets google setup 386 | google.reset(); 387 | }; 388 | -------------------------------------------------------------------------------- /logo/in-app-purchase-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 57 | 58 | 59 | 61 | 62 | 63 | 104 | 105 | 106 | 107 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 | 143 | 145 | 148 | 149 | 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://user-images.githubusercontent.com/2267361/42299566-e25ee852-8046-11e8-9cc3-a776770fcc8e.png) 2 | 3 | ©Nobuyori Takahashi < > 4 | 5 | [![Build Status](https://travis-ci.org/voltrue2/in-app-purchase.svg?branch=master)](https://travis-ci.org/voltrue2/in-app-purchase) 6 | 7 | A node.js module for in-app purchase (in-app billing) and subscription for Apple, Google Play, Amazon Store, Roku, and Windows. 8 | 9 | It supports Unity receipt also: [Unity Documentation](https://docs.unity3d.com/Manual/UnityIAPValidatingReceipts.html) 10 | 11 | **NOTE** Unity receipt supports the following: Apple, Google Play, and Amazon. 12 | 13 | ## What is new 14 | 15 | As of version `1.10.0`, The module lets you validate Google's receipts using Google Service Account also! 16 | 17 | Thank you for the input [maxs15](https://github.com/maxs15) 18 | 19 | ## Required node.js version 20 | 21 | `0.12.0 >=` 22 | 23 | ## Online Demo and Documention 24 | 25 | Online Demo 26 | 27 | ## How to install 28 | 29 | ``` 30 | npm install in-app-purchase 31 | ``` 32 | 33 | ## How to use 34 | 35 | The module supports both Promise and callbacks. 36 | 37 | ```javascript 38 | var iap = require('in-app-purchase'); 39 | iap.config({ 40 | 41 | /* Configurations for HTTP request */ 42 | requestDefaults: { /* Please refer to the request module documentation here: https://www.npmjs.com/package/request#requestoptions-callback */ }, 43 | 44 | /* Configurations for Amazon Store */ 45 | amazonAPIVersion: 2, // tells the module to use API version 2 46 | secret: 'abcdefghijklmnoporstuvwxyz', // this comes from Amazon 47 | // amazonValidationHost: http://localhost:8080/RVSSandbox, // Local sandbox URL for testing amazon sandbox receipts. 48 | 49 | /* Configurations for Apple */ 50 | appleExcludeOldTransactions: true, // if you want to exclude old transaction, set this to true. Default is false 51 | applePassword: 'abcdefg...', // this comes from iTunes Connect (You need this to valiate subscriptions) 52 | 53 | /* Configurations for Google Service Account validation: You can validate with just packageName, productId, and purchaseToken */ 54 | googleServiceAccount: { 55 | clientEmail: '', 56 | privateKey: '' 57 | }, 58 | 59 | /* Configurations for Google Play */ 60 | googlePublicKeyPath: 'path/to/public/key/directory/', // this is the path to the directory containing iap-sanbox/iap-live files 61 | googlePublicKeyStrSandbox: 'publicKeySandboxString', // this is the google iap-sandbox public key string 62 | googlePublicKeyStrLive: 'publicKeyLiveString', // this is the google iap-live public key string 63 | googleAccToken: 'abcdef...', // optional, for Google Play subscriptions 64 | googleRefToken: 'dddd...', // optional, for Google Play subscritions 65 | googleClientID: 'aaaa', // optional, for Google Play subscriptions 66 | googleClientSecret: 'bbbb', // optional, for Google Play subscriptions 67 | 68 | /* Configurations for Roku */ 69 | rokuApiKey: 'aaaa...', // this comes from Roku Developer Dashboard 70 | 71 | /* Configurations for Facebook (Payments Lite) */ 72 | facebookAppId: '112233445566778', 73 | facebookAppSecret: 'cafebabedeadbeefabcdef0123456789', 74 | 75 | /* Configurations all platforms */ 76 | test: true, // For Apple and Googl Play to force Sandbox validation only 77 | verbose: true // Output debug logs to stdout stream 78 | }); 79 | iap.setup() 80 | .then(() => { 81 | // iap.validate(...) automatically detects what type of receipt you are trying to validate 82 | iap.validate(receipt).then(onSuccess).catch(onError); 83 | }) 84 | .catch((error) => { 85 | // error... 86 | }); 87 | 88 | function onSuccess(validatedData) { 89 | // validatedData: the actual content of the validated receipt 90 | // validatedData also contains the original receipt 91 | var options = { 92 | ignoreCanceled: true, // Apple ONLY (for now...): purchaseData will NOT contain cancceled items 93 | ignoreExpired: true // purchaseData will NOT contain exipired subscription items 94 | }; 95 | // validatedData contains sandbox: true/false for Apple and Amazon 96 | var purchaseData = iap.getPurchaseData(validatedData, options); 97 | } 98 | 99 | function onError(error) { 100 | // failed to validate the receipt... 101 | } 102 | ``` 103 | 104 | ## Receipt data format 105 | 106 | ### Apple 107 | 108 | An Apple's receipt is a base64 encoded string. 109 | 110 | ### Google Play 111 | 112 | A Google Play's receipt consists of two components. 113 | 114 | - Signed Data: A JSON data of what the end user purchased. 115 | 116 | - Signature: A base64 encoded string. 117 | 118 | The module requires the above two compoents to be as a JSON object. 119 | 120 | ``` 121 | { 122 | "data": {Signed Data JSON}, 123 | "signature": "Base 64 encoded signature string" 124 | } 125 | ``` 126 | 127 | `data` property in the receipt object can be either an object or a stringified JSON string. 128 | 129 | ### Google Play Using Google Service Account 130 | 131 | If you are using Google service account instead of OAuth for Google, the receipt should look like: 132 | 133 | The API used is v3. 134 | 135 | ``` 136 | { 137 | packageName: 'The packge name of the item purchased', 138 | productId: 'The product ID of the item purchased', 139 | purchaseToken: 'PurchaseToken of the receipt from Google', 140 | subscription: true/false // if the receipt is a subscription, then true 141 | } 142 | ``` 143 | 144 | ### Google Play Using Google Service Account (with Unity receipt) 145 | 146 | If you are using Google service account with unity receipt, you need to add a 'Subscription' field to your unity receipt. 147 | The receipt should look like: 148 | 149 | ``` 150 | { 151 | Store: 'The name of the store in use, such as GooglePlay or AppleAppStore', 152 | TransactionID: 'This transaction's unique identifier, provided by the store', 153 | Payload: 'Varies by platform, see [Unity Receipt Documentation](https://docs.unity3d.com/Manual/UnityIAPPurchaseReceipts.html)', 154 | Subscription: true/false // if the receipt is a subscription, then true 155 | } 156 | ``` 157 | 158 | ### Amazon 159 | 160 | An Amazon's receipt contains the following: 161 | 162 | - User ID: A string of Amazon Store user ID. 163 | 164 | - Receipt ID: A string of Amazon receipt. 165 | 166 | The module requires the above two components to be as a JSON object or a string 167 | 168 | ``` 169 | { 170 | "userId": "User ID", 171 | "receiptId": "Receipt ID" 172 | } 173 | ``` 174 | 175 | ### Roku 176 | 177 | A Roku's receipt is a transaction ID string. 178 | 179 | ### Windows 180 | 181 | A Windows' receipt is an XML string. 182 | 183 | ### Facebook (Payments Lite) 184 | 185 | A Facebook's receipt is signed_request string of payment response. 186 | 187 | ## Validate Receipts From Multiple Applications 188 | 189 | You may feed different Google public key or Apple password etc to validate receipts of different applications with the same code: 190 | 191 | ### Windows is NOT Supported 192 | 193 | ### Google Public Key 194 | 195 | ```javascript 196 | iap.config(configObject); 197 | iap.setup() 198 | .then(() => { 199 | iap.validateOnce(receipt, pubKeyString).then(onSuccess).catch(onError); 200 | }) 201 | .catch((error) => { 202 | // error... 203 | }); 204 | ``` 205 | 206 | ### Google Subscription 207 | 208 | ```javascript 209 | iap.config(configObject); 210 | iap.setup() 211 | .then(() => { 212 | var credentials = { 213 | clientId: 'xxxx', 214 | clientSecret: 'yyyy', 215 | refreshToken: 'zzzz' 216 | }; 217 | iap.validateOnce(receipt, credentials).then(onSuccess).catch(onError); 218 | }) 219 | ``` 220 | 221 | ### Apple Subscription 222 | 223 | ```javascript 224 | iap.config(configObject); 225 | iap.setup() 226 | .then(() => { 227 | iap.validateOnce(receipt, appleSecretString).then(onSuccess).catch(onError); 228 | }) 229 | .catch((error) => { 230 | // error... 231 | }); 232 | ``` 233 | 234 | ### Amazon 235 | 236 | ```javascript 237 | iap.config(configObject); 238 | iap.setup() 239 | .then(() => { 240 | iap.validateOnce(receipt, amazonSecretString).then(onSuccess).catch(onError); 241 | }) 242 | .catch((error) => { 243 | // error... 244 | }); 245 | ``` 246 | 247 | ### Roku 248 | 249 | ```javascript 250 | iap.config(configObject); 251 | iap.setup() 252 | .then(() => { 253 | iap.validateOnce(receipt, rokuApiKeyString).then(onSuccess).catch(onError); 254 | }) 255 | .catch((error) => { 256 | // error... 257 | }); 258 | ``` 259 | 260 | ### Facebook (Payments Lite) 261 | 262 | ```javascript 263 | iap.config(configObject); 264 | iap.setup() 265 | .then(() => { 266 | iap.validateOnce(receipt, appAccessToken).then(onSuccess).catch(onError); 267 | }) 268 | .catch((error) => { 269 | // error... 270 | }); 271 | ``` 272 | 273 | ## Helper Methods 274 | 275 | ### Array<[object]> getPurchaseData([object] response, [object] options) 276 | 277 | Returns an Array of objects that to be used by `isExpired` and `isCanceled`. 278 | 279 | #### [bool] options.ignoreCanceled 280 | 281 | If `true`, the returned purchaseData excludes canceled item(s). 282 | 283 | #### [bool] options.ignoreExpired 284 | 285 | If `true`, the returned purchaseData excludes expired item(s). 286 | 287 | ### [bool] isValidated([object] response) 288 | 289 | Returns a boolean `true` if the given response of a receipt validation is a valid. 290 | 291 | ```javascript 292 | iap.validate(receipt) 293 | .then((response) => { 294 | if (iap.isValidated(response)) { 295 | // valid receipt 296 | } 297 | }) 298 | .catch((error) => { 299 | // error... 300 | }); 301 | ``` 302 | 303 | ### [bool] isCanceled([object] purchaseData) 304 | 305 | Returns a boolean `true` if a canceled receipt is validated. 306 | 307 | ```javascript 308 | iap.validate(receipt) 309 | .then((response) => { 310 | var purchaseData = iap.getPurchaseData(response); 311 | if (iap.isCanceled(purchaseData[0])) { 312 | // receipt has been canceled 313 | } 314 | }) 315 | .catch((error) => { 316 | // error... 317 | }); 318 | ``` 319 | 320 | ### [bool] isExpired(object] purchaseData) 321 | 322 | Returns a boolean `true` if a canceled receipt is validated. 323 | 324 | **NOTE** This is subscription only. 325 | 326 | ```javascript 327 | iap.validate(receipt) 328 | .then((response) => { 329 | var purchaseData = iap.getPurchaseData(response); 330 | if (iap.isExpired(purchaseData[0])) { 331 | // receipt has been expired 332 | } 333 | }) 334 | .catch((error) => { 335 | // error... 336 | }); 337 | ``` 338 | 339 | ### [void] setAmazonValidationHost([string] host) 340 | 341 | Allows you to set custom validation host name for tests. 342 | 343 | ### [void] resetAmazonValidationHost() 344 | 345 | Resets to Amazon's validation host name. 346 | 347 | ## Google Play Public Key With An Environment Variable 348 | 349 | You may not want to keep the public key files on your server(s). 350 | 351 | The module also supports environment variables for this. 352 | 353 | Instead of using `googlePublicKeyPath: 'path/to...'` in your configurations, you the following: 354 | 355 | ``` 356 | export=GOOGLE_IAB_PUBLICKEY_LIVE=PublicKeyHerePlz 357 | export=GOOGLE_IAB_PUBLICKEY_SANDBOX=PublicKeyHerePlz 358 | ``` 359 | 360 | ## Google In-app-Billing Set Up 361 | 362 | To set up your server-side Android in-app-billing correctly, you must provide the public key string as a file from your Developer Console account. 363 | 364 | **Reference:** Implementing In-app Billing 365 | 366 | Once you copy the public key string from the Developer Console account for your application, you simply need to copy and paste it to a file and name it `iap-live` as shown in the example above. 367 | 368 | **NOTE:** The public key string you copy from the Developer Console account is actually a base64 string. You do NOT have to convert this to anything yourself. The module converts it to the public key automatically for you. 369 | 370 | ### Google Play Store API 371 | 372 | To check expiration date or auto renewal status of an Android subscription, you should first setup the access to the Google Play Store API. You should follow these steps: 373 | 374 | ##### Part 1 - Get ClientID and ClientSecret 375 | 1. Go to https://play.google.com/apps/publish/ 376 | 2. Click on `Settings` 377 | 3. Click on `API Access` 378 | 4. There should be a linked project already, if not, create one. If you have it, click it. 379 | * You should now be at: https://console.developers.google.com/apis/library?project=xxxx 380 | 5. Under Mobile API's, make sure "Google Play Developer API is enabled". 381 | 6. Go back, on the left click on `Credentials` 382 | 7. Click `Create Credentials` button 383 | 8. Choose `OAuth Client ID` 384 | 9. Choose `Web Application` 385 | * Give it a name, skip the `Authorized JS origins` 386 | * Add this to `Authorized Redirect URIs`: https://developers.google.com/oauthplayground 387 | * Hit Save and copy the **clientID** and **clientSecret** somewhere safe. 388 | 389 | ##### Part 2 - Get Access and Refresh Tokens 390 | 1. Go to: https://developers.google.com/oauthplayground 391 | 2. On the right, hit the gear/settings. 392 | 3. Check the box: `Use your own OAuth credentials` 393 | * Enter in clientID and clientSecret 394 | * Close 395 | 4. On the left, find "Google Play Developer API v3" 396 | * Select "https://www.googleapis.com/auth/androidpublisher" 397 | 5. Hit Authorize Api's button 398 | 6. Save `Authorization Code` 399 | * This is your: **googleAccToken** 400 | 7. Hit `Exchange Authorization code for token` 401 | 8. Grab: `Refresh Token` 402 | * This is your: **googleRefToken** 403 | 404 | Now you are able to query for Android subscription status! 405 | 406 | ## Amazon App Store Reference 407 | 408 | https://developer.amazon.com/docs/in-app-purchasing/iap-rvs-for-android-apps.html 409 | 410 | ## Windows Signed XML 411 | 412 | in-app-purchase module supports the following algorithms: 413 | 414 | ### Canonicalization and Transformation Algorithms 415 | 416 | - Exclusive Canonicalization http://www.w3.org/2001/10/xml-exc-c14n# 417 | 418 | - Exclusive Canonicalization with comments http://www.w3.org/2001/10/xml-exc-c14n#WithComments 419 | 420 | - Enveloped Signature transform http://www.w3.org/2000/09/xmldsig#enveloped-signature 421 | 422 | ### Hashing Algorithms 423 | 424 | - SHA1 digests http://www.w3.org/2000/09/xmldsig#sha1 425 | 426 | - SHA256 digests http://www.w3.org/2001/04/xmlenc#sha256 427 | 428 | - SHA512 digests http://www.w3.org/2001/04/xmlenc#sha512 429 | 430 | ## Facebook Order Fulfillment and Signed Request Parsing 431 | 432 | - Facebook Payments Order Fulfillment: https://developers.facebook.com/docs/games_payments/fulfillment#orderfulfillment 433 | 434 | - Facebook Signed Request Parsing: https://developers.facebook.com/docs/games/gamesonfacebook/login#parsingsr 435 | 436 | - **NOTE:** Payments `Lite` does not support subscription. 437 | 438 | ## HTTP Request Configurations 439 | 440 | The module supports the same configurations as [npm request module] (https://www.npmjs.com/package/request#requestoptions-callback) 441 | 442 | -------------------------------------------------------------------------------- /lib/apple.js: -------------------------------------------------------------------------------- 1 | var async = require('./async'); 2 | var verbose = require('./verbose'); 3 | var constants = require('../constants'); 4 | var responseData = require('./responseData'); 5 | var request = require('request'); 6 | var errorMap = { 7 | 21000: 'The App Store could not read the JSON object you provided.', 8 | 21002: 'The data in the receipt-data property was malformed.', 9 | 21003: 'The receipt could not be authenticated.', 10 | 21004: 'The shared secret you provided does not match the shared secret on file for your account.', 11 | 21005: 'The receipt server is not currently available.', 12 | 21006: 'This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.', 13 | 21007: 'This receipt is a sandbox receipt, but it was sent to the production service for verification.', 14 | 21008: 'This receipt is a production receipt, but it was sent to the sandbox service for verification.', 15 | 2: 'The receipt is valid, but purchased nothing.' 16 | }; 17 | var REC_KEYS = { 18 | IN_APP: 'in_app', 19 | LRI: 'latest_receipt_info', 20 | LERI: 'latest_expired_receipt_info', 21 | BUNDLE_ID: 'bundle_id', 22 | BID: 'bid', 23 | TRANSACTION_ID: 'transaction_id', 24 | ORIGINAL_TRANSACTION_ID: 'original_transaction_id', 25 | PRODUCT_ID: 'product_id', 26 | ITEM_ID: 'item_id', 27 | ORIGINAL_PURCHASE_DATE_MS: 'original_purchase_date_ms', 28 | EXPIRES_DATE_MS: 'expires_date_ms', 29 | EXPIRES_DATE: 'expires_date', 30 | EXPIRATION_DATE: 'expiration_date', 31 | EXPIRATION_INTENT: 'expiration_intent', 32 | CANCELLATION_DATE: 'cancellation_date', 33 | CANCELLATION_DATE_MS: 'cancellation_date_ms', 34 | PURCHASE_DATE_MS: 'purchase_date_ms', 35 | IS_TRIAL: 'is_trial_period' 36 | }; 37 | var config = null; 38 | var sandboxHost = 'sandbox.itunes.apple.com'; 39 | var liveHost = 'buy.itunes.apple.com'; 40 | var path = '/verifyReceipt'; 41 | var testMode = false; 42 | 43 | function isExpired(responseData) { 44 | if (responseData[REC_KEYS.LRI] && responseData[REC_KEYS.LRI][REC_KEYS.EXPIRES_DATE]) { 45 | var exp = parseInt(responseData[REC_KEYS.LRI][REC_KEYS.EXPIRES_DATE]); 46 | if (exp > Date.now()) { 47 | return true; 48 | } 49 | return false; 50 | } 51 | // old receipt 52 | } 53 | 54 | function isValidConfigKey(key) { 55 | return key.match(/^apple/); 56 | } 57 | 58 | module.exports.readConfig = function (configIn) { 59 | if (!configIn) { 60 | // no apple iap or password not required 61 | return; 62 | } 63 | 64 | // set up verbose logging 65 | verbose.setup(configIn); 66 | // we do NOT use liveHost if true 67 | testMode = configIn.test || false; 68 | verbose.log(' test mode?', testMode); 69 | 70 | config = {}; 71 | var configValueSet = false; 72 | // Apply any default settings to Request. 73 | if ('requestDefaults' in configIn) { 74 | request = request.defaults(configIn.requestDefaults); 75 | } 76 | Object.keys(configIn).forEach(function (key) { 77 | if (isValidConfigKey(key)) { 78 | config[key] = configIn[key]; 79 | configValueSet = true; 80 | } 81 | }); 82 | 83 | if (!configValueSet) { 84 | config = null; 85 | } 86 | }; 87 | 88 | module.exports.setup = function (cb) { 89 | if (!config || !config.applePassword) { 90 | 91 | if (process.env.APPLE_IAP_PASSWORD) { 92 | config = config || {}; 93 | config.applePassword = process.env.APPLE_IAP_PASSWORD; 94 | } 95 | 96 | } 97 | 98 | return cb(); 99 | }; 100 | 101 | module.exports.validatePurchase = function (secret, receipt, cb) { 102 | var prodPath = 'https://' + liveHost + path; 103 | var sandboxPath = 'https://' + sandboxHost + path; 104 | var status; 105 | var validatedData; 106 | var isValid = false; 107 | var content = { 'receipt-data': receipt }; 108 | 109 | if (config && config.applePassword) { 110 | content.password = config.applePassword; 111 | } 112 | 113 | if (config && config.appleExcludeOldTransactions) { 114 | content['exclude-old-transactions'] = config.appleExcludeOldTransactions; 115 | } 116 | 117 | // override applePassword from config to allow dynamically fed secret to validate 118 | if (secret) { 119 | verbose.log(' Using dynamic applePassword:', secret); 120 | content.password = secret; 121 | } 122 | 123 | verbose.log(' Validatation data:', content); 124 | 125 | var tryProd = function (next) { 126 | if (testMode) { 127 | verbose.log(' test mode: skip production validation'); 128 | return next(); 129 | } 130 | verbose.log(' Try validate against production:', prodPath); 131 | send(prodPath, content, function (error, res, data) { 132 | verbose.log('', prodPath, 'validation response:', data); 133 | // request error 134 | if (error) { 135 | // 1 is unknown 136 | status = data ? data.status : 1; 137 | validatedData = { 138 | sandbox: false, 139 | status: status, 140 | message: errorMap[status] || 'Unknown' 141 | }; 142 | applyResponseData(validatedData, data); 143 | verbose.log('', prodPath, 'failed:', error, validatedData); 144 | error.validatedData = validatedData; 145 | return next(error); 146 | } 147 | // apple responded with error 148 | if (data.status > 0 && data.status !== 21007 && data.status !== 21002) { 149 | if (data.status === 21006 && !isExpired(data)) { 150 | /* valid subscription receipt, 151 | but cancelled and it has not been expired 152 | status code is 21006 for both expired receipt and cancelled receipt... 153 | */ 154 | validatedData = data; 155 | validatedData.sandbox = false; 156 | // force status to be 0 157 | validatedData.status = 0; 158 | verbose.log(' Valid receipt, but has been cancelled (not expired yet)'); 159 | isValid = true; 160 | return next(); 161 | } 162 | verbose.log('', prodPath, 'failed:', data); 163 | status = data.status; 164 | var emsg = errorMap[status] || 'Unknown'; 165 | var err = new Error(emsg); 166 | validatedData = { 167 | sandbox: false, 168 | status: status, 169 | message: emsg 170 | }; 171 | applyResponseData(validatedData, data); 172 | verbose.log('', prodPath, 'failed:', validatedData); 173 | err.validatedData = validatedData; 174 | return next(err); 175 | } 176 | // try sandbox... 177 | if (data.status === 21007 || data.status === 21002) { 178 | return next(); 179 | } 180 | // production validated 181 | validatedData = data; 182 | validatedData.sandbox = false; 183 | verbose.log(' Production validation successful:', validatedData); 184 | isValid = true; 185 | next(); 186 | }); 187 | }; 188 | 189 | var trySandbox = function (next) { 190 | if (isValid) { 191 | return next(); 192 | } 193 | verbose.log(' Try validate against sandbox:', sandboxPath); 194 | send(sandboxPath, content, function (error, res, data) { 195 | verbose.log('', sandboxPath, 'validation response:', data); 196 | if (error) { 197 | // 1 is unknown 198 | status = data ? data.status : 1; 199 | validatedData = { 200 | sandbox: true, 201 | status: status, 202 | message: errorMap[status] || 'Unknown' 203 | }; 204 | applyResponseData(validatedData, data); 205 | verbose.log('', sandboxPath, 'failed:', error, validatedData); 206 | error.validatedData = validatedData; 207 | return next(error); 208 | } 209 | if (data.status > 0 && data.status !== 21002) { 210 | if (data.status === 21006 && !isExpired(data)) { 211 | /* valid subscription receipt, 212 | but cancelled and it has not been expired 213 | status code is 21006 for both expired receipt and cancelled receipt... 214 | */ 215 | validatedData = data; 216 | validatedData.sandbox = true; 217 | // force status to be 0 218 | validatedData.status = 0; 219 | verbose.log(' Valid receipt, but has been cancelled (not expired yet)'); 220 | isValid = true; 221 | return next(); 222 | } 223 | verbose.log('', sandboxPath, 'failed:', data); 224 | status = data.status; 225 | var emsg = errorMap[status] || 'Unknown'; 226 | var err = new Error(emsg); 227 | validatedData = { 228 | sandbox: true, 229 | status: status, 230 | message: emsg 231 | }; 232 | applyResponseData(validatedData, data); 233 | verbose.log('', sandboxPath, 'failed:', validatedData); 234 | err.validatedData = validatedData; 235 | return next(err); 236 | } 237 | // sandbox validated 238 | validatedData = data; 239 | validatedData.sandbox = true; 240 | verbose.log(' Sandbox validation successful:', validatedData); 241 | next(); 242 | }); 243 | }; 244 | 245 | var done = function (error) { 246 | if (error) { 247 | return cb(error, validatedData); 248 | } 249 | handleResponse(receipt, validatedData, cb); 250 | }; 251 | 252 | var tasks = [ 253 | tryProd, 254 | trySandbox 255 | ]; 256 | async.series(tasks, done); 257 | }; 258 | 259 | module.exports.getPurchaseData = function (purchase, options) { 260 | if (!purchase || !purchase.receipt) { 261 | return null; 262 | } 263 | var data = []; 264 | if (purchase.receipt[REC_KEYS.IN_APP]) { 265 | // iOS 6+ 266 | var now = Date.now(); 267 | var tids = []; 268 | var list = purchase.receipt[REC_KEYS.IN_APP]; 269 | var lri = purchase[REC_KEYS.LRI] || purchase.receipt[REC_KEYS.LRI]; 270 | if (lri && Array.isArray(lri)) { 271 | list = list.concat(lri); 272 | } 273 | /* 274 | we sort list by purchase_date_ms to make it easier 275 | to weed out duplicates (items with the same original_transaction_id) 276 | purchase_date_ms DESC 277 | */ 278 | list.sort(function (a, b) { 279 | return parseInt(b[REC_KEYS.PURCHASE_DATE_MS], 10) - parseInt(a[REC_KEYS.PURCHASE_DATE_MS], 10); 280 | }); 281 | for (var i = 0, len = list.length; i < len; i++) { 282 | var item = list[i]; 283 | var tid = item['original_' + REC_KEYS.TRANSACTION_ID]; 284 | var exp = getSubscriptionExpireDate(item); 285 | 286 | if ( 287 | options && 288 | options.ignoreCanceled && 289 | item[REC_KEYS.CANCELLATION_DATE] && 290 | item[REC_KEYS.CANCELLATION_DATE].length && 291 | /* if a subscription has been cancelled, 292 | we need to check if the receipt has expired or not... 293 | if it is not subscription (exp is 0 in that case), we ignore right away... 294 | */ 295 | (!exp || now - exp >= 0) 296 | ) { 297 | continue; 298 | } 299 | 300 | if (options && options.ignoreExpired && exp && now - exp >= 0) { 301 | // we are told to ignore expired item and it is expired 302 | continue; 303 | } 304 | if (tids.indexOf(tid) > -1) { 305 | /* avoid duplicate and keep the latest 306 | there are cases where we could have 307 | the same "time" so we evaludate <= instead of < alone */ 308 | continue; 309 | } 310 | 311 | tids.push(tid); 312 | var parsed = responseData.parse(item); 313 | // transaction ID should be a string: 314 | // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier 315 | parsed.transactionId = parsed.transactionId.toString(); 316 | 317 | // originalTransactionId should also be a string 318 | if (parsed.originalTransactionId && !isNaN(parsed.originalTransactionId)) { 319 | parsed.originalTransactionId = parsed.originalTransactionId.toString(); 320 | } 321 | 322 | // we need to stick to the name isTrial 323 | if (parsed.isTrialPeriod !== undefined) { 324 | parsed.isTrial = bool(parsed.isTrialPeriod); 325 | } else { 326 | parsed.isTrial = false; 327 | } 328 | 329 | parsed.bundleId = purchase.receipt[REC_KEYS.BUNDLE_ID] || purchase.receipt[REC_KEYS.BID]; 330 | parsed.expirationDate = exp; 331 | data.push(parsed); 332 | } 333 | return data; 334 | } 335 | // old and will be deprecated by Apple 336 | var receipt = purchase[REC_KEYS.LRI] || purchase[REC_KEYS.LERI] || purchase.receipt; 337 | data.push({ 338 | bundleId: receipt[REC_KEYS.BUNDLE_ID] || receipt[REC_KEYS.BID], 339 | appItemId: receipt[REC_KEYS.ITEM_ID], 340 | originalTransactionId: receipt[REC_KEYS.ORIGINAL_TRANSACTION_ID], 341 | transactionId: receipt[REC_KEYS.TRANSACTION_ID], 342 | productId: receipt[REC_KEYS.PRODUCT_ID], 343 | originalPurchaseDate: receipt[REC_KEYS.ORIGINAL_PURCHASE_DATE_MS], 344 | purchaseDate: receipt[REC_KEYS.PURCHASE_DATE_MS], 345 | quantity: parseInt(receipt.quantity, 10), 346 | expirationDate: getSubscriptionExpireDate(receipt), 347 | isTrial: bool(receipt[REC_KEYS.IS_TRIAL]), 348 | cancellationDate: parseInt(receipt[REC_KEYS.CANCELLATION_DATE_MS]) || 0 349 | }); 350 | return data; 351 | }; 352 | 353 | function bool(val) { 354 | return val === 'true' ? true : false; 355 | } 356 | 357 | function getSubscriptionExpireDate(data) { 358 | if (!data) { 359 | return 0; 360 | } 361 | if (data[REC_KEYS.EXPIRES_DATE_MS]) { 362 | return parseInt(data[REC_KEYS.EXPIRES_DATE_MS], 10); 363 | } 364 | if (data[REC_KEYS.EXPIRES_DATE]) { 365 | return data[REC_KEYS.EXPIRES_DATE]; 366 | } 367 | if (data[REC_KEYS.EXPIRATION_DATE]) { 368 | return data[REC_KEYS.EXPIRATION_DATE]; 369 | } 370 | if (data[REC_KEYS.EXPIRATION_INTENT]) { 371 | return parseInt(data[REC_KEYS.EXPIRATION_INTENT], 10); 372 | } 373 | return 0; 374 | } 375 | 376 | function handleResponse(receipt, data, cb) { 377 | data.service = constants.SERVICES.APPLE; 378 | if (data.status === constants.VALIDATION.SUCCESS) { 379 | if (data.receipt[REC_KEYS.IN_APP] && !data.receipt[REC_KEYS.IN_APP].length) { 380 | // receipt is valid, but the receipt bought nothing 381 | // probably hacked: https://forums.developer.apple.com/thread/8954 382 | // https://developer.apple.com/library/mac/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPT-HOW_DO_I_USE_THE_CANCELLATION_DATE_FIELD_ 383 | data.status = constants.VALIDATION.POSSIBLE_HACK; 384 | data.message = errorMap[data.status]; 385 | verbose.log( 386 | '', 387 | 'Empty purchased detected: in_app array is empty:', 388 | 'consider invalid and does not validate', 389 | data 390 | ); 391 | return cb(new Error('failed to validate for empty purchased list'), data); 392 | } 393 | // validated successfully 394 | return cb(null, data); 395 | } else { 396 | // error -> add error message 397 | data.message = errorMap[data.status] || 'Unkown'; 398 | } 399 | 400 | // failed to validate 401 | cb(new Error('failed to validate purchase'), data); 402 | } 403 | 404 | function send(url, content, cb) { 405 | var options = { 406 | encoding: null, 407 | url: url, 408 | body: content, 409 | json: true 410 | }; 411 | request.post(options, function (error, res, body) { 412 | return cb(error, res, body); 413 | }); 414 | } 415 | 416 | function applyResponseData(target, source) { 417 | for (var key in source) { 418 | if (target[key] === undefined) { 419 | target[key] = source[key]; 420 | } 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /logo/75x75.svg: -------------------------------------------------------------------------------- 1 | 348 | 349 | --------------------------------------------------------------------------------