@unboundcx/sdk 2.8.11 → 4.0.1
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/base.js +22 -0
- package/package.json +2 -2
- package/services/storage.js +71 -2
- package/services/video.js +67 -0
package/base.js
CHANGED
|
@@ -297,6 +297,17 @@ export class BaseSDK {
|
|
|
297
297
|
return true;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
// Streaming bodies (Node Readable or Web ReadableStream) — caller is
|
|
301
|
+
// expected to set an explicit content-type header alongside.
|
|
302
|
+
if (
|
|
303
|
+
typeof body === 'object' &&
|
|
304
|
+
body !== null &&
|
|
305
|
+
(typeof body.pipe === 'function' ||
|
|
306
|
+
typeof body.getReader === 'function')
|
|
307
|
+
) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
300
311
|
return false;
|
|
301
312
|
}
|
|
302
313
|
|
|
@@ -353,9 +364,20 @@ export class BaseSDK {
|
|
|
353
364
|
typeof Buffer !== 'undefined' &&
|
|
354
365
|
Buffer.isBuffer &&
|
|
355
366
|
Buffer.isBuffer(body);
|
|
367
|
+
const isStream =
|
|
368
|
+
body &&
|
|
369
|
+
typeof body === 'object' &&
|
|
370
|
+
(typeof body.pipe === 'function' ||
|
|
371
|
+
typeof body.getReader === 'function');
|
|
356
372
|
|
|
357
373
|
if (isFormData || isBuffer) {
|
|
358
374
|
options.body = body;
|
|
375
|
+
} else if (isStream) {
|
|
376
|
+
// Node 18+ native fetch (undici) requires duplex: 'half' for
|
|
377
|
+
// streaming request bodies. Without this the fetch throws
|
|
378
|
+
// synchronously before the first byte is sent.
|
|
379
|
+
options.body = body;
|
|
380
|
+
options.duplex = 'half';
|
|
359
381
|
} else {
|
|
360
382
|
options.body = JSON.stringify(body);
|
|
361
383
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unboundcx/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Official JavaScript SDK for the Unbound API - A comprehensive toolkit for integrating with Unbound's communication, AI, and data management services",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "echo 'Build complete - ESM modules ready'",
|
|
62
|
-
"test": "
|
|
62
|
+
"test": "node --test 'test/*.test.js'",
|
|
63
63
|
"lint": "echo 'Linting would run here'",
|
|
64
64
|
"prepublishOnly": "npm run build"
|
|
65
65
|
},
|
package/services/storage.js
CHANGED
|
@@ -87,6 +87,69 @@ export class StorageService {
|
|
|
87
87
|
return commonTypes[ext] || 'application/octet-stream';
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Private helper to detect if a value is a Node Readable or Web ReadableStream
|
|
91
|
+
_isStreamLike(value) {
|
|
92
|
+
if (!value || typeof value !== 'object') return false;
|
|
93
|
+
if (typeof value.pipe === 'function') return true; // Node Readable
|
|
94
|
+
if (typeof value.getReader === 'function') return true; // Web ReadableStream
|
|
95
|
+
if (typeof value[Symbol.asyncIterator] === 'function') return true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Private helper to create a streaming multipart body for Node.js.
|
|
100
|
+
// Returns { body: ReadableStream, headers } for fetch() with duplex: 'half'.
|
|
101
|
+
// Byte-identical framing to _createNodeFormData — emitted in chunks so the full
|
|
102
|
+
// file never has to sit in memory.
|
|
103
|
+
_createNodeFormDataStream(fileStream, fileName, formFields) {
|
|
104
|
+
const boundary = `----formdata-${Date.now()}-${Math.random()
|
|
105
|
+
.toString(36)
|
|
106
|
+
.slice(2)}`;
|
|
107
|
+
const CRLF = '\r\n';
|
|
108
|
+
const contentType = this._getContentType(fileName);
|
|
109
|
+
|
|
110
|
+
const header =
|
|
111
|
+
`--${boundary}${CRLF}` +
|
|
112
|
+
`Content-Disposition: form-data; name="files"; filename="${
|
|
113
|
+
fileName || 'file'
|
|
114
|
+
}"${CRLF}` +
|
|
115
|
+
`Content-Type: ${contentType}${CRLF}${CRLF}`;
|
|
116
|
+
|
|
117
|
+
let tail = '';
|
|
118
|
+
for (const [name, value] of formFields) {
|
|
119
|
+
tail += `${CRLF}--${boundary}${CRLF}Content-Disposition: form-data; name="${name}"${CRLF}${CRLF}${value}`;
|
|
120
|
+
}
|
|
121
|
+
tail += `${CRLF}--${boundary}--${CRLF}`;
|
|
122
|
+
|
|
123
|
+
const body = new ReadableStream({
|
|
124
|
+
async start(controller) {
|
|
125
|
+
try {
|
|
126
|
+
controller.enqueue(Buffer.from(header, 'utf8'));
|
|
127
|
+
for await (const chunk of fileStream) {
|
|
128
|
+
controller.enqueue(
|
|
129
|
+
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
controller.enqueue(Buffer.from(tail, 'utf8'));
|
|
133
|
+
controller.close();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
controller.error(err);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
cancel(reason) {
|
|
139
|
+
if (fileStream && typeof fileStream.destroy === 'function') {
|
|
140
|
+
fileStream.destroy(reason);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
body,
|
|
147
|
+
headers: {
|
|
148
|
+
'content-type': `multipart/form-data; boundary=${boundary}`,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
90
153
|
// Private helper to create FormData for Node.js environment
|
|
91
154
|
_createNodeFormData(file, fileName, formFields) {
|
|
92
155
|
const boundary = `----formdata-${Date.now()}-${Math.random().toString(36)}`;
|
|
@@ -234,7 +297,13 @@ export class StorageService {
|
|
|
234
297
|
// Default behavior: Use fetch via sdk._fetch
|
|
235
298
|
let formData, headers;
|
|
236
299
|
|
|
237
|
-
if (isNode) {
|
|
300
|
+
if (isNode && this._isStreamLike(file)) {
|
|
301
|
+
// Streaming path — file is piped chunk-by-chunk, no full-buffer copy.
|
|
302
|
+
// Caller is responsible for creating a fresh stream per retry.
|
|
303
|
+
const result = this._createNodeFormDataStream(file, fileName, formFields);
|
|
304
|
+
formData = result.body;
|
|
305
|
+
headers = result.headers;
|
|
306
|
+
} else if (isNode) {
|
|
238
307
|
const result = this._createNodeFormData(file, fileName, formFields);
|
|
239
308
|
formData = result.formData;
|
|
240
309
|
headers = result.headers;
|
|
@@ -381,7 +450,7 @@ Response:
|
|
|
381
450
|
/**
|
|
382
451
|
* Upload a file to storage with optional format conversion
|
|
383
452
|
* @param {Object} config - Configuration object
|
|
384
|
-
* @param {Object} config.file - File content
|
|
453
|
+
* @param {Object} config.file - File content: Buffer, File, Node Readable stream, or Web ReadableStream. Streams upload chunk-by-chunk (no full-buffer copy) — recommended for files > ~100 MB. For retries, create a fresh stream per attempt. REQUIRED
|
|
385
454
|
* @param {string} [config.classification='generic'] - File classification (e.g., 'fax', 'files', 'generic')
|
|
386
455
|
* @param {string} [config.folder] - Folder path for organizing files
|
|
387
456
|
* @param {string} [config.fileName] - Original file name
|
package/services/video.js
CHANGED
|
@@ -503,6 +503,27 @@ export class VideoService {
|
|
|
503
503
|
return result;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Validate a guest JWT for a video room. Used by app1-socket's
|
|
508
|
+
* `authorizeVideoSocketConnection` middleware during the `/video` Socket.IO
|
|
509
|
+
* handshake to decide whether to admit the connection.
|
|
510
|
+
*
|
|
511
|
+
* @param {string} id - videoRoom id (must match the token's `roomId` claim)
|
|
512
|
+
* @param {string} [token] - raw JWT. Omit to let the server read it from
|
|
513
|
+
* the `videoAuthToken` cookie on the SDK's request.
|
|
514
|
+
* @returns {Promise<{
|
|
515
|
+
* valid: boolean,
|
|
516
|
+
* account: { id: string, namespace: string, accountCode: string },
|
|
517
|
+
* participant: Object,
|
|
518
|
+
* token: { id: string, type: 'videoRoomGuest', expiresAt: string },
|
|
519
|
+
* videoRoom: Object,
|
|
520
|
+
* podName: string,
|
|
521
|
+
* }>} The `podName` field (added with centralized signaling) is the
|
|
522
|
+
* video-server pod assigned to this room at token-mint time, re-validated
|
|
523
|
+
* against app1-api's live podRegistry. If the pod is no longer alive the
|
|
524
|
+
* server returns HTTP 409 `POD_UNAVAILABLE` — the client reacts by calling
|
|
525
|
+
* `/video/rooms/:id/join` for a fresh assignment.
|
|
526
|
+
*/
|
|
506
527
|
async validateGuestToken(id, token) {
|
|
507
528
|
this.sdk.validateParams(
|
|
508
529
|
{ id, token },
|
|
@@ -524,6 +545,52 @@ export class VideoService {
|
|
|
524
545
|
return result;
|
|
525
546
|
}
|
|
526
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Construct a `VideoMeetingClient` configured for this SDK instance.
|
|
550
|
+
*
|
|
551
|
+
* Dynamically imports `@unboundcx/video-sdk-client` so WebRTC peer deps
|
|
552
|
+
* (`mediasoup-client`, `socket.io-client`) stay out of backend bundles that
|
|
553
|
+
* never touch video. Returns a client with `this` SDK injected so it can
|
|
554
|
+
* transparently call `sdk.video.joinRoom()` for reassignment recovery and
|
|
555
|
+
* `sdk.video.endSession()` on leave.
|
|
556
|
+
*
|
|
557
|
+
* **Bundler note**: SvelteKit/Vite handle the dynamic import natively. If
|
|
558
|
+
* you use a bundler that eagerly resolves imports, pin the import path or
|
|
559
|
+
* ensure the package is marked external.
|
|
560
|
+
*
|
|
561
|
+
* @param {Object} [options] - Forwarded to the `VideoMeetingClient` constructor
|
|
562
|
+
* @returns {Promise<import('@unboundcx/video-sdk-client').VideoMeetingClient>}
|
|
563
|
+
*/
|
|
564
|
+
async createMeetingClient(options = {}) {
|
|
565
|
+
const { VideoMeetingClient } = await import('@unboundcx/video-sdk-client');
|
|
566
|
+
return new VideoMeetingClient({ ...options, sdk: this.sdk });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* End the meeting session server-side. Invalidates the token row in the
|
|
571
|
+
* `tokens` table and clears the `videoAuthToken` cookie so a stale cookie
|
|
572
|
+
* from a prior meeting can't be replayed.
|
|
573
|
+
*
|
|
574
|
+
* The endpoint is deliberately permissive: accepts expired and
|
|
575
|
+
* room-mismatched cookies and never returns 401/403 — rejection would
|
|
576
|
+
* strand stale cookies.
|
|
577
|
+
*
|
|
578
|
+
* @param {string} roomId
|
|
579
|
+
*/
|
|
580
|
+
async endSession(roomId) {
|
|
581
|
+
this.sdk.validateParams(
|
|
582
|
+
{ roomId },
|
|
583
|
+
{ roomId: { type: 'string', required: true } },
|
|
584
|
+
);
|
|
585
|
+
const result = await this.sdk._fetch(
|
|
586
|
+
`/video/session/end`,
|
|
587
|
+
'POST',
|
|
588
|
+
{ body: { roomId } },
|
|
589
|
+
true,
|
|
590
|
+
);
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
|
|
527
594
|
async logStats(roomId, stats) {
|
|
528
595
|
this.sdk.validateParams(
|
|
529
596
|
{ roomId, stats },
|