@unboundcx/sdk-internal 2.1.0 → 2.3.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/index.js +39 -0
- package/package.json +5 -1
- package/services/video.js +111 -0
- package/services/voice.js +39 -0
- package/services/voicemail.js +47 -0
- package/utils/buildObjectEventV2.js +74 -0
package/index.js
CHANGED
|
@@ -9,6 +9,10 @@ import { InternalSocketService } from './services/socket.js';
|
|
|
9
9
|
import { InternalMasterAuthService } from './services/masterAuth.js';
|
|
10
10
|
import { InternalBrandingService } from './services/branding.js';
|
|
11
11
|
import { InternalPhoneNumbersService } from './services/phoneNumbers.js';
|
|
12
|
+
import { InternalVideoService } from './services/video.js';
|
|
13
|
+
import { InternalVoicemailService } from './services/voicemail.js';
|
|
14
|
+
import { InternalVoiceService } from './services/voice.js';
|
|
15
|
+
import { buildObjectEventV2 } from './utils/buildObjectEventV2.js';
|
|
12
16
|
|
|
13
17
|
export class InternalSDK {
|
|
14
18
|
constructor(sdk, buildMasterApiAuthFn) {
|
|
@@ -24,6 +28,9 @@ export class InternalSDK {
|
|
|
24
28
|
this.masterAuth = new InternalMasterAuthService(sdk);
|
|
25
29
|
this.branding = new InternalBrandingService(sdk);
|
|
26
30
|
this.phoneNumbers = new InternalPhoneNumbersService(sdk);
|
|
31
|
+
this.video = new InternalVideoService(sdk);
|
|
32
|
+
this.voicemail = new InternalVoicemailService(sdk);
|
|
33
|
+
this.voice = new InternalVoiceService(sdk);
|
|
27
34
|
|
|
28
35
|
// Proxy all base SDK properties and methods
|
|
29
36
|
this._proxyBaseSDK();
|
|
@@ -97,6 +104,34 @@ export class InternalSDK {
|
|
|
97
104
|
}
|
|
98
105
|
return this.sdk.token;
|
|
99
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Governed escape hatch for internal routes reached by path-as-data
|
|
110
|
+
* mechanisms (e.g. a voice-app webhook command that carries its target
|
|
111
|
+
* `url` at runtime), where a typed per-route method isn't possible
|
|
112
|
+
* because the caller doesn't know the route ahead of time. This is the
|
|
113
|
+
* ONLY sanctioned way to hit an internal route this way — if you know
|
|
114
|
+
* the route at write time, add (or use) a typed method on the matching
|
|
115
|
+
* service instead of calling this directly.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} path - Must start with '/internal/'.
|
|
118
|
+
* @param {object} body
|
|
119
|
+
* @param {object} [options]
|
|
120
|
+
* @param {number} [options.timeoutMs]
|
|
121
|
+
*/
|
|
122
|
+
async post(path, body, { timeoutMs } = {}) {
|
|
123
|
+
if (typeof path !== 'string' || !path.startsWith('/internal/')) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`InternalSDK.post: path must be a string starting with '/internal/', got: ${path}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const params = { body };
|
|
130
|
+
if (timeoutMs) params.timeoutMs = timeoutMs;
|
|
131
|
+
|
|
132
|
+
const result = await this.sdk._fetch(path, 'POST', params);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
100
135
|
}
|
|
101
136
|
|
|
102
137
|
// Extension pattern for the main SDK
|
|
@@ -126,3 +161,7 @@ export { InternalSocketService } from './services/socket.js';
|
|
|
126
161
|
export { InternalMasterAuthService } from './services/masterAuth.js';
|
|
127
162
|
export { InternalBrandingService } from './services/branding.js';
|
|
128
163
|
export { InternalPhoneNumbersService } from './services/phoneNumbers.js';
|
|
164
|
+
export { InternalVideoService } from './services/video.js';
|
|
165
|
+
export { InternalVoicemailService } from './services/voicemail.js';
|
|
166
|
+
export { InternalVoiceService } from './services/voice.js';
|
|
167
|
+
export { buildObjectEventV2 } from './utils/buildObjectEventV2.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unboundcx/sdk-internal",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Internal SDK extension for Unbound - Provides access to internal APIs and administrative functions",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"files": [
|
|
33
33
|
"*.js",
|
|
34
34
|
"services/**/*.js",
|
|
35
|
+
"utils/**/*.js",
|
|
35
36
|
"types/**/*.d.ts",
|
|
36
37
|
"README.md",
|
|
37
38
|
"LICENSE"
|
|
@@ -43,6 +44,9 @@
|
|
|
43
44
|
},
|
|
44
45
|
"./services/*": {
|
|
45
46
|
"import": "./services/*.js"
|
|
47
|
+
},
|
|
48
|
+
"./utils/*": {
|
|
49
|
+
"import": "./utils/*.js"
|
|
46
50
|
}
|
|
47
51
|
},
|
|
48
52
|
"scripts": {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export class InternalVideoService {
|
|
2
|
+
constructor(sdk) {
|
|
3
|
+
this.sdk = sdk;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Meet PSTN bridge join (D2/D3/D10). Called by media-manager's `webhook`
|
|
8
|
+
* v2 command from the meetDialIn app once the caller has DTMF'd a
|
|
9
|
+
* meetingId + PIN. Matches
|
|
10
|
+
* app1-api/src/services/INTERNAL/video/controllers/meetJoin.js.
|
|
11
|
+
*
|
|
12
|
+
* POST /internal/video/meetJoin
|
|
13
|
+
* Body: { callId, from, variables: { meetingId, meetingPin, attempt? } }
|
|
14
|
+
*/
|
|
15
|
+
async meetJoin({ callId, from, variables }) {
|
|
16
|
+
this.sdk.validateParams(
|
|
17
|
+
{ callId },
|
|
18
|
+
{
|
|
19
|
+
callId: { type: 'string', required: true },
|
|
20
|
+
from: { type: 'string', required: false },
|
|
21
|
+
variables: { type: 'object', required: false },
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const body = { callId };
|
|
26
|
+
if (from) body.from = from;
|
|
27
|
+
if (variables) body.variables = variables;
|
|
28
|
+
|
|
29
|
+
const params = {
|
|
30
|
+
body,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const result = await this.sdk._fetch(
|
|
34
|
+
'/internal/video/meetJoin',
|
|
35
|
+
'POST',
|
|
36
|
+
params,
|
|
37
|
+
);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Meet PSTN bridge D9 (phone-hangs-up teardown). POST when a callId ==
|
|
43
|
+
* sipCallId leg observed on an active Meet participant ends. Matches
|
|
44
|
+
* app1-api/src/services/INTERNAL/video/controllers/meetCallEnded.js.
|
|
45
|
+
*
|
|
46
|
+
* POST /internal/video/meetCallEnded
|
|
47
|
+
* Body: { sipCallId, accountId }
|
|
48
|
+
*/
|
|
49
|
+
async meetCallEnded({ sipCallId, accountId }) {
|
|
50
|
+
this.sdk.validateParams(
|
|
51
|
+
{ sipCallId, accountId },
|
|
52
|
+
{
|
|
53
|
+
sipCallId: { type: 'string', required: true },
|
|
54
|
+
accountId: { type: 'string', required: true },
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const params = {
|
|
59
|
+
body: { sipCallId, accountId },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = await this.sdk._fetch(
|
|
63
|
+
'/internal/video/meetCallEnded',
|
|
64
|
+
'POST',
|
|
65
|
+
params,
|
|
66
|
+
);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Meet PSTN bridge dial-out status callback. media-manager's
|
|
72
|
+
* DialEventHandler posts here (fire-and-forget) for pre-answer progress
|
|
73
|
+
* ('trying' | 'ringing' | 'answered') and terminal 'failed' outcomes on a
|
|
74
|
+
* standalone-outbound leg placed via placeOutboundCall's statusWebhook.
|
|
75
|
+
* Matches
|
|
76
|
+
* app1-api/src/services/INTERNAL/video/controllers/meetOutboundStatus.js.
|
|
77
|
+
*
|
|
78
|
+
* The api controller is intentionally tolerant of unknown/extra fields
|
|
79
|
+
* (it only requires participantId, roomId, event), so this method passes
|
|
80
|
+
* the payload through as-is rather than enumerating every static field —
|
|
81
|
+
* add a typed field here if a fixed shape solidifies.
|
|
82
|
+
*
|
|
83
|
+
* POST /internal/video/meetOutboundStatus
|
|
84
|
+
* Body: { event, callId, requestId, participantId, roomId, reason? }
|
|
85
|
+
*/
|
|
86
|
+
async meetOutboundStatus(payload) {
|
|
87
|
+
this.sdk.validateParams(
|
|
88
|
+
{ payload },
|
|
89
|
+
{
|
|
90
|
+
payload: { type: 'object', required: true },
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
const { event, participantId, roomId } = payload || {};
|
|
94
|
+
if (!event || !participantId || !roomId) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'meetOutboundStatus requires event, participantId, and roomId.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const params = {
|
|
101
|
+
body: payload,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = await this.sdk._fetch(
|
|
105
|
+
'/internal/video/meetOutboundStatus',
|
|
106
|
+
'POST',
|
|
107
|
+
params,
|
|
108
|
+
);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class InternalVoiceService {
|
|
2
|
+
constructor(sdk) {
|
|
3
|
+
this.sdk = sdk;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bulk write endpoint for the CDR discrete-event timeline (WP6.5).
|
|
8
|
+
* Emitters (sip-processor / media-manager / task-router) call this to
|
|
9
|
+
* report events; fire-and-forget semantics are the CALLER's job, not
|
|
10
|
+
* this method's. Matches
|
|
11
|
+
* app1-api/src/services/INTERNAL/voice/controllers/createCdrEvents.js.
|
|
12
|
+
*
|
|
13
|
+
* POST /internal/voice/cdrEvents
|
|
14
|
+
* Body: {
|
|
15
|
+
* accountId,
|
|
16
|
+
* events: [{ cdrId, sipCallId?, eventType, ts, actorType, actorId?, data? }]
|
|
17
|
+
* } -- max 50 events per request (enforced by the api).
|
|
18
|
+
*/
|
|
19
|
+
async createCdrEvents({ accountId, events }) {
|
|
20
|
+
this.sdk.validateParams(
|
|
21
|
+
{ accountId, events },
|
|
22
|
+
{
|
|
23
|
+
accountId: { type: 'string', required: true },
|
|
24
|
+
events: { type: 'array', required: true },
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const params = {
|
|
29
|
+
body: { accountId, events },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const result = await this.sdk._fetch(
|
|
33
|
+
'/internal/voice/cdrEvents',
|
|
34
|
+
'POST',
|
|
35
|
+
params,
|
|
36
|
+
);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class InternalVoicemailService {
|
|
2
|
+
constructor(sdk) {
|
|
3
|
+
this.sdk = sdk;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* app1-transcription reports the result of a batch voicemail
|
|
8
|
+
* transcription here after POST /transcription/voicemail finishes
|
|
9
|
+
* (success or failure). Matches
|
|
10
|
+
* app1-api/src/services/INTERNAL/voicemail/controllers/transcriptionComplete.js.
|
|
11
|
+
*
|
|
12
|
+
* POST /internal/voicemail/transcription
|
|
13
|
+
* Body: { accountId, voicemailMessageId, transcription, transcriptionStatus }
|
|
14
|
+
*/
|
|
15
|
+
async transcriptionComplete({
|
|
16
|
+
accountId,
|
|
17
|
+
voicemailMessageId,
|
|
18
|
+
transcription,
|
|
19
|
+
transcriptionStatus,
|
|
20
|
+
}) {
|
|
21
|
+
this.sdk.validateParams(
|
|
22
|
+
{ accountId, voicemailMessageId, transcriptionStatus },
|
|
23
|
+
{
|
|
24
|
+
accountId: { type: 'string', required: true },
|
|
25
|
+
voicemailMessageId: { type: 'string', required: true },
|
|
26
|
+
transcription: { type: 'string', required: false },
|
|
27
|
+
transcriptionStatus: { type: 'string', required: true },
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const params = {
|
|
32
|
+
body: {
|
|
33
|
+
accountId,
|
|
34
|
+
voicemailMessageId,
|
|
35
|
+
transcription,
|
|
36
|
+
transcriptionStatus,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const result = await this.sdk._fetch(
|
|
41
|
+
'/internal/voicemail/transcription',
|
|
42
|
+
'POST',
|
|
43
|
+
params,
|
|
44
|
+
);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a spec-valid `objects.event.v2` envelope for non-api writers
|
|
3
|
+
* (task-router, cdr-user-status, ...) that mutate object-backed tables with
|
|
4
|
+
* raw SQL and need to emit live-update events without an HTTP round-trip
|
|
5
|
+
* through app1-api.
|
|
6
|
+
*
|
|
7
|
+
* Pure helper — no NATS/publish side effects. Callers publish the returned
|
|
8
|
+
* `{ subject, payload }` themselves via their own service's NATS client and
|
|
9
|
+
* subject-prefix handling.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} params
|
|
12
|
+
* @param {'create'|'update'|'delete'} params.action
|
|
13
|
+
* @param {string} params.objectName - plural object/table name, e.g. 'tasks'
|
|
14
|
+
* @param {string} params.accountId
|
|
15
|
+
* @param {string|number} params.recordTypeId
|
|
16
|
+
* @param {Object|null} [params.oldRow] - full pre-mutation row image; null for 'create'
|
|
17
|
+
* @param {Object|null} [params.newRow] - full post-mutation row image; null for 'delete'
|
|
18
|
+
* @param {number} [params.now] - ms timestamp used for rowVersion/ts; defaults to Date.now()
|
|
19
|
+
* @returns {{ subject: string, payload: Object }}
|
|
20
|
+
*/
|
|
21
|
+
export function buildObjectEventV2({
|
|
22
|
+
action,
|
|
23
|
+
objectName,
|
|
24
|
+
accountId,
|
|
25
|
+
recordTypeId,
|
|
26
|
+
oldRow = null,
|
|
27
|
+
newRow = null,
|
|
28
|
+
now,
|
|
29
|
+
}) {
|
|
30
|
+
const changedFields =
|
|
31
|
+
oldRow && newRow ? diffChangedFields(oldRow, newRow) : [];
|
|
32
|
+
|
|
33
|
+
const ts = now ?? Date.now();
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
subject: 'objects.event.v2',
|
|
37
|
+
payload: {
|
|
38
|
+
action,
|
|
39
|
+
objectName,
|
|
40
|
+
accountId,
|
|
41
|
+
recordTypeId,
|
|
42
|
+
oldRow,
|
|
43
|
+
newRow,
|
|
44
|
+
changedFields,
|
|
45
|
+
rowVersion: ts,
|
|
46
|
+
ts,
|
|
47
|
+
sourceCluster: process.env.CLUSTER_NAME || 'unknown',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function diffChangedFields(oldRow, newRow) {
|
|
53
|
+
const fields = new Set([...Object.keys(oldRow), ...Object.keys(newRow)]);
|
|
54
|
+
const changed = [];
|
|
55
|
+
for (const field of fields) {
|
|
56
|
+
if (!isEqualValue(oldRow[field], newRow[field])) {
|
|
57
|
+
changed.push(field);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return changed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isEqualValue(a, b) {
|
|
64
|
+
if (a === b) return true;
|
|
65
|
+
if (a == null || b == null) return a === b;
|
|
66
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|