@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 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.1.0",
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
+ }