@saleslane/sdk 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.
- package/README.md +57 -0
- package/package.json +36 -0
- package/src/client.js +140 -0
- package/src/index.js +8 -0
- package/src/signing.js +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @saleslane/sdk
|
|
2
|
+
|
|
3
|
+
JavaScript SDK for the Saleslane API. Handles RS256 JWT signing automatically.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @saleslane/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { SaleslaneClient } = require('@saleslane/sdk');
|
|
15
|
+
|
|
16
|
+
const client = new SaleslaneClient({
|
|
17
|
+
baseUrl: 'https://your-instance.saleslane.nl',
|
|
18
|
+
apiRecordId: process.env.SALESLANE_API_RECORD_ID,
|
|
19
|
+
privateKey: process.env.SALESLANE_PRIVATE_KEY,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const me = await client.getMe();
|
|
23
|
+
console.log(me.data);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Authentication
|
|
27
|
+
|
|
28
|
+
Every request is signed with an RS256 JWT:
|
|
29
|
+
|
|
30
|
+
- **GET** requests: params are signed and sent as `?signed=<jwt>`
|
|
31
|
+
- **POST/PUT/PATCH/DELETE**: the body is signed and sent as the JWT string
|
|
32
|
+
|
|
33
|
+
You need an API record ID and the matching RSA private key. Register the public key on the API record in Saleslane.
|
|
34
|
+
|
|
35
|
+
## Methods
|
|
36
|
+
|
|
37
|
+
| Method | Description |
|
|
38
|
+
|--------|-------------|
|
|
39
|
+
| `getMe()` | Get the authenticated API record |
|
|
40
|
+
| `revokeMe()` | Deactivate the API record (irreversible) |
|
|
41
|
+
| `getContactByPhone({ phoneNumber })` | Look up contacts by E.164 phone number |
|
|
42
|
+
| `addTransactionTag({ referenceId, tag, ... })` | Add a tag to a subtransaction |
|
|
43
|
+
| `listUsers()` | List all users |
|
|
44
|
+
| `listTeams()` | List all teams |
|
|
45
|
+
|
|
46
|
+
## Low-level Exports
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const { signPayload, createSignedHooks } = require('@saleslane/sdk');
|
|
50
|
+
|
|
51
|
+
const token = signPayload(payload, privateKey, apiRecordId);
|
|
52
|
+
const hooks = createSignedHooks(privateKey, apiRecordId);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
ISC
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saleslane/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JavaScript SDK for the Saleslane API (RS256 signed requests)",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./package.json": "./package.json"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "jest __tests__/signing.test.js __tests__/client.test.js --verbose",
|
|
16
|
+
"test:integration": "bash test1.sh"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"saleslane",
|
|
20
|
+
"sdk",
|
|
21
|
+
"api",
|
|
22
|
+
"rs256",
|
|
23
|
+
"jwt"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/saleslane/saleslane.git",
|
|
28
|
+
"directory": "packages/sdk"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"got": "^11.8.2",
|
|
32
|
+
"jsonwebtoken": "^9.0.0",
|
|
33
|
+
"uuid": "^8.3.2"
|
|
34
|
+
},
|
|
35
|
+
"license": "ISC"
|
|
36
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const got = require('got');
|
|
2
|
+
const { createSignedHooks } = require('./signing');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} SaleslaneClientOptions
|
|
6
|
+
* @property {string} baseUrl – Backend URL, e.g. "https://api.saleslane.com" or "http://localhost:4002".
|
|
7
|
+
* @property {string} apiRecordId – The API record ID (24-char hex ObjectId or referenceId).
|
|
8
|
+
* @property {string} privateKey – RSA private key in PEM format for RS256 signing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* JavaScript SDK client for the Saleslane API.
|
|
13
|
+
*
|
|
14
|
+
* All requests are automatically signed with RS256 JWTs.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { SaleslaneClient } = require('@saleslane/sdk');
|
|
18
|
+
*
|
|
19
|
+
* const client = new SaleslaneClient({
|
|
20
|
+
* baseUrl: 'https://api.saleslane.com',
|
|
21
|
+
* apiRecordId: '64a1b2c3d4e5f6a7b8c9d0e1',
|
|
22
|
+
* privateKey: process.env.API_PRIVATE_KEY,
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* const me = await client.getMe();
|
|
26
|
+
* console.log(me.data);
|
|
27
|
+
*/
|
|
28
|
+
class SaleslaneClient {
|
|
29
|
+
/**
|
|
30
|
+
* @param {SaleslaneClientOptions} options
|
|
31
|
+
*/
|
|
32
|
+
constructor({ baseUrl, apiRecordId, privateKey }) {
|
|
33
|
+
if (!baseUrl) throw new Error('SaleslaneClient: baseUrl is required');
|
|
34
|
+
if (!apiRecordId) throw new Error('SaleslaneClient: apiRecordId is required');
|
|
35
|
+
if (!privateKey) throw new Error('SaleslaneClient: privateKey is required');
|
|
36
|
+
|
|
37
|
+
this._client = got.extend({
|
|
38
|
+
prefixUrl: `${baseUrl.replace(/\/+$/, '')}/api/v1`,
|
|
39
|
+
responseType: 'json',
|
|
40
|
+
hooks: createSignedHooks(privateKey, apiRecordId),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// API (self)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the authenticated API record.
|
|
50
|
+
* @returns {Promise<{data: {_id: string, title: string, description: string, exp: string}}>}
|
|
51
|
+
*/
|
|
52
|
+
getMe() {
|
|
53
|
+
return this._client.get('api/me').json();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Revoke / deactivate the API record.
|
|
58
|
+
* @returns {Promise<{data: object}>}
|
|
59
|
+
*/
|
|
60
|
+
revokeMe() {
|
|
61
|
+
return this._client.delete('api/me').json();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Contact
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Look up contacts by E.164 phone number.
|
|
70
|
+
* Returns contacts with their transactions and subtransactions.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} params
|
|
73
|
+
* @param {string} params.phoneNumber – E.164 formatted phone number, e.g. "+31612345678".
|
|
74
|
+
* @returns {Promise<{data: Array}>}
|
|
75
|
+
*/
|
|
76
|
+
getContactByPhone({ phoneNumber }) {
|
|
77
|
+
return this._client.get('contact/by-phone', {
|
|
78
|
+
searchParams: { phoneNumber },
|
|
79
|
+
}).json();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Transaction
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
/**
|
|
86
|
+
* Add a tag to a subtransaction by its friendly referenceId.
|
|
87
|
+
*
|
|
88
|
+
* The referenceId format is: the transaction referenceId + a 2-digit
|
|
89
|
+
* 1-based subtransaction index (e.g. "2607700101").
|
|
90
|
+
*
|
|
91
|
+
* @param {object} params
|
|
92
|
+
* @param {string} params.referenceId – Full referenceId including subtransaction suffix.
|
|
93
|
+
* @param {string} params.tag – Tag name.
|
|
94
|
+
* @param {string} [params.description]
|
|
95
|
+
* @param {string} [params.color] – Hex color, e.g. "#6366f1".
|
|
96
|
+
* @param {string} [params.labelType] – e.g. "info".
|
|
97
|
+
* @param {string} [params.value]
|
|
98
|
+
* @param {string} [params.date] – ISO 8601 date string.
|
|
99
|
+
* @returns {Promise<{data: object}>}
|
|
100
|
+
*/
|
|
101
|
+
addTransactionTag({ referenceId, tag, description, color, labelType, value, date }) {
|
|
102
|
+
return this._client.post('transaction/transaction-tag', {
|
|
103
|
+
json: {
|
|
104
|
+
referenceId,
|
|
105
|
+
tag,
|
|
106
|
+
...(description !== undefined && { description }),
|
|
107
|
+
...(color !== undefined && { color }),
|
|
108
|
+
...(labelType !== undefined && { labelType }),
|
|
109
|
+
...(value !== undefined && { value }),
|
|
110
|
+
...(date !== undefined && { date }),
|
|
111
|
+
},
|
|
112
|
+
}).json();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// User
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List all users.
|
|
121
|
+
* @returns {Promise<{data: Array<{_id: string, email: string, first_name: string, last_name: string, nickName?: string, team?: string, deleted?: boolean}>}>}
|
|
122
|
+
*/
|
|
123
|
+
listUsers() {
|
|
124
|
+
return this._client.get('user').json();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Team
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List all teams.
|
|
133
|
+
* @returns {Promise<{data: Array<{_id: string, name: string, deleted?: boolean}>}>}
|
|
134
|
+
*/
|
|
135
|
+
listTeams() {
|
|
136
|
+
return this._client.get('team').json();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { SaleslaneClient };
|
package/src/index.js
ADDED
package/src/signing.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sign a payload using RS256 for the Saleslane API.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} data – The actual query params (GET) or body (POST/PUT/PATCH/DELETE).
|
|
8
|
+
* @param {string} privateKey – RSA private key in PEM format.
|
|
9
|
+
* @param {string} subject – API record ID (the `sub` claim).
|
|
10
|
+
* @returns {string} Signed JWT.
|
|
11
|
+
*/
|
|
12
|
+
function signPayload(data, privateKey, subject) {
|
|
13
|
+
return jwt.sign({ data }, privateKey, {
|
|
14
|
+
algorithm: 'RS256',
|
|
15
|
+
expiresIn: 900,
|
|
16
|
+
subject,
|
|
17
|
+
jwtid: uuidv4(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create `got` hooks that transparently sign every request.
|
|
23
|
+
*
|
|
24
|
+
* - GET requests: query params are wrapped in a JWT and sent as `?signed=<token>`.
|
|
25
|
+
* - POST/PUT/PATCH/DELETE: the JSON body is wrapped in a JWT and sent as the raw body.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} privateKey – RSA private key in PEM format.
|
|
28
|
+
* @param {string} apiRecordId – The API record ID used as JWT `sub`.
|
|
29
|
+
* @returns {object} Hooks object for `got.extend()`.
|
|
30
|
+
*/
|
|
31
|
+
function createSignedHooks(privateKey, apiRecordId) {
|
|
32
|
+
return {
|
|
33
|
+
init: [
|
|
34
|
+
(plain) => {
|
|
35
|
+
if (['get'].includes(plain.method)) {
|
|
36
|
+
const signed = signPayload(plain.searchParams || {}, privateKey, apiRecordId);
|
|
37
|
+
plain.searchParams = { signed };
|
|
38
|
+
}
|
|
39
|
+
if (['post', 'put', 'delete', 'patch'].includes(plain.method)) {
|
|
40
|
+
const signed = signPayload(plain.json || {}, privateKey, apiRecordId);
|
|
41
|
+
plain.json = signed;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
beforeError: [
|
|
46
|
+
(error) => {
|
|
47
|
+
const { response, options } = error;
|
|
48
|
+
if (response && response.body && options) {
|
|
49
|
+
error.name = 'SaleslaneApiError';
|
|
50
|
+
error.message = `${options.method} ${options.url.href} -> ${response.statusCode}\n${
|
|
51
|
+
typeof response.body === 'object'
|
|
52
|
+
? JSON.stringify(response.body, null, 2)
|
|
53
|
+
: response.body
|
|
54
|
+
}`;
|
|
55
|
+
}
|
|
56
|
+
return error;
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { signPayload, createSignedHooks };
|