qranswers 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+ const crypto = require('crypto');
3
+
4
+ const Webhook = {
5
+ DEFAULT_TOLERANCE: 300000, // in ms
6
+ EXPECTED_SCHEME: 'v1',
7
+
8
+ constructEvent(payload, header, secret, tolerance) {
9
+ console.log('constructEvent', payload);
10
+ this.verifyHeader(payload, header, secret, tolerance || this.DEFAULT_TOLERANCE);
11
+ console.log('returning ', JSON.parse(payload));
12
+ return JSON.parse(payload);
13
+ },
14
+ verifyHeader(
15
+ encodedPayload,
16
+ encodedHeader,
17
+ secret,
18
+ tolerance
19
+ ) {
20
+ const {
21
+ decodedHeader: header,
22
+ decodedPayload: payload,
23
+ details,
24
+ } = parseEventDetails(encodedPayload, encodedHeader, this.EXPECTED_SCHEME);
25
+
26
+ const expectedSignature = crypto.createHmac('sha256', secret)
27
+ .update(`${details.timestamp}.${encodedPayload.toString('utf8')}`)
28
+ .digest('hex');
29
+
30
+ for (var i=0; i<details.signatures.length; i++) {
31
+ if (crypto.timingSafeEqual(Buffer.from(details.signatures[i]), Buffer.from(expectedSignature))) {
32
+
33
+ const timestampAge = Date.now() - details.timestamp;
34
+ if (tolerance > 0 && timestampAge > tolerance) {
35
+ // @ts-ignore
36
+ throw new Error('Timestamp outside the tolerance zone');
37
+ }
38
+ return true;
39
+ }
40
+ }
41
+
42
+ throw new Error('Invalid signature');
43
+ }
44
+ };
45
+
46
+ function parseEventDetails(encodedPayload, encodedHeader, expectedScheme) {
47
+
48
+ const decodedPayload = Buffer.isBuffer(encodedPayload)
49
+ ? encodedPayload.toString('utf8')
50
+ : encodedPayload;
51
+ if (Array.isArray(encodedHeader)) {
52
+ throw new Error(
53
+ 'Unexpected: An array was passed as a header, which should not be possible for the qranswers--signature header.'
54
+ );
55
+ }
56
+ const decodedHeader = Buffer.isBuffer(encodedHeader)
57
+ ? encodedHeader.toString('utf8')
58
+ : encodedHeader;
59
+ const details = parseHeader(decodedHeader, expectedScheme);
60
+ if (!details || details.timestamp === -1) {
61
+ throw new Error('Unable to extract timestamp and signatures from header');
62
+ }
63
+ if (!details.signatures.length) {
64
+ throw new Error('No signatures found with expected scheme');
65
+ }
66
+ return {
67
+ decodedPayload,
68
+ decodedHeader,
69
+ details,
70
+ };
71
+ };
72
+ function parseHeader(header, scheme) {
73
+ if (typeof header !== 'string') {
74
+ return null;
75
+ }
76
+ return header.split(',').reduce(
77
+ (accum, item) => {
78
+ const kv = item.split('=');
79
+ if (kv[0] === 't') {
80
+ accum.timestamp = parseInt(kv[1], 10);
81
+ }
82
+ if (kv[0] === scheme) {
83
+ accum.signatures.push(kv[1]);
84
+ }
85
+ return accum;
86
+ },
87
+ {
88
+ timestamp: -1,
89
+ signatures: [],
90
+ }
91
+ );
92
+ }
93
+ module.exports = Webhook;
94
+
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+ const DEFAULT_HOST = 'api.qr-answers.com';
3
+ const DEFAULT_PORT = '443';
4
+ const DEFAULT_TIMEOUT = 80000;
5
+ QRAnswers.PACKAGE_VERSION = require('../package.json').version;
6
+ const ALLOWED_CONFIG_PROPERTIES = [
7
+ 'timeout',
8
+ 'host',
9
+ 'port',
10
+ 'protocol',
11
+ ];
12
+
13
+ function QRAnswers(key, config = {}) {
14
+ if (!(this instanceof QRAnswers)) {
15
+ return new QRAnswers(key, config);
16
+ }
17
+ const props = this._getPropsFromConfig(config);
18
+
19
+ this.VERSION = QRAnswers.PACKAGE_VERSION;
20
+
21
+ if (
22
+ props.protocol &&
23
+ props.protocol !== 'https' &&
24
+ (!props.host || /\.stripe\.com$/.test(props.host))
25
+ ) {
26
+ throw new Error(
27
+ 'The `https` protocol must be used when sending requests to `*.stripe.com`'
28
+ );
29
+ }
30
+ const agent = props.httpAgent || null;
31
+ this._api = {
32
+ auth: null,
33
+ host: props.host || DEFAULT_HOST,
34
+ port: props.port || DEFAULT_PORT,
35
+ protocol: props.protocol || 'https',
36
+ basePath: DEFAULT_BASE_PATH,
37
+ version: props.apiVersion || DEFAULT_API_VERSION,
38
+ timeout: utils.validateInteger('timeout', props.timeout, DEFAULT_TIMEOUT),
39
+ };
40
+
41
+ }
42
+ QRAnswers.webhooks = require("./Webhooks");
43
+ QRAnswers.prototype = {
44
+ VERSION: null,
45
+ webhooks: null,
46
+ _api: null,
47
+ _setApiKey(key) {
48
+ if (key) {
49
+ this._setApiField('auth', `Bearer ${key}`);
50
+ }
51
+ },
52
+ _setApiField(key, value) {
53
+ this._api[key] = value;
54
+ },
55
+ getApiField(key) {
56
+ return this._api[key];
57
+ },
58
+ getMaxNetworkRetries() {
59
+ return this.getApiField('maxNetworkRetries');
60
+ },
61
+
62
+ /**
63
+ * @private
64
+ * This may be removed in the future.
65
+ */
66
+ _getPropsFromConfig(config) {
67
+ if (!config) {
68
+ return {};
69
+ }
70
+ const isObject = config === Object(config) && !Array.isArray(config);
71
+ if (!isObject) {
72
+ throw new Error('Config must be an object');
73
+ }
74
+ // If config is an object, we assume the new behavior and make sure it doesn't contain any unexpected values
75
+ const values = Object.keys(config).filter(
76
+ (value) => !ALLOWED_CONFIG_PROPERTIES.includes(value)
77
+ );
78
+ if (values.length > 0) {
79
+ throw new Error(
80
+ `Config object may only contain the following: ${ALLOWED_CONFIG_PROPERTIES.join(
81
+ ', '
82
+ )}`
83
+ );
84
+ }
85
+ return config;
86
+ },
87
+
88
+ };
89
+ module.exports = QRAnswers;
90
+ // expose constructor as a named property to enable mocking with Sinon.JS
91
+ module.exports.Stripe = QRAnswers;
92
+ // Allow use with the TypeScript compiler without `esModuleInterop`.
93
+ // We may also want to add `Object.defineProperty(exports, "__esModule", {value: true});` in the future, so that Babel users will use the `default` version.
94
+ module.exports.default = QRAnswers;
95
+
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "qranswers",
3
+ "version": "1.0.0",
4
+ "description": "Helper module to access QR-Answers API",
5
+ "main": "lib/qranswers.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git://github.com/ggcespia/qranswers_module.git"
12
+ },
13
+ "keywords": [
14
+ "qranswers",
15
+ "qrcontest",
16
+ "qrcode",
17
+ "api"
18
+ ],
19
+ "author": "Greg Carpenter",
20
+ "license": "MIT",
21
+ "bugs": {
22
+ "url": "https://github.com/ggcespia/qranswers_module/issues"
23
+ },
24
+ "homepage": "https://github.com/ggcespia/qranswers_module#readme",
25
+ "dependencies": {
26
+ "crypto": "^1.0.1"
27
+ }
28
+ }