@signalhousellc/sdk 1.0.51 → 1.0.53
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/package.json +11 -2
- package/src/SignalHouseSDK.js +5 -0
- package/src/domains/Auth.js +46 -0
- package/src/domains/Brands.js +54 -2
- package/src/domains/Campaigns.js +53 -2
- package/src/domains/Groups.js +24 -0
- package/src/domains/Numbers.js +39 -0
- package/src/domains/Voice.js +26 -0
- package/src/domains/voice/Calls.js +112 -0
- package/src/domains/voice/SipProfiles.js +191 -0
- package/src/domains/voice/SipTrunks.js +160 -0
- package/src/domains/voice/Tokens.js +64 -0
- package/src/domains/voice/browser/Call.js +266 -0
- package/src/domains/voice/browser/Device.js +226 -0
- package/src/domains/voice/browser/index.js +17 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} CreateSipProfileData
|
|
3
|
+
* @property {string} name - Profile name (required).
|
|
4
|
+
* @property {boolean} [recordingAllowed] - Permit call recording on this endpoint.
|
|
5
|
+
* @property {boolean} [transcriptionEnabled] - Permit transcription on calls to/from this endpoint.
|
|
6
|
+
* @property {boolean} [sentimentFlag] - Permit sentiment analysis on this endpoint.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} UpdateSipProfileData
|
|
11
|
+
* @property {string} [name] - Profile name.
|
|
12
|
+
* @property {string} [sipUsername] - SIP username override.
|
|
13
|
+
* @property {string} [password] - New password (also the way to "regenerate" — pass a fresh value).
|
|
14
|
+
* @property {"UDP"|"TCP"|"TLS"} [transport] - SIP transport.
|
|
15
|
+
* @property {boolean} [recordingAllowed] - Permit call recording.
|
|
16
|
+
* @property {boolean} [transcriptionEnabled] - Permit transcription.
|
|
17
|
+
* @property {boolean} [sentimentFlag] - Permit sentiment analysis.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* SIP Profile / endpoint management. A SIP profile is a single registerable
|
|
22
|
+
* UA — a desk phone, a softphone, a SIP-capable device — that registers to
|
|
23
|
+
* Signal House with a username and password. Distinct from a SIP trunk,
|
|
24
|
+
* which represents a peer-to-peer link to another PBX or carrier.
|
|
25
|
+
*
|
|
26
|
+
* Calls the voice-backend service mounted under /voice. Accessed via
|
|
27
|
+
* `sdk.voice.sipProfiles`.
|
|
28
|
+
*/
|
|
29
|
+
export class SipProfiles {
|
|
30
|
+
constructor(client, enableAdmin) {
|
|
31
|
+
this.client = client;
|
|
32
|
+
this.enableAdmin = enableAdmin;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* List all SIP profiles belonging to the current account.
|
|
37
|
+
* @async
|
|
38
|
+
* @roles api, admin, developer, billing, user
|
|
39
|
+
* @param {Object} [params] - Request parameters.
|
|
40
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
41
|
+
* @returns {Promise<Object>} `{ sipProfiles: [...] }`.
|
|
42
|
+
*/
|
|
43
|
+
async list({ options = {} } = {}) {
|
|
44
|
+
return this.client(`/voice/sip-profiles`, { method: "GET", ...options });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get a single SIP profile by ID. NOTE: response does NOT include the
|
|
49
|
+
* password — use `getPassword({ id })` to retrieve it.
|
|
50
|
+
* @async
|
|
51
|
+
* @roles api, admin, developer, billing, user
|
|
52
|
+
* @param {Object} params - Request parameters.
|
|
53
|
+
* @param {string} params.id - The SIP profile UUID.
|
|
54
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
55
|
+
* @throws {Error} If id is missing.
|
|
56
|
+
* @returns {Promise<Object>} `{ sipProfile: {...} }`.
|
|
57
|
+
*/
|
|
58
|
+
async get({ id, options = {} }) {
|
|
59
|
+
this.client._require({ id });
|
|
60
|
+
const safeId = encodeURIComponent(id);
|
|
61
|
+
return this.client(`/voice/sip-profiles/${safeId}`, { method: "GET", ...options });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the password for a SIP profile. The password is generated by the
|
|
66
|
+
* server on create and stored; this endpoint returns the stored value
|
|
67
|
+
* (it is NOT one-time-visible like the create response — calling getPassword
|
|
68
|
+
* later still returns the password as long as it has not been replaced).
|
|
69
|
+
* @async
|
|
70
|
+
* @roles api, admin, developer, billing, user
|
|
71
|
+
* @param {Object} params - Request parameters.
|
|
72
|
+
* @param {string} params.id - The SIP profile UUID.
|
|
73
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
74
|
+
* @throws {Error} If id is missing.
|
|
75
|
+
* @returns {Promise<Object>} `{ password: string|null }`.
|
|
76
|
+
*/
|
|
77
|
+
async getPassword({ id, options = {} }) {
|
|
78
|
+
this.client._require({ id });
|
|
79
|
+
const safeId = encodeURIComponent(id);
|
|
80
|
+
return this.client(`/voice/sip-profiles/${safeId}/password`, { method: "GET", ...options });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List valid SIP transports + their address/port per region from the DB.
|
|
85
|
+
* Used by edit-profile UI to populate transport selectors.
|
|
86
|
+
* @async
|
|
87
|
+
* @roles api, admin, developer, billing, user
|
|
88
|
+
* @param {Object} [params] - Request parameters.
|
|
89
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
90
|
+
* @returns {Promise<Object>} `{ transports: [{ transport, address, port }, ...] }`.
|
|
91
|
+
*/
|
|
92
|
+
async getTransports({ options = {} } = {}) {
|
|
93
|
+
return this.client(`/voice/sip-profiles/transports`, { method: "GET", ...options });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a new SIP profile. The server generates the SIP username and
|
|
98
|
+
* password; the password is returned on the create response at the top
|
|
99
|
+
* level (NOT nested inside `sipProfile`) and is the only chance to surface
|
|
100
|
+
* it without an explicit `getPassword` lookup.
|
|
101
|
+
* @async
|
|
102
|
+
* @roles api, admin, developer, billing, user
|
|
103
|
+
* @param {Object} params - Request parameters.
|
|
104
|
+
* @param {CreateSipProfileData} params.profileData - The SIP profile to create.
|
|
105
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
106
|
+
* @throws {Error} If profileData is missing.
|
|
107
|
+
* @returns {Promise<Object>} `{ sipProfile: {...}, password: string, sipServer: string, message: string }`.
|
|
108
|
+
*/
|
|
109
|
+
async create({ profileData, options = {} }) {
|
|
110
|
+
this.client._require({ profileData });
|
|
111
|
+
return this.client(`/voice/sip-profiles`, { method: "POST", body: profileData, ...options });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Update an existing SIP profile. To rotate the password, pass a new
|
|
116
|
+
* `password` value here — there is no dedicated regenerate endpoint.
|
|
117
|
+
* @async
|
|
118
|
+
* @roles api, admin, developer, billing, user
|
|
119
|
+
* @param {Object} params - Request parameters.
|
|
120
|
+
* @param {string} params.id - The SIP profile UUID.
|
|
121
|
+
* @param {UpdateSipProfileData} params.updateData - Fields to update.
|
|
122
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
123
|
+
* @throws {Error} If id or updateData is missing.
|
|
124
|
+
* @returns {Promise<Object>} `{ sipProfile: {...} }`.
|
|
125
|
+
*/
|
|
126
|
+
async update({ id, updateData, options = {} }) {
|
|
127
|
+
this.client._require({ id, updateData });
|
|
128
|
+
const safeId = encodeURIComponent(id);
|
|
129
|
+
return this.client(`/voice/sip-profiles/${safeId}`, { method: "PATCH", body: updateData, ...options });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete a SIP profile by ID. Unassigns any linked numbers and frees the
|
|
134
|
+
* extension as a side-effect.
|
|
135
|
+
* @async
|
|
136
|
+
* @roles api, admin, developer, billing, user
|
|
137
|
+
* @param {Object} params - Request parameters.
|
|
138
|
+
* @param {string} params.id - The SIP profile UUID.
|
|
139
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
140
|
+
* @throws {Error} If id is missing.
|
|
141
|
+
* @returns {Promise<Object>} `{ message: string }`.
|
|
142
|
+
*/
|
|
143
|
+
async delete({ id, options = {} }) {
|
|
144
|
+
this.client._require({ id });
|
|
145
|
+
const safeId = encodeURIComponent(id);
|
|
146
|
+
return this.client(`/voice/sip-profiles/${safeId}`, { method: "DELETE", ...options });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Assign a phone number to this SIP profile (routes inbound calls on that
|
|
151
|
+
* number to the endpoint).
|
|
152
|
+
* @async
|
|
153
|
+
* @roles api, admin, developer, billing, user
|
|
154
|
+
* @param {Object} params - Request parameters.
|
|
155
|
+
* @param {string} params.id - The SIP profile UUID.
|
|
156
|
+
* @param {string} params.phoneNumberId - The phone number UUID to assign.
|
|
157
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
158
|
+
* @throws {Error} If id or phoneNumberId is missing.
|
|
159
|
+
* @returns {Promise<Object>} `{ sipProfile: {...} }`.
|
|
160
|
+
*/
|
|
161
|
+
async assignNumber({ id, phoneNumberId, options = {} }) {
|
|
162
|
+
this.client._require({ id, phoneNumberId });
|
|
163
|
+
const safeId = encodeURIComponent(id);
|
|
164
|
+
return this.client(`/voice/sip-profiles/${safeId}/assign-number`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: { phoneNumberId },
|
|
167
|
+
...options,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Unassign a phone number from this SIP profile.
|
|
173
|
+
* @async
|
|
174
|
+
* @roles api, admin, developer, billing, user
|
|
175
|
+
* @param {Object} params - Request parameters.
|
|
176
|
+
* @param {string} params.id - The SIP profile UUID.
|
|
177
|
+
* @param {string} params.phoneNumberId - The phone number UUID to unassign.
|
|
178
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
179
|
+
* @throws {Error} If id or phoneNumberId is missing.
|
|
180
|
+
* @returns {Promise<Object>} `{ sipProfile: {...} }`.
|
|
181
|
+
*/
|
|
182
|
+
async unassignNumber({ id, phoneNumberId, options = {} }) {
|
|
183
|
+
this.client._require({ id, phoneNumberId });
|
|
184
|
+
const safeId = encodeURIComponent(id);
|
|
185
|
+
return this.client(`/voice/sip-profiles/${safeId}/unassign-number`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: { phoneNumberId },
|
|
188
|
+
...options,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} SipTrunkHost
|
|
3
|
+
* @property {string} host - IP address, hostname, FQDN, or CIDR.
|
|
4
|
+
* @property {string} [port] - Port number as a string (1025-65535). Destination hosts only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} CreateSipTrunkData
|
|
9
|
+
* @property {string} name - Trunk name (required, 1-255 chars).
|
|
10
|
+
* @property {string} region - SIP POP region the trunk lives in (required), e.g. "us-east-1".
|
|
11
|
+
* @property {"IP_AUTH"|"REGISTRATION"} connectionType - IP_AUTH authenticates by source-IP allow-list; REGISTRATION authenticates via SIP REGISTER with username/password.
|
|
12
|
+
* @property {string[]} [allowedIps] - Allowed source IPs (IP_AUTH only). Defaults to [].
|
|
13
|
+
* @property {"tcp"|"udp"|"tls"} [transport] - SIP transport protocol. Defaults to "tcp".
|
|
14
|
+
* @property {number} [maxSpendPerMinute] - Per-minute spend cap override for this trunk.
|
|
15
|
+
* @property {SipTrunkHost[]} [destinationHosts] - Where to send outbound SIP traffic. Defaults to [].
|
|
16
|
+
* @property {SipTrunkHost[]} [sourceHosts] - Allowed source hosts for inbound traffic. Defaults to [].
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} UpdateSipTrunkData
|
|
21
|
+
* @property {string} [name] - Trunk name (1-255 chars).
|
|
22
|
+
* @property {string} [region] - SIP POP region.
|
|
23
|
+
* @property {"IP_AUTH"|"REGISTRATION"} [connectionType] - Connection/auth type.
|
|
24
|
+
* @property {string[]} [allowedIps] - Allowed source IPs (IP_AUTH only).
|
|
25
|
+
* @property {"tcp"|"udp"|"tls"} [transport] - SIP transport protocol.
|
|
26
|
+
* @property {number} [maxSpendPerMinute] - Per-minute spend cap override.
|
|
27
|
+
* @property {SipTrunkHost[]} [destinationHosts] - Outbound SIP destinations.
|
|
28
|
+
* @property {SipTrunkHost[]} [sourceHosts] - Allowed inbound source hosts.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* SIP Trunk management. Calls the voice-backend service mounted under /voice.
|
|
33
|
+
* Accessed via `sdk.voice.sipTrunks`.
|
|
34
|
+
*/
|
|
35
|
+
export class SipTrunks {
|
|
36
|
+
constructor(client, enableAdmin) {
|
|
37
|
+
this.client = client;
|
|
38
|
+
this.enableAdmin = enableAdmin;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List all SIP trunks belonging to the current account.
|
|
43
|
+
* @async
|
|
44
|
+
* @roles api, admin, developer, billing, user
|
|
45
|
+
* @param {Object} [params] - Request parameters.
|
|
46
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
47
|
+
* @returns {Promise<Object>} The list of SIP trunks: `{ sipTrunks: [...] }`.
|
|
48
|
+
*/
|
|
49
|
+
async list({ options = {} } = {}) {
|
|
50
|
+
return this.client(`/voice/sip-trunks`, { method: "GET", ...options });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get a single SIP trunk by ID.
|
|
55
|
+
* @async
|
|
56
|
+
* @roles api, admin, developer, billing, user
|
|
57
|
+
* @param {Object} params - Request parameters.
|
|
58
|
+
* @param {string} params.id - The SIP trunk UUID.
|
|
59
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
60
|
+
* @throws {Error} If id is missing.
|
|
61
|
+
* @returns {Promise<Object>} The SIP trunk: `{ sipTrunk: {...} }`.
|
|
62
|
+
*/
|
|
63
|
+
async get({ id, options = {} }) {
|
|
64
|
+
this.client._require({ id });
|
|
65
|
+
const safeId = encodeURIComponent(id);
|
|
66
|
+
return this.client(`/voice/sip-trunks/${safeId}`, { method: "GET", ...options });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a new SIP trunk.
|
|
71
|
+
* @async
|
|
72
|
+
* @roles api, admin, developer, billing, user
|
|
73
|
+
* @param {Object} params - Request parameters.
|
|
74
|
+
* @param {CreateSipTrunkData} params.trunkData - The SIP trunk to create.
|
|
75
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
76
|
+
* @throws {Error} If trunkData is missing.
|
|
77
|
+
* @returns {Promise<Object>} The created SIP trunk: `{ sipTrunk: {...} }`.
|
|
78
|
+
*/
|
|
79
|
+
async create({ trunkData, options = {} }) {
|
|
80
|
+
this.client._require({ trunkData });
|
|
81
|
+
return this.client(`/voice/sip-trunks`, { method: "POST", body: trunkData, ...options });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update an existing SIP trunk.
|
|
86
|
+
* @async
|
|
87
|
+
* @roles api, admin, developer, billing, user
|
|
88
|
+
* @param {Object} params - Request parameters.
|
|
89
|
+
* @param {string} params.id - The SIP trunk UUID.
|
|
90
|
+
* @param {UpdateSipTrunkData} params.updateData - Fields to update.
|
|
91
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
92
|
+
* @throws {Error} If id or updateData is missing.
|
|
93
|
+
* @returns {Promise<Object>} The updated SIP trunk: `{ sipTrunk: {...} }`.
|
|
94
|
+
*/
|
|
95
|
+
async update({ id, updateData, options = {} }) {
|
|
96
|
+
this.client._require({ id, updateData });
|
|
97
|
+
const safeId = encodeURIComponent(id);
|
|
98
|
+
return this.client(`/voice/sip-trunks/${safeId}`, { method: "PATCH", body: updateData, ...options });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Delete a SIP trunk by ID.
|
|
103
|
+
* @async
|
|
104
|
+
* @roles api, admin, developer, billing, user
|
|
105
|
+
* @param {Object} params - Request parameters.
|
|
106
|
+
* @param {string} params.id - The SIP trunk UUID.
|
|
107
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
108
|
+
* @throws {Error} If id is missing.
|
|
109
|
+
* @returns {Promise<Object>} `{ success: boolean, message: string }`.
|
|
110
|
+
*/
|
|
111
|
+
async delete({ id, options = {} }) {
|
|
112
|
+
this.client._require({ id });
|
|
113
|
+
const safeId = encodeURIComponent(id);
|
|
114
|
+
return this.client(`/voice/sip-trunks/${safeId}`, { method: "DELETE", ...options });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Toggle a SIP trunk's active/inactive status.
|
|
119
|
+
* @async
|
|
120
|
+
* @roles api, admin, developer, billing, user
|
|
121
|
+
* @param {Object} params - Request parameters.
|
|
122
|
+
* @param {string} params.id - The SIP trunk UUID.
|
|
123
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
124
|
+
* @throws {Error} If id is missing.
|
|
125
|
+
* @returns {Promise<Object>} The updated SIP trunk: `{ sipTrunk: {...} }`.
|
|
126
|
+
*/
|
|
127
|
+
async toggleActive({ id, options = {} }) {
|
|
128
|
+
this.client._require({ id });
|
|
129
|
+
const safeId = encodeURIComponent(id);
|
|
130
|
+
return this.client(`/voice/sip-trunks/${safeId}/toggle-active`, { method: "POST", ...options });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Regenerate the SIP password for a REGISTRATION-type trunk.
|
|
135
|
+
* @async
|
|
136
|
+
* @roles api, admin, developer, billing, user
|
|
137
|
+
* @param {Object} params - Request parameters.
|
|
138
|
+
* @param {string} params.id - The SIP trunk UUID.
|
|
139
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
140
|
+
* @throws {Error} If id is missing.
|
|
141
|
+
* @returns {Promise<Object>} `{ password: string }`.
|
|
142
|
+
*/
|
|
143
|
+
async regeneratePassword({ id, options = {} }) {
|
|
144
|
+
this.client._require({ id });
|
|
145
|
+
const safeId = encodeURIComponent(id);
|
|
146
|
+
return this.client(`/voice/sip-trunks/${safeId}/regenerate-password`, { method: "POST", ...options });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List available SIP Points of Presence (POPs) and their domains/ports.
|
|
151
|
+
* @async
|
|
152
|
+
* @roles api, admin, developer, billing, user
|
|
153
|
+
* @param {Object} [params] - Request parameters.
|
|
154
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
155
|
+
* @returns {Promise<Object>} `{ sipPops: [...] }`.
|
|
156
|
+
*/
|
|
157
|
+
async getPops({ options = {} } = {}) {
|
|
158
|
+
return this.client(`/voice/sip-trunks/pops`, { method: "GET", ...options });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} VoiceGrants
|
|
3
|
+
* @property {Object} [voice] - Voice grants object.
|
|
4
|
+
* @property {Object} [voice.incoming] - Inbound call grants.
|
|
5
|
+
* @property {boolean} [voice.incoming.allow] - Allow inbound calls to this identity.
|
|
6
|
+
* @property {Object} [voice.outgoing] - Outbound call grants.
|
|
7
|
+
* @property {string[]} [voice.outgoing.allowedNumbers] - E.164 numbers this identity may present as caller ID.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} CreateTokenData
|
|
12
|
+
* @property {string} [identity] - Customer-facing logical identity name (e.g. "alice"). Persisted as `requested_identity` so server-initiated `calls.create({to_identity: "alice"})` can resolve it later. Defaults to "anonymous".
|
|
13
|
+
* @property {number} [ttl] - Token TTL in seconds. Clamped to the server's max. Defaults to VOICE_TOKEN_DEFAULT_TTL_SEC.
|
|
14
|
+
* @property {VoiceGrants} [grants] - Capability grants attached to the token.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} SipCredentials
|
|
19
|
+
* @property {string} username - Server-generated SIP username (e.g. "wrtc_<hex>"). Use for the SIP-over-WSS registration.
|
|
20
|
+
* @property {string} password - Plaintext SIP password — sent ONCE, never recoverable. Use for digest auth on REGISTER + INVITE.
|
|
21
|
+
* @property {string} domain - SIP domain to register against (the part after `@` in the URI).
|
|
22
|
+
* @property {string} wss_url - WebSocket Secure URL the SDK opens to send SIP traffic.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} CreateTokenResponse
|
|
27
|
+
* @property {string} token - JWT to pass to the SignalHouse browser voice SDK.
|
|
28
|
+
* @property {string} identity - The server-generated SIP username (wrtc_<hex>). Distinct from the logical `requested_identity` passed in.
|
|
29
|
+
* @property {string} expires_at - ISO-8601 expiry timestamp.
|
|
30
|
+
* @property {SipCredentials} sip_credentials - Ephemeral SIP creds embedded in the token; surfaced separately for direct SIP clients.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* WebRTC voice tokens. Mints ephemeral SIP identities the browser/mobile voice
|
|
35
|
+
* SDK uses to register against the SignalHouse voice fabric. Wraps voice-
|
|
36
|
+
* backend's `/voice/tokens` endpoint. Accessed via `sdk.voice.tokens`.
|
|
37
|
+
*
|
|
38
|
+
* Customer flow:
|
|
39
|
+
* 1. Backend calls `sdk.voice.tokens.create({ identity: "alice", ttl: 1800 })`
|
|
40
|
+
* and returns the resulting `token` to the customer's browser/app.
|
|
41
|
+
* 2. Browser SDK registers using the embedded `sip_credentials`.
|
|
42
|
+
* 3. Outbound calls from the SDK go through OpenSIPS → Asterisk → carrier;
|
|
43
|
+
* the server-side `calls.create({ to_identity: "alice", ... })` flow can
|
|
44
|
+
* also ring this identity for click-to-call patterns.
|
|
45
|
+
*/
|
|
46
|
+
export class Tokens {
|
|
47
|
+
constructor(client, enableAdmin) {
|
|
48
|
+
this.client = client;
|
|
49
|
+
this.enableAdmin = enableAdmin;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mint an ephemeral voice token + SIP credentials.
|
|
54
|
+
* @async
|
|
55
|
+
* @roles api, admin, developer, billing, user
|
|
56
|
+
* @param {Object} [params] - Request parameters.
|
|
57
|
+
* @param {CreateTokenData} [params.tokenData] - Token mint options (identity, ttl, grants). All fields optional.
|
|
58
|
+
* @param {import('../../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request.
|
|
59
|
+
* @returns {Promise<CreateTokenResponse>} The token + embedded SIP credentials.
|
|
60
|
+
*/
|
|
61
|
+
async create({ tokenData = {}, options = {} } = {}) {
|
|
62
|
+
return this.client(`/voice/tokens`, { method: "POST", body: tokenData, ...options });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {"inbound"|"outbound"} CallDirection
|
|
3
|
+
* @typedef {"initial"|"ringing"|"in_progress"|"ended"|"failed"} CallStatus
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wraps a single SIP session — outbound or inbound — and exposes a
|
|
8
|
+
* Twilio-shape Call API on top of the underlying JsSIP RTCSession.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle (outbound):
|
|
11
|
+
* new (status="initial") → ringing → in_progress → ended | failed
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle (inbound, before .accept()):
|
|
14
|
+
* new (status="ringing") → in_progress (on accept) → ended | failed
|
|
15
|
+
*
|
|
16
|
+
* Custom SIP headers from inbound INVITEs are exposed on the instance
|
|
17
|
+
* (notably `initiatorId` from `X-SignalHouse-Initiator`) so the host app
|
|
18
|
+
* can correlate a server-initiated callback to its triggering REST call.
|
|
19
|
+
*
|
|
20
|
+
* Methods reject when the session is no longer in a state that supports
|
|
21
|
+
* them (e.g., `.accept()` on an already-accepted call).
|
|
22
|
+
*/
|
|
23
|
+
export class Call {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} init - Initializer.
|
|
26
|
+
* @param {Object} init.session - The JsSIP RTCSession.
|
|
27
|
+
* @param {CallDirection} init.direction
|
|
28
|
+
* @param {Object} [init.request] - Inbound INVITE request, when direction === "inbound".
|
|
29
|
+
*/
|
|
30
|
+
constructor({ session, direction, request }) {
|
|
31
|
+
this._session = session;
|
|
32
|
+
this._listeners = new Map();
|
|
33
|
+
this._remoteAudio = null;
|
|
34
|
+
|
|
35
|
+
this.direction = direction;
|
|
36
|
+
this.id = session.id || session._request?.call_id || null;
|
|
37
|
+
this.status = direction === "inbound" ? "ringing" : "initial";
|
|
38
|
+
|
|
39
|
+
// From / To URIs. For inbound the session._request carries them; for
|
|
40
|
+
// outbound JsSIP populates from session.remote_identity / local_identity
|
|
41
|
+
// once the dialog is established.
|
|
42
|
+
this.from = session.remote_identity?.uri?.toString?.() ?? request?.from?.uri?.toString?.() ?? null;
|
|
43
|
+
this.to = session.local_identity?.uri?.toString?.() ?? request?.to?.uri?.toString?.() ?? null;
|
|
44
|
+
|
|
45
|
+
// Surface useful inbound headers. Most important: X-SignalHouse-Initiator,
|
|
46
|
+
// which carries the call_id of the REST POST /v1/calls that triggered a
|
|
47
|
+
// server-side two-leg-with-bridge ring. Host app can compare against its
|
|
48
|
+
// pending dial set to decide whether to auto-accept.
|
|
49
|
+
this.headers = {};
|
|
50
|
+
if (request && typeof request.getHeader === "function") {
|
|
51
|
+
for (const name of [
|
|
52
|
+
"X-SignalHouse-Initiator",
|
|
53
|
+
"X-Call-UUID",
|
|
54
|
+
"X-Call-Type",
|
|
55
|
+
"X-Route-Type",
|
|
56
|
+
"X-Dest",
|
|
57
|
+
]) {
|
|
58
|
+
const v = request.getHeader(name);
|
|
59
|
+
if (v) this.headers[name] = v;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.initiatorId = this.headers["X-SignalHouse-Initiator"] || null;
|
|
63
|
+
this.callUuid = this.headers["X-Call-UUID"] || null;
|
|
64
|
+
|
|
65
|
+
this._wireSessionEvents();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Public API ─────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Accept an inbound call. No-op (warning) if outbound or already accepted.
|
|
72
|
+
* @param {Object} [opts]
|
|
73
|
+
* @param {MediaStream} [opts.mediaStream] - Pre-acquired stream from getUserMedia.
|
|
74
|
+
* If omitted, the SDK calls getUserMedia({audio: true}) and uses the result.
|
|
75
|
+
* @param {RTCConfiguration} [opts.pcConfig] - WebRTC peer-connection config.
|
|
76
|
+
* Falls back to {iceServers: [{urls: "stun:stun.l.google.com:19302"}]}.
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async accept({ mediaStream, pcConfig } = {}) {
|
|
80
|
+
if (this.direction !== "inbound") {
|
|
81
|
+
console.warn("[Call.accept] no-op: only inbound calls can be accepted");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (this.status !== "ringing") {
|
|
85
|
+
console.warn(`[Call.accept] no-op: call is not ringing (status=${this.status})`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const stream = mediaStream || (await navigator.mediaDevices.getUserMedia({ audio: true, video: false }));
|
|
89
|
+
this._session.answer({
|
|
90
|
+
mediaConstraints: { audio: true, video: false },
|
|
91
|
+
mediaStream: stream,
|
|
92
|
+
// Default: no STUN (rtpengine trust-address handles NAT). Override via pcConfig.
|
|
93
|
+
pcConfig: pcConfig || { iceServers: [] },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reject an inbound call with 486 Busy (or supplied SIP code).
|
|
99
|
+
* @param {Object} [opts]
|
|
100
|
+
* @param {number} [opts.statusCode=486]
|
|
101
|
+
* @param {string} [opts.reason="Busy Here"]
|
|
102
|
+
*/
|
|
103
|
+
reject({ statusCode = 486, reason = "Busy Here" } = {}) {
|
|
104
|
+
if (this.direction !== "inbound" || this.status !== "ringing") {
|
|
105
|
+
console.warn("[Call.reject] no-op: only ringing inbound calls can be rejected");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
this._session.terminate({ status_code: statusCode, reason_phrase: reason });
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// JsSIP throws if the session is already gone — non-fatal here
|
|
112
|
+
console.warn(`[Call.reject] terminate threw: ${err?.message || err}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Hang up an in-progress or ringing outbound call. Idempotent.
|
|
118
|
+
*/
|
|
119
|
+
hangup() {
|
|
120
|
+
if (this.status === "ended" || this.status === "failed") return;
|
|
121
|
+
try {
|
|
122
|
+
this._session.terminate();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.warn(`[Call.hangup] terminate threw: ${err?.message || err}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Send DTMF digits during an in-progress call. Accepts 0-9, *, #, w (wait).
|
|
130
|
+
* @param {string} digits
|
|
131
|
+
*/
|
|
132
|
+
sendDigits(digits) {
|
|
133
|
+
if (this.status !== "in_progress") {
|
|
134
|
+
console.warn(`[Call.sendDigits] no-op: call is not in_progress (status=${this.status})`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
this._session.sendDTMF(String(digits));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.warn(`[Call.sendDigits] sendDTMF threw: ${err?.message || err}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Attach an event listener. Events:
|
|
146
|
+
* "accepted" — 200 OK received (outbound) or sent (inbound after accept)
|
|
147
|
+
* "confirmed" — ACK exchanged, media flowing
|
|
148
|
+
* "ended" — normal hangup; payload: {cause}
|
|
149
|
+
* "failed" — error before/during; payload: {cause, status_code, reason}
|
|
150
|
+
* "track" — remote MediaStreamTrack added; payload: {track, streams}
|
|
151
|
+
* @param {string} event
|
|
152
|
+
* @param {Function} handler
|
|
153
|
+
*/
|
|
154
|
+
on(event, handler) {
|
|
155
|
+
if (!this._listeners.has(event)) this._listeners.set(event, new Set());
|
|
156
|
+
this._listeners.get(event).add(handler);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Remove a previously-registered listener.
|
|
161
|
+
*/
|
|
162
|
+
off(event, handler) {
|
|
163
|
+
this._listeners.get(event)?.delete(handler);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Internal: wire JsSIP RTCSession events to Call events ───────────────
|
|
167
|
+
|
|
168
|
+
_emit(event, payload) {
|
|
169
|
+
for (const fn of this._listeners.get(event) || []) {
|
|
170
|
+
try { fn(payload); }
|
|
171
|
+
catch (err) { console.error(`[Call.${event} handler]`, err); }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_wireSessionEvents() {
|
|
176
|
+
const s = this._session;
|
|
177
|
+
|
|
178
|
+
// Attach the RTCPeerConnection "track" listener as soon as the PC
|
|
179
|
+
// exists. The browser can fire `track` when the remote SDP is applied,
|
|
180
|
+
// which happens BEFORE JsSIP emits "confirmed" — waiting until then
|
|
181
|
+
// can drop the event entirely (call connects, no audio).
|
|
182
|
+
//
|
|
183
|
+
// For outbound calls JsSIP creates the PC inside `ua.call()`, so
|
|
184
|
+
// session.connection may already exist by the time our Call is
|
|
185
|
+
// constructed — handle that synchronously. For inbound calls the PC
|
|
186
|
+
// is created inside session.answer(); JsSIP emits "peerconnection"
|
|
187
|
+
// when it's ready — attach there.
|
|
188
|
+
if (s.connection) this._attachTrackListener(s.connection);
|
|
189
|
+
s.on("peerconnection", (e) => {
|
|
190
|
+
const pc = e?.peerconnection || s.connection;
|
|
191
|
+
if (pc) this._attachTrackListener(pc);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
s.on("progress", (e) => {
|
|
195
|
+
this.status = "ringing";
|
|
196
|
+
this._emit("progress", { response: e.response });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
s.on("accepted", () => {
|
|
200
|
+
this.status = "in_progress";
|
|
201
|
+
this._emit("accepted");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
s.on("confirmed", () => {
|
|
205
|
+
this.status = "in_progress";
|
|
206
|
+
// Defensive: in case "peerconnection" never fired but a PC exists by now.
|
|
207
|
+
if (this._session.connection) this._attachTrackListener(this._session.connection);
|
|
208
|
+
this._emit("confirmed");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
s.on("ended", (e) => {
|
|
212
|
+
this.status = "ended";
|
|
213
|
+
this._cleanupRemoteAudio();
|
|
214
|
+
this._emit("ended", { cause: e?.cause });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
s.on("failed", (e) => {
|
|
218
|
+
this.status = "failed";
|
|
219
|
+
this._cleanupRemoteAudio();
|
|
220
|
+
this._emit("failed", {
|
|
221
|
+
cause: e?.cause,
|
|
222
|
+
statusCode: e?.message?.status_code,
|
|
223
|
+
reason: e?.message?.reason_phrase,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Idempotent — wires the RTCPeerConnection "track" handler. Uses
|
|
230
|
+
* addEventListener (not pc.ontrack) so host apps can attach their own
|
|
231
|
+
* track listener without clobbering ours, and vice versa.
|
|
232
|
+
*
|
|
233
|
+
* When a remote track arrives:
|
|
234
|
+
* - Emit "track" so the host app can route audio anywhere it wants.
|
|
235
|
+
* - If `call.disableDefaultAudioOutput` is falsy, attach the stream to
|
|
236
|
+
* a hidden <audio> element so the user just hears the call. Host
|
|
237
|
+
* apps that want custom audio routing should set
|
|
238
|
+
* `call.disableDefaultAudioOutput = true` before accepting/connecting.
|
|
239
|
+
*/
|
|
240
|
+
_attachTrackListener(pc) {
|
|
241
|
+
if (this._trackListenerAttached) return;
|
|
242
|
+
this._trackListenerAttached = true;
|
|
243
|
+
|
|
244
|
+
pc.addEventListener("track", (evt) => {
|
|
245
|
+
this._emit("track", { track: evt.track, streams: evt.streams });
|
|
246
|
+
if (this.disableDefaultAudioOutput) return;
|
|
247
|
+
if (!this._remoteAudio) {
|
|
248
|
+
this._remoteAudio = document.createElement("audio");
|
|
249
|
+
this._remoteAudio.autoplay = true;
|
|
250
|
+
this._remoteAudio.setAttribute("data-signalhouse-call", this.id || "");
|
|
251
|
+
document.body.appendChild(this._remoteAudio);
|
|
252
|
+
}
|
|
253
|
+
this._remoteAudio.srcObject = evt.streams[0];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_cleanupRemoteAudio() {
|
|
258
|
+
if (this._remoteAudio) {
|
|
259
|
+
try {
|
|
260
|
+
this._remoteAudio.srcObject = null;
|
|
261
|
+
this._remoteAudio.remove();
|
|
262
|
+
} catch {}
|
|
263
|
+
this._remoteAudio = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|