@coventia/softphone 0.3.0 → 0.4.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,4 +1,4 @@
1
- import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, r as SoftphoneState, n as SoftphoneError } from './call-CaPDWVk3.cjs';
1
+ import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, w as SoftphoneState, s as SoftphoneError } from './call-C6YpT6uR.cjs';
2
2
 
3
3
  /**
4
4
  * The `CallEngine` seam — INTERNAL, not exported from src/index.ts.
@@ -1,4 +1,4 @@
1
- import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, r as SoftphoneState, n as SoftphoneError } from './call-CaPDWVk3.js';
1
+ import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, w as SoftphoneState, s as SoftphoneError } from './call-C6YpT6uR.js';
2
2
 
3
3
  /**
4
4
  * The `CallEngine` seam — INTERNAL, not exported from src/index.ts.
package/dist/index.cjs CHANGED
@@ -110,9 +110,9 @@ function createRequest(config, method) {
110
110
  });
111
111
  const body = await res.json().catch(() => null);
112
112
  const message = body?.message ?? `${path} \u2014 HTTP ${res.status}`;
113
- if (!res.ok) throw new ApiError(res.status, message);
113
+ if (!res.ok) throw new ApiError(res.status, message, body);
114
114
  if (body?.success === false) {
115
- throw new ApiError(res.status, message);
115
+ throw new ApiError(res.status, message, body);
116
116
  }
117
117
  return body;
118
118
  }
@@ -138,15 +138,32 @@ var init_api = __esm({
138
138
  init_token_cache();
139
139
  ApiError = class extends Error {
140
140
  status;
141
- constructor(status, message) {
141
+ body;
142
+ constructor(status, message, body = null) {
142
143
  super(message);
143
144
  this.name = "ApiError";
144
145
  this.status = status;
146
+ this.body = body;
145
147
  }
146
148
  };
147
149
  }
148
150
  });
149
151
 
152
+ // src/core/outbound-numbers.ts
153
+ function isOutboundNumberRefused(value) {
154
+ const refusal = value?.refusal;
155
+ return value instanceof Error && !!refusal && (refusal.reason === "platform" || refusal.reason === "user") && typeof refusal.retryAfterSeconds === "number";
156
+ }
157
+ function outboundNumberRefused(refusal) {
158
+ const error = new Error(`Outbound number change refused (${refusal.reason}); retry in ${refusal.retryAfterSeconds}s`);
159
+ error.name = "OutboundNumberRefused";
160
+ return Object.assign(error, { refusal });
161
+ }
162
+ var init_outbound_numbers = __esm({
163
+ "src/core/outbound-numbers.ts"() {
164
+ }
165
+ });
166
+
150
167
  // src/coventia/sources.ts
151
168
  var sources_exports = {};
152
169
  __export(sources_exports, {
@@ -228,6 +245,8 @@ function coventiaSources(config) {
228
245
  // row's `called_did` is the OTHER party (already `number` above); showing it again as "DID"
229
246
  // would just repeat the row under a wrong label.
230
247
  ...toDirection(row.direction) === "inbound" && row.called_did ? { did: row.called_did } : {},
248
+ // And the other way round: which of the org's numbers an outbound call showed.
249
+ ...toDirection(row.direction) === "outbound" && row.from_number ? { from: row.from_number } : {},
231
250
  recorded,
232
251
  handledBy,
233
252
  // The detail page's one enrichment fetch — signed recording URLs, plus this SAME chain with
@@ -345,10 +364,10 @@ function coventiaSources(config) {
345
364
  return { catalogue, orgDefault: body.orgDefault ?? null, mine: body.user ?? null, effective };
346
365
  };
347
366
  const CACHE_PREFIX = "coventia-softphone:ringtones:";
348
- const cacheKey = async () => {
367
+ const cacheKey = async (prefix = CACHE_PREFIX) => {
349
368
  try {
350
369
  const { identity } = await config.tokenSource.getToken();
351
- return `${CACHE_PREFIX}${fileBase}|${config.organizationId ?? "-"}|${identity}`;
370
+ return `${prefix}${fileBase}|${config.organizationId ?? "-"}|${identity}`;
352
371
  } catch {
353
372
  return null;
354
373
  }
@@ -381,6 +400,68 @@ function coventiaSources(config) {
381
400
  ),
382
401
  cached: recall
383
402
  };
403
+ const KINDS = /* @__PURE__ */ new Set(["landline", "mobile", "commercial", "other"]);
404
+ const STATES = /* @__PURE__ */ new Set(["applied", "pending", "failed", "unassigned"]);
405
+ const toOutboundSettings = (body) => {
406
+ const numbers = (body.numbers ?? []).map((n) => ({
407
+ id: n.id,
408
+ number: n.number,
409
+ label: n.label ?? null,
410
+ kind: KINDS.has(n.kind ?? "") ? n.kind : "other"
411
+ }));
412
+ const known = (id) => id && numbers.some((n) => n.id === id) ? id : null;
413
+ return {
414
+ numbers,
415
+ orgDefault: body.orgDefault ?? null,
416
+ mine: known(body.mine),
417
+ // Defensive: a malformed answer naming a number that is not in the list reads as the org default.
418
+ effective: known(body.effective) ?? known(body.orgDefault),
419
+ extension: body.extension ?? null,
420
+ state: STATES.has(body.state ?? "") ? body.state : "applied",
421
+ changeAvailableInSeconds: Math.max(0, Math.ceil(Number(body.changeAvailableInSeconds) || 0))
422
+ };
423
+ };
424
+ const OUTBOUND_PREFIX = "coventia-softphone:outbound-number:";
425
+ const rememberOutbound = async (settings) => {
426
+ const key = await cacheKey(OUTBOUND_PREFIX);
427
+ try {
428
+ if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));
429
+ } catch {
430
+ }
431
+ return settings;
432
+ };
433
+ const recallOutbound = async () => {
434
+ const key = await cacheKey(OUTBOUND_PREFIX);
435
+ try {
436
+ const raw = key ? globalThis.localStorage?.getItem(key) : null;
437
+ const s = raw ? JSON.parse(raw) : null;
438
+ const ok = s && Array.isArray(s.numbers) && s.numbers.every((n) => typeof n?.id === "string" && typeof n.number === "string") && (s.effective === null || s.numbers.some((n) => n.id === s.effective));
439
+ return ok ? { ...s, changeAvailableInSeconds: 0 } : null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ };
444
+ const outboundNumbers = {
445
+ get: async (signal) => rememberOutbound(toOutboundSettings(await get(`${base}/outbound-numbers`, org, signal))),
446
+ // `null` = back to the org default: the empty id is dropped from the query, which the backend reads that way.
447
+ setMine: async (id) => {
448
+ try {
449
+ return rememberOutbound(
450
+ toOutboundSettings(await post(`${base}/outbound-numbers/user`, { ...org, did_id: id ?? "" }))
451
+ );
452
+ } catch (error) {
453
+ const body = error instanceof ApiError ? error.body : null;
454
+ if (error instanceof ApiError && error.status === 429 && body?.code === "rate_limited") {
455
+ throw outboundNumberRefused({
456
+ reason: body.reason === "user" ? "user" : "platform",
457
+ retryAfterSeconds: Math.max(1, Math.ceil(Number(body.retryAfterSeconds) || 1))
458
+ });
459
+ }
460
+ throw error;
461
+ }
462
+ },
463
+ cached: recallOutbound
464
+ };
384
465
  const contacts = config.contacts ?? coventiaContacts;
385
466
  const resolve = config.resolveContact ?? (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);
386
467
  const history = withNameResolution(rawHistory, resolve);
@@ -393,7 +474,8 @@ function coventiaSources(config) {
393
474
  listAgents: coventiaListAgents,
394
475
  transferOutcomes,
395
476
  presence,
396
- ringtones
477
+ ringtones,
478
+ outboundNumbers
397
479
  };
398
480
  }
399
481
  var toContact, toDirection, otherParty, isMissed, toOutcome, toDate, toTransferOutcome, toPresenceState, toPresenceEntry, toWebSocketUrl;
@@ -401,6 +483,7 @@ var init_sources2 = __esm({
401
483
  "src/coventia/sources.ts"() {
402
484
  init_sources();
403
485
  init_api();
486
+ init_outbound_numbers();
404
487
  toContact = (row) => ({
405
488
  id: row.wa_id,
406
489
  name: row.name,
@@ -2062,6 +2145,11 @@ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
2062
2145
  get: async (signal) => (await load()).ringtones.get(signal),
2063
2146
  setMine: async (id) => (await load()).ringtones.setMine(id),
2064
2147
  cached: async () => (await load()).ringtones.cached?.() ?? null
2148
+ },
2149
+ outboundNumbers: {
2150
+ get: async (signal) => (await load()).outboundNumbers.get(signal),
2151
+ setMine: async (id) => (await load()).outboundNumbers.setMine(id),
2152
+ cached: async () => (await load()).outboundNumbers.cached?.() ?? null
2065
2153
  }
2066
2154
  };
2067
2155
  }
@@ -2784,7 +2872,8 @@ function isCallEngine(value) {
2784
2872
 
2785
2873
  // src/index.ts
2786
2874
  init_errors();
2787
- var VERSION = "0.3.0";
2875
+ init_outbound_numbers();
2876
+ var VERSION = "0.4.0";
2788
2877
 
2789
2878
  exports.CALL_TRANSITIONS = CALL_TRANSITIONS;
2790
2879
  exports.CallEvent = CallEvent;
@@ -2795,6 +2884,7 @@ exports.canTransition = canTransition;
2795
2884
  exports.createSoftphone = createSoftphone;
2796
2885
  exports.isBrowser = isBrowser;
2797
2886
  exports.isFatal = isFatal;
2887
+ exports.isOutboundNumberRefused = isOutboundNumberRefused;
2798
2888
  exports.isTerminal = isTerminal;
2799
2889
  exports.isWebRtcSupported = isWebRtcSupported;
2800
2890
  //# sourceMappingURL=index.cjs.map