sipgate-mcp 0.3.0 → 0.5.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/CHANGELOG.md +137 -0
- package/README.md +161 -13
- package/SKILL.md +58 -8
- package/dist/backend/access-controlled-backend.d.ts +152 -1
- package/dist/backend/access-controlled-backend.d.ts.map +1 -1
- package/dist/backend/access-controlled-backend.js +1213 -25
- package/dist/backend/access-controlled-backend.js.map +1 -1
- package/dist/backend/sipgate-backend.d.ts +149 -1
- package/dist/backend/sipgate-backend.d.ts.map +1 -1
- package/dist/backend/sipgate-backend.js +1299 -12
- package/dist/backend/sipgate-backend.js.map +1 -1
- package/dist/backend/sipgate-client.d.ts +3 -0
- package/dist/backend/sipgate-client.d.ts.map +1 -1
- package/dist/backend/sipgate-client.js +22 -12
- package/dist/backend/sipgate-client.js.map +1 -1
- package/dist/backend/telephony-backend.d.ts +301 -0
- package/dist/backend/telephony-backend.d.ts.map +1 -1
- package/dist/credentials.d.ts +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +11 -5
- package/dist/credentials.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +13 -2
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +72 -17
- package/dist/setup.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +1455 -60
- package/dist/tools/definitions.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -16,15 +16,65 @@ function stringField(value, key) {
|
|
|
16
16
|
const field = value[key];
|
|
17
17
|
return typeof field === "string" ? field : undefined;
|
|
18
18
|
}
|
|
19
|
+
function stringOrNumberField(value, key) {
|
|
20
|
+
const field = value[key];
|
|
21
|
+
return typeof field === "string" || typeof field === "number" ? String(field) : undefined;
|
|
22
|
+
}
|
|
23
|
+
function addressIds(value) {
|
|
24
|
+
const direct = ["addressId", "emergencyAddressId"]
|
|
25
|
+
.map((key) => stringOrNumberField(value, key))
|
|
26
|
+
.filter((id) => Boolean(id));
|
|
27
|
+
const addressUrl = stringField(value, "addressUrl");
|
|
28
|
+
const fromUrl = addressUrl?.match(/\/addresses\/(\d+)(?:\/|$)/)?.[1];
|
|
29
|
+
return fromUrl ? [...direct, fromUrl] : direct;
|
|
30
|
+
}
|
|
19
31
|
function items(value) {
|
|
20
32
|
return objectArray(asObject(value)?.items);
|
|
21
33
|
}
|
|
34
|
+
function calls(value) {
|
|
35
|
+
return objectArray(asObject(value)?.data);
|
|
36
|
+
}
|
|
37
|
+
function nestedNotificationIds(value) {
|
|
38
|
+
const found = new Set();
|
|
39
|
+
const response = asObject(value);
|
|
40
|
+
if (!response)
|
|
41
|
+
return found;
|
|
42
|
+
for (const category of ["call", "fax", "sms", "voicemail"]) {
|
|
43
|
+
for (const endpoint of objectArray(response[category])) {
|
|
44
|
+
for (const targetType of ["emails", "sms", "reports"]) {
|
|
45
|
+
for (const target of objectArray(endpoint[targetType])) {
|
|
46
|
+
const notificationId = stringField(target, "id");
|
|
47
|
+
if (notificationId)
|
|
48
|
+
found.add(notificationId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
22
55
|
function withAccessScope(value, context, scope) {
|
|
23
56
|
const object = asObject(value);
|
|
24
57
|
return object
|
|
25
58
|
? { ...object, accessScope: scope }
|
|
26
59
|
: { value, authenticatedUser: context.identity, accessScope: scope };
|
|
27
60
|
}
|
|
61
|
+
function unavailablePhonelineRead(items = false) {
|
|
62
|
+
return {
|
|
63
|
+
...(items ? { items: [] } : {}),
|
|
64
|
+
phonelinesAvailable: false,
|
|
65
|
+
note: "This sipgate account does not provide the phoneline feature.",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function unavailablePhonelineMutation() {
|
|
69
|
+
return {
|
|
70
|
+
before: null,
|
|
71
|
+
after: {
|
|
72
|
+
changed: false,
|
|
73
|
+
phonelinesAvailable: false,
|
|
74
|
+
note: "This sipgate account does not provide the phoneline feature; no change was attempted.",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
28
78
|
/**
|
|
29
79
|
* Enforces MCP-level resource boundaries in addition to sipgate's own role and
|
|
30
80
|
* token-scope checks. Account scope is rejected unless sipgate identifies the
|
|
@@ -70,16 +120,215 @@ export class AccessControlledBackend {
|
|
|
70
120
|
this.assertUser(userId);
|
|
71
121
|
return this.delegate.listUserNumbers(userId, pagination);
|
|
72
122
|
}
|
|
123
|
+
getUserNumbers(userId) {
|
|
124
|
+
this.assertUser(userId);
|
|
125
|
+
return this.delegate.getUserNumbers(userId);
|
|
126
|
+
}
|
|
73
127
|
listPhonelines(userId) {
|
|
74
128
|
this.assertUser(userId);
|
|
75
129
|
return this.delegate.listPhonelines(userId);
|
|
76
130
|
}
|
|
131
|
+
async getPhoneline(userId, phonelineId) {
|
|
132
|
+
this.assertUser(userId);
|
|
133
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
134
|
+
return unavailablePhonelineRead();
|
|
135
|
+
}
|
|
136
|
+
return this.delegate.getPhoneline(userId, phonelineId);
|
|
137
|
+
}
|
|
138
|
+
async getPhonelineBlockAnonymous(userId, phonelineId) {
|
|
139
|
+
this.assertUser(userId);
|
|
140
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
141
|
+
return unavailablePhonelineRead();
|
|
142
|
+
}
|
|
143
|
+
return this.delegate.getPhonelineBlockAnonymous(userId, phonelineId);
|
|
144
|
+
}
|
|
145
|
+
async listPhonelineDevices(userId, phonelineId) {
|
|
146
|
+
this.assertUser(userId);
|
|
147
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
148
|
+
return unavailablePhonelineRead(true);
|
|
149
|
+
}
|
|
150
|
+
const response = await this.delegate.listPhonelineDevices(userId, phonelineId);
|
|
151
|
+
if (this.scope === "account" || asObject(response)?.phonelinesAvailable === false) {
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const deviceIds = await this.ownedDeviceIds();
|
|
156
|
+
return {
|
|
157
|
+
...asObject(response),
|
|
158
|
+
items: items(response).filter((device) => {
|
|
159
|
+
const deviceId = stringField(device, "id");
|
|
160
|
+
return deviceId !== undefined && deviceIds.has(deviceId);
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new AccessPolicyError("User scope could not establish ownership of the phoneline's attached devices.");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async listParallelForwardings(userId, phonelineId) {
|
|
169
|
+
this.assertUser(userId);
|
|
170
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
171
|
+
return unavailablePhonelineRead(true);
|
|
172
|
+
}
|
|
173
|
+
return this.delegate.listParallelForwardings(userId, phonelineId);
|
|
174
|
+
}
|
|
175
|
+
async listPhonelineVoicemails(userId, phonelineId) {
|
|
176
|
+
this.assertUser(userId);
|
|
177
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
178
|
+
return unavailablePhonelineRead(true);
|
|
179
|
+
}
|
|
180
|
+
return this.delegate.listPhonelineVoicemails(userId, phonelineId);
|
|
181
|
+
}
|
|
182
|
+
async listVoicemailGreetings(userId, phonelineId, voicemailId) {
|
|
183
|
+
this.assertUser(userId);
|
|
184
|
+
if (this.scope === "user" && !await this.assertOwnedVoicemail(phonelineId, voicemailId)) {
|
|
185
|
+
return unavailablePhonelineRead(true);
|
|
186
|
+
}
|
|
187
|
+
return this.delegate.listVoicemailGreetings(userId, phonelineId, voicemailId);
|
|
188
|
+
}
|
|
189
|
+
async listVoicemails() {
|
|
190
|
+
if (this.scope === "account")
|
|
191
|
+
return this.delegate.listVoicemails();
|
|
192
|
+
const owned = await this.ownedVoicemailState();
|
|
193
|
+
if (!owned.available)
|
|
194
|
+
return unavailablePhonelineRead(true);
|
|
195
|
+
const response = await this.delegate.listVoicemails();
|
|
196
|
+
return {
|
|
197
|
+
...asObject(response),
|
|
198
|
+
items: items(response).filter((voicemail) => {
|
|
199
|
+
const voicemailId = stringField(voicemail, "id");
|
|
200
|
+
return voicemailId !== undefined && owned.ids.has(voicemailId);
|
|
201
|
+
}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async getVoicemail(voicemailId) {
|
|
205
|
+
if (this.scope === "user" && !await this.assertOwnedVoicemailId(voicemailId)) {
|
|
206
|
+
return unavailablePhonelineRead();
|
|
207
|
+
}
|
|
208
|
+
return this.delegate.getVoicemail(voicemailId);
|
|
209
|
+
}
|
|
210
|
+
listAutorecordingGreetings() {
|
|
211
|
+
if (this.scope === "user") {
|
|
212
|
+
throw new AccessPolicyError("User scope cannot access the account-wide automated-recording greeting because it has no user ownership relationship. Use administrator account scope.");
|
|
213
|
+
}
|
|
214
|
+
return this.delegate.listAutorecordingGreetings();
|
|
215
|
+
}
|
|
216
|
+
async getAutorecordingSettings(extension) {
|
|
217
|
+
if (this.scope === "user")
|
|
218
|
+
await this.assertOwnedAutorecordingExtension(extension);
|
|
219
|
+
return this.delegate.getAutorecordingSettings(extension);
|
|
220
|
+
}
|
|
77
221
|
listDevices(userId, types) {
|
|
78
222
|
if (this.scope === "account")
|
|
79
223
|
return this.delegate.listDevices(userId, types);
|
|
80
224
|
this.assertUser(userId);
|
|
81
225
|
return this.delegate.listDevices(this.context.userId, types);
|
|
82
226
|
}
|
|
227
|
+
async getDevice(deviceId) {
|
|
228
|
+
if (this.scope === "user")
|
|
229
|
+
await this.assertOwnedDevice(deviceId);
|
|
230
|
+
return this.delegate.getDevice(deviceId);
|
|
231
|
+
}
|
|
232
|
+
async getDeviceCallerId(deviceId) {
|
|
233
|
+
if (this.scope === "user")
|
|
234
|
+
await this.assertOwnedDevice(deviceId);
|
|
235
|
+
return this.delegate.getDeviceCallerId(deviceId);
|
|
236
|
+
}
|
|
237
|
+
async getDeviceLocalPrefix(deviceId) {
|
|
238
|
+
if (this.scope === "user")
|
|
239
|
+
await this.assertOwnedDevice(deviceId);
|
|
240
|
+
return this.delegate.getDeviceLocalPrefix(deviceId);
|
|
241
|
+
}
|
|
242
|
+
async getDeviceTariffAnnouncement(deviceId) {
|
|
243
|
+
if (this.scope === "user")
|
|
244
|
+
await this.assertOwnedDevice(deviceId);
|
|
245
|
+
return this.delegate.getDeviceTariffAnnouncement(deviceId);
|
|
246
|
+
}
|
|
247
|
+
async getDeviceSingleRowDisplay(deviceId) {
|
|
248
|
+
if (this.scope === "user")
|
|
249
|
+
await this.assertOwnedDevice(deviceId);
|
|
250
|
+
return this.delegate.getDeviceSingleRowDisplay(deviceId);
|
|
251
|
+
}
|
|
252
|
+
async getDeviceContingents(userId, deviceId) {
|
|
253
|
+
if (this.scope === "user") {
|
|
254
|
+
this.assertUser(userId);
|
|
255
|
+
await this.assertOwnedDevice(deviceId);
|
|
256
|
+
}
|
|
257
|
+
return this.delegate.getDeviceContingents(userId, deviceId);
|
|
258
|
+
}
|
|
259
|
+
async listAddresses() {
|
|
260
|
+
const response = await this.delegate.listAddresses();
|
|
261
|
+
if (this.scope === "account")
|
|
262
|
+
return response;
|
|
263
|
+
const candidates = items(response);
|
|
264
|
+
try {
|
|
265
|
+
const candidateIds = candidates
|
|
266
|
+
.map((address) => stringOrNumberField(address, "addressId"))
|
|
267
|
+
.filter((value) => Boolean(value && /^\d+$/.test(value)))
|
|
268
|
+
.map(Number);
|
|
269
|
+
const ownedIds = await this.ownedAddressIdsFor(candidateIds);
|
|
270
|
+
return {
|
|
271
|
+
items: candidates.filter((address) => {
|
|
272
|
+
const value = stringOrNumberField(address, "addressId");
|
|
273
|
+
return value !== undefined && ownedIds.has(value);
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
throw new AccessPolicyError("User scope could not establish ownership of the account's addresses.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async getAddress(addressId) {
|
|
282
|
+
if (this.scope === "user")
|
|
283
|
+
await this.assertOwnedAddress(addressId);
|
|
284
|
+
return this.delegate.getAddress(addressId);
|
|
285
|
+
}
|
|
286
|
+
async listAddressNumbers(addressId) {
|
|
287
|
+
if (this.scope === "account")
|
|
288
|
+
return this.delegate.listAddressNumbers(addressId);
|
|
289
|
+
await this.assertOwnedAddress(addressId);
|
|
290
|
+
// An address can be shared with numbers belonging to other users. Ownership
|
|
291
|
+
// of one number must not expose the rest of them.
|
|
292
|
+
const numbers = await this.ownedNumbers();
|
|
293
|
+
const response = await this.delegate.listAddressNumbers(addressId);
|
|
294
|
+
return {
|
|
295
|
+
items: items(response).filter((number) => this.isOwnedNumber(number, numbers)),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
validateQuickDialNumber(quickDialNumber) {
|
|
299
|
+
return this.delegate.validateQuickDialNumber(quickDialNumber);
|
|
300
|
+
}
|
|
301
|
+
listContacts(query) {
|
|
302
|
+
return this.delegate.listContacts(query);
|
|
303
|
+
}
|
|
304
|
+
getContact(contactId) {
|
|
305
|
+
return this.delegate.getContact(contactId);
|
|
306
|
+
}
|
|
307
|
+
listInternalContacts() {
|
|
308
|
+
return this.delegate.listInternalContacts();
|
|
309
|
+
}
|
|
310
|
+
exportContactsCsv(scopes) {
|
|
311
|
+
return this.delegate.exportContactsCsv(scopes);
|
|
312
|
+
}
|
|
313
|
+
getContactsVcard(query) {
|
|
314
|
+
return this.delegate.getContactsVcard(query);
|
|
315
|
+
}
|
|
316
|
+
listIncomingBlacklist() {
|
|
317
|
+
return this.delegate.listIncomingBlacklist();
|
|
318
|
+
}
|
|
319
|
+
getCallRestrictions(userIds) {
|
|
320
|
+
if (this.scope === "account")
|
|
321
|
+
return this.delegate.getCallRestrictions(userIds);
|
|
322
|
+
if (userIds) {
|
|
323
|
+
for (const userId of userIds)
|
|
324
|
+
this.assertUser(userId);
|
|
325
|
+
}
|
|
326
|
+
return this.delegate.getCallRestrictions([this.context.userId]);
|
|
327
|
+
}
|
|
328
|
+
getRestrictions(userId, restrictions) {
|
|
329
|
+
this.assertUser(userId);
|
|
330
|
+
return this.delegate.getRestrictions(userId, restrictions);
|
|
331
|
+
}
|
|
83
332
|
getRouting(userId) {
|
|
84
333
|
if (this.scope === "account")
|
|
85
334
|
return this.delegate.getRouting(userId);
|
|
@@ -110,20 +359,206 @@ export class AccessControlledBackend {
|
|
|
110
359
|
}
|
|
111
360
|
return this.delegate.getCallHistory({ ...query, connectionIds: scopedConnectionIds });
|
|
112
361
|
}
|
|
362
|
+
async exportHistory(query) {
|
|
363
|
+
if (this.scope === "account")
|
|
364
|
+
return this.delegate.exportHistory(query);
|
|
365
|
+
const connectionIds = await this.ownedConnectionIds();
|
|
366
|
+
const requested = query.connectionIds;
|
|
367
|
+
if (requested) {
|
|
368
|
+
for (const connectionId of requested) {
|
|
369
|
+
this.assertOwned(connectionIds, connectionId, "connection");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const scopedConnectionIds = requested ?? [...connectionIds];
|
|
373
|
+
if (scopedConnectionIds.length === 0) {
|
|
374
|
+
return {
|
|
375
|
+
content: "",
|
|
376
|
+
contentType: "text/csv",
|
|
377
|
+
note: "The authenticated user has no owned connections to export.",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return this.delegate.exportHistory({ ...query, connectionIds: scopedConnectionIds });
|
|
381
|
+
}
|
|
382
|
+
async listCalls() {
|
|
383
|
+
if (this.scope === "account")
|
|
384
|
+
return this.delegate.listCalls();
|
|
385
|
+
try {
|
|
386
|
+
const [response, deviceIds, numbers] = await Promise.all([
|
|
387
|
+
this.delegate.listCalls(),
|
|
388
|
+
this.ownedDeviceIds(),
|
|
389
|
+
this.ownedNumbers(),
|
|
390
|
+
]);
|
|
391
|
+
return {
|
|
392
|
+
data: calls(response).filter((call) => this.isOwnedCall(call, deviceIds, numbers)),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (error instanceof AccessPolicyError)
|
|
397
|
+
throw error;
|
|
398
|
+
throw new AccessPolicyError("User scope could not establish ownership of the account's active calls.");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
listNotifications(userId) {
|
|
402
|
+
this.assertUser(userId);
|
|
403
|
+
return this.delegate.listNotifications(userId);
|
|
404
|
+
}
|
|
405
|
+
listFaxlines(userId) {
|
|
406
|
+
this.assertUser(userId);
|
|
407
|
+
return this.delegate.listFaxlines(userId);
|
|
408
|
+
}
|
|
409
|
+
async listFaxlineNumbers(userId, faxlineId) {
|
|
410
|
+
this.assertUser(userId);
|
|
411
|
+
if (this.scope === "user")
|
|
412
|
+
await this.assertOwnedFaxline(faxlineId);
|
|
413
|
+
return this.delegate.listFaxlineNumbers(userId, faxlineId);
|
|
414
|
+
}
|
|
415
|
+
async getFaxlineCallerId(userId, faxlineId) {
|
|
416
|
+
this.assertUser(userId);
|
|
417
|
+
if (this.scope === "user")
|
|
418
|
+
await this.assertOwnedFaxline(faxlineId);
|
|
419
|
+
return this.delegate.getFaxlineCallerId(userId, faxlineId);
|
|
420
|
+
}
|
|
421
|
+
async getHistoryEntry(entryId) {
|
|
422
|
+
if (this.scope === "user")
|
|
423
|
+
await this.assertOwnedHistoryEntry(entryId);
|
|
424
|
+
return this.delegate.getHistoryEntry(entryId);
|
|
425
|
+
}
|
|
113
426
|
getSettings(userId) {
|
|
114
427
|
if (this.scope === "account")
|
|
115
428
|
return this.delegate.getSettings(userId);
|
|
116
429
|
this.assertUser(userId);
|
|
117
430
|
return this.delegate.getSettings(this.context.userId);
|
|
118
431
|
}
|
|
432
|
+
getBalance() {
|
|
433
|
+
return this.delegate.getBalance();
|
|
434
|
+
}
|
|
435
|
+
listPortings() {
|
|
436
|
+
return this.delegate.listPortings();
|
|
437
|
+
}
|
|
438
|
+
getPorting(portingId) {
|
|
439
|
+
return this.delegate.getPorting(portingId);
|
|
440
|
+
}
|
|
441
|
+
getSipgateIoSettings() {
|
|
442
|
+
return this.delegate.getSipgateIoSettings();
|
|
443
|
+
}
|
|
444
|
+
listWebhookLogs() {
|
|
445
|
+
return this.delegate.listWebhookLogs();
|
|
446
|
+
}
|
|
447
|
+
createContact(input, confirmAccountWide) {
|
|
448
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "contact creation");
|
|
449
|
+
return this.delegate.createContact(input, confirmAccountWide);
|
|
450
|
+
}
|
|
451
|
+
updateContact(contactId, input, confirmAccountWide) {
|
|
452
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "contact update");
|
|
453
|
+
return this.delegate.updateContact(contactId, input, confirmAccountWide);
|
|
454
|
+
}
|
|
455
|
+
deleteContact(contactId, scopes, confirmAccountWide) {
|
|
456
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "contact deletion");
|
|
457
|
+
return this.delegate.deleteContact(contactId, scopes, confirmAccountWide);
|
|
458
|
+
}
|
|
459
|
+
deleteContacts(input, confirmAccountWide) {
|
|
460
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "bulk contact deletion");
|
|
461
|
+
return this.delegate.deleteContacts(input, confirmAccountWide);
|
|
462
|
+
}
|
|
463
|
+
importContactsCsv(base64Content, confirmAccountWide) {
|
|
464
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "CSV contact import");
|
|
465
|
+
return this.delegate.importContactsCsv(base64Content, confirmAccountWide);
|
|
466
|
+
}
|
|
467
|
+
putContactsVcard(scope, data, confirmAccountWide) {
|
|
468
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "vCard contact update");
|
|
469
|
+
return this.delegate.putContactsVcard(scope, data, confirmAccountWide);
|
|
470
|
+
}
|
|
471
|
+
addIncomingBlacklist(phoneNumber, isBlock, confirmAccountWide) {
|
|
472
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "incoming blacklist update");
|
|
473
|
+
return this.delegate.addIncomingBlacklist(phoneNumber, isBlock, confirmAccountWide);
|
|
474
|
+
}
|
|
475
|
+
removeIncomingBlacklist(phoneNumber, confirmAccountWide) {
|
|
476
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "incoming blacklist deletion");
|
|
477
|
+
return this.delegate.removeIncomingBlacklist(phoneNumber, confirmAccountWide);
|
|
478
|
+
}
|
|
479
|
+
setCallRestriction(restriction, enabled) {
|
|
480
|
+
return this.delegate.setCallRestriction(restriction, enabled);
|
|
481
|
+
}
|
|
482
|
+
async setHistoryRead(entryId, value) {
|
|
483
|
+
if (this.scope === "user")
|
|
484
|
+
await this.assertOwnedHistoryEntry(entryId);
|
|
485
|
+
return this.delegate.setHistoryRead(entryId, value);
|
|
486
|
+
}
|
|
487
|
+
async setHistoryNote(entryId, note) {
|
|
488
|
+
if (this.scope === "user")
|
|
489
|
+
await this.assertOwnedHistoryEntry(entryId);
|
|
490
|
+
return this.delegate.setHistoryNote(entryId, note);
|
|
491
|
+
}
|
|
492
|
+
async setHistoryArchive(entryId, value) {
|
|
493
|
+
if (this.scope === "user")
|
|
494
|
+
await this.assertOwnedHistoryEntry(entryId);
|
|
495
|
+
return this.delegate.setHistoryArchive(entryId, value);
|
|
496
|
+
}
|
|
497
|
+
async updateHistoryEntry(entryId, input) {
|
|
498
|
+
if (this.scope === "user")
|
|
499
|
+
await this.assertOwnedHistoryEntry(entryId);
|
|
500
|
+
return this.delegate.updateHistoryEntry(entryId, input);
|
|
501
|
+
}
|
|
502
|
+
async deleteHistoryEntry(entryId) {
|
|
503
|
+
if (this.scope === "user")
|
|
504
|
+
await this.assertOwnedHistoryEntry(entryId);
|
|
505
|
+
return this.delegate.deleteHistoryEntry(entryId);
|
|
506
|
+
}
|
|
507
|
+
async updateHistoryEntries(inputs) {
|
|
508
|
+
if (this.scope === "user") {
|
|
509
|
+
for (const input of inputs)
|
|
510
|
+
await this.assertOwnedHistoryEntry(input.id);
|
|
511
|
+
}
|
|
512
|
+
return this.delegate.updateHistoryEntries(inputs);
|
|
513
|
+
}
|
|
514
|
+
async deleteHistoryEntries(entryIds) {
|
|
515
|
+
if (this.scope === "account")
|
|
516
|
+
return this.delegate.deleteHistoryEntries(entryIds);
|
|
517
|
+
if (entryIds) {
|
|
518
|
+
for (const entryId of entryIds)
|
|
519
|
+
await this.assertOwnedHistoryEntry(entryId);
|
|
520
|
+
return this.delegate.deleteHistoryEntries(entryIds);
|
|
521
|
+
}
|
|
522
|
+
const ownedEntryIds = await this.ownedHistoryEntryIds();
|
|
523
|
+
if (ownedEntryIds.length === 0) {
|
|
524
|
+
return {
|
|
525
|
+
before: [],
|
|
526
|
+
after: {
|
|
527
|
+
changed: false,
|
|
528
|
+
deleted: false,
|
|
529
|
+
note: "The authenticated user has no owned history entries to delete; no account-wide request was issued.",
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
return this.delegate.deleteHistoryEntries(ownedEntryIds);
|
|
534
|
+
}
|
|
535
|
+
cancelPorting(portingId, confirmAccountWide) {
|
|
536
|
+
if (confirmAccountWide !== true) {
|
|
537
|
+
throw new AccessPolicyError("Cancelling a number porting requires confirm_account_wide: true.");
|
|
538
|
+
}
|
|
539
|
+
return this.delegate.cancelPorting(portingId, confirmAccountWide);
|
|
540
|
+
}
|
|
541
|
+
updateSipgateIoSettings(input, confirmAccountWide) {
|
|
542
|
+
this.assertAccountWideConfirmation(confirmAccountWide, "sipgate.io account settings update");
|
|
543
|
+
return this.delegate.updateSipgateIoSettings(input, confirmAccountWide);
|
|
544
|
+
}
|
|
119
545
|
async setNumberRouting(numberId, endpointId) {
|
|
120
546
|
if (this.scope === "user") {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
547
|
+
try {
|
|
548
|
+
// Accounts without a phoneline layer route numbers straight to a
|
|
549
|
+
// device, so an owned device is a legitimate destination endpoint.
|
|
550
|
+
const [numbers, endpointIds] = await Promise.all([
|
|
551
|
+
this.ownedNumbers(),
|
|
552
|
+
this.ownedConnectionIds(),
|
|
553
|
+
]);
|
|
554
|
+
this.assertOwned(numbers.numberIds, numberId, "phone number");
|
|
555
|
+
this.assertOwned(endpointIds, endpointId, "destination endpoint");
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
if (error instanceof AccessPolicyError)
|
|
559
|
+
throw error;
|
|
560
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested number or destination endpoint.");
|
|
561
|
+
}
|
|
127
562
|
}
|
|
128
563
|
return this.scope === "user"
|
|
129
564
|
? this.delegate.setUserNumberRouting(this.context.userId, numberId, endpointId)
|
|
@@ -136,16 +571,278 @@ export class AccessControlledBackend {
|
|
|
136
571
|
async setForwarding(userId, phonelineId, forwardings) {
|
|
137
572
|
if (this.scope === "user") {
|
|
138
573
|
this.assertUser(userId);
|
|
139
|
-
|
|
574
|
+
if (!await this.assertOwnedPhoneline(phonelineId))
|
|
575
|
+
return unavailablePhonelineMutation();
|
|
140
576
|
}
|
|
141
577
|
return this.delegate.setForwarding(userId, phonelineId, forwardings);
|
|
142
578
|
}
|
|
143
|
-
|
|
579
|
+
createPhoneline(userId) {
|
|
580
|
+
this.assertUser(userId);
|
|
581
|
+
return this.delegate.createPhoneline(userId);
|
|
582
|
+
}
|
|
583
|
+
async updatePhonelineAlias(userId, phonelineId, alias) {
|
|
584
|
+
this.assertUser(userId);
|
|
585
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
586
|
+
return unavailablePhonelineMutation();
|
|
587
|
+
}
|
|
588
|
+
return this.delegate.updatePhonelineAlias(userId, phonelineId, alias);
|
|
589
|
+
}
|
|
590
|
+
async deletePhoneline(userId, phonelineId) {
|
|
591
|
+
this.assertUser(userId);
|
|
592
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
593
|
+
return unavailablePhonelineMutation();
|
|
594
|
+
}
|
|
595
|
+
return this.delegate.deletePhoneline(userId, phonelineId);
|
|
596
|
+
}
|
|
597
|
+
async setPhonelineBlockAnonymous(userId, phonelineId, input) {
|
|
598
|
+
this.assertUser(userId);
|
|
599
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
600
|
+
return unavailablePhonelineMutation();
|
|
601
|
+
}
|
|
602
|
+
return this.delegate.setPhonelineBlockAnonymous(userId, phonelineId, input);
|
|
603
|
+
}
|
|
604
|
+
async attachDeviceToPhoneline(userId, phonelineId, deviceId) {
|
|
605
|
+
this.assertUser(userId);
|
|
144
606
|
if (this.scope === "user") {
|
|
145
|
-
|
|
607
|
+
if (!await this.assertOwnedPhoneline(phonelineId))
|
|
608
|
+
return unavailablePhonelineMutation();
|
|
609
|
+
await this.assertOwnedDevice(deviceId);
|
|
610
|
+
}
|
|
611
|
+
return this.delegate.attachDeviceToPhoneline(userId, phonelineId, deviceId);
|
|
612
|
+
}
|
|
613
|
+
async detachDeviceFromPhoneline(userId, phonelineId, deviceId) {
|
|
614
|
+
this.assertUser(userId);
|
|
615
|
+
if (this.scope === "user") {
|
|
616
|
+
if (!await this.assertOwnedPhoneline(phonelineId))
|
|
617
|
+
return unavailablePhonelineMutation();
|
|
618
|
+
await this.assertOwnedDevice(deviceId);
|
|
619
|
+
}
|
|
620
|
+
return this.delegate.detachDeviceFromPhoneline(userId, phonelineId, deviceId);
|
|
621
|
+
}
|
|
622
|
+
async createParallelForwarding(userId, phonelineId, input) {
|
|
623
|
+
this.assertUser(userId);
|
|
624
|
+
if (this.scope === "user" && !await this.assertOwnedPhoneline(phonelineId)) {
|
|
625
|
+
return unavailablePhonelineMutation();
|
|
626
|
+
}
|
|
627
|
+
return this.delegate.createParallelForwarding(userId, phonelineId, input);
|
|
628
|
+
}
|
|
629
|
+
async updateParallelForwarding(userId, phonelineId, parallelForwardingId, input) {
|
|
630
|
+
this.assertUser(userId);
|
|
631
|
+
if (this.scope === "user"
|
|
632
|
+
&& !await this.assertOwnedParallelForwarding(phonelineId, parallelForwardingId))
|
|
633
|
+
return unavailablePhonelineMutation();
|
|
634
|
+
return this.delegate.updateParallelForwarding(userId, phonelineId, parallelForwardingId, input);
|
|
635
|
+
}
|
|
636
|
+
async deleteParallelForwarding(userId, phonelineId, parallelForwardingId) {
|
|
637
|
+
this.assertUser(userId);
|
|
638
|
+
if (this.scope === "user"
|
|
639
|
+
&& !await this.assertOwnedParallelForwarding(phonelineId, parallelForwardingId))
|
|
640
|
+
return unavailablePhonelineMutation();
|
|
641
|
+
return this.delegate.deleteParallelForwarding(userId, phonelineId, parallelForwardingId);
|
|
642
|
+
}
|
|
643
|
+
async updateVoicemail(userId, phonelineId, voicemailId, input) {
|
|
644
|
+
this.assertUser(userId);
|
|
645
|
+
if (this.scope === "user" && !await this.assertOwnedVoicemail(phonelineId, voicemailId)) {
|
|
646
|
+
return unavailablePhonelineMutation();
|
|
647
|
+
}
|
|
648
|
+
return this.delegate.updateVoicemail(userId, phonelineId, voicemailId, input);
|
|
649
|
+
}
|
|
650
|
+
async createVoicemailGreeting(userId, phonelineId, voicemailId, input) {
|
|
651
|
+
this.assertUser(userId);
|
|
652
|
+
if (this.scope === "user" && !await this.assertOwnedVoicemail(phonelineId, voicemailId)) {
|
|
653
|
+
return unavailablePhonelineMutation();
|
|
654
|
+
}
|
|
655
|
+
return this.delegate.createVoicemailGreeting(userId, phonelineId, voicemailId, input);
|
|
656
|
+
}
|
|
657
|
+
async updateVoicemailGreeting(userId, phonelineId, voicemailId, greetingId, active) {
|
|
658
|
+
this.assertUser(userId);
|
|
659
|
+
if (this.scope === "user"
|
|
660
|
+
&& !await this.assertOwnedGreeting(phonelineId, voicemailId, greetingId))
|
|
661
|
+
return unavailablePhonelineMutation();
|
|
662
|
+
return this.delegate.updateVoicemailGreeting(userId, phonelineId, voicemailId, greetingId, active);
|
|
663
|
+
}
|
|
664
|
+
async deleteVoicemailGreeting(userId, phonelineId, voicemailId, greetingId) {
|
|
665
|
+
this.assertUser(userId);
|
|
666
|
+
if (this.scope === "user"
|
|
667
|
+
&& !await this.assertOwnedGreeting(phonelineId, voicemailId, greetingId))
|
|
668
|
+
return unavailablePhonelineMutation();
|
|
669
|
+
return this.delegate.deleteVoicemailGreeting(userId, phonelineId, voicemailId, greetingId);
|
|
670
|
+
}
|
|
671
|
+
async setVoicemailTranscription(userId, phonelineId, voicemailId, active) {
|
|
672
|
+
this.assertUser(userId);
|
|
673
|
+
if (this.scope === "user" && !await this.assertOwnedVoicemail(phonelineId, voicemailId)) {
|
|
674
|
+
return unavailablePhonelineMutation();
|
|
675
|
+
}
|
|
676
|
+
return this.delegate.setVoicemailTranscription(userId, phonelineId, voicemailId, active);
|
|
677
|
+
}
|
|
678
|
+
async playVoicemail(input) {
|
|
679
|
+
if (this.scope === "user") {
|
|
680
|
+
if (!input.deviceId || !input.dataId) {
|
|
681
|
+
throw new AccessPolicyError("User scope requires both device and voicemail-data IDs to establish playback ownership.");
|
|
682
|
+
}
|
|
683
|
+
await this.assertOwnedDevice(input.deviceId);
|
|
684
|
+
await this.assertOwnedHistoryEntry(input.dataId);
|
|
685
|
+
}
|
|
686
|
+
return this.delegate.playVoicemail(input);
|
|
687
|
+
}
|
|
688
|
+
async recordVoicemailGreeting(input) {
|
|
689
|
+
if (this.scope === "user") {
|
|
690
|
+
if (!input.deviceId || !input.targetId) {
|
|
691
|
+
throw new AccessPolicyError("User scope requires both device and target voicemail IDs to establish recording ownership.");
|
|
692
|
+
}
|
|
693
|
+
await this.assertOwnedDevice(input.deviceId);
|
|
694
|
+
if (!await this.assertOwnedVoicemailId(input.targetId))
|
|
695
|
+
return unavailablePhonelineMutation();
|
|
696
|
+
}
|
|
697
|
+
return this.delegate.recordVoicemailGreeting(input);
|
|
698
|
+
}
|
|
699
|
+
createAutorecordingGreeting(input) {
|
|
700
|
+
if (this.scope === "user") {
|
|
701
|
+
throw new AccessPolicyError("User scope cannot change the account-wide automated-recording greeting because it has no user ownership relationship. Use administrator account scope.");
|
|
702
|
+
}
|
|
703
|
+
return this.delegate.createAutorecordingGreeting(input);
|
|
704
|
+
}
|
|
705
|
+
async deleteAutorecordingGreeting(greetingId) {
|
|
706
|
+
if (this.scope === "user") {
|
|
707
|
+
throw new AccessPolicyError("User scope cannot delete the account-wide automated-recording greeting because it has no user ownership relationship. Use administrator account scope.");
|
|
708
|
+
}
|
|
709
|
+
return this.delegate.deleteAutorecordingGreeting(greetingId);
|
|
710
|
+
}
|
|
711
|
+
async setAutorecordingSettings(extension, active) {
|
|
712
|
+
if (this.scope === "user")
|
|
713
|
+
await this.assertOwnedAutorecordingExtension(extension);
|
|
714
|
+
return this.delegate.setAutorecordingSettings(extension, active);
|
|
715
|
+
}
|
|
716
|
+
createFaxline(userId) {
|
|
717
|
+
this.assertUser(userId);
|
|
718
|
+
return this.delegate.createFaxline(userId);
|
|
719
|
+
}
|
|
720
|
+
async updateFaxlineAlias(userId, faxlineId, alias) {
|
|
721
|
+
this.assertUser(userId);
|
|
722
|
+
if (this.scope === "user")
|
|
723
|
+
await this.assertOwnedFaxline(faxlineId);
|
|
724
|
+
return this.delegate.updateFaxlineAlias(userId, faxlineId, alias);
|
|
725
|
+
}
|
|
726
|
+
async deleteFaxline(userId, faxlineId) {
|
|
727
|
+
this.assertUser(userId);
|
|
728
|
+
if (this.scope === "user")
|
|
729
|
+
await this.assertOwnedFaxline(faxlineId);
|
|
730
|
+
return this.delegate.deleteFaxline(userId, faxlineId);
|
|
731
|
+
}
|
|
732
|
+
async setFaxlineCallerId(userId, faxlineId, value) {
|
|
733
|
+
this.assertUser(userId);
|
|
734
|
+
if (this.scope === "user") {
|
|
735
|
+
await this.assertOwnedFaxline(faxlineId);
|
|
736
|
+
if (value)
|
|
737
|
+
await this.assertOwnedPhoneNumber(value, "fax caller ID phone number");
|
|
146
738
|
}
|
|
739
|
+
return this.delegate.setFaxlineCallerId(userId, faxlineId, value);
|
|
740
|
+
}
|
|
741
|
+
async setFaxlineTagline(userId, faxlineId, value) {
|
|
742
|
+
this.assertUser(userId);
|
|
743
|
+
if (this.scope === "user")
|
|
744
|
+
await this.assertOwnedFaxline(faxlineId);
|
|
745
|
+
return this.delegate.setFaxlineTagline(userId, faxlineId, value);
|
|
746
|
+
}
|
|
747
|
+
async setDnd(deviceId, enabled) {
|
|
748
|
+
if (this.scope === "user")
|
|
749
|
+
await this.assertOwnedDevice(deviceId);
|
|
147
750
|
return this.delegate.setDnd(deviceId, enabled);
|
|
148
751
|
}
|
|
752
|
+
async updateDevice(deviceId, settings) {
|
|
753
|
+
if (this.scope === "user") {
|
|
754
|
+
await this.assertOwnedDevice(deviceId);
|
|
755
|
+
if (settings.emergencyAddressId !== undefined) {
|
|
756
|
+
await this.assertOwnedAddress(settings.emergencyAddressId);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return this.delegate.updateDevice(deviceId, settings);
|
|
760
|
+
}
|
|
761
|
+
async deleteDevice(deviceId) {
|
|
762
|
+
if (this.scope === "user")
|
|
763
|
+
await this.assertOwnedDevice(deviceId);
|
|
764
|
+
return this.delegate.deleteDevice(deviceId);
|
|
765
|
+
}
|
|
766
|
+
async setDeviceAlias(deviceId, value) {
|
|
767
|
+
if (this.scope === "user")
|
|
768
|
+
await this.assertOwnedDevice(deviceId);
|
|
769
|
+
return this.delegate.setDeviceAlias(deviceId, value);
|
|
770
|
+
}
|
|
771
|
+
async setDeviceCallerId(deviceId, value) {
|
|
772
|
+
if (this.scope === "user") {
|
|
773
|
+
await this.assertOwnedDevice(deviceId);
|
|
774
|
+
if (value !== undefined) {
|
|
775
|
+
await this.assertOwnedPhoneNumber(value, "caller ID phone number");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return this.delegate.setDeviceCallerId(deviceId, value);
|
|
779
|
+
}
|
|
780
|
+
async setDeviceLocalPrefix(deviceId, input) {
|
|
781
|
+
if (this.scope === "user")
|
|
782
|
+
await this.assertOwnedDevice(deviceId);
|
|
783
|
+
return this.delegate.setDeviceLocalPrefix(deviceId, input);
|
|
784
|
+
}
|
|
785
|
+
async setDeviceTariffAnnouncement(deviceId, enabled) {
|
|
786
|
+
if (this.scope === "user")
|
|
787
|
+
await this.assertOwnedDevice(deviceId);
|
|
788
|
+
return this.delegate.setDeviceTariffAnnouncement(deviceId, enabled);
|
|
789
|
+
}
|
|
790
|
+
async setDeviceSingleRowDisplay(deviceId, enabled) {
|
|
791
|
+
if (this.scope === "user")
|
|
792
|
+
await this.assertOwnedDevice(deviceId);
|
|
793
|
+
return this.delegate.setDeviceSingleRowDisplay(deviceId, enabled);
|
|
794
|
+
}
|
|
795
|
+
async setExternalDeviceTargetNumber(deviceId, number) {
|
|
796
|
+
if (this.scope === "user")
|
|
797
|
+
await this.assertOwnedDevice(deviceId);
|
|
798
|
+
return this.delegate.setExternalDeviceTargetNumber(deviceId, number);
|
|
799
|
+
}
|
|
800
|
+
async setExternalDeviceIncomingCallDisplay(deviceId, incomingCallDisplay) {
|
|
801
|
+
if (this.scope === "user")
|
|
802
|
+
await this.assertOwnedDevice(deviceId);
|
|
803
|
+
return this.delegate.setExternalDeviceIncomingCallDisplay(deviceId, incomingCallDisplay);
|
|
804
|
+
}
|
|
805
|
+
async changeDevicePassword(deviceId) {
|
|
806
|
+
if (this.scope === "user")
|
|
807
|
+
await this.assertOwnedDevice(deviceId);
|
|
808
|
+
return this.delegate.changeDevicePassword(deviceId);
|
|
809
|
+
}
|
|
810
|
+
createRegisterDevice(userId, alias) {
|
|
811
|
+
this.assertUser(userId);
|
|
812
|
+
return this.delegate.createRegisterDevice(userId, alias);
|
|
813
|
+
}
|
|
814
|
+
createMobileDevice(userId, alias) {
|
|
815
|
+
this.assertUser(userId);
|
|
816
|
+
return this.delegate.createMobileDevice(userId, alias);
|
|
817
|
+
}
|
|
818
|
+
createExternalDevice(userId, alias, number) {
|
|
819
|
+
this.assertUser(userId);
|
|
820
|
+
return this.delegate.createExternalDevice(userId, alias, number);
|
|
821
|
+
}
|
|
822
|
+
createQuickDial(input) {
|
|
823
|
+
this.assertUser(input.userId);
|
|
824
|
+
return this.delegate.createQuickDial(input);
|
|
825
|
+
}
|
|
826
|
+
async updateQuickDial(quickDialId, input) {
|
|
827
|
+
if (this.scope === "user") {
|
|
828
|
+
this.assertUser(input.userId);
|
|
829
|
+
await this.assertOwnedNumberId(quickDialId, "quick-dial number");
|
|
830
|
+
}
|
|
831
|
+
return this.delegate.updateQuickDial(quickDialId, input);
|
|
832
|
+
}
|
|
833
|
+
async deleteQuickDial(numberId) {
|
|
834
|
+
if (this.scope === "user") {
|
|
835
|
+
await this.assertOwnedNumberId(numberId, "quick-dial number");
|
|
836
|
+
}
|
|
837
|
+
return this.delegate.deleteQuickDial(numberId);
|
|
838
|
+
}
|
|
839
|
+
async updateAddress(addressId, input) {
|
|
840
|
+
if (this.scope === "user") {
|
|
841
|
+
await this.assertOwnedAddress(addressId);
|
|
842
|
+
await this.assertExclusivelyOwnedAddress(addressId);
|
|
843
|
+
}
|
|
844
|
+
return this.delegate.updateAddress(addressId, input);
|
|
845
|
+
}
|
|
149
846
|
async sendSms(input) {
|
|
150
847
|
if (this.scope === "user")
|
|
151
848
|
this.assertUser(input.userId);
|
|
@@ -153,19 +850,26 @@ export class AccessControlledBackend {
|
|
|
153
850
|
}
|
|
154
851
|
async initiateCall(input) {
|
|
155
852
|
if (this.scope === "user") {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
853
|
+
try {
|
|
854
|
+
const [numbers, deviceIds] = await Promise.all([
|
|
855
|
+
this.ownedNumbers(),
|
|
856
|
+
this.ownedDeviceIds(),
|
|
857
|
+
]);
|
|
858
|
+
const callerIsOwned = deviceIds.has(input.caller)
|
|
859
|
+
|| numbers.phoneNumbers.has(input.caller);
|
|
860
|
+
if (!callerIsOwned) {
|
|
861
|
+
throw new AccessPolicyError("User scope only permits calls from the authenticated user's devices or phone numbers.");
|
|
862
|
+
}
|
|
863
|
+
if (input.deviceId)
|
|
864
|
+
this.assertOwned(deviceIds, input.deviceId, "device");
|
|
865
|
+
if (input.callerId)
|
|
866
|
+
this.assertOwned(numbers.phoneNumbers, input.callerId, "caller ID");
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
if (error instanceof AccessPolicyError)
|
|
870
|
+
throw error;
|
|
871
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested call origin.");
|
|
164
872
|
}
|
|
165
|
-
if (input.deviceId)
|
|
166
|
-
this.assertOwned(deviceIds, input.deviceId, "device");
|
|
167
|
-
if (input.callerId)
|
|
168
|
-
this.assertOwned(numbers.phoneNumbers, input.callerId, "caller ID");
|
|
169
873
|
}
|
|
170
874
|
return this.scope === "user"
|
|
171
875
|
? this.delegate.initiateUserCall(input)
|
|
@@ -176,6 +880,186 @@ export class AccessControlledBackend {
|
|
|
176
880
|
return this.delegate.initiateUserCall(input);
|
|
177
881
|
return this.initiateCall(input);
|
|
178
882
|
}
|
|
883
|
+
async createCallEmailNotification(input) {
|
|
884
|
+
this.assertUser(input.userId);
|
|
885
|
+
if (this.scope === "user")
|
|
886
|
+
await this.assertOwnedConnectionId(input.endpointId);
|
|
887
|
+
return this.delegate.createCallEmailNotification(input);
|
|
888
|
+
}
|
|
889
|
+
async createCallSmsNotification(input) {
|
|
890
|
+
this.assertUser(input.userId);
|
|
891
|
+
if (this.scope === "user")
|
|
892
|
+
await this.assertOwnedConnectionId(input.endpointId);
|
|
893
|
+
return this.delegate.createCallSmsNotification(input);
|
|
894
|
+
}
|
|
895
|
+
async createFaxEmailNotification(input) {
|
|
896
|
+
this.assertUser(input.userId);
|
|
897
|
+
if (this.scope === "user")
|
|
898
|
+
await this.assertOwnedFaxline(input.faxlineId);
|
|
899
|
+
return this.delegate.createFaxEmailNotification(input);
|
|
900
|
+
}
|
|
901
|
+
async createFaxSmsNotification(input) {
|
|
902
|
+
this.assertUser(input.userId);
|
|
903
|
+
if (this.scope === "user")
|
|
904
|
+
await this.assertOwnedFaxline(input.faxlineId);
|
|
905
|
+
return this.delegate.createFaxSmsNotification(input);
|
|
906
|
+
}
|
|
907
|
+
async createFaxReportNotification(input) {
|
|
908
|
+
this.assertUser(input.userId);
|
|
909
|
+
if (this.scope === "user")
|
|
910
|
+
await this.assertOwnedFaxline(input.faxlineId);
|
|
911
|
+
return this.delegate.createFaxReportNotification(input);
|
|
912
|
+
}
|
|
913
|
+
createSmsEmailNotification(input) {
|
|
914
|
+
this.assertUser(input.userId);
|
|
915
|
+
return this.delegate.createSmsEmailNotification(input);
|
|
916
|
+
}
|
|
917
|
+
async createVoicemailEmailNotification(input) {
|
|
918
|
+
this.assertUser(input.userId);
|
|
919
|
+
if (this.scope === "user" && !await this.assertOwnedVoicemailId(input.voicemailId)) {
|
|
920
|
+
return unavailablePhonelineMutation();
|
|
921
|
+
}
|
|
922
|
+
return this.delegate.createVoicemailEmailNotification(input);
|
|
923
|
+
}
|
|
924
|
+
async createVoicemailSmsNotification(input) {
|
|
925
|
+
this.assertUser(input.userId);
|
|
926
|
+
if (this.scope === "user" && !await this.assertOwnedVoicemailId(input.voicemailId)) {
|
|
927
|
+
return unavailablePhonelineMutation();
|
|
928
|
+
}
|
|
929
|
+
return this.delegate.createVoicemailSmsNotification(input);
|
|
930
|
+
}
|
|
931
|
+
async deleteNotification(userId, notificationId) {
|
|
932
|
+
this.assertUser(userId);
|
|
933
|
+
if (this.scope === "user") {
|
|
934
|
+
try {
|
|
935
|
+
const ownedIds = nestedNotificationIds(await this.delegate.listNotifications(userId));
|
|
936
|
+
this.assertOwned(ownedIds, notificationId, "notification");
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
if (error instanceof AccessPolicyError)
|
|
940
|
+
throw error;
|
|
941
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested notification.");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return this.delegate.deleteNotification(userId, notificationId);
|
|
945
|
+
}
|
|
946
|
+
async hangupCall(callId) {
|
|
947
|
+
if (this.scope === "user")
|
|
948
|
+
await this.assertOwnedCall(callId);
|
|
949
|
+
return this.delegate.hangupCall(callId);
|
|
950
|
+
}
|
|
951
|
+
async setCallHold(callId, value) {
|
|
952
|
+
if (this.scope === "user")
|
|
953
|
+
await this.assertOwnedCall(callId);
|
|
954
|
+
return this.delegate.setCallHold(callId, value);
|
|
955
|
+
}
|
|
956
|
+
async setCallMuted(callId, value) {
|
|
957
|
+
if (this.scope === "user")
|
|
958
|
+
await this.assertOwnedCall(callId);
|
|
959
|
+
return this.delegate.setCallMuted(callId, value);
|
|
960
|
+
}
|
|
961
|
+
async setCallRecording(callId, value, announcement) {
|
|
962
|
+
if (this.scope === "user")
|
|
963
|
+
await this.assertOwnedCall(callId);
|
|
964
|
+
return this.delegate.setCallRecording(callId, value, announcement);
|
|
965
|
+
}
|
|
966
|
+
async transferCall(callId, input) {
|
|
967
|
+
if (this.scope === "user") {
|
|
968
|
+
await this.assertOwnedCall(callId);
|
|
969
|
+
if (input.callerId !== undefined) {
|
|
970
|
+
await this.assertOwnedPhoneNumber(input.callerId, "transfer caller ID phone number");
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return this.delegate.transferCall(callId, input);
|
|
974
|
+
}
|
|
975
|
+
async sendCallDtmf(callId, sequence) {
|
|
976
|
+
if (this.scope === "user")
|
|
977
|
+
await this.assertOwnedCall(callId);
|
|
978
|
+
return this.delegate.sendCallDtmf(callId, sequence);
|
|
979
|
+
}
|
|
980
|
+
async startCallAnnouncement(callId, url) {
|
|
981
|
+
if (this.scope === "user")
|
|
982
|
+
await this.assertOwnedCall(callId);
|
|
983
|
+
return this.delegate.startCallAnnouncement(callId, url);
|
|
984
|
+
}
|
|
985
|
+
async sendFax(input) {
|
|
986
|
+
if (this.scope === "user")
|
|
987
|
+
await this.assertOwnedFaxline(input.faxlineId);
|
|
988
|
+
return this.delegate.sendFax(input);
|
|
989
|
+
}
|
|
990
|
+
async resendFax(input) {
|
|
991
|
+
if (this.scope === "user") {
|
|
992
|
+
if (!input.faxlineId) {
|
|
993
|
+
throw new AccessPolicyError("User scope requires a faxline ID to establish ownership before resending a fax.");
|
|
994
|
+
}
|
|
995
|
+
await this.assertOwnedFaxline(input.faxlineId);
|
|
996
|
+
await this.assertOwnedHistoryEntry(input.faxId);
|
|
997
|
+
}
|
|
998
|
+
return this.delegate.resendFax(input);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* A fax ID is a history entry. Resending one that belongs to another user
|
|
1002
|
+
* would retransmit their document and bill it, so the entry's connection must
|
|
1003
|
+
* be one the authenticated user owns.
|
|
1004
|
+
*/
|
|
1005
|
+
async assertOwnedHistoryEntry(entryId) {
|
|
1006
|
+
let connectionIds;
|
|
1007
|
+
let entry;
|
|
1008
|
+
try {
|
|
1009
|
+
[connectionIds, entry] = await Promise.all([
|
|
1010
|
+
this.ownedConnectionIds(),
|
|
1011
|
+
this.delegate.getHistoryEntry(entryId).then((value) => asObject(value)),
|
|
1012
|
+
]);
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested history entry.");
|
|
1016
|
+
}
|
|
1017
|
+
const entryConnections = Array.isArray(entry?.connectionIds)
|
|
1018
|
+
? entry.connectionIds.filter((value) => typeof value === "string")
|
|
1019
|
+
: [];
|
|
1020
|
+
if (entryConnections.some((connectionId) => connectionIds.has(connectionId)))
|
|
1021
|
+
return;
|
|
1022
|
+
throw new AccessPolicyError("User scope does not permit access to the requested history entry.");
|
|
1023
|
+
}
|
|
1024
|
+
assertAccountWideConfirmation(confirmAccountWide, operation) {
|
|
1025
|
+
if (this.scope === "account" || confirmAccountWide === true)
|
|
1026
|
+
return;
|
|
1027
|
+
throw new AccessPolicyError(`User scope requires confirm_account_wide: true for this account-wide ${operation}.`);
|
|
1028
|
+
}
|
|
1029
|
+
async ownedHistoryEntryIds() {
|
|
1030
|
+
try {
|
|
1031
|
+
const connectionIds = [...await this.ownedConnectionIds()];
|
|
1032
|
+
if (connectionIds.length === 0)
|
|
1033
|
+
return [];
|
|
1034
|
+
const found = new Set();
|
|
1035
|
+
const pageSize = 1000;
|
|
1036
|
+
for (const archived of [false, true]) {
|
|
1037
|
+
for (let offset = 0;; offset += pageSize) {
|
|
1038
|
+
const response = await this.delegate.getCallHistory({
|
|
1039
|
+
archived,
|
|
1040
|
+
connectionIds,
|
|
1041
|
+
limit: pageSize,
|
|
1042
|
+
offset,
|
|
1043
|
+
types: ["CALL", "VOICEMAIL", "SMS", "FAX"],
|
|
1044
|
+
});
|
|
1045
|
+
const page = items(response);
|
|
1046
|
+
for (const entry of page) {
|
|
1047
|
+
const entryId = stringField(entry, "id");
|
|
1048
|
+
if (entryId)
|
|
1049
|
+
found.add(entryId);
|
|
1050
|
+
}
|
|
1051
|
+
if (page.length < pageSize)
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return [...found];
|
|
1056
|
+
}
|
|
1057
|
+
catch (error) {
|
|
1058
|
+
if (error instanceof AccessPolicyError)
|
|
1059
|
+
throw error;
|
|
1060
|
+
throw new AccessPolicyError("User scope could not enumerate owned history entries for bulk deletion.");
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
179
1063
|
assertUser(userId) {
|
|
180
1064
|
if (this.scope === "account" || userId === undefined || userId === this.context.userId)
|
|
181
1065
|
return;
|
|
@@ -193,24 +1077,328 @@ export class AccessControlledBackend {
|
|
|
193
1077
|
]);
|
|
194
1078
|
return new Set([...phonelineIds, ...deviceIds]);
|
|
195
1079
|
}
|
|
1080
|
+
async assertOwnedConnectionId(connectionId) {
|
|
1081
|
+
try {
|
|
1082
|
+
this.assertOwned(await this.ownedConnectionIds(), connectionId, "notification endpoint");
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
if (error instanceof AccessPolicyError)
|
|
1086
|
+
throw error;
|
|
1087
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested notification endpoint.");
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async assertOwnedFaxline(faxlineId) {
|
|
1091
|
+
try {
|
|
1092
|
+
this.assertOwned(await this.ownedFaxlineIds(), faxlineId, "faxline");
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
if (error instanceof AccessPolicyError)
|
|
1096
|
+
throw error;
|
|
1097
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested faxline.");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async assertOwnedCall(callId) {
|
|
1101
|
+
try {
|
|
1102
|
+
const [response, deviceIds, numbers] = await Promise.all([
|
|
1103
|
+
this.delegate.listCalls(),
|
|
1104
|
+
this.ownedDeviceIds(),
|
|
1105
|
+
this.ownedNumbers(),
|
|
1106
|
+
]);
|
|
1107
|
+
const call = calls(response).find((candidate) => stringField(candidate, "callId") === callId);
|
|
1108
|
+
if (call && this.isOwnedCall(call, deviceIds, numbers))
|
|
1109
|
+
return;
|
|
1110
|
+
throw new AccessPolicyError("User scope does not permit access to the requested active call.");
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
if (error instanceof AccessPolicyError)
|
|
1114
|
+
throw error;
|
|
1115
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested active call.");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
isOwnedCall(call, deviceIds, numbers) {
|
|
1119
|
+
// Being a participant in someone else's call is not ownership: the remote
|
|
1120
|
+
// party of a foreign call carries an owned number too. Only the participant
|
|
1121
|
+
// sipgate marks as the call owner counts, and an unmarked call fails closed.
|
|
1122
|
+
const owner = objectArray(call.participants)
|
|
1123
|
+
.find((participant) => participant.owner === true);
|
|
1124
|
+
if (!owner)
|
|
1125
|
+
return false;
|
|
1126
|
+
const participantId = stringField(owner, "participantId");
|
|
1127
|
+
const phoneNumber = stringField(owner, "phoneNumber");
|
|
1128
|
+
return (participantId !== undefined && deviceIds.has(participantId))
|
|
1129
|
+
|| (phoneNumber !== undefined && numbers.phoneNumbers.has(phoneNumber));
|
|
1130
|
+
}
|
|
196
1131
|
async ownedPhonelineIds() {
|
|
1132
|
+
return (await this.ownedPhonelineState()).ids;
|
|
1133
|
+
}
|
|
1134
|
+
async ownedPhonelineState() {
|
|
197
1135
|
const phonelinesResponse = await this.delegate.listPhonelines(this.context.userId);
|
|
198
|
-
return
|
|
199
|
-
|
|
1136
|
+
return {
|
|
1137
|
+
available: asObject(phonelinesResponse)?.phonelinesAvailable !== false,
|
|
1138
|
+
ids: new Set(items(phonelinesResponse)
|
|
1139
|
+
.map((phoneline) => stringField(phoneline, "id"))
|
|
1140
|
+
.filter((id) => Boolean(id))),
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
async assertOwnedPhoneline(phonelineId) {
|
|
1144
|
+
try {
|
|
1145
|
+
const owned = await this.ownedPhonelineState();
|
|
1146
|
+
if (!owned.available)
|
|
1147
|
+
return false;
|
|
1148
|
+
this.assertOwned(owned.ids, phonelineId, "phoneline");
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
if (error instanceof AccessPolicyError)
|
|
1153
|
+
throw error;
|
|
1154
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested phoneline.");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async assertOwnedParallelForwarding(phonelineId, parallelForwardingId) {
|
|
1158
|
+
if (!await this.assertOwnedPhoneline(phonelineId))
|
|
1159
|
+
return false;
|
|
1160
|
+
try {
|
|
1161
|
+
const response = await this.delegate.listParallelForwardings(this.context.userId, phonelineId);
|
|
1162
|
+
if (asObject(response)?.phonelinesAvailable === false)
|
|
1163
|
+
return false;
|
|
1164
|
+
const ids = new Set(items(response)
|
|
1165
|
+
.map((forwarding) => stringField(forwarding, "id"))
|
|
1166
|
+
.filter((id) => Boolean(id)));
|
|
1167
|
+
this.assertOwned(ids, parallelForwardingId, "parallel forwarding");
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
1170
|
+
catch (error) {
|
|
1171
|
+
if (error instanceof AccessPolicyError)
|
|
1172
|
+
throw error;
|
|
1173
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested parallel forwarding.");
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async assertOwnedVoicemail(phonelineId, voicemailId) {
|
|
1177
|
+
if (!await this.assertOwnedPhoneline(phonelineId))
|
|
1178
|
+
return false;
|
|
1179
|
+
try {
|
|
1180
|
+
const response = await this.delegate.listPhonelineVoicemails(this.context.userId, phonelineId);
|
|
1181
|
+
if (asObject(response)?.phonelinesAvailable === false)
|
|
1182
|
+
return false;
|
|
1183
|
+
const ids = new Set(items(response)
|
|
1184
|
+
.map((voicemail) => stringField(voicemail, "id"))
|
|
1185
|
+
.filter((id) => Boolean(id)));
|
|
1186
|
+
this.assertOwned(ids, voicemailId, "voicemail");
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
if (error instanceof AccessPolicyError)
|
|
1191
|
+
throw error;
|
|
1192
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested voicemail.");
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
async ownedVoicemailState() {
|
|
1196
|
+
try {
|
|
1197
|
+
const phonelines = await this.ownedPhonelineState();
|
|
1198
|
+
if (!phonelines.available)
|
|
1199
|
+
return { available: false, ids: new Set() };
|
|
1200
|
+
const responses = await Promise.all([...phonelines.ids].map((phonelineId) => this.delegate.listPhonelineVoicemails(this.context.userId, phonelineId)));
|
|
1201
|
+
if (responses.some((response) => asObject(response)?.phonelinesAvailable === false)) {
|
|
1202
|
+
return { available: false, ids: new Set() };
|
|
1203
|
+
}
|
|
1204
|
+
return {
|
|
1205
|
+
available: true,
|
|
1206
|
+
ids: new Set(responses.flatMap((response) => items(response)
|
|
1207
|
+
.map((voicemail) => stringField(voicemail, "id"))
|
|
1208
|
+
.filter((id) => Boolean(id)))),
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested voicemail.");
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async assertOwnedVoicemailId(voicemailId) {
|
|
1216
|
+
const owned = await this.ownedVoicemailState();
|
|
1217
|
+
if (!owned.available)
|
|
1218
|
+
return false;
|
|
1219
|
+
this.assertOwned(owned.ids, voicemailId, "voicemail");
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
async assertOwnedGreeting(phonelineId, voicemailId, greetingId) {
|
|
1223
|
+
if (!await this.assertOwnedVoicemail(phonelineId, voicemailId))
|
|
1224
|
+
return false;
|
|
1225
|
+
try {
|
|
1226
|
+
const response = await this.delegate.listVoicemailGreetings(this.context.userId, phonelineId, voicemailId);
|
|
1227
|
+
if (asObject(response)?.phonelinesAvailable === false)
|
|
1228
|
+
return false;
|
|
1229
|
+
const ids = new Set(items(response)
|
|
1230
|
+
.map((greeting) => stringField(greeting, "id"))
|
|
1231
|
+
.filter((id) => Boolean(id)));
|
|
1232
|
+
this.assertOwned(ids, greetingId, "voicemail greeting");
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
if (error instanceof AccessPolicyError)
|
|
1237
|
+
throw error;
|
|
1238
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested voicemail greeting.");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async ownedFaxlineIds() {
|
|
1242
|
+
const response = await this.delegate.listFaxlines(this.context.userId);
|
|
1243
|
+
return new Set(items(response)
|
|
1244
|
+
.map((faxline) => stringField(faxline, "id"))
|
|
200
1245
|
.filter((id) => Boolean(id)));
|
|
201
1246
|
}
|
|
1247
|
+
async assertOwnedAutorecordingExtension(extension) {
|
|
1248
|
+
try {
|
|
1249
|
+
// sipgate documents automated recording for register endpoints, so an
|
|
1250
|
+
// owned device is the primary case; phonelines and faxlines are accepted
|
|
1251
|
+
// for accounts that expose them.
|
|
1252
|
+
const [phonelines, faxlineIds, deviceIds] = await Promise.all([
|
|
1253
|
+
this.ownedPhonelineState(),
|
|
1254
|
+
this.ownedFaxlineIds(),
|
|
1255
|
+
this.ownedDeviceIds(),
|
|
1256
|
+
]);
|
|
1257
|
+
if (deviceIds.has(extension) || phonelines.ids.has(extension) || faxlineIds.has(extension))
|
|
1258
|
+
return;
|
|
1259
|
+
throw new AccessPolicyError("User scope does not permit access to the requested automated-recording extension.");
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
if (error instanceof AccessPolicyError)
|
|
1263
|
+
throw error;
|
|
1264
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested automated-recording extension.");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
202
1267
|
async ownedDeviceIds() {
|
|
203
1268
|
const devicesResponse = await this.delegate.listDevices(this.context.userId);
|
|
204
1269
|
return new Set(items(devicesResponse)
|
|
205
1270
|
.map((device) => stringField(device, "id"))
|
|
206
1271
|
.filter((id) => Boolean(id)));
|
|
207
1272
|
}
|
|
1273
|
+
async assertOwnedDevice(deviceId) {
|
|
1274
|
+
try {
|
|
1275
|
+
this.assertOwned(await this.ownedDeviceIds(), deviceId, "device");
|
|
1276
|
+
}
|
|
1277
|
+
catch (error) {
|
|
1278
|
+
if (error instanceof AccessPolicyError)
|
|
1279
|
+
throw error;
|
|
1280
|
+
throw new AccessPolicyError("User scope could not establish ownership of the requested device.");
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async assertOwnedNumberId(numberId, resource) {
|
|
1284
|
+
try {
|
|
1285
|
+
this.assertOwned((await this.ownedNumbers()).numberIds, numberId, resource);
|
|
1286
|
+
}
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
if (error instanceof AccessPolicyError)
|
|
1289
|
+
throw error;
|
|
1290
|
+
throw new AccessPolicyError(`User scope could not establish ownership of the requested ${resource}.`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async assertOwnedPhoneNumber(phoneNumber, resource) {
|
|
1294
|
+
try {
|
|
1295
|
+
this.assertOwned((await this.ownedNumbers()).phoneNumbers, phoneNumber, resource);
|
|
1296
|
+
}
|
|
1297
|
+
catch (error) {
|
|
1298
|
+
if (error instanceof AccessPolicyError)
|
|
1299
|
+
throw error;
|
|
1300
|
+
throw new AccessPolicyError(`User scope could not establish ownership of the requested ${resource}.`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async assertOwnedAddress(addressId) {
|
|
1304
|
+
try {
|
|
1305
|
+
if (await this.isOwnedAddress(addressId))
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
catch {
|
|
1309
|
+
// Ownership lookup failures are deliberately exposed as policy denials.
|
|
1310
|
+
}
|
|
1311
|
+
throw new AccessPolicyError("User scope does not permit access to the requested address because ownership could not be established.");
|
|
1312
|
+
}
|
|
1313
|
+
async isOwnedAddress(addressId) {
|
|
1314
|
+
return (await this.ownedAddressIdsFor([addressId])).has(String(addressId));
|
|
1315
|
+
}
|
|
1316
|
+
async ownedAddressIdsFor(addressIdsToCheck) {
|
|
1317
|
+
if (addressIdsToCheck.length === 0)
|
|
1318
|
+
return new Set();
|
|
1319
|
+
const expected = new Set(addressIdsToCheck.map(String));
|
|
1320
|
+
const [numbers, devicesResponse] = await Promise.all([
|
|
1321
|
+
this.ownedNumbers(),
|
|
1322
|
+
this.delegate.listDevices(this.context.userId),
|
|
1323
|
+
]);
|
|
1324
|
+
const owned = new Set([...numbers.addressIds].filter((addressId) => expected.has(addressId)));
|
|
1325
|
+
const devices = items(devicesResponse);
|
|
1326
|
+
for (const device of devices) {
|
|
1327
|
+
for (const addressId of addressIds(device)) {
|
|
1328
|
+
if (expected.has(addressId))
|
|
1329
|
+
owned.add(addressId);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
const detailedDevices = await Promise.allSettled(devices.map(async (device) => {
|
|
1333
|
+
const deviceId = stringField(device, "id");
|
|
1334
|
+
return deviceId ? this.delegate.getDevice(deviceId) : undefined;
|
|
1335
|
+
}));
|
|
1336
|
+
for (const result of detailedDevices) {
|
|
1337
|
+
if (result.status !== "fulfilled")
|
|
1338
|
+
continue;
|
|
1339
|
+
const device = asObject(result.value);
|
|
1340
|
+
if (!device)
|
|
1341
|
+
continue;
|
|
1342
|
+
for (const addressId of addressIds(device)) {
|
|
1343
|
+
if (expected.has(addressId))
|
|
1344
|
+
owned.add(addressId);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
const unresolved = addressIdsToCheck.filter((addressId) => !owned.has(String(addressId)));
|
|
1348
|
+
const addressNumberResponses = await Promise.all(unresolved.map(async (addressId) => ({
|
|
1349
|
+
addressId: String(addressId),
|
|
1350
|
+
response: await this.delegate.listAddressNumbers(addressId),
|
|
1351
|
+
})));
|
|
1352
|
+
for (const { addressId, response } of addressNumberResponses) {
|
|
1353
|
+
const containsOwnedNumber = items(response).some((number) => {
|
|
1354
|
+
const numberId = stringField(number, "id");
|
|
1355
|
+
const phoneNumber = stringField(number, "number");
|
|
1356
|
+
return (numberId !== undefined && numbers.numberIds.has(numberId))
|
|
1357
|
+
|| (phoneNumber !== undefined && numbers.phoneNumbers.has(phoneNumber));
|
|
1358
|
+
});
|
|
1359
|
+
if (containsOwnedNumber)
|
|
1360
|
+
owned.add(addressId);
|
|
1361
|
+
}
|
|
1362
|
+
return owned;
|
|
1363
|
+
}
|
|
1364
|
+
isOwnedNumber(number, numbers) {
|
|
1365
|
+
const numberId = stringField(number, "id");
|
|
1366
|
+
const phoneNumber = stringField(number, "number");
|
|
1367
|
+
return (numberId !== undefined && numbers.numberIds.has(numberId))
|
|
1368
|
+
|| (phoneNumber !== undefined && numbers.phoneNumbers.has(phoneNumber));
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Editing an address rewrites it for every number attached to it, so a
|
|
1372
|
+
* user-scope write is only safe when the user owns all of them.
|
|
1373
|
+
*/
|
|
1374
|
+
async assertExclusivelyOwnedAddress(addressId) {
|
|
1375
|
+
const [numbers, response] = await Promise.all([
|
|
1376
|
+
this.ownedNumbers(),
|
|
1377
|
+
this.delegate.listAddressNumbers(addressId),
|
|
1378
|
+
]);
|
|
1379
|
+
const attached = items(response);
|
|
1380
|
+
const foreign = attached.filter((number) => !this.isOwnedNumber(number, numbers));
|
|
1381
|
+
if (foreign.length === 0)
|
|
1382
|
+
return;
|
|
1383
|
+
throw new AccessPolicyError("User scope does not permit editing an address that is shared with numbers of other users.");
|
|
1384
|
+
}
|
|
208
1385
|
async ownedNumbers() {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
1386
|
+
const pageSize = 1000;
|
|
1387
|
+
const routed = [];
|
|
1388
|
+
for (let offset = 0;; offset += pageSize) {
|
|
1389
|
+
const page = items(await this.delegate.listUserNumbers(this.context.userId, { offset, limit: pageSize }));
|
|
1390
|
+
routed.push(...page);
|
|
1391
|
+
if (page.length < pageSize)
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
// Quick dials and other number types are listed by the direct user-number
|
|
1395
|
+
// endpoint but may never appear in phoneline or device routing.
|
|
1396
|
+
const direct = items(await this.delegate.getUserNumbers(this.context.userId));
|
|
1397
|
+
const numbers = [...routed, ...direct];
|
|
211
1398
|
return {
|
|
212
1399
|
numberIds: new Set(numbers.map((number) => stringField(number, "id")).filter((id) => Boolean(id))),
|
|
213
1400
|
phoneNumbers: new Set(numbers.map((number) => stringField(number, "number")).filter((number) => Boolean(number))),
|
|
1401
|
+
addressIds: new Set(numbers.flatMap((number) => addressIds(number))),
|
|
214
1402
|
};
|
|
215
1403
|
}
|
|
216
1404
|
}
|