@bunnyapp/api-client 1.0.7

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Bunny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # bunny-node
2
+
3
+ A node sdk for Bunny CRM
4
+
5
+ ## Setup
6
+
7
+ Install the latest package.
8
+
9
+ ```sh
10
+ npm install bunny --save
11
+ ```
12
+
13
+ Create a Bunny api client using either a valid access token or client credentials.
14
+
15
+ ### Access Token
16
+
17
+ The benefit of providing an accessToken is the request will be faster as an access token does not need to be generated. The
18
+ downside of this approach is that if the token expires then your requests will start to fail.
19
+
20
+ ```js
21
+ const Bunny = require("bunny");
22
+ const bunny = new Bunny({
23
+ baseUrl: "https://<subdomain>.bunny.com",
24
+ accessToken: "<bunny-access-token>",
25
+ });
26
+ ```
27
+
28
+ ### Client Credentials
29
+
30
+ Alternately you can provide clientId, clientSecret, & scope. In this case the client will generate an access token and if the token expires it will generate another one.
31
+
32
+ ```js
33
+ const Bunny = require("bunny");
34
+ const bunny = new Bunny({
35
+ baseUrl: "https://<subdomain>.bunny.com",
36
+ clientId: "<bunny-client-id>",
37
+ clientSecret: "<bunny-client-secret>",
38
+ scope: "standard:read standard:write",
39
+ });
40
+ ```
41
+
42
+ ## Convenience methods
43
+
44
+ This SDK wrappers several of the common Bunny API requests.
45
+
46
+ ```js
47
+ bunny.createSubscription(
48
+ accountName,
49
+ firstName,
50
+ lastName,
51
+ email,
52
+ productPlanCode,
53
+ options
54
+ );
55
+ bunny.createTenant(name, code, platformCode, subscriptionId);
56
+ bunny.trackUsage(featureCode, quantity, tenantCode, usageAt);
57
+ ```
58
+
59
+ ## Perform a query
60
+
61
+ If the convenience methods on this SDK are not enough and you need more control over queries or mutations then you can make an async request against the Bunny GraphQL API.
62
+
63
+ ```js
64
+ let query = `query tenants ($filter: String, $limit: Int) {
65
+ tenants (filter: $filter, limit: $limit) {
66
+ platform {
67
+ id
68
+ name
69
+ code
70
+ }
71
+ id
72
+ name
73
+ code
74
+ }
75
+ }`;
76
+
77
+ let variables = {
78
+ filter: "",
79
+ limit: 10,
80
+ };
81
+
82
+ let res = await bunny.query(query, variables);
83
+ ```
84
+
85
+ ## Validate a webhook payload
86
+
87
+ When Bunny sends a webhook request it includes a `x-bunny-signature` header which can be used to validate the authenticity of the payload body.
88
+
89
+ Bunny will provide a signing token which you will need to store in your application and use for validating the webhook.
90
+
91
+ ```js
92
+ let signature = req.headers["x-bunny-signature"];
93
+ let payload = req.body;
94
+ let signingToken = "<secret signing token>";
95
+
96
+ let valid = bunny.webhooks.validate(signature, payload, signingToken);
97
+ ```
98
+
99
+ ## Test
100
+
101
+ Run unit tests
102
+
103
+ ```sh
104
+ npm test
105
+ ```
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require("./src");
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@bunnyapp/api-client",
3
+ "version": "1.0.7",
4
+ "description": "Node.js client for Bunny CRM",
5
+ "main": "index.js",
6
+ "directories": {
7
+ "test": "tests"
8
+ },
9
+ "scripts": {
10
+ "test": "node_modules/.bin/mocha"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/bunnyapp/bunny-node.git"
15
+ },
16
+ "keywords": [
17
+ "crm",
18
+ "billing",
19
+ "saas",
20
+ "subscriptions"
21
+ ],
22
+ "author": "Rich Chetwynd",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/bunnyapp/bunny-node/issues"
26
+ },
27
+ "homepage": "https://github.com/bunnyapp/bunny-node#readme",
28
+ "dependencies": {
29
+ "axios": "^0.26.0",
30
+ "safe-buffer": "^5.2.1",
31
+ "scmp": "^2.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "mocha": "^9.2.0",
35
+ "sinon": "^14.0.0"
36
+ },
37
+ "files": [
38
+ "LICENSE",
39
+ "README.md",
40
+ "index.js",
41
+ "src/"
42
+ ]
43
+ }
@@ -0,0 +1,39 @@
1
+ const query = `mutation subscriptionCreate ($attributes: SubscriptionAttributes!) {
2
+ subscriptionCreate (attributes: $attributes) {
3
+ errors
4
+ subscription {
5
+ id
6
+ trialStartDate
7
+ trialEndDate
8
+ startDate
9
+ endDate
10
+ state
11
+ productPlan {
12
+ name
13
+ }
14
+ }
15
+ }
16
+ }`;
17
+
18
+ module.exports = async function (
19
+ accountName,
20
+ firstName,
21
+ lastName,
22
+ email,
23
+ productPlanCode,
24
+ options = {}
25
+ ) {
26
+ let variables = {
27
+ attributes: {
28
+ accountName: accountName,
29
+ firstName: firstName,
30
+ lastName: lastName,
31
+ email: email,
32
+ productPlanCode: productPlanCode,
33
+ tenantCode: options["tenantCode"]?.toString(),
34
+ trialStartDate: options["trialStartDate"],
35
+ trial: options["trial"],
36
+ },
37
+ };
38
+ return this.query(query, variables);
39
+ };
@@ -0,0 +1,26 @@
1
+ const query = `mutation tenantCreate ($attributes: TenantAttributes!, $subscriptionId: ID!) {
2
+ tenantCreate (attributes: $attributes, subscriptionId: $subscriptionId) {
3
+ tenant {
4
+ code
5
+ id
6
+ name
7
+ platform {
8
+ id
9
+ name
10
+ code
11
+ }
12
+ }
13
+ }
14
+ }`;
15
+
16
+ module.exports = async function (name, code, platformCode, subscriptionId) {
17
+ let variables = {
18
+ attributes: {
19
+ name: name,
20
+ code: code,
21
+ platformCode: platformCode,
22
+ },
23
+ subscriptionId: subscriptionId,
24
+ };
25
+ return this.query(query, variables);
26
+ };
@@ -0,0 +1,41 @@
1
+ const query = `mutation featureUsageCreate ($attributes: FeatureUsageAttributes!) {
2
+ featureUsageCreate (attributes: $attributes) {
3
+ errors
4
+ featureUsage {
5
+ id
6
+ quantity
7
+ usageAt
8
+ tenant {
9
+ id
10
+ code
11
+ name
12
+ }
13
+ feature {
14
+ id
15
+ code
16
+ name
17
+ }
18
+ }
19
+ }
20
+ }`;
21
+
22
+ module.exports = async function (
23
+ featureCode,
24
+ quantity,
25
+ tenantCode,
26
+ usageAt = null
27
+ ) {
28
+ let variables = {
29
+ attributes: {
30
+ quantity: quantity,
31
+ tenantCode: tenantCode,
32
+ featureCode: featureCode,
33
+ },
34
+ };
35
+
36
+ if (usageAt) {
37
+ variables["attributes"]["usageAt"] = usageAt;
38
+ }
39
+
40
+ return this.query(query, variables);
41
+ };
@@ -0,0 +1,25 @@
1
+ const query = `mutation tenantUpdate ($id: ID!, $attributes: TenantAttributes!) {
2
+ tenantUpdate (id: $id, attributes: $attributes) {
3
+ tenant {
4
+ code
5
+ id
6
+ name
7
+ platform {
8
+ id
9
+ name
10
+ code
11
+ }
12
+ }
13
+ }
14
+ }`;
15
+
16
+ module.exports = async function (id, code, name) {
17
+ let variables = {
18
+ id: id,
19
+ attributes: {
20
+ code: code?.toString(),
21
+ name: name,
22
+ },
23
+ };
24
+ return this.query(query, variables);
25
+ };
package/src/index.js ADDED
@@ -0,0 +1,90 @@
1
+ var assert = require("assert");
2
+ var axios = require("axios");
3
+
4
+ var Webhooks = require("./webhooks");
5
+
6
+ class Bunny {
7
+ constructor(options = {}) {
8
+ if (!(this instanceof Bunny)) return new Bunny();
9
+ assert(options.baseUrl, "Bunny base url required");
10
+
11
+ this.options = options;
12
+
13
+ if (options.accessToken == undefined) {
14
+ assert(options.clientId, "Bunny API clientId required");
15
+ assert(options.clientSecret, "Bunny API clientSecret required");
16
+ assert(options.scope, "Bunny API scope required");
17
+ }
18
+
19
+ this.client = axios.create({
20
+ headers: {
21
+ "User-Agent": "Bunny-node",
22
+ },
23
+ baseURL: options.baseUrl,
24
+ });
25
+
26
+ this.client.interceptors.response.use(null, async (error) => {
27
+ if (
28
+ error.config &&
29
+ error.response &&
30
+ error.response.status === 401 &&
31
+ !error.config.retry &&
32
+ error.config.url != "/oauth/token"
33
+ ) {
34
+ const accessToken = await this.fetchAccessToken();
35
+
36
+ error.config.retry = true;
37
+ error.config.headers["Authorization"] = `bearer ${accessToken}`;
38
+
39
+ return axios.request(error.config);
40
+ }
41
+
42
+ return Promise.reject(error);
43
+ });
44
+
45
+ this.webhooks = new Webhooks(options.webhookSigningToken);
46
+ }
47
+
48
+ async fetchAccessToken() {
49
+ const params = new URLSearchParams({
50
+ grant_type: "client_credentials",
51
+ client_id: this.options.clientId,
52
+ client_secret: this.options.clientSecret,
53
+ scope: this.options.scope,
54
+ });
55
+
56
+ let res = await this.client.post("/oauth/token", params, {
57
+ headers: {
58
+ "Content-Type": "application/x-www-form-urlencoded",
59
+ },
60
+ });
61
+
62
+ return res?.data?.access_token;
63
+ }
64
+
65
+ async query(query, variables) {
66
+ let body = {
67
+ query,
68
+ variables,
69
+ };
70
+
71
+ if (this.options.accessToken == undefined) {
72
+ this.options.accessToken = await this.fetchAccessToken();
73
+ }
74
+
75
+ let res = await this.client.post("/graphql", body, {
76
+ headers: {
77
+ Authorization: `bearer ${this.options.accessToken}`,
78
+ },
79
+ });
80
+
81
+ return res.data;
82
+ }
83
+ }
84
+
85
+ Bunny.prototype.createSubscription = require("./helpers/create-subscription.js");
86
+ Bunny.prototype.createTenant = require("./helpers/create-tenant.js");
87
+ Bunny.prototype.updateTenant = require("./helpers/update-tenant.js");
88
+ Bunny.prototype.trackUsage = require("./helpers/track-usage.js");
89
+
90
+ module.exports = Bunny;
@@ -0,0 +1,24 @@
1
+ const crypto = require("crypto");
2
+ const scmp = require("scmp");
3
+ const Buffer = require("safe-buffer").Buffer;
4
+
5
+ class Webhooks {
6
+ constructor(signingToken = null) {
7
+ this.signingToken = signingToken;
8
+ }
9
+ validate(signature, payload, signingToken = null) {
10
+ let key = signingToken || this.signingToken;
11
+
12
+ let payloadSignature = crypto
13
+ .createHmac("sha1", key)
14
+ .update(JSON.stringify(payload))
15
+ .digest("hex");
16
+
17
+ const ps = Buffer.from(payloadSignature, "hex");
18
+ const s = Buffer.from(signature, "hex");
19
+
20
+ return scmp(ps, s);
21
+ }
22
+ }
23
+
24
+ module.exports = Webhooks;