├── .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 |
172 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ©Nobuyori Takahashi < >
4 |
5 | [](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 |
349 |
--------------------------------------------------------------------------------