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.
@@ -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
- const [numbers, phonelineIds] = await Promise.all([
122
- this.ownedNumbers(),
123
- this.ownedPhonelineIds(),
124
- ]);
125
- this.assertOwned(numbers.numberIds, numberId, "phone number");
126
- this.assertOwned(phonelineIds, endpointId, "destination phoneline");
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
- this.assertOwned(await this.ownedPhonelineIds(), phonelineId, "phoneline");
574
+ if (!await this.assertOwnedPhoneline(phonelineId))
575
+ return unavailablePhonelineMutation();
140
576
  }
141
577
  return this.delegate.setForwarding(userId, phonelineId, forwardings);
142
578
  }
143
- async setDnd(deviceId, enabled) {
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
- this.assertOwned(await this.ownedDeviceIds(), deviceId, "device");
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
- const [numbers, deviceIds] = await Promise.all([
157
- this.ownedNumbers(),
158
- this.ownedDeviceIds(),
159
- ]);
160
- const callerIsOwned = deviceIds.has(input.caller)
161
- || numbers.phoneNumbers.has(input.caller);
162
- if (!callerIsOwned) {
163
- throw new AccessPolicyError("User scope only permits calls from the authenticated user's devices or phone numbers.");
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 new Set(items(phonelinesResponse)
199
- .map((phoneline) => stringField(phoneline, "id"))
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 numbersResponse = await this.delegate.listUserNumbers(this.context.userId, { offset: 0, limit: 1000 });
210
- const numbers = items(numbersResponse);
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
  }