@unboundcx/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/LICENSE +21 -0
- package/README.md +456 -0
- package/base.js +263 -0
- package/index.js +181 -0
- package/package.json +88 -0
- package/services/ai.js +151 -0
- package/services/enroll.js +238 -0
- package/services/externalOAuth.js +117 -0
- package/services/generateId.js +39 -0
- package/services/googleCalendar.js +92 -0
- package/services/layouts.js +91 -0
- package/services/login.js +76 -0
- package/services/lookup.js +53 -0
- package/services/messaging.js +687 -0
- package/services/notes.js +137 -0
- package/services/objects.js +163 -0
- package/services/phoneNumbers.js +215 -0
- package/services/portals.js +127 -0
- package/services/recordTypes.js +173 -0
- package/services/sipEndpoints.js +93 -0
- package/services/storage.js +168 -0
- package/services/subscriptions.js +73 -0
- package/services/verification.js +87 -0
- package/services/video.js +380 -0
- package/services/voice.js +434 -0
- package/services/workflows.js +291 -0
- package/test-backwards-compatibility.js +195 -0
- package/test-complete-coverage.js +247 -0
- package/test-constructor-patterns.js +111 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Unbound (Cameron J. Weeks)
|
|
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,456 @@
|
|
|
1
|
+
# Unbound JavaScript SDK
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40unbound%2Fsdk)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
The official JavaScript SDK for Unbound's comprehensive communication and AI platform. This SDK provides easy access to messaging (SMS/Email), voice calling, video conferencing, AI services, workflows, and data management capabilities.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🚀 **Universal**: Works in Node.js and browsers
|
|
11
|
+
- 📱 **Messaging**: SMS/MMS and Email with templates and campaigns
|
|
12
|
+
- 📞 **Voice**: Call management, conferencing, recording, transcription
|
|
13
|
+
- 📹 **Video**: Video conferencing with advanced controls
|
|
14
|
+
- 🤖 **AI**: Generative AI chat and text-to-speech
|
|
15
|
+
- 💾 **Data**: Object management with queries and relationships
|
|
16
|
+
- 🔄 **Workflows**: Programmable workflow execution
|
|
17
|
+
- 🔌 **Extensible**: Plugin system for transports and extensions
|
|
18
|
+
- ⚡ **Performance**: Automatic transport optimization (NATS/Socket/HTTP)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @unbound/sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Optional Dependencies
|
|
27
|
+
|
|
28
|
+
For enhanced functionality, you may also install:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# For Socket.io transport (browser environments)
|
|
32
|
+
npm install socket.io-client
|
|
33
|
+
|
|
34
|
+
# For improved MIME type detection
|
|
35
|
+
npm install mime-types
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import SDK from '@unbound/sdk';
|
|
44
|
+
|
|
45
|
+
// Initialize the SDK
|
|
46
|
+
const api = new SDK({
|
|
47
|
+
namespace: 'your-namespace',
|
|
48
|
+
token: 'your-jwt-token'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Or using legacy positional parameters (backwards compatible)
|
|
52
|
+
const api = new SDK('your-namespace', null, 'your-jwt-token');
|
|
53
|
+
|
|
54
|
+
// Login (gets JWT token)
|
|
55
|
+
const login = await api.login.login('username', 'password');
|
|
56
|
+
|
|
57
|
+
// Send SMS
|
|
58
|
+
const sms = await api.messaging.sms.send({
|
|
59
|
+
from: '+1234567890',
|
|
60
|
+
to: '+0987654321',
|
|
61
|
+
message: 'Hello from Unbound!'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create video meeting
|
|
65
|
+
const meeting = await api.video.createRoom({
|
|
66
|
+
name: 'Team Meeting',
|
|
67
|
+
startTime: '2024-01-15T10:00:00Z',
|
|
68
|
+
duration: 60
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Client-Side Usage (Browser/Svelte)
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
import SDK from '@unbound/sdk';
|
|
76
|
+
import { socketAppStore } from './stores/socket.js';
|
|
77
|
+
|
|
78
|
+
// Initialize with socket transport for optimal performance
|
|
79
|
+
const api = new SDK('your-namespace', null, null, null, 'api.yourdomain.com', socketAppStore);
|
|
80
|
+
|
|
81
|
+
// The SDK will automatically use WebSocket when available, HTTP as fallback
|
|
82
|
+
const objects = await api.objects.query('contacts', {
|
|
83
|
+
limit: 10,
|
|
84
|
+
orderBy: 'createdAt'
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Server-Side Usage (Node.js)
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
import SDK from '@unbound/sdk';
|
|
92
|
+
|
|
93
|
+
// Initialize for server-side usage
|
|
94
|
+
const api = new SDK('your-namespace', 'call-id', 'jwt-token', 'request-id');
|
|
95
|
+
|
|
96
|
+
// Server-side exclusive: Master authentication
|
|
97
|
+
await api.buildMasterAuth({
|
|
98
|
+
namespace: 'target-namespace',
|
|
99
|
+
accountId: 'account-123',
|
|
100
|
+
userId: 'user-456'
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Use NATS transport automatically when available
|
|
104
|
+
const result = await api.objects.create('leads', {
|
|
105
|
+
name: 'John Doe',
|
|
106
|
+
email: 'john@example.com',
|
|
107
|
+
phone: '+1234567890'
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Constructor Options
|
|
112
|
+
|
|
113
|
+
The SDK constructor supports both object-based and legacy positional parameters:
|
|
114
|
+
|
|
115
|
+
### Object-Based Constructor (Recommended)
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
const api = new SDK({
|
|
119
|
+
namespace: 'your-namespace', // Required: API namespace
|
|
120
|
+
token: 'jwt-token', // Optional: JWT authentication token
|
|
121
|
+
callId: 'call-123', // Optional: Call tracking ID
|
|
122
|
+
fwRequestId: 'request-456', // Optional: Request forwarding ID
|
|
123
|
+
url: 'api.example.com', // Optional: Custom API URL (browser only)
|
|
124
|
+
socketStore: socketAppStore // Optional: Socket.io store (Svelte/browser)
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Legacy Positional Constructor (Backwards Compatible)
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
const api = new SDK(
|
|
132
|
+
'your-namespace', // namespace
|
|
133
|
+
'call-123', // callId
|
|
134
|
+
'jwt-token', // token
|
|
135
|
+
'request-456', // fwRequestId
|
|
136
|
+
'api.example.com', // url (browser only)
|
|
137
|
+
socketAppStore // socketStore (browser only)
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Factory Function
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
import { createSDK } from '@unbound/sdk';
|
|
145
|
+
|
|
146
|
+
const api = createSDK({
|
|
147
|
+
namespace: 'your-namespace',
|
|
148
|
+
token: 'jwt-token'
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## API Reference
|
|
153
|
+
|
|
154
|
+
### Core Services
|
|
155
|
+
|
|
156
|
+
#### Authentication (`api.login`)
|
|
157
|
+
```javascript
|
|
158
|
+
// Login and get session
|
|
159
|
+
await api.login.login('username', 'password');
|
|
160
|
+
await api.login.logout();
|
|
161
|
+
await api.login.validate();
|
|
162
|
+
await api.login.changePassword('current', 'new');
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Objects (`api.objects`)
|
|
166
|
+
```javascript
|
|
167
|
+
// CRUD operations on data objects
|
|
168
|
+
await api.objects.create('contacts', { name: 'John', email: 'john@example.com' });
|
|
169
|
+
await api.objects.query('contacts', { limit: 10 });
|
|
170
|
+
await api.objects.byId('contact-123');
|
|
171
|
+
await api.objects.updateById('contacts', 'contact-123', { name: 'Jane' });
|
|
172
|
+
await api.objects.deleteById('contacts', 'contact-123');
|
|
173
|
+
await api.objects.describe('contacts'); // Get schema
|
|
174
|
+
await api.objects.list(); // List all object types
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Messaging (`api.messaging`)
|
|
178
|
+
```javascript
|
|
179
|
+
// SMS/MMS
|
|
180
|
+
await api.messaging.sms.send({
|
|
181
|
+
from: '+1234567890',
|
|
182
|
+
to: '+0987654321',
|
|
183
|
+
message: 'Hello!',
|
|
184
|
+
mediaUrls: ['https://example.com/image.jpg']
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Email
|
|
188
|
+
await api.messaging.email.send({
|
|
189
|
+
from: 'sender@example.com',
|
|
190
|
+
to: 'recipient@example.com',
|
|
191
|
+
subject: 'Hello',
|
|
192
|
+
htmlBody: '<h1>Hello World!</h1>'
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Templates
|
|
196
|
+
await api.messaging.sms.templates.create({
|
|
197
|
+
name: 'welcome',
|
|
198
|
+
message: 'Welcome {{name}}!',
|
|
199
|
+
variables: { name: 'string' }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Campaign Management
|
|
203
|
+
await api.messaging.campaigns.tollFree.create({
|
|
204
|
+
companyName: 'Acme Corp',
|
|
205
|
+
phoneNumber: '+1234567890',
|
|
206
|
+
description: 'Marketing campaign'
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Video Conferencing (`api.video`)
|
|
211
|
+
```javascript
|
|
212
|
+
// Room Management
|
|
213
|
+
const room = await api.video.createRoom({
|
|
214
|
+
name: 'Team Meeting',
|
|
215
|
+
password: 'secret123',
|
|
216
|
+
startTime: '2024-01-15T10:00:00Z',
|
|
217
|
+
waitingRoom: true,
|
|
218
|
+
hosts: ['user@example.com']
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Join meeting
|
|
222
|
+
await api.video.joinRoom(room.id, 'password', 'user@example.com');
|
|
223
|
+
|
|
224
|
+
// Participant controls
|
|
225
|
+
await api.video.mute(room.id, 'participant-id', 'microphone', true);
|
|
226
|
+
await api.video.removeParticipant(room.id, 'participant-id');
|
|
227
|
+
|
|
228
|
+
// Analytics
|
|
229
|
+
const analytics = await api.video.getMeetingAnalytics(room.id, {
|
|
230
|
+
startTime: '2024-01-15T10:00:00Z',
|
|
231
|
+
endTime: '2024-01-15T11:00:00Z'
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Voice Calling (`api.voice`)
|
|
236
|
+
```javascript
|
|
237
|
+
// Make calls
|
|
238
|
+
const call = await api.voice.createCall({
|
|
239
|
+
to: '+1234567890',
|
|
240
|
+
from: '+0987654321',
|
|
241
|
+
record: true,
|
|
242
|
+
transcribe: true
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Call controls
|
|
246
|
+
await api.voice.mute(call.callControlId);
|
|
247
|
+
await api.voice.hold(call.callControlId);
|
|
248
|
+
await api.voice.sendDtmf(call.callControlId, '1234');
|
|
249
|
+
await api.voice.transfer(call.callControlId, '+1555555555');
|
|
250
|
+
await api.voice.hangup(call.callControlId);
|
|
251
|
+
|
|
252
|
+
// Conference calls
|
|
253
|
+
const conference = await api.voice.createConference({ name: 'Team Call' });
|
|
254
|
+
await api.voice.joinConference(call.callControlId, conference.id);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### AI Services (`api.ai`)
|
|
258
|
+
```javascript
|
|
259
|
+
// Generative AI
|
|
260
|
+
const response = await api.ai.generative.chat({
|
|
261
|
+
messages: [{ role: 'user', content: 'Hello AI!' }],
|
|
262
|
+
model: 'gpt-4',
|
|
263
|
+
temperature: 0.7,
|
|
264
|
+
method: 'openai'
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Text-to-Speech
|
|
268
|
+
const audio = await api.ai.tts.create({
|
|
269
|
+
text: 'Hello, this is a test message',
|
|
270
|
+
voice: 'en-US-Standard-A',
|
|
271
|
+
audioEncoding: 'MP3'
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Utility Services
|
|
276
|
+
|
|
277
|
+
#### File Storage (`api.storage`)
|
|
278
|
+
```javascript
|
|
279
|
+
// Upload files
|
|
280
|
+
const files = await api.storage.uploadFiles(fileData, {
|
|
281
|
+
classification: 'documents',
|
|
282
|
+
isPublic: false,
|
|
283
|
+
expireAfter: '30d'
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Get files
|
|
287
|
+
const fileUrl = api.storage.getFileUrl(files[0].storageId);
|
|
288
|
+
await api.storage.deleteFile(files[0].storageId);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Workflows (`api.workflows`)
|
|
292
|
+
```javascript
|
|
293
|
+
// Workflow items
|
|
294
|
+
await api.workflows.items.create({
|
|
295
|
+
workflowVersionId: 'wf-123',
|
|
296
|
+
category: 'communication',
|
|
297
|
+
type: 'sendEmail',
|
|
298
|
+
settings: { template: 'welcome' }
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Connections
|
|
302
|
+
await api.workflows.connections.create({
|
|
303
|
+
workflowItemId: 'item-1',
|
|
304
|
+
workflowItemPortId: 'output',
|
|
305
|
+
inWorkflowItemId: 'item-2',
|
|
306
|
+
inWorkflowItemPortId: 'input'
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Transport Plugins
|
|
311
|
+
|
|
312
|
+
The SDK automatically optimizes transport based on environment:
|
|
313
|
+
|
|
314
|
+
- **Node.js**: NATS → HTTP fallback
|
|
315
|
+
- **Browser**: WebSocket → HTTP fallback
|
|
316
|
+
- **Always available**: HTTP fetch
|
|
317
|
+
|
|
318
|
+
### Custom Transports
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
import SDK from '@unbound/sdk';
|
|
322
|
+
|
|
323
|
+
class CustomTransport {
|
|
324
|
+
constructor(config) {
|
|
325
|
+
this.config = config;
|
|
326
|
+
this.name = 'custom';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
getPriority() { return 10; } // Lower = higher priority
|
|
330
|
+
|
|
331
|
+
async isAvailable() {
|
|
332
|
+
return true; // Check if transport is ready
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async request(endpoint, method, params, options) {
|
|
336
|
+
// Custom transport logic
|
|
337
|
+
return response;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const api = new SDK('namespace', null, 'token');
|
|
342
|
+
api.addTransport(new CustomTransport({}));
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Extensions
|
|
346
|
+
|
|
347
|
+
### Internal SDK (Server-side only)
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
import SDK from '@unbound/sdk';
|
|
351
|
+
|
|
352
|
+
const api = new SDK('namespace');
|
|
353
|
+
|
|
354
|
+
// Now has access to internal APIs
|
|
355
|
+
await api.buildMasterAuth({ accountId: '123', userId: '456' });
|
|
356
|
+
await api.internal.sip.router('+1234567890', '+0987654321');
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Environment Support
|
|
360
|
+
|
|
361
|
+
### Node.js
|
|
362
|
+
```javascript
|
|
363
|
+
import SDK from '@unbound/sdk';
|
|
364
|
+
|
|
365
|
+
// Automatic environment detection
|
|
366
|
+
const api = new SDK(process.env.UNBOUND_NAMESPACE);
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Browser/Webpack
|
|
370
|
+
```javascript
|
|
371
|
+
import SDK from '@unbound/sdk';
|
|
372
|
+
|
|
373
|
+
const api = new SDK('namespace', null, null, null, 'api.yourdomain.com');
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Svelte
|
|
377
|
+
```javascript
|
|
378
|
+
import SDK from '@unbound/sdk';
|
|
379
|
+
import { socketAppStore } from '$lib/stores/socket.js';
|
|
380
|
+
|
|
381
|
+
const api = new SDK('namespace', null, null, null, 'api.yourdomain.com', socketAppStore);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Error Handling
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
try {
|
|
388
|
+
await api.messaging.sms.send({ to: 'invalid' });
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.log(error.name); // 'API :: Error :: https :: POST :: /messaging/sms :: ...'
|
|
391
|
+
console.log(error.message); // 'Invalid phone number format'
|
|
392
|
+
console.log(error.status); // 400
|
|
393
|
+
console.log(error.method); // 'POST'
|
|
394
|
+
console.log(error.endpoint); // '/messaging/sms'
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## TypeScript Support
|
|
399
|
+
|
|
400
|
+
The SDK includes TypeScript definitions:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import SDK, { MessagingService, VideoService } from '@unbound/sdk';
|
|
404
|
+
|
|
405
|
+
const api: SDK = new SDK('namespace', null, 'token');
|
|
406
|
+
|
|
407
|
+
// Full type safety
|
|
408
|
+
const sms: any = await api.messaging.sms.send({
|
|
409
|
+
from: '+1234567890',
|
|
410
|
+
to: '+0987654321',
|
|
411
|
+
message: 'Hello TypeScript!'
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Development
|
|
416
|
+
|
|
417
|
+
### Setup
|
|
418
|
+
```bash
|
|
419
|
+
git clone https://github.com/unbound/sdk-js.git
|
|
420
|
+
cd sdk-js
|
|
421
|
+
npm install
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Testing
|
|
425
|
+
```bash
|
|
426
|
+
npm test # Run all tests
|
|
427
|
+
npm run test:unit # Unit tests only
|
|
428
|
+
npm run test:integration # Integration tests
|
|
429
|
+
npm run test:watch # Watch mode
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Building
|
|
433
|
+
```bash
|
|
434
|
+
npm run build # Build for production
|
|
435
|
+
npm run lint # Check code style
|
|
436
|
+
npm run docs # Generate documentation
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Contributing
|
|
440
|
+
|
|
441
|
+
1. Fork the repository
|
|
442
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
443
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
444
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
445
|
+
5. Open a Pull Request
|
|
446
|
+
|
|
447
|
+
## License
|
|
448
|
+
|
|
449
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
450
|
+
|
|
451
|
+
## Support
|
|
452
|
+
|
|
453
|
+
- 📚 [Documentation](https://docs.unbound.cx/sdk)
|
|
454
|
+
- 🐛 [Issue Tracker](https://github.com/unbound/sdk-js/issues)
|
|
455
|
+
- 💬 [Community Forum](https://community.unbound.cx)
|
|
456
|
+
- 📧 [Email Support](mailto:support@unbound.cx)
|