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.
@@ -1,5 +1,5 @@
1
1
  import { SipgateApiError, SipgateClient } from "./sipgate-client.js";
2
- const SENSITIVE_KEY = /authorization|credential|password|secret|token/i;
2
+ const SENSITIVE_KEY = /authorization|credential|password|secret|token|puk\d?|iccid/i;
3
3
  function sanitize(value) {
4
4
  if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
5
5
  return value;
@@ -17,13 +17,117 @@ function asItems(value) {
17
17
  const items = value.items;
18
18
  return Array.isArray(items) ? items : [];
19
19
  }
20
+ function asCalls(value) {
21
+ if (!value || Array.isArray(value) || typeof value !== "object")
22
+ return [];
23
+ return Array.isArray(value.data)
24
+ ? value.data.filter((item) => Boolean(item) && !Array.isArray(item) && typeof item === "object")
25
+ : [];
26
+ }
20
27
  function stringField(value, key) {
21
28
  const field = value[key];
22
29
  return typeof field === "string" ? field : undefined;
23
30
  }
31
+ /**
32
+ * Account-wide writes are confirmed at the access boundary, but SipgateBackend
33
+ * is exported on its own, so the invariant is repeated here rather than trusted.
34
+ */
35
+ function assertConfirmed(confirmed, action) {
36
+ if (confirmed === true)
37
+ return;
38
+ throw new SipgateApiError(`Refusing to ${action} without an explicit account-wide confirmation.`, 400);
39
+ }
40
+ /**
41
+ * Webhook URLs routinely carry their authentication in the query string, and
42
+ * the log returns them as plain strings that key-based redaction cannot see.
43
+ */
44
+ function stripUrlQueries(value) {
45
+ if (typeof value === "string") {
46
+ const separator = value.indexOf("?");
47
+ return value.startsWith("http") && separator !== -1
48
+ ? `${value.slice(0, separator)}?[REDACTED]`
49
+ : value;
50
+ }
51
+ if (Array.isArray(value))
52
+ return value.map(stripUrlQueries);
53
+ if (value !== null && typeof value === "object") {
54
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, stripUrlQueries(child)]));
55
+ }
56
+ return value;
57
+ }
24
58
  function encodeId(id) {
25
59
  return encodeURIComponent(id);
26
60
  }
61
+ function unavailableNote(status, subject) {
62
+ if (status === 403) {
63
+ return `sipgate denied access to ${subject} (HTTP 403). Either this account does not provide it, or the Personal Access Token lacks the scope for it.`;
64
+ }
65
+ if (status === 404) {
66
+ return `sipgate reports no ${subject} for this account (HTTP 404).`;
67
+ }
68
+ return `This sipgate account does not provide ${subject}.`;
69
+ }
70
+ function phonelineUnavailable(items = false, status) {
71
+ return {
72
+ ...(items ? { items: [] } : {}),
73
+ phonelinesAvailable: false,
74
+ ...(status === undefined ? {} : { httpStatus: status }),
75
+ note: unavailableNote(status, "the phoneline feature"),
76
+ };
77
+ }
78
+ function phonelineMutationUnavailable(status) {
79
+ return {
80
+ before: null,
81
+ after: {
82
+ changed: false,
83
+ phonelinesAvailable: false,
84
+ ...(status === undefined ? {} : { httpStatus: status }),
85
+ note: `${unavailableNote(status, "the phoneline feature")} No change was attempted.`,
86
+ },
87
+ };
88
+ }
89
+ function unavailableFeature(subject, status, items = false) {
90
+ return {
91
+ ...(items ? { items: [] } : {}),
92
+ available: false,
93
+ ...(status === undefined ? {} : { httpStatus: status }),
94
+ note: unavailableNote(status, subject),
95
+ };
96
+ }
97
+ function unavailableFeatureMutation(subject, status) {
98
+ return {
99
+ before: null,
100
+ after: {
101
+ changed: false,
102
+ ...unavailableFeature(subject, status),
103
+ note: `${unavailableNote(status, subject)} No change was attempted.`,
104
+ },
105
+ };
106
+ }
107
+ /**
108
+ * sipgate answers 403/404 for endpoints an account does not provide. Newer
109
+ * accounts have no phoneline layer at all: their numbers hang directly off a
110
+ * device. Treat those statuses as "feature absent" instead of a hard failure so
111
+ * a missing phoneline layer cannot take down number, history, and settings
112
+ * lookups that can be served from devices instead.
113
+ */
114
+ const UNAVAILABLE_STATUS = new Set([403, 404]);
115
+ async function optional(request) {
116
+ try {
117
+ return { value: await request, available: true };
118
+ }
119
+ catch (error) {
120
+ if (error instanceof SipgateApiError
121
+ && error.status !== undefined
122
+ && UNAVAILABLE_STATUS.has(error.status)) {
123
+ return { available: false, status: error.status };
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+ function idSet(items) {
129
+ return new Set(items.map((item) => stringField(item, "id")).filter((id) => Boolean(id)));
130
+ }
27
131
  export class SipgateBackend {
28
132
  client;
29
133
  constructor(client) {
@@ -61,11 +165,120 @@ export class SipgateBackend {
61
165
  return sanitize({ items, pagination: { offset, limit, returned: items.length } });
62
166
  }
63
167
  async listPhonelines(userId) {
64
- const response = await this.client.request(`/${encodeId(userId)}/phonelines`);
168
+ const { value, available } = await this.tryListPhonelines(userId);
169
+ if (!available)
170
+ return { items: [], phonelinesAvailable: false };
171
+ return sanitize(value ?? { items: [] });
172
+ }
173
+ tryListPhonelines(userId) {
174
+ return optional(this.client.request(`/${encodeId(userId)}/phonelines`));
175
+ }
176
+ phonelinePath(userId, phonelineId, suffix = "") {
177
+ return `/${encodeId(userId)}/phonelines/${encodeId(phonelineId)}${suffix}`;
178
+ }
179
+ async optionalPhonelineRead(path, list = false) {
180
+ const { value, available, status } = await optional(this.client.request(path));
181
+ return available ? sanitize(value ?? (list ? { items: [] } : {})) : phonelineUnavailable(list, status);
182
+ }
183
+ getPhoneline(userId, phonelineId) {
184
+ return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId));
185
+ }
186
+ getPhonelineBlockAnonymous(userId, phonelineId) {
187
+ return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, "/blockanonymous"));
188
+ }
189
+ listPhonelineDevices(userId, phonelineId) {
190
+ return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, "/devices"), true);
191
+ }
192
+ listParallelForwardings(userId, phonelineId) {
193
+ return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, "/parallelforwardings"), true);
194
+ }
195
+ listPhonelineVoicemails(userId, phonelineId) {
196
+ return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, "/voicemails"), true);
197
+ }
198
+ listVoicemailGreetings(userId, phonelineId, voicemailId) {
199
+ return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, `/voicemails/${encodeId(voicemailId)}/greetings`), true);
200
+ }
201
+ async listVoicemails() {
202
+ const response = await this.client.request("/voicemails");
65
203
  return sanitize(response ?? { items: [] });
66
204
  }
205
+ async getVoicemail(voicemailId) {
206
+ const response = await this.client.request(`/voicemails/${encodeId(voicemailId)}`);
207
+ return sanitize(response ?? {});
208
+ }
209
+ async listAutorecordingGreetings() {
210
+ const { value, available } = await optional(this.client.request("/autorecordings/greetings"));
211
+ return available
212
+ ? sanitize(value ?? {})
213
+ : {
214
+ autorecordingsAvailable: false,
215
+ note: "Automated call recording is not activated for this sipgate account.",
216
+ };
217
+ }
218
+ async getAutorecordingSettings(extension) {
219
+ const { value, available } = await optional(this.client.request(`/autorecordings/${encodeId(extension)}/settings`));
220
+ return available
221
+ ? sanitize(value ?? {})
222
+ : {
223
+ autorecordingsAvailable: false,
224
+ note: "Automated call recording is not activated for this extension.",
225
+ };
226
+ }
227
+ async listDeviceIds(userId) {
228
+ const response = await this.client.request(`/${encodeId(userId)}/devices`);
229
+ return idSet(asItems(response));
230
+ }
231
+ /**
232
+ * Fallback for accounts without a phoneline layer: numbers are matched to the
233
+ * user through the device their endpoint points at.
234
+ */
235
+ async listAllAccountNumbers() {
236
+ const pageSize = 1000;
237
+ const all = [];
238
+ for (let offset = 0;; offset += pageSize) {
239
+ const response = await this.client.request("/numbers", {
240
+ query: { offset, limit: pageSize },
241
+ });
242
+ const page = asItems(response);
243
+ all.push(...page);
244
+ const envelope = response && !Array.isArray(response) && typeof response === "object"
245
+ ? response
246
+ : {};
247
+ const totalCount = typeof envelope.totalCount === "number" ? envelope.totalCount : undefined;
248
+ // sipgate may clamp the requested limit, so advance by what it actually
249
+ // returned and stop once a page comes back empty or the count is reached.
250
+ if (page.length === 0)
251
+ break;
252
+ if (totalCount !== undefined && all.length >= totalCount)
253
+ break;
254
+ if (page.length < pageSize)
255
+ break;
256
+ }
257
+ return all;
258
+ }
259
+ async listDeviceNumbers(userId, { offset, limit }, phonelinesAvailable) {
260
+ const [deviceIds, accountNumbers] = await Promise.all([
261
+ this.listDeviceIds(userId),
262
+ this.listAllAccountNumbers(),
263
+ ]);
264
+ const owned = accountNumbers.filter((number) => {
265
+ const endpointId = stringField(number, "endpointId");
266
+ return endpointId !== undefined && deviceIds.has(endpointId);
267
+ });
268
+ const page = owned.slice(offset, offset + limit);
269
+ return sanitize({
270
+ items: page,
271
+ pagination: { offset, limit, returned: page.length, totalCount: owned.length },
272
+ source: "devices",
273
+ phonelinesAvailable,
274
+ });
275
+ }
67
276
  async listUserNumbers(userId, { offset, limit }) {
68
- const phonelines = asItems(await this.listPhonelines(userId));
277
+ const { value: phonelinesValue, available } = await this.tryListPhonelines(userId);
278
+ const phonelines = asItems(phonelinesValue);
279
+ if (!available || phonelines.length === 0) {
280
+ return this.listDeviceNumbers(userId, { offset, limit }, available);
281
+ }
69
282
  const numberGroups = await Promise.all(phonelines.map(async (phoneline) => {
70
283
  const phonelineId = stringField(phoneline, "id");
71
284
  if (!phonelineId)
@@ -87,6 +300,10 @@ export class SipgateBackend {
87
300
  pagination: { offset, limit, returned: page.length, totalCount: uniqueNumbers.length },
88
301
  });
89
302
  }
303
+ async getUserNumbers(userId) {
304
+ const response = await this.client.request(`/${encodeId(userId)}/numbers`);
305
+ return sanitize(response ?? { items: [] });
306
+ }
90
307
  async listDevices(userId, types) {
91
308
  const userIds = userId ? [userId] : asItems(await this.client.request("/users"))
92
309
  .map((user) => stringField(user, "id"))
@@ -99,6 +316,104 @@ export class SipgateBackend {
99
316
  const items = responses.flatMap(({ userId: ownerId, response }) => asItems(response).map((device) => ({ ...device, userId: ownerId })));
100
317
  return sanitize({ items });
101
318
  }
319
+ async getDevice(deviceId) {
320
+ const response = await this.client.request(`/devices/${encodeId(deviceId)}`);
321
+ return sanitize(response ?? {});
322
+ }
323
+ async getDeviceCallerId(deviceId) {
324
+ const response = await this.client.request(`/devices/${encodeId(deviceId)}/callerid`);
325
+ return sanitize(response ?? {});
326
+ }
327
+ async getDeviceLocalPrefix(deviceId) {
328
+ const response = await this.client.request(`/devices/${encodeId(deviceId)}/localprefix`);
329
+ return sanitize(response ?? {});
330
+ }
331
+ async getDeviceTariffAnnouncement(deviceId) {
332
+ const response = await this.client.request(`/devices/${encodeId(deviceId)}/tariffannouncement`);
333
+ return sanitize(response ?? {});
334
+ }
335
+ async getDeviceSingleRowDisplay(deviceId) {
336
+ const response = await this.client.request(`/devices/${encodeId(deviceId)}/singlerowdisplay`);
337
+ return sanitize(response ?? {});
338
+ }
339
+ async getDeviceContingents(userId, deviceId) {
340
+ const response = await this.client.request(`/${encodeId(userId)}/devices/${encodeId(deviceId)}/contingents`);
341
+ return sanitize(response ?? { contingents: [] });
342
+ }
343
+ async listAddresses() {
344
+ const response = await this.client.request("/addresses");
345
+ return sanitize(response ?? { items: [] });
346
+ }
347
+ async getAddress(addressId) {
348
+ const response = await this.client.request(`/addresses/${addressId}`);
349
+ return sanitize(response ?? {});
350
+ }
351
+ async listAddressNumbers(addressId) {
352
+ const response = await this.client.request(`/addresses/${addressId}/numbers`);
353
+ return sanitize(response ?? { items: [] });
354
+ }
355
+ async validateQuickDialNumber(quickDialNumber) {
356
+ const response = await this.client.request(`/numbers/quickdial/validation/${encodeId(quickDialNumber)}`);
357
+ return sanitize(response ?? {});
358
+ }
359
+ async listContacts(query) {
360
+ const response = await this.client.request("/contacts", {
361
+ query: {
362
+ phonenumbers: query.phoneNumbers,
363
+ limit: query.limit,
364
+ offset: query.offset,
365
+ lastId: query.lastId,
366
+ scopes: query.scopes,
367
+ },
368
+ });
369
+ return sanitize(response ?? { items: [], totalCount: 0 });
370
+ }
371
+ async getContact(contactId) {
372
+ const response = await this.client.request(`/contacts/${encodeId(contactId)}`);
373
+ return sanitize(response ?? {});
374
+ }
375
+ async listInternalContacts() {
376
+ const response = await this.client.request("/contacts/internal");
377
+ return sanitize(response ?? { items: [] });
378
+ }
379
+ async exportContactsCsv(scopes) {
380
+ const content = await this.client.requestText("/contacts/csv", {
381
+ query: { scope: scopes },
382
+ accept: "text/csv",
383
+ });
384
+ return { content, contentType: "text/csv" };
385
+ }
386
+ async getContactsVcard(query) {
387
+ const response = await this.client.request("/contacts/vcard", {
388
+ query: {
389
+ scope: query.scopes,
390
+ labels: query.labels,
391
+ contactIds: query.contactIds,
392
+ wantedFields: query.wantedFields,
393
+ filter: query.filter,
394
+ limit: query.limit,
395
+ offset: query.offset,
396
+ lastId: query.lastId,
397
+ },
398
+ });
399
+ return sanitize(response ?? { contacts: [], overallMatches: 0 });
400
+ }
401
+ async listIncomingBlacklist() {
402
+ const response = await this.client.request("/blacklist/incoming");
403
+ return sanitize(response ?? { items: [] });
404
+ }
405
+ async getCallRestrictions(userIds) {
406
+ const response = await this.client.request("/callrestrictions", {
407
+ query: { userIds },
408
+ });
409
+ return sanitize(response ?? {});
410
+ }
411
+ async getRestrictions(userId, restrictions) {
412
+ const response = await this.client.request("/restrictions", {
413
+ query: { userId, restriction: restrictions },
414
+ });
415
+ return sanitize(response ?? { items: [] });
416
+ }
102
417
  async getRouting(userId) {
103
418
  const [numbersResponse, usersResponse] = await Promise.all([
104
419
  userId
@@ -110,8 +425,8 @@ export class SipgateBackend {
110
425
  .map((user) => stringField(user, "id"))
111
426
  .filter((id) => Boolean(id));
112
427
  const users = await Promise.all(userIds.map(async (id) => {
113
- const phonelinesResponse = await this.listPhonelines(id);
114
- const phonelines = await Promise.all(asItems(phonelinesResponse).map(async (phoneline) => {
428
+ const phonelinesResult = await this.tryListPhonelines(id);
429
+ const phonelines = await Promise.all(asItems(phonelinesResult.value).map(async (phoneline) => {
115
430
  const phonelineId = stringField(phoneline, "id");
116
431
  if (!phonelineId)
117
432
  return { ...phoneline, numbers: [], forwardings: [] };
@@ -130,10 +445,13 @@ export class SipgateBackend {
130
445
  forwardings: asItems(forwardings),
131
446
  };
132
447
  }));
133
- return { userId: id, phonelines };
448
+ return { userId: id, phonelines, phonelinesAvailable: phonelinesResult.available };
134
449
  }));
450
+ const routedNumbers = users.flatMap(({ phonelines }) => phonelines.flatMap((phoneline) => phoneline.numbers));
135
451
  const numbers = userId
136
- ? users.flatMap(({ phonelines }) => phonelines.flatMap((phoneline) => phoneline.numbers))
452
+ ? (routedNumbers.length > 0
453
+ ? routedNumbers
454
+ : asItems(await this.listUserNumbers(userId, { offset: 0, limit: 1000 })))
137
455
  : asItems(numbersResponse);
138
456
  const uniqueNumbers = [...new Map(numbers.map((number) => [
139
457
  stringField(number, "id") ?? stringField(number, "number") ?? JSON.stringify(number),
@@ -141,6 +459,9 @@ export class SipgateBackend {
141
459
  ])).values()];
142
460
  return sanitize({ numbers: uniqueNumbers, users });
143
461
  }
462
+ async getHistoryEntry(entryId) {
463
+ return sanitize(await this.client.request(`/history/${encodeId(entryId)}`) ?? {});
464
+ }
144
465
  async getCallHistory(query) {
145
466
  const response = await this.client.request("/history", {
146
467
  query: {
@@ -152,6 +473,8 @@ export class SipgateBackend {
152
473
  from: query.from,
153
474
  to: query.to,
154
475
  phonenumber: query.phoneNumber,
476
+ archived: query.archived,
477
+ starred: query.starred,
155
478
  },
156
479
  });
157
480
  const object = response && !Array.isArray(response) && typeof response === "object" ? response : {};
@@ -163,6 +486,43 @@ export class SipgateBackend {
163
486
  pagination: { offset: query.offset, limit: query.limit, totalCount, nextOffset },
164
487
  });
165
488
  }
489
+ async exportHistory(query) {
490
+ const content = await this.client.requestText("/history/export", {
491
+ query: {
492
+ connectionIds: query.connectionIds,
493
+ types: query.types,
494
+ directions: query.directions,
495
+ offset: query.offset,
496
+ limit: query.limit,
497
+ archived: query.archived,
498
+ starred: query.starred,
499
+ from: query.from,
500
+ to: query.to,
501
+ },
502
+ accept: "application/octet-stream",
503
+ });
504
+ return { content, contentType: "text/csv" };
505
+ }
506
+ async listCalls() {
507
+ const response = await this.client.request("/calls");
508
+ return sanitize(response ?? { data: [] });
509
+ }
510
+ async listNotifications(userId) {
511
+ const response = await this.client.request(`/${encodeId(userId)}/notifications`);
512
+ return sanitize(response ?? { call: [], fax: [], sms: [], voicemail: [] });
513
+ }
514
+ async listFaxlines(userId) {
515
+ const response = await this.client.request(`/${encodeId(userId)}/faxlines`);
516
+ return sanitize(response ?? { items: [] });
517
+ }
518
+ async listFaxlineNumbers(userId, faxlineId) {
519
+ const response = await this.client.request(`/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}/numbers`);
520
+ return sanitize(response ?? { items: [] });
521
+ }
522
+ async getFaxlineCallerId(userId, faxlineId) {
523
+ const response = await this.client.request(`/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}/callerid`);
524
+ return sanitize(response ?? {});
525
+ }
166
526
  async getSettings(userId) {
167
527
  const users = userId
168
528
  ? [await this.getUser(userId)]
@@ -174,11 +534,11 @@ export class SipgateBackend {
174
534
  const id = stringField(user, "id");
175
535
  if (!id)
176
536
  return null;
177
- const [devicesResponse, phonelinesResponse] = await Promise.all([
537
+ const [devicesResponse, phonelinesResult] = await Promise.all([
178
538
  this.client.request(`/${encodeId(id)}/devices`),
179
- this.client.request(`/${encodeId(id)}/phonelines`),
539
+ this.tryListPhonelines(id),
180
540
  ]);
181
- const phonelines = await Promise.all(asItems(phonelinesResponse).map(async (line) => {
541
+ const phonelines = await Promise.all(asItems(phonelinesResult.value).map(async (line) => {
182
542
  const lineId = stringField(line, "id");
183
543
  if (!lineId)
184
544
  return line;
@@ -192,10 +552,304 @@ export class SipgateBackend {
192
552
  devices: asItems(devicesResponse),
193
553
  },
194
554
  phonelines,
555
+ phonelinesAvailable: phonelinesResult.available,
195
556
  };
196
557
  }));
197
558
  return sanitize({ users: settings.filter((item) => item !== null) });
198
559
  }
560
+ async getBalance() {
561
+ const response = await this.client.request("/balance");
562
+ return sanitize(response ?? {});
563
+ }
564
+ async listPortings() {
565
+ const response = await this.client.request("/portings");
566
+ return sanitize(response ?? { items: [] });
567
+ }
568
+ async getPorting(portingId) {
569
+ const response = await this.client.request(`/portings/${portingId}`);
570
+ return sanitize(response ?? {});
571
+ }
572
+ async getSipgateIoSettings() {
573
+ const { value, available, status } = await optional(this.client.request("/settings/sipgateio"));
574
+ return available
575
+ ? sanitize(value ?? {})
576
+ : unavailableFeature("the account-wide sipgate.io settings feature", status);
577
+ }
578
+ async listWebhookLogs() {
579
+ const { value, available, status } = await optional(this.client.request("/log/webhooks"));
580
+ if (!available) {
581
+ return unavailableFeature("the account-wide sipgate.io webhook log", status, true);
582
+ }
583
+ const logs = sanitize(value ?? { items: [] });
584
+ return stripUrlQueries(logs);
585
+ }
586
+ async createContact(input, confirmAccountWide) {
587
+ assertConfirmed(confirmAccountWide, "create an account-wide contact");
588
+ const response = await this.client.request("/contacts", {
589
+ method: "POST",
590
+ body: this.contactBody(input),
591
+ });
592
+ return {
593
+ before: null,
594
+ after: sanitize({
595
+ created: true,
596
+ response: response ?? null,
597
+ note: "sipgate does not return the created contact ID, so no exact post-create read-back is possible.",
598
+ }),
599
+ };
600
+ }
601
+ async updateContact(contactId, input, confirmAccountWide) {
602
+ assertConfirmed(confirmAccountWide, "change an account-wide contact");
603
+ const path = `/contacts/${encodeId(contactId)}`;
604
+ const before = await this.client.request(path);
605
+ await this.client.request(path, {
606
+ method: "PUT",
607
+ body: this.contactBody(input),
608
+ });
609
+ const after = await this.client.request(path);
610
+ return { before: sanitize(before ?? {}), after: sanitize(after ?? {}) };
611
+ }
612
+ async deleteContact(contactId, scopes, confirmAccountWide) {
613
+ assertConfirmed(confirmAccountWide, "delete an account-wide contact");
614
+ const before = await this.client.request(`/contacts/${encodeId(contactId)}`);
615
+ const response = await this.client.request(`/contacts/${encodeId(contactId)}`, { method: "DELETE", query: { scope: scopes } });
616
+ return {
617
+ before: sanitize(before ?? {}),
618
+ after: sanitize({
619
+ deleted: true,
620
+ response: response ?? null,
621
+ note: "sipgate exposes no read-back for a deleted contact.",
622
+ }),
623
+ };
624
+ }
625
+ async deleteContacts(input, confirmAccountWide) {
626
+ assertConfirmed(confirmAccountWide, "delete account-wide contacts");
627
+ const before = input.contactIds && input.contactIds.length > 0
628
+ ? await this.readContacts(input.contactIds)
629
+ : input.source === undefined
630
+ ? await this.listAllContacts(input.scope?.flatMap((scope) => scope === "PRIVATE,SHARED" ? ["PRIVATE", "SHARED"] : [scope]))
631
+ : null;
632
+ const response = await this.client.request("/contacts", {
633
+ method: "DELETE",
634
+ body: {
635
+ ...(input.contactIds === undefined ? {} : { contactIds: input.contactIds }),
636
+ ...(input.scope === undefined ? {} : { scope: input.scope }),
637
+ ...(input.source === undefined ? {} : { source: input.source }),
638
+ },
639
+ });
640
+ return {
641
+ before: sanitize(before),
642
+ after: sanitize({
643
+ deleted: true,
644
+ response: response ?? null,
645
+ note: input.source !== undefined && !input.contactIds
646
+ ? "sipgate's contact read endpoint has no source filter, so before is null for a source-selected delete; deleted contacts have no read-back."
647
+ : "sipgate exposes no read-back for contacts removed by the bulk-delete endpoint.",
648
+ }),
649
+ };
650
+ }
651
+ async importContactsCsv(base64Content, confirmAccountWide) {
652
+ assertConfirmed(confirmAccountWide, "import account-wide contacts");
653
+ const before = await this.listAllContacts();
654
+ await this.client.request("/contacts/import/csv", {
655
+ method: "POST",
656
+ body: { base64Content },
657
+ });
658
+ const after = await this.listAllContacts();
659
+ return { before, after };
660
+ }
661
+ async putContactsVcard(scope, data, confirmAccountWide) {
662
+ assertConfirmed(confirmAccountWide, "replace account-wide contacts");
663
+ const contactIds = data
664
+ .map((entry) => entry.contactId)
665
+ .filter((contactId) => Boolean(contactId));
666
+ const beforeQuery = {
667
+ scopes: [scope],
668
+ ...(contactIds.length === 0 ? {} : { contactIds }),
669
+ };
670
+ const before = contactIds.length === 0 ? null : await this.getContactsVcard(beforeQuery);
671
+ const response = await this.client.request("/contacts/vcard", {
672
+ method: "PUT",
673
+ query: { scope },
674
+ body: { data: sanitize(data) },
675
+ });
676
+ const responseObject = response && !Array.isArray(response) && typeof response === "object"
677
+ ? response
678
+ : {};
679
+ const resultItems = Array.isArray(responseObject.result)
680
+ ? responseObject.result.filter((item) => Boolean(item) && !Array.isArray(item) && typeof item === "object")
681
+ : [];
682
+ const resultIds = resultItems
683
+ .map((entry) => stringField(entry, "contactId"))
684
+ .filter((contactId) => Boolean(contactId));
685
+ const readbackIds = [...new Set([...contactIds, ...resultIds])];
686
+ const after = await this.getContactsVcard({
687
+ scopes: [scope],
688
+ ...(readbackIds.length === 0 ? {} : { contactIds: readbackIds }),
689
+ });
690
+ return {
691
+ before,
692
+ after: sanitize({
693
+ contacts: after,
694
+ response: response ?? null,
695
+ ...(contactIds.length === data.length ? {} : {
696
+ note: "New vCards had no before-state; sipgate's result IDs were used for post-write read-back when available.",
697
+ }),
698
+ }),
699
+ };
700
+ }
701
+ async addIncomingBlacklist(phoneNumber, isBlock, confirmAccountWide) {
702
+ assertConfirmed(confirmAccountWide, "change the account-wide incoming blocklist");
703
+ const before = await this.listIncomingBlacklist();
704
+ await this.client.request("/blacklist/incoming", {
705
+ method: "POST",
706
+ body: { phoneNumber, ...(isBlock === undefined ? {} : { isBlock }) },
707
+ });
708
+ const after = await this.listIncomingBlacklist();
709
+ return { before, after };
710
+ }
711
+ async removeIncomingBlacklist(phoneNumber, confirmAccountWide) {
712
+ assertConfirmed(confirmAccountWide, "change the account-wide incoming blocklist");
713
+ const entries = asItems(await this.listIncomingBlacklist());
714
+ const normalized = phoneNumber.startsWith("+") ? phoneNumber : `+${phoneNumber}`;
715
+ const before = entries.find((entry) => stringField(entry, "phoneNumber") === normalized) ?? null;
716
+ const response = await this.client.request(`/blacklist/incoming/${encodeId(phoneNumber)}`, { method: "DELETE" });
717
+ return {
718
+ before: sanitize(before),
719
+ after: sanitize({
720
+ deleted: true,
721
+ response: response ?? null,
722
+ note: "sipgate exposes no read-back for a deleted incoming blacklist entry.",
723
+ }),
724
+ };
725
+ }
726
+ async setCallRestriction(restriction, enabled) {
727
+ const { userId } = await this.getAuthenticatedUser();
728
+ const before = await this.getCallRestrictions([userId]);
729
+ await this.client.request(`/${encodeId(userId)}/callrestrictions/${encodeId(restriction)}`, {
730
+ method: "POST",
731
+ body: { ...(enabled === undefined ? {} : { enabled }) },
732
+ });
733
+ const after = await this.getCallRestrictions([userId]);
734
+ return { before, after };
735
+ }
736
+ setHistoryRead(entryId, value) {
737
+ return this.mutateHistoryWithReadback(entryId, `/history/${encodeId(entryId)}/read`, { ...(value === undefined ? {} : { value }) });
738
+ }
739
+ setHistoryNote(entryId, note) {
740
+ return this.mutateHistoryWithReadback(entryId, `/history/${encodeId(entryId)}/note`, { note });
741
+ }
742
+ setHistoryArchive(entryId, value) {
743
+ return this.mutateHistoryWithReadback(entryId, `/history/${encodeId(entryId)}/archive`, { ...(value === undefined ? {} : { value }) });
744
+ }
745
+ updateHistoryEntry(entryId, input) {
746
+ return this.mutateHistoryWithReadback(entryId, `/history/${encodeId(entryId)}`, {
747
+ ...(input.archived === undefined ? {} : { archived: input.archived }),
748
+ ...(input.note === undefined ? {} : { note: input.note }),
749
+ ...(input.read === undefined ? {} : { read: input.read }),
750
+ ...(input.starred === undefined ? {} : { starred: input.starred }),
751
+ });
752
+ }
753
+ async deleteHistoryEntry(entryId) {
754
+ const before = await this.client.request(`/history/${encodeId(entryId)}`);
755
+ const response = await this.client.request(`/history/${encodeId(entryId)}`, {
756
+ method: "DELETE",
757
+ });
758
+ return {
759
+ before: sanitize(before ?? {}),
760
+ after: sanitize({
761
+ deleted: true,
762
+ response: response ?? null,
763
+ note: "History deletion is permanent; sipgate exposes no deleted-entry read-back.",
764
+ }),
765
+ };
766
+ }
767
+ assertBulkHistoryLimit(count) {
768
+ if (count < 150)
769
+ return;
770
+ throw new SipgateApiError("sipgate accepts fewer than 150 history entries per bulk update.", 400);
771
+ }
772
+ async updateHistoryEntries(inputs) {
773
+ this.assertBulkHistoryLimit(inputs.length);
774
+ const before = await this.readHistoryEntries(inputs.map((input) => input.id));
775
+ await this.client.request("/history", {
776
+ method: "PUT",
777
+ body: inputs.map((input) => ({
778
+ id: input.id,
779
+ ...(input.archived === undefined ? {} : { archived: input.archived }),
780
+ ...(input.read === undefined ? {} : { read: input.read }),
781
+ ...(input.starred === undefined ? {} : { starred: input.starred }),
782
+ })),
783
+ });
784
+ const after = await this.readHistoryEntries(inputs.map((input) => input.id));
785
+ return { before: sanitize(before), after: sanitize(after) };
786
+ }
787
+ async deleteHistoryEntries(entryIds) {
788
+ if (entryIds !== undefined && entryIds.length === 0) {
789
+ // An empty list serializes to no query parameter at all, which sipgate
790
+ // reads as "delete the entire account history". Refuse it outright.
791
+ throw new SipgateApiError("Refusing to delete history with an empty entry list: omit the list deliberately to target the whole account.", 400);
792
+ }
793
+ const before = entryIds === undefined
794
+ ? await this.listAllHistoryEntries()
795
+ : await this.readHistoryEntries(entryIds);
796
+ const response = await this.client.request("/history", {
797
+ method: "DELETE",
798
+ query: { id: entryIds },
799
+ });
800
+ return {
801
+ before: sanitize(before),
802
+ after: sanitize({
803
+ deleted: true,
804
+ entryIds: entryIds ?? null,
805
+ response: response ?? null,
806
+ note: "History deletion is permanent; sipgate exposes no deleted-entry read-back.",
807
+ }),
808
+ };
809
+ }
810
+ async cancelPorting(portingId, confirmAccountWide) {
811
+ assertConfirmed(confirmAccountWide, "cancel a number porting");
812
+ const before = await this.getPorting(portingId);
813
+ const response = await this.client.request(`/portings/${portingId}`, {
814
+ method: "DELETE",
815
+ });
816
+ return {
817
+ before,
818
+ after: sanitize({
819
+ cancelled: true,
820
+ response: response ?? null,
821
+ note: "Cancelling a number porting is irreversible through the sipgate v2 API.",
822
+ }),
823
+ };
824
+ }
825
+ async updateSipgateIoSettings(input, confirmAccountWide) {
826
+ assertConfirmed(confirmAccountWide, "change the account-wide sipgate.io settings");
827
+ const before = await optional(this.client.request("/settings/sipgateio"));
828
+ if (!before.available) {
829
+ return unavailableFeatureMutation("the account-wide sipgate.io settings feature", before.status);
830
+ }
831
+ await this.client.request("/settings/sipgateio", {
832
+ method: "PUT",
833
+ body: {
834
+ incomingUrl: input.incomingUrl,
835
+ outgoingUrl: input.outgoingUrl,
836
+ ...(input.log === undefined ? {} : { log: input.log }),
837
+ ...(input.pushApiVersion === undefined ? {} : { pushApiVersion: input.pushApiVersion }),
838
+ ...(input.whitelist === undefined ? {} : { whitelist: input.whitelist }),
839
+ },
840
+ });
841
+ const after = await optional(this.client.request("/settings/sipgateio"));
842
+ return {
843
+ before: sanitize(before.value ?? {}),
844
+ after: after.available
845
+ ? sanitize(after.value ?? {})
846
+ : {
847
+ ...unavailableFeature("the account-wide sipgate.io settings feature", after.status),
848
+ changed: true,
849
+ note: "The change was accepted, but sipgate denied the settings read-back.",
850
+ },
851
+ };
852
+ }
199
853
  async setNumberRouting(numberId, endpointId) {
200
854
  const before = await this.findNumber(numberId);
201
855
  await this.client.request(`/numbers/${encodeId(numberId)}`, {
@@ -224,6 +878,209 @@ export class SipgateBackend {
224
878
  const after = await this.client.request(path);
225
879
  return { before: sanitize(before ?? { items: [] }), after: sanitize(after ?? { items: [] }) };
226
880
  }
881
+ async createPhoneline(userId) {
882
+ const { value, available } = await optional(this.client.request(`/${encodeId(userId)}/phonelines`, { method: "POST" }));
883
+ if (!available)
884
+ return phonelineMutationUnavailable();
885
+ return {
886
+ before: null,
887
+ after: sanitize({
888
+ phoneline: value ?? {},
889
+ created: true,
890
+ note: "No phoneline existed before this create operation; the returned phoneline is the initial state.",
891
+ }),
892
+ };
893
+ }
894
+ updatePhonelineAlias(userId, phonelineId, alias) {
895
+ const path = this.phonelinePath(userId, phonelineId);
896
+ return this.mutateOptionalPhonelineWithReadback(path, path, "PUT", { ...(alias === undefined ? {} : { alias }) });
897
+ }
898
+ async deletePhoneline(userId, phonelineId) {
899
+ const path = this.phonelinePath(userId, phonelineId);
900
+ const { value: before, available } = await optional(this.client.request(path));
901
+ if (!available)
902
+ return phonelineMutationUnavailable();
903
+ const response = await this.client.request(path, { method: "DELETE" });
904
+ return {
905
+ before: sanitize(before ?? {}),
906
+ after: sanitize({
907
+ deleted: true,
908
+ response: response ?? null,
909
+ note: "sipgate does not provide a deleted-phoneline read-back endpoint.",
910
+ }),
911
+ };
912
+ }
913
+ setPhonelineBlockAnonymous(userId, phonelineId, input) {
914
+ const path = this.phonelinePath(userId, phonelineId, "/blockanonymous");
915
+ return this.mutateOptionalPhonelineWithReadback(path, path, "PUT", {
916
+ ...(input.enabled === undefined ? {} : { enabled: input.enabled }),
917
+ ...(input.target === undefined ? {} : { target: input.target }),
918
+ });
919
+ }
920
+ attachDeviceToPhoneline(userId, phonelineId, deviceId) {
921
+ const path = this.phonelinePath(userId, phonelineId, "/devices");
922
+ return this.mutateOptionalPhonelineWithReadback(path, path, "POST", { deviceId }, true);
923
+ }
924
+ detachDeviceFromPhoneline(userId, phonelineId, deviceId) {
925
+ const readPath = this.phonelinePath(userId, phonelineId, "/devices");
926
+ return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(deviceId)}`, "DELETE", undefined, true);
927
+ }
928
+ createParallelForwarding(userId, phonelineId, input) {
929
+ const path = this.phonelinePath(userId, phonelineId, "/parallelforwardings");
930
+ return this.mutateOptionalPhonelineWithReadback(path, path, "POST", this.parallelForwardingBody(input), true);
931
+ }
932
+ updateParallelForwarding(userId, phonelineId, parallelForwardingId, input) {
933
+ const readPath = this.phonelinePath(userId, phonelineId, "/parallelforwardings");
934
+ return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(parallelForwardingId)}`, "PUT", this.parallelForwardingBody(input), true);
935
+ }
936
+ deleteParallelForwarding(userId, phonelineId, parallelForwardingId) {
937
+ const readPath = this.phonelinePath(userId, phonelineId, "/parallelforwardings");
938
+ return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(parallelForwardingId)}`, "DELETE", undefined, true);
939
+ }
940
+ updateVoicemail(userId, phonelineId, voicemailId, input) {
941
+ const readPath = this.phonelinePath(userId, phonelineId, "/voicemails");
942
+ return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(voicemailId)}`, "PUT", {
943
+ active: input.active,
944
+ transcription: input.transcription,
945
+ ...(input.timeout === undefined ? {} : { timeout: input.timeout }),
946
+ }, true);
947
+ }
948
+ createVoicemailGreeting(userId, phonelineId, voicemailId, input) {
949
+ const path = this.phonelinePath(userId, phonelineId, `/voicemails/${encodeId(voicemailId)}/greetings`);
950
+ return this.mutateOptionalPhonelineWithReadback(path, path, "POST", this.greetingBody(input), true);
951
+ }
952
+ updateVoicemailGreeting(userId, phonelineId, voicemailId, greetingId, active) {
953
+ const readPath = this.phonelinePath(userId, phonelineId, `/voicemails/${encodeId(voicemailId)}/greetings`);
954
+ return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(greetingId)}`, "PUT", { ...(active === undefined ? {} : { active }) }, true);
955
+ }
956
+ deleteVoicemailGreeting(userId, phonelineId, voicemailId, greetingId) {
957
+ const readPath = this.phonelinePath(userId, phonelineId, `/voicemails/${encodeId(voicemailId)}/greetings`);
958
+ return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(greetingId)}`, "DELETE", undefined, true);
959
+ }
960
+ setVoicemailTranscription(userId, phonelineId, voicemailId, active) {
961
+ const readPath = this.phonelinePath(userId, phonelineId, "/voicemails");
962
+ return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(voicemailId)}/transcriptions`, "PUT", { ...(active === undefined ? {} : { active }) }, true);
963
+ }
964
+ async playVoicemail(input) {
965
+ const response = await this.client.request("/sessions/voicemail/play", {
966
+ method: "POST",
967
+ body: {
968
+ ...(input.dataId === undefined ? {} : { datadId: input.dataId }),
969
+ ...(input.deviceId === undefined ? {} : { deviceId: input.deviceId }),
970
+ },
971
+ });
972
+ return this.sessionMutationResult(response, "voicemail playback");
973
+ }
974
+ async recordVoicemailGreeting(input) {
975
+ const response = await this.client.request("/sessions/voicemail/recording", {
976
+ method: "POST",
977
+ body: {
978
+ ...(input.deviceId === undefined ? {} : { deviceId: input.deviceId }),
979
+ ...(input.endpoint === undefined ? {} : { endpoint: input.endpoint }),
980
+ ...(input.targetId === undefined ? {} : { targetId: input.targetId }),
981
+ },
982
+ });
983
+ return this.sessionMutationResult(response, "voicemail-greeting recording");
984
+ }
985
+ async createAutorecordingGreeting(input) {
986
+ const before = await optional(this.client.request("/autorecordings/greetings"));
987
+ if (!before.available)
988
+ return this.autorecordingUnavailableMutation();
989
+ await this.client.request("/autorecordings/greetings", {
990
+ method: "POST",
991
+ body: this.greetingBody(input),
992
+ });
993
+ const after = await optional(this.client.request("/autorecordings/greetings"));
994
+ return {
995
+ before: sanitize(before.value ?? {}),
996
+ after: after.available
997
+ ? sanitize(after.value ?? {})
998
+ : {
999
+ autorecordingsAvailable: false,
1000
+ note: "The greeting was accepted, but automated call recording was unavailable during read-back.",
1001
+ },
1002
+ };
1003
+ }
1004
+ async deleteAutorecordingGreeting(greetingId) {
1005
+ const before = await optional(this.client.request("/autorecordings/greetings"));
1006
+ if (!before.available)
1007
+ return this.autorecordingUnavailableMutation();
1008
+ const current = before.value && !Array.isArray(before.value) && typeof before.value === "object"
1009
+ ? before.value
1010
+ : undefined;
1011
+ const currentId = current === undefined ? undefined : stringField(current, "id");
1012
+ if (currentId !== undefined && currentId !== greetingId) {
1013
+ throw new SipgateApiError("The requested automated-recording greeting is not the one currently configured.", 404);
1014
+ }
1015
+ const response = await this.client.request(`/autorecordings/greetings/${encodeId(greetingId)}`, { method: "DELETE" });
1016
+ return {
1017
+ before: sanitize(before.value ?? {}),
1018
+ after: sanitize({
1019
+ deleted: true,
1020
+ response: response ?? null,
1021
+ note: "sipgate exposes no separate read-back for a deleted automated-recording greeting.",
1022
+ }),
1023
+ };
1024
+ }
1025
+ async setAutorecordingSettings(extension, active) {
1026
+ const path = `/autorecordings/${encodeId(extension)}/settings`;
1027
+ const before = await optional(this.client.request(path));
1028
+ if (!before.available)
1029
+ return this.autorecordingUnavailableMutation();
1030
+ await this.client.request(path, {
1031
+ method: "PUT",
1032
+ body: { ...(active === undefined ? {} : { active }) },
1033
+ });
1034
+ const after = await optional(this.client.request(path));
1035
+ return {
1036
+ before: sanitize(before.value ?? {}),
1037
+ after: after.available
1038
+ ? sanitize(after.value ?? {})
1039
+ : {
1040
+ autorecordingsAvailable: false,
1041
+ note: "The setting was accepted, but automated call recording was unavailable during read-back.",
1042
+ },
1043
+ };
1044
+ }
1045
+ async createFaxline(userId) {
1046
+ const response = await this.client.request(`/${encodeId(userId)}/faxlines`, {
1047
+ method: "POST",
1048
+ });
1049
+ return {
1050
+ before: null,
1051
+ after: sanitize({
1052
+ faxline: response ?? {},
1053
+ created: true,
1054
+ note: "No faxline existed before this create operation; the returned faxline is the initial state.",
1055
+ }),
1056
+ };
1057
+ }
1058
+ updateFaxlineAlias(userId, faxlineId, alias) {
1059
+ return this.mutateFaxlineWithReadback(userId, faxlineId, "", {
1060
+ ...(alias === undefined ? {} : { alias }),
1061
+ });
1062
+ }
1063
+ async deleteFaxline(userId, faxlineId) {
1064
+ const before = await this.findFaxline(userId, faxlineId);
1065
+ const response = await this.client.request(`/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}`, { method: "DELETE" });
1066
+ return {
1067
+ before: sanitize(before),
1068
+ after: sanitize({
1069
+ deleted: true,
1070
+ response: response ?? null,
1071
+ note: "sipgate does not provide a deleted-faxline read-back endpoint.",
1072
+ }),
1073
+ };
1074
+ }
1075
+ setFaxlineCallerId(userId, faxlineId, value) {
1076
+ const path = `/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}/callerid`;
1077
+ return this.mutateWithReadback(() => this.client.request(path), path, { ...(value === undefined ? {} : { value }) });
1078
+ }
1079
+ setFaxlineTagline(userId, faxlineId, value) {
1080
+ return this.mutateFaxlineWithReadback(userId, faxlineId, "/tagline", {
1081
+ ...(value === undefined ? {} : { value }),
1082
+ });
1083
+ }
227
1084
  async setDnd(deviceId, enabled) {
228
1085
  const path = `/devices/${encodeId(deviceId)}`;
229
1086
  const before = await this.client.request(path);
@@ -231,6 +1088,137 @@ export class SipgateBackend {
231
1088
  const after = await this.client.request(path);
232
1089
  return { before: sanitize(before ?? {}), after: sanitize(after ?? {}) };
233
1090
  }
1091
+ async updateDevice(deviceId, settings) {
1092
+ const path = `/devices/${encodeId(deviceId)}`;
1093
+ return this.mutateWithReadback(() => this.client.request(path), path, {
1094
+ ...(settings.dnd === undefined ? {} : { dnd: settings.dnd }),
1095
+ ...(settings.emergencyAddressId === undefined
1096
+ ? {}
1097
+ : { emergencyAddressId: settings.emergencyAddressId }),
1098
+ });
1099
+ }
1100
+ async deleteDevice(deviceId) {
1101
+ const path = `/devices/${encodeId(deviceId)}`;
1102
+ const before = await this.client.request(path);
1103
+ const response = await this.client.request(path, { method: "DELETE" });
1104
+ return {
1105
+ before: sanitize(before ?? {}),
1106
+ after: sanitize({
1107
+ deleted: true,
1108
+ response: response ?? null,
1109
+ note: "sipgate does not provide a deleted-device read-back endpoint.",
1110
+ }),
1111
+ };
1112
+ }
1113
+ setDeviceAlias(deviceId, value) {
1114
+ return this.mutateDeviceWithReadback(deviceId, `/devices/${encodeId(deviceId)}/alias`, {
1115
+ ...(value === undefined ? {} : { value }),
1116
+ });
1117
+ }
1118
+ async setDeviceCallerId(deviceId, value) {
1119
+ const path = `/devices/${encodeId(deviceId)}/callerid`;
1120
+ return this.mutateWithReadback(() => this.client.request(path), path, { ...(value === undefined ? {} : { value }) });
1121
+ }
1122
+ async setDeviceLocalPrefix(deviceId, input) {
1123
+ const path = `/devices/${encodeId(deviceId)}/localprefix`;
1124
+ return this.mutateWithReadback(() => this.client.request(path), path, {
1125
+ ...(input.active === undefined ? {} : { active: input.active }),
1126
+ ...(input.value === undefined ? {} : { value: input.value }),
1127
+ });
1128
+ }
1129
+ async setDeviceTariffAnnouncement(deviceId, enabled) {
1130
+ const path = `/devices/${encodeId(deviceId)}/tariffannouncement`;
1131
+ return this.mutateWithReadback(() => this.client.request(path), path, { ...(enabled === undefined ? {} : { enabled }) });
1132
+ }
1133
+ async setDeviceSingleRowDisplay(deviceId, enabled) {
1134
+ const path = `/devices/${encodeId(deviceId)}/singlerowdisplay`;
1135
+ return this.mutateWithReadback(() => this.client.request(path), path, { ...(enabled === undefined ? {} : { enabled }) });
1136
+ }
1137
+ setExternalDeviceTargetNumber(deviceId, number) {
1138
+ return this.mutateDeviceWithReadback(deviceId, `/devices/${encodeId(deviceId)}/external/targetnumber`, { ...(number === undefined ? {} : { number }) });
1139
+ }
1140
+ setExternalDeviceIncomingCallDisplay(deviceId, incomingCallDisplay) {
1141
+ return this.mutateDeviceWithReadback(deviceId, `/devices/${encodeId(deviceId)}/external/incomingcalldisplay`, { incomingCallDisplay });
1142
+ }
1143
+ async changeDevicePassword(deviceId) {
1144
+ const devicePath = `/devices/${encodeId(deviceId)}`;
1145
+ const before = await this.client.request(devicePath);
1146
+ const response = sanitize(await this.client.request(`${devicePath}/credentials/password`, { method: "POST" }) ?? {});
1147
+ return {
1148
+ before: sanitize(before ?? {}),
1149
+ after: sanitize({
1150
+ credentials: response,
1151
+ passwordChanged: true,
1152
+ note: "sipgate returns the new credential only once; credential values are intentionally redacted.",
1153
+ }),
1154
+ };
1155
+ }
1156
+ createRegisterDevice(userId, alias) {
1157
+ return this.createDevice(`/${encodeId(userId)}/devices/register`, { ...(alias === undefined ? {} : { alias }) });
1158
+ }
1159
+ createMobileDevice(userId, alias) {
1160
+ return this.createDevice(`/${encodeId(userId)}/devices/mobile`, { ...(alias === undefined ? {} : { alias }) });
1161
+ }
1162
+ createExternalDevice(userId, alias, number) {
1163
+ return this.createDevice(`/${encodeId(userId)}/devices/external`, {
1164
+ ...(alias === undefined ? {} : { alias }),
1165
+ ...(number === undefined ? {} : { number }),
1166
+ });
1167
+ }
1168
+ async createQuickDial(input) {
1169
+ const response = await this.client.request("/numbers/quickdial", {
1170
+ method: "POST",
1171
+ body: {
1172
+ userId: input.userId,
1173
+ ...(input.number === undefined ? {} : { number: input.number }),
1174
+ },
1175
+ });
1176
+ return {
1177
+ before: null,
1178
+ after: sanitize({
1179
+ response: response ?? null,
1180
+ requestAccepted: true,
1181
+ note: "sipgate does not document a quick-dial creation read-back response; use list_user_numbers to inspect the result.",
1182
+ }),
1183
+ };
1184
+ }
1185
+ async updateQuickDial(quickDialId, input) {
1186
+ const before = await this.findNumber(quickDialId);
1187
+ await this.client.request(`/numbers/quickdial/${encodeId(quickDialId)}`, {
1188
+ method: "PUT",
1189
+ body: {
1190
+ userId: input.userId,
1191
+ ...(input.number === undefined ? {} : { number: input.number }),
1192
+ },
1193
+ });
1194
+ const after = await this.findNumber(quickDialId);
1195
+ return { before: sanitize(before), after: sanitize(after) };
1196
+ }
1197
+ async deleteQuickDial(numberId) {
1198
+ const before = await this.findNumber(numberId);
1199
+ const response = await this.client.request(`/numbers/quickdial/${encodeId(numberId)}`, { method: "DELETE" });
1200
+ return {
1201
+ before: sanitize(before),
1202
+ after: sanitize({
1203
+ deleted: true,
1204
+ response: response ?? null,
1205
+ note: "sipgate does not provide a deleted quick-dial read-back endpoint.",
1206
+ }),
1207
+ };
1208
+ }
1209
+ async updateAddress(addressId, input) {
1210
+ const path = `/addresses/${addressId}`;
1211
+ return this.mutateWithReadback(() => this.client.request(path), path, {
1212
+ city: input.city,
1213
+ countrycode: input.countrycode,
1214
+ postcode: input.postcode,
1215
+ ...(input.address1 === undefined ? {} : { address1: input.address1 }),
1216
+ ...(input.address2 === undefined ? {} : { address2: input.address2 }),
1217
+ ...(input.number === undefined ? {} : { number: input.number }),
1218
+ ...(input.state === undefined ? {} : { state: input.state }),
1219
+ ...(input.street === undefined ? {} : { street: input.street }),
1220
+ });
1221
+ }
234
1222
  async sendSms(input) {
235
1223
  const extensions = asItems(await this.client.request(`/${encodeId(input.userId)}/sms`));
236
1224
  const extension = input.smsId
@@ -304,9 +1292,148 @@ export class SipgateBackend {
304
1292
  },
305
1293
  };
306
1294
  }
1295
+ createCallEmailNotification(input) {
1296
+ return this.createNotification(input.userId, "/call/email", {
1297
+ cause: input.cause,
1298
+ direction: input.direction,
1299
+ email: input.email,
1300
+ endpointId: input.endpointId,
1301
+ });
1302
+ }
1303
+ createCallSmsNotification(input) {
1304
+ return this.createNotification(input.userId, "/call/sms", {
1305
+ cause: input.cause,
1306
+ direction: input.direction,
1307
+ endpointId: input.endpointId,
1308
+ number: input.number,
1309
+ });
1310
+ }
1311
+ createFaxEmailNotification(input) {
1312
+ return this.createNotification(input.userId, "/fax/email", {
1313
+ direction: input.direction,
1314
+ email: input.email,
1315
+ faxlineId: input.faxlineId,
1316
+ });
1317
+ }
1318
+ createFaxSmsNotification(input) {
1319
+ return this.createNotification(input.userId, "/fax/sms", {
1320
+ direction: input.direction,
1321
+ faxlineId: input.faxlineId,
1322
+ number: input.number,
1323
+ });
1324
+ }
1325
+ createFaxReportNotification(input) {
1326
+ return this.createNotification(input.userId, "/fax/report", { email: input.email, faxlineId: input.faxlineId });
1327
+ }
1328
+ createSmsEmailNotification(input) {
1329
+ return this.createNotification(input.userId, "/sms/email", { email: input.email, endpointId: input.endpointId });
1330
+ }
1331
+ createVoicemailEmailNotification(input) {
1332
+ return this.createNotification(input.userId, "/voicemail/email", { email: input.email, voicemailId: input.voicemailId });
1333
+ }
1334
+ createVoicemailSmsNotification(input) {
1335
+ return this.createNotification(input.userId, "/voicemail/sms", { number: input.number, voicemailId: input.voicemailId });
1336
+ }
1337
+ async deleteNotification(userId, notificationId) {
1338
+ const before = await this.listNotifications(userId);
1339
+ await this.client.request(`/${encodeId(userId)}/notifications/${encodeId(notificationId)}`, { method: "DELETE" });
1340
+ const after = await this.listNotifications(userId);
1341
+ return { before, after };
1342
+ }
1343
+ async hangupCall(callId) {
1344
+ return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}`, "DELETE");
1345
+ }
1346
+ setCallHold(callId, value) {
1347
+ return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/hold`, "PUT", { value });
1348
+ }
1349
+ setCallMuted(callId, value) {
1350
+ return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/muted`, "PUT", { value });
1351
+ }
1352
+ setCallRecording(callId, value, announcement) {
1353
+ return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/recording`, "PUT", { value, ...(announcement === undefined ? {} : { announcement }) });
1354
+ }
1355
+ transferCall(callId, input) {
1356
+ return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/transfer`, "POST", {
1357
+ attended: input.attended,
1358
+ phoneNumber: input.phoneNumber,
1359
+ ...(input.callerId === undefined ? {} : { callerId: input.callerId }),
1360
+ });
1361
+ }
1362
+ sendCallDtmf(callId, sequence) {
1363
+ return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/dtmf`, "POST", { sequence });
1364
+ }
1365
+ startCallAnnouncement(callId, url) {
1366
+ return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/announcements`, "POST", { url });
1367
+ }
1368
+ async sendFax(input) {
1369
+ const response = await this.client.request("/sessions/fax", {
1370
+ method: "POST",
1371
+ body: {
1372
+ base64Content: input.base64Content,
1373
+ faxlineId: input.faxlineId,
1374
+ filename: input.filename,
1375
+ recipient: input.recipient,
1376
+ },
1377
+ });
1378
+ return {
1379
+ before: null,
1380
+ after: sanitize({
1381
+ session: response ?? null,
1382
+ requestAccepted: true,
1383
+ note: "sipgate does not expose a synchronous fax-session read-back; sending a fax may incur charges.",
1384
+ }),
1385
+ };
1386
+ }
1387
+ async resendFax(input) {
1388
+ const response = await this.client.request("/sessions/fax/resend", {
1389
+ method: "POST",
1390
+ body: {
1391
+ faxId: input.faxId,
1392
+ ...(input.faxlineId === undefined ? {} : { faxlineId: input.faxlineId }),
1393
+ },
1394
+ });
1395
+ return {
1396
+ before: null,
1397
+ after: sanitize({
1398
+ response: response ?? null,
1399
+ requestAccepted: true,
1400
+ note: "sipgate does not document a fax-resend read-back response; resending a fax may incur charges.",
1401
+ }),
1402
+ };
1403
+ }
1404
+ async createNotification(userId, suffix, body) {
1405
+ const before = await this.listNotifications(userId);
1406
+ await this.client.request(`/${encodeId(userId)}/notifications${suffix}`, { method: "POST", body });
1407
+ const after = await this.listNotifications(userId);
1408
+ return { before, after };
1409
+ }
1410
+ async findActiveCall(callId) {
1411
+ const response = await this.client.request("/calls");
1412
+ return asCalls(response).find((call) => stringField(call, "callId") === callId);
1413
+ }
1414
+ async mutateCallWithReadback(callId, path, method, body) {
1415
+ const before = await this.findActiveCall(callId);
1416
+ if (!before)
1417
+ throw new SipgateApiError("The requested active sipgate call was not found.", 404);
1418
+ await this.client.request(path, {
1419
+ method,
1420
+ ...(body === undefined ? {} : { body }),
1421
+ });
1422
+ const after = await this.findActiveCall(callId);
1423
+ return {
1424
+ before: sanitize(before),
1425
+ after: after
1426
+ ? sanitize(after)
1427
+ : {
1428
+ call: null,
1429
+ active: false,
1430
+ note: "The call no longer appears in sipgate's established-calls list after the operation.",
1431
+ },
1432
+ };
1433
+ }
307
1434
  async findNumber(numberId) {
308
- const response = await this.client.request("/numbers", { query: { offset: 0, limit: 1000 } });
309
- const number = asItems(response).find((item) => stringField(item, "id") === numberId);
1435
+ const number = (await this.listAllAccountNumbers())
1436
+ .find((item) => stringField(item, "id") === numberId);
310
1437
  if (!number)
311
1438
  throw new SipgateApiError("The requested sipgate phone number was not found.", 404);
312
1439
  return number;
@@ -331,5 +1458,165 @@ export class SipgateBackend {
331
1458
  });
332
1459
  return asItems(response)[0] ?? null;
333
1460
  }
1461
+ async mutateWithReadback(read, path, body) {
1462
+ const before = await read();
1463
+ await this.client.request(path, { method: "PUT", body });
1464
+ const after = await read();
1465
+ return { before: sanitize(before ?? {}), after: sanitize(after ?? {}) };
1466
+ }
1467
+ mutateDeviceWithReadback(deviceId, path, body) {
1468
+ return this.mutateWithReadback(() => this.client.request(`/devices/${encodeId(deviceId)}`), path, body);
1469
+ }
1470
+ async mutateOptionalPhonelineWithReadback(readPath, writePath, method, body, list = false) {
1471
+ const before = await optional(this.client.request(readPath));
1472
+ if (!before.available)
1473
+ return phonelineMutationUnavailable(before.status);
1474
+ await this.client.request(writePath, {
1475
+ method,
1476
+ ...(body === undefined ? {} : { body }),
1477
+ });
1478
+ const after = await optional(this.client.request(readPath));
1479
+ return {
1480
+ before: sanitize(before.value ?? (list ? { items: [] } : {})),
1481
+ after: after.available
1482
+ ? sanitize(after.value ?? (list ? { items: [] } : {}))
1483
+ : {
1484
+ ...phonelineUnavailable(list, after.status),
1485
+ changed: true,
1486
+ note: "The change was applied, but sipgate denied the read-back so the new state could not be confirmed.",
1487
+ },
1488
+ };
1489
+ }
1490
+ parallelForwardingBody(input) {
1491
+ return {
1492
+ ...(input.active === undefined ? {} : { active: input.active }),
1493
+ ...(input.alias === undefined ? {} : { alias: input.alias }),
1494
+ ...(input.destination === undefined ? {} : { destination: input.destination }),
1495
+ };
1496
+ }
1497
+ greetingBody(input) {
1498
+ return {
1499
+ ...(input.base64Content === undefined ? {} : { base64Content: input.base64Content }),
1500
+ ...(input.filename === undefined ? {} : { filename: input.filename }),
1501
+ };
1502
+ }
1503
+ sessionMutationResult(response, action) {
1504
+ return {
1505
+ before: null,
1506
+ after: sanitize({
1507
+ session: response ?? null,
1508
+ requestAccepted: true,
1509
+ note: `sipgate exposes no synchronous ${action} read-back. The call may incur charges; the caller is responsible for consent where recording law requires it.`,
1510
+ }),
1511
+ };
1512
+ }
1513
+ autorecordingUnavailableMutation() {
1514
+ return {
1515
+ before: null,
1516
+ after: {
1517
+ changed: false,
1518
+ autorecordingsAvailable: false,
1519
+ note: "Automated call recording is not activated for this sipgate account; no change was attempted.",
1520
+ },
1521
+ };
1522
+ }
1523
+ async findFaxline(userId, faxlineId) {
1524
+ const response = await this.listFaxlines(userId);
1525
+ const faxline = asItems(response).find((item) => stringField(item, "id") === faxlineId);
1526
+ if (!faxline)
1527
+ throw new SipgateApiError("The requested sipgate faxline was not found.", 404);
1528
+ return faxline;
1529
+ }
1530
+ async mutateFaxlineWithReadback(userId, faxlineId, suffix, body) {
1531
+ const before = await this.findFaxline(userId, faxlineId);
1532
+ await this.client.request(`/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}${suffix}`, { method: "PUT", body });
1533
+ const after = await this.findFaxline(userId, faxlineId);
1534
+ return { before: sanitize(before), after: sanitize(after) };
1535
+ }
1536
+ contactBody(input) {
1537
+ return {
1538
+ ...(input.addresses === undefined ? {} : { addresses: sanitize(input.addresses) }),
1539
+ ...(input.emails === undefined ? {} : { emails: sanitize(input.emails) }),
1540
+ ...(input.family === undefined ? {} : { family: input.family }),
1541
+ ...(input.given === undefined ? {} : { given: input.given }),
1542
+ ...("id" in input && typeof input.id === "string" ? { id: input.id } : {}),
1543
+ ...(input.name === undefined ? {} : { name: input.name }),
1544
+ ...(input.note === undefined ? {} : { note: input.note }),
1545
+ ...(input.numbers === undefined ? {} : { numbers: sanitize(input.numbers) }),
1546
+ ...(input.organization === undefined ? {} : { organization: input.organization }),
1547
+ ...(input.picture === undefined ? {} : { picture: input.picture }),
1548
+ ...(input.scope === undefined ? {} : { scope: input.scope }),
1549
+ ...(input.websites === undefined ? {} : { websites: sanitize(input.websites) }),
1550
+ };
1551
+ }
1552
+ async readContacts(contactIds) {
1553
+ const contacts = [];
1554
+ for (const contactId of contactIds)
1555
+ contacts.push(await this.getContact(contactId));
1556
+ return contacts;
1557
+ }
1558
+ async listAllContacts(scopes) {
1559
+ const all = [];
1560
+ const limit = 5000;
1561
+ let lastId;
1562
+ for (;;) {
1563
+ const response = await this.listContacts({
1564
+ limit,
1565
+ ...(lastId === undefined ? {} : { lastId }),
1566
+ ...(scopes === undefined ? {} : { scopes }),
1567
+ });
1568
+ const page = asItems(response);
1569
+ all.push(...page);
1570
+ if (page.length < limit)
1571
+ break;
1572
+ const nextLastId = stringField(page[page.length - 1] ?? {}, "id");
1573
+ if (!nextLastId || nextLastId === lastId)
1574
+ break;
1575
+ lastId = nextLastId;
1576
+ }
1577
+ return sanitize({ items: all, totalCount: all.length });
1578
+ }
1579
+ async readHistoryEntries(entryIds) {
1580
+ const entries = [];
1581
+ for (const entryId of entryIds)
1582
+ entries.push(await this.getHistoryEntry(entryId));
1583
+ return entries;
1584
+ }
1585
+ async listAllHistoryEntries() {
1586
+ const all = [];
1587
+ const pageSize = 1000;
1588
+ for (const archived of [false, true]) {
1589
+ for (let offset = 0;; offset += pageSize) {
1590
+ const response = await this.getCallHistory({
1591
+ archived,
1592
+ offset,
1593
+ limit: pageSize,
1594
+ types: ["CALL", "VOICEMAIL", "SMS", "FAX"],
1595
+ });
1596
+ const page = asItems(response);
1597
+ all.push(...page);
1598
+ if (page.length < pageSize)
1599
+ break;
1600
+ }
1601
+ }
1602
+ return all;
1603
+ }
1604
+ async mutateHistoryWithReadback(entryId, path, body) {
1605
+ const before = await this.client.request(`/history/${encodeId(entryId)}`);
1606
+ await this.client.request(path, { method: "PUT", body });
1607
+ const after = await this.client.request(`/history/${encodeId(entryId)}`);
1608
+ return { before: sanitize(before ?? {}), after: sanitize(after ?? {}) };
1609
+ }
1610
+ async createDevice(path, body) {
1611
+ const response = await this.client.request(path, { method: "POST", body });
1612
+ return {
1613
+ before: null,
1614
+ after: sanitize({
1615
+ device: response ?? {},
1616
+ created: true,
1617
+ note: "No device existed before this create operation; the returned device is the initial state.",
1618
+ }),
1619
+ };
1620
+ }
334
1621
  }
335
1622
  //# sourceMappingURL=sipgate-backend.js.map