@coventia/softphone 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{call-DUVnDxsz.d.cts → call-VKq1I8yP.d.cts} +6 -0
- package/dist/{call-DUVnDxsz.d.ts → call-VKq1I8yP.d.ts} +6 -0
- package/dist/{chunk-YWI6EQYS.js → chunk-EYEDPD7Y.js} +108 -15
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/element/index.d.ts +8 -2
- package/dist/element/index.js +334 -249
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-CDjkrXHY.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
- package/dist/{engine-EPCKf-Bl.d.ts → engine-C0BOkIcW.d.ts} +1 -1
- package/dist/index.cjs +199 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +90 -11
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/{softphone-BtdgZWkm.d.ts → softphone-Cyq3hiIM.d.ts} +1 -1
- package/dist/{softphone-DHG9MTNg.d.cts → softphone-DlGV12Xp.d.cts} +1 -1
- package/dist/softphone.css +82 -34
- package/dist/{sources-5L2R4HTV.js → sources-IRKNQPWW.js} +6 -2
- package/dist/sources-IRKNQPWW.js.map +1 -0
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/ui/index.cjs +332 -247
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +17 -6
- package/dist/ui/index.d.ts +17 -6
- package/dist/ui/index.js +332 -247
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +106 -13
- package/dist/zadarma/index.cjs.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/dist/zadarma/index.js +1 -1
- package/dist/zadarma-engine-L5DEFXUS.js +5 -0
- package/dist/{zadarma-engine-4VOPIBXG.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-YWI6EQYS.js.map +0 -1
- package/dist/sources-5L2R4HTV.js.map +0 -1
- package/dist/zadarma-engine-4VOPIBXG.js +0 -5
package/dist/softphone.css
CHANGED
|
@@ -240,8 +240,6 @@
|
|
|
240
240
|
.cv-dial-prefix { padding: 0.7rem 0.6rem 0; }
|
|
241
241
|
.cv-dial-head { padding: 0.3rem 0.6rem 0.6rem; }
|
|
242
242
|
.cv-display { font-size: 1.5rem; }
|
|
243
|
-
.cv-controls-row { gap: 1.1rem; }
|
|
244
|
-
|
|
245
243
|
}
|
|
246
244
|
|
|
247
245
|
/* Short hosts. The flex chain already lets the keypad absorb what is left, but below this the keys
|
|
@@ -720,7 +718,7 @@
|
|
|
720
718
|
}
|
|
721
719
|
.cv-badge-hold { color: var(--cv-hold); font-weight: 600; }
|
|
722
720
|
|
|
723
|
-
/* ──
|
|
721
|
+
/* ── Device check (ringing / dialing / in-call) ────────────────────────────────
|
|
724
722
|
Before a call connects there is only ONE other control on screen (Colgar, or
|
|
725
723
|
Rechazar/Contestar) — putting Dispositivos in that same neighbourhood, even
|
|
726
724
|
spaced out, keeps it a mis-tap away from hanging up or answering. The entry
|
|
@@ -736,6 +734,11 @@
|
|
|
736
734
|
cramped and scrolly for just two rows. `.cv-call-devices-inline` REPLACES the
|
|
737
735
|
avatar/name/status block in `.cv-call` instead (see that JSX's own comment) —
|
|
738
736
|
an inline swap, not an overlay, so it gets the full box, not a leftover sliver.
|
|
737
|
+
|
|
738
|
+
Once connected it is the SAME chip and the SAME swap. Dispositivos used to be a fifth control in
|
|
739
|
+
the call row (with a floating popover), which is what forced that row down to 40px buttons and
|
|
740
|
+
9px labels in a 310px host. A set-once setting does not need a slot among the four things you do
|
|
741
|
+
mid-call (mute, hold, transfer, keypad), and the control row stays live under the swap.
|
|
739
742
|
───────────────────────────────────────────────────────────────────────────── */
|
|
740
743
|
.cv-call-devices-chip {
|
|
741
744
|
display: inline-flex;
|
|
@@ -808,16 +811,22 @@
|
|
|
808
811
|
display: flex;
|
|
809
812
|
flex-direction: column;
|
|
810
813
|
align-items: center;
|
|
811
|
-
gap: 0.
|
|
814
|
+
gap: 0.9rem;
|
|
812
815
|
padding: 0.35rem 1rem 1.1rem;
|
|
813
|
-
/* The containing block for `.cv-
|
|
816
|
+
/* The containing block for `.cv-keypad-popover` — see that rule's own comment for why it is
|
|
814
817
|
anchored here rather than to the trigger button. */
|
|
815
818
|
position: relative;
|
|
816
819
|
}
|
|
820
|
+
/* FOUR controls, sized so they never need to shrink to fit. This row held five (Dispositivos too) and
|
|
821
|
+
the only way to fit five in the CRM's 310px host was a 40px button with a 9px label and ~6px gaps —
|
|
822
|
+
below the 44/48px a thumb needs, and below legible for a word read mid-call. The default 48px size
|
|
823
|
+
below was never actually seen: it only applied above a 360px host, which no real embed is.
|
|
824
|
+
Dispositivos is the `.cv-call-chips` chip up by the call info instead (the same entry point the
|
|
825
|
+
ringing and dialing screens already use). 4 × 52 + 3 × 20 = 268px, inside 310 - 2 × 16 padding. */
|
|
817
826
|
.cv-controls-row {
|
|
818
827
|
display: flex;
|
|
819
828
|
justify-content: center;
|
|
820
|
-
gap: 1.
|
|
829
|
+
gap: 1.25rem;
|
|
821
830
|
}
|
|
822
831
|
.cv-controls-primary {
|
|
823
832
|
display: flex;
|
|
@@ -840,9 +849,12 @@
|
|
|
840
849
|
white-space: nowrap;
|
|
841
850
|
user-select: none;
|
|
842
851
|
}
|
|
852
|
+
/* The four secondaries' words get a notch more than Colgar / Rechazar / Contestar's 10px — they are
|
|
853
|
+
the ones read mid-call, and the five-up row used to shrink them to 9px (8px in a sidebar). */
|
|
854
|
+
.cv-controls-row .cv-ctrl-label { font-size: 0.6875rem; }
|
|
843
855
|
.cv-ctrl {
|
|
844
|
-
width:
|
|
845
|
-
height:
|
|
856
|
+
width: 52px;
|
|
857
|
+
height: 52px;
|
|
846
858
|
border-radius: 999px;
|
|
847
859
|
border: 1px solid var(--cv-border-strong);
|
|
848
860
|
background: var(--cv-bg);
|
|
@@ -855,6 +867,11 @@
|
|
|
855
867
|
}
|
|
856
868
|
.cv-ctrl:hover { background: var(--cv-surface); }
|
|
857
869
|
.cv-ctrl:active { transform: scale(0.94); }
|
|
870
|
+
.cv-ctrl:disabled {
|
|
871
|
+
opacity: 0.4;
|
|
872
|
+
cursor: not-allowed;
|
|
873
|
+
pointer-events: none;
|
|
874
|
+
}
|
|
858
875
|
|
|
859
876
|
/* The ON states say what they MEAN, in the widget's own tokens: muted is danger,
|
|
860
877
|
held is hold, transfer-open is accent. Colour is the state, not decoration. */
|
|
@@ -900,6 +917,28 @@
|
|
|
900
917
|
border-color: var(--cv-text);
|
|
901
918
|
}
|
|
902
919
|
|
|
920
|
+
/*
|
|
921
|
+
Sidebar-narrow (this file's own ≤300px tier, up in "Narrow hosts"). The four 52px controls with
|
|
922
|
+
20px gaps need 268px, which a 260px host cannot give — so this tier steps down to 44px with 12px
|
|
923
|
+
gaps (4 × 44 + 3 × 12 = 212px) and reclaims `.cv-controls`'s own horizontal padding, the first
|
|
924
|
+
lever that block's own comment names. 44px is still the Apple HIG minimum tap target; the five-up
|
|
925
|
+
row this replaced went to 36px here.
|
|
926
|
+
|
|
927
|
+
Placed AFTER `.cv-controls`/`.cv-controls-row` above rather than up with this file's other
|
|
928
|
+
`@container` blocks near the top: `gap` and `padding-left`/`-right` here have the same specificity as
|
|
929
|
+
those base rules' own shorthand, so — unlike a compound selector such as `.cv-controls-row .cv-ctrl`,
|
|
930
|
+
which wins on specificity regardless of position — source order is what decides, and a narrower-width
|
|
931
|
+
override declared BEFORE the base rule it means to narrow loses to it. Confirmed live: moved up into
|
|
932
|
+
the "Narrow hosts" section, the `.cv-ctrl` width and label font-size reductions took effect (compound
|
|
933
|
+
selectors) but `gap` and `.cv-controls`'s padding silently did not (plain selectors, later base rule won).
|
|
934
|
+
*/
|
|
935
|
+
@container cv (max-width: 300px) {
|
|
936
|
+
.cv-controls { padding-left: 0.5rem; padding-right: 0.5rem; }
|
|
937
|
+
.cv-controls-row { gap: 0.75rem; }
|
|
938
|
+
.cv-controls-row .cv-ctrl { width: 44px; height: 44px; }
|
|
939
|
+
.cv-controls-row .cv-ctrl-label { font-size: 0.625rem; }
|
|
940
|
+
}
|
|
941
|
+
|
|
903
942
|
/*
|
|
904
943
|
The attended-transfer target-entry panel (`docs/specs/transfer-availability.md` §5): an optional
|
|
905
944
|
outcome line, then a row of whatever's actionable (input+button today). Once a consult exists,
|
|
@@ -1146,49 +1185,58 @@
|
|
|
1146
1185
|
transform: scale(0.98);
|
|
1147
1186
|
}
|
|
1148
1187
|
|
|
1188
|
+
@keyframes cv-pop-in-up {
|
|
1189
|
+
from { opacity: 0; transform: translateY(4px) scale(0.97); }
|
|
1190
|
+
to { opacity: 1; transform: none; }
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1149
1193
|
/*
|
|
1150
|
-
The in-call
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
parent, never the narrow trigger itself — the same fix CountryPicker's own popover needed (see its
|
|
1159
|
-
comment).
|
|
1194
|
+
The in-call DTMF keypad — the one floating popover left on the call screen. (Dispositivos was the
|
|
1195
|
+
other; it is inline content now, `.cv-call-devices-inline` above, in every call state.)
|
|
1196
|
+
|
|
1197
|
+
Opens UPWARD (`bottom: 100%`): the trigger sits at the bottom of the call screen, and there is no
|
|
1198
|
+
room to open downward. Anchored to `.cv-controls`'s own horizontal padding — a container whose width
|
|
1199
|
+
is bounded by ITS parent, never the narrow trigger itself — the same fix CountryPicker's own popover
|
|
1200
|
+
needed (see its comment): anchoring to a narrow, off-centre trigger let the popover's edge reach
|
|
1201
|
+
`.cv-softphone`'s `overflow: hidden` and get clipped.
|
|
1160
1202
|
*/
|
|
1161
|
-
.cv-
|
|
1203
|
+
.cv-keypad-popover {
|
|
1162
1204
|
position: absolute;
|
|
1163
1205
|
z-index: 20;
|
|
1164
|
-
display:
|
|
1165
|
-
|
|
1166
|
-
gap:
|
|
1167
|
-
padding: 0.
|
|
1206
|
+
display: grid;
|
|
1207
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1208
|
+
gap: 8px;
|
|
1209
|
+
padding: 0.85rem 1rem;
|
|
1168
1210
|
background: var(--cv-surface);
|
|
1169
1211
|
border: 1px solid var(--cv-border);
|
|
1170
1212
|
border-radius: var(--cv-radius);
|
|
1171
1213
|
box-shadow: var(--cv-shadow);
|
|
1172
1214
|
}
|
|
1173
|
-
.cv-
|
|
1215
|
+
.cv-keypad-popover--above {
|
|
1174
1216
|
bottom: 100%;
|
|
1175
1217
|
left: 1rem;
|
|
1176
1218
|
right: 1rem;
|
|
1177
1219
|
margin-bottom: 0.6rem;
|
|
1178
1220
|
transform-origin: bottom center;
|
|
1179
|
-
animation: cv-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1221
|
+
animation: cv-pop-in-up 0.14s cubic-bezier(0.2, 0, 0.2, 1);
|
|
1222
|
+
}
|
|
1223
|
+
/* `.cv-key` already carries the full key look (border, hover, active-scale, digit/letter type) from
|
|
1224
|
+
the pre-call dialer — only the size needs setting here, since this grid has no flex ancestor to
|
|
1225
|
+
stretch the rows the way `.cv-keypad` gets to.
|
|
1226
|
+
|
|
1227
|
+
A fixed HEIGHT, not an `aspect-ratio`. This popover opens upward from `.cv-controls`, so it has
|
|
1228
|
+
(host height - controls block - margin) to work with, and a ratio ties its height to the host's
|
|
1229
|
+
WIDTH, which says nothing about how much height there is. Found live: the CRM's real body is 310×473
|
|
1230
|
+
(a 520px panel minus its 45px header), and a 1.3 ratio made this popover 285px — with the 184px
|
|
1231
|
+
controls block that is 479px, so its top ~7px were clipped by `.cv-softphone`'s `overflow: hidden`.
|
|
1232
|
+
48px keys give a 243px popover at ANY width, fit that host with room to spare, and stay at the
|
|
1233
|
+
44-48px minimum tap target (the old ratio gave 45px keys at a 260px host, 58px at 310). */
|
|
1234
|
+
.cv-keypad-popover .cv-key {
|
|
1235
|
+
height: 48px;
|
|
1184
1236
|
}
|
|
1185
1237
|
@media (prefers-reduced-motion: reduce) {
|
|
1186
|
-
.cv-
|
|
1238
|
+
.cv-keypad-popover--above { animation: none; }
|
|
1187
1239
|
}
|
|
1188
|
-
/* No per-row override needed here: `.cv-settings-row`/`.cv-settings-note` carry no border,
|
|
1189
|
-
background or padding of their own (see the Settings tab block) — this popover's own `gap` and
|
|
1190
|
-
`padding` are the only spacing in play, which is what keeps it lighter than the full Ajustes
|
|
1191
|
-
page's bordered section for what is meant to be a quick, transient panel. */
|
|
1192
1240
|
|
|
1193
1241
|
/* ── Lists (contacts / history) ──────────────────────────────────────────────── */
|
|
1194
1242
|
.cv-list-head { flex-shrink: 0; padding: 0.85rem 1rem 0.5rem; }
|
|
@@ -271,6 +271,10 @@ function coventiaSources(config) {
|
|
|
271
271
|
if (ws.readyState !== WS.OPEN) return;
|
|
272
272
|
ws.send(JSON.stringify({ type: "call-state", onCall }));
|
|
273
273
|
},
|
|
274
|
+
bye: () => {
|
|
275
|
+
if (ws.readyState !== WS.OPEN) return;
|
|
276
|
+
ws.send(JSON.stringify({ type: "bye" }));
|
|
277
|
+
},
|
|
274
278
|
close: () => {
|
|
275
279
|
ws.onclose = null;
|
|
276
280
|
ws.onerror = null;
|
|
@@ -321,5 +325,5 @@ function coventiaSources(config) {
|
|
|
321
325
|
}
|
|
322
326
|
|
|
323
327
|
export { coventiaSources, resolverFromContacts };
|
|
324
|
-
//# sourceMappingURL=sources-
|
|
325
|
-
//# sourceMappingURL=sources-
|
|
328
|
+
//# sourceMappingURL=sources-IRKNQPWW.js.map
|
|
329
|
+
//# sourceMappingURL=sources-IRKNQPWW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sources.ts","../src/coventia/api.ts","../src/coventia/sources.ts"],"names":["resolve"],"mappings":"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","file":"sources-IRKNQPWW.js","sourcesContent":["/**\r\n * **Internal.** Not exported from any entry point.\r\n *\r\n * The rules that are about **phone numbers and names** rather than about any backend: universal,\r\n * easy to get subtly wrong, and worth getting right once.\r\n *\r\n * These were public while a host had to build their own sources. They no longer do — a provider\r\n * supplies `coventia.contacts` and everything else (caller ID, history names) is derived from it in\r\n * `coventia/sources.ts`, the only caller left. Exported API is a contract owed forever, so it should\r\n * not outlive its consumers.\r\n */\r\nimport type { Contact } from './core/types.js';\r\nimport type { CallHistoryEntry, HistorySource } from './ui/types.js';\r\n\r\n/**\r\n * A number reduced to the digits telephony actually matches on — `'+34 600 111 222'` → `'34600111222'`.\r\n *\r\n * Comparing numbers as raw strings is the bug this exists to prevent: the same line arrives as\r\n * `+34600111222` from a SIP header, `34600111222` from a database, and `+34 600 111 222` from a human,\r\n * and all three are one contact.\r\n */\r\nexport function toDigits(number: string): string {\r\n return String(number ?? '').replace(/\\D/g, '');\r\n}\r\n\r\n/**\r\n * Decide whether a resolved string is really a **name** — the \"don't echo the bare number\" rule.\r\n *\r\n * A contact saved as its own number (\"+34 600 111 222\") resolves to a \"name\" that is just the number\r\n * reformatted. Showing it puts `+34600111222` where a name belongs, or worse, a *differently*\r\n * formatted copy of the number the widget is already displaying an inch away.\r\n *\r\n * This is the SDK deciding what it is willing to call a name, so it holds for every host — not only\r\n * for a backend that happens to be careful. Returns `null` when there is no name worth showing, which\r\n * is the signal to keep displaying the number.\r\n */\r\nexport function normalizeResolvedName(raw: string | null | undefined, number: string): string | null {\r\n if (!raw) return null;\r\n const trimmed = String(raw).trim();\r\n if (!trimmed) return null;\r\n const digits = toDigits(number);\r\n if (!digits) return trimmed;\r\n return toDigits(trimmed) === digits ? null : trimmed;\r\n}\r\n\r\n/**\r\n * Wrap a `HistorySource` so its entries get caller names, resolved once per distinct number.\r\n *\r\n * A call log is the same handful of numbers over and over, so the naive \"resolve each row\" costs a\r\n * request per row and hammers the backend with duplicates. This dedupes **by digits** (see\r\n * `toDigits` — `+34600111222` and `34600111222` are one lookup), caches across pages, and remembers\r\n * misses so an unknown number is not re-asked forever.\r\n *\r\n * A failed lookup leaves the entry's `name` null and is NOT cached, so a later page retries: the\r\n * number is still callable, and an unreachable name service must not empty the tab.\r\n *\r\n * const history = withNameResolution(myHistorySource, myResolveContact);\r\n */\r\nexport function withNameResolution(\r\n source: HistorySource,\r\n resolve: (number: string, signal?: AbortSignal) => Promise<Contact | null>,\r\n): HistorySource {\r\n const cache = new Map<string, string | null>();\r\n\r\n return async (params) => {\r\n const entries = await source(params);\r\n\r\n // Only entries that still NEED a name. A source that already knows one (its own join, its own\r\n // cache) must not cost a lookup — the numbers it named are not our business.\r\n const wanted = [\r\n ...new Set(entries.filter((e) => e.name === null).map((e) => toDigits(e.number))),\r\n ].filter((d) => d !== '' && !cache.has(d));\r\n await Promise.all(\r\n wanted.map(async (digits) => {\r\n try {\r\n const contact = await resolve(digits);\r\n cache.set(digits, normalizeResolvedName(contact?.name ?? null, digits));\r\n } catch {\r\n // Deliberately not cached — a transient failure must not become a permanent blank.\r\n }\r\n }),\r\n );\r\n\r\n return entries.map((e): CallHistoryEntry => {\r\n // Never overwrite a name the source already knew.\r\n if (e.name !== null) return e;\r\n const resolved = cache.get(toDigits(e.number)) ?? null;\r\n return resolved === null ? e : { ...e, name: resolved };\r\n });\r\n };\r\n}\r\n","import { invalidateToken } from '../core/token-cache.js';\r\nimport type { TokenSource } from '../core/types.js';\r\n\r\n/** A failed backend call. `status` is the HTTP status, or 0 when the request never completed. */\r\nexport class ApiError extends Error {\r\n readonly status: number;\r\n\r\n constructor(status: number, message: string) {\r\n super(message);\r\n this.name = 'ApiError';\r\n this.status = status;\r\n }\r\n}\r\n\r\nexport type QueryParams = Readonly<Record<string, string | number | undefined>>;\r\n\r\n/**\r\n * A minimal authenticated GET against Coventia's phone service.\r\n *\r\n * Lives under `coventia/` rather than `core/`: the core ships no HTTP at all, so a partner on their\r\n * own backend never loads this.\r\n */\r\ninterface RequestConfig {\r\n readonly baseUrl: string;\r\n readonly tokenSource: TokenSource;\r\n readonly fetch?: typeof globalThis.fetch;\r\n}\r\n\r\n/**\r\n * Shared by `createGet` and `createPost`: one attempt against the backend, with the 401-retry-once\r\n * policy both need (see `createGet`'s own note on why). `method` is the only thing that differs\r\n * between the two — this backend's phone routes take every parameter as a query string, GET or POST\r\n * alike (the presence ticket mint is a POST with no body, `organization_id` on the query, same as any\r\n * GET here), so there is nothing else for a request body to carry.\r\n */\r\nfunction createRequest(config: RequestConfig, method: 'GET' | 'POST') {\r\n // Bound, not passed by reference: an unbound `fetch` throws \"Illegal invocation\" in some browsers.\r\n const doFetch: typeof globalThis.fetch = config.fetch\r\n ? config.fetch\r\n : (...args) => globalThis.fetch(...args);\r\n const base = config.baseUrl.replace(/\\/+$/, '');\r\n\r\n /**\r\n * One attempt. Split out so a 401 can be retried exactly once with a fresh token — see the\r\n * returned function's own note.\r\n */\r\n async function attempt<T>(path: string, params: QueryParams, signal?: AbortSignal): Promise<T> {\r\n const { token } = await config.tokenSource.getToken(signal);\r\n const qs = new URLSearchParams();\r\n for (const [k, v] of Object.entries(params)) {\r\n if (v !== undefined && v !== '') qs.set(k, String(v));\r\n }\r\n const query = qs.toString();\r\n\r\n const res = await doFetch(`${base}${path}${query ? `?${query}` : ''}`, {\r\n method,\r\n // The RAW token, no `Bearer ` prefix — see `VoiceToken.token` (SPEC §6.1).\r\n headers: { Authorization: token },\r\n ...(signal ? { signal } : {}),\r\n });\r\n\r\n const body: unknown = await res.json().catch(() => null);\r\n const message = (body as { message?: string } | null)?.message ?? `${path} — HTTP ${res.status}`;\r\n\r\n if (!res.ok) throw new ApiError(res.status, message);\r\n // This backend answers 200 with `{ success: false }` on handled failures, so the status alone is\r\n // not enough to know a call worked.\r\n if ((body as { success?: boolean } | null)?.success === false) {\r\n throw new ApiError(res.status, message);\r\n }\r\n return body as T;\r\n }\r\n\r\n /**\r\n * Retry once on 401, with a freshly minted token.\r\n *\r\n * A cached token can be rejected before its nominal expiry — revoked on logout, or invalidated when\r\n * the extension is re-provisioned. Without this the cache would keep handing back the dead one for\r\n * up to its full TTL, so `revokeVoiceTokensForUser` would not actually stop a running softphone.\r\n *\r\n * Exactly one retry: a second 401 means the credential itself is refused, not stale, and retrying\r\n * would be a loop against the host's token endpoint.\r\n */\r\n return async function request<T>(path: string, params: QueryParams = {}, signal?: AbortSignal): Promise<T> {\r\n try {\r\n return await attempt<T>(path, params, signal);\r\n } catch (error) {\r\n if (!(error instanceof ApiError) || error.status !== 401) throw error;\r\n invalidateToken(config.tokenSource);\r\n return attempt<T>(path, params, signal);\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * A minimal authenticated GET against Coventia's phone service.\r\n *\r\n * Lives under `coventia/` rather than `core/`: the core ships no HTTP at all, so a partner on their\r\n * own backend never loads this.\r\n */\r\nexport function createGet(config: RequestConfig) {\r\n return createRequest(config, 'GET');\r\n}\r\n\r\n/** The POST counterpart — today, only the presence ticket mint (`POST /presence/ticket`) uses it. */\r\nexport function createPost(config: RequestConfig) {\r\n return createRequest(config, 'POST');\r\n}\r\n","import { normalizeResolvedName, toDigits, withNameResolution } from '../sources.js';\r\nimport { createGet, createPost } from './api.js';\r\nimport type {\r\n Contact,\r\n PresenceCallbacks,\r\n PresenceConnection,\r\n PresenceEntry,\r\n PresenceSource,\r\n PresenceState,\r\n TokenSource,\r\n TransferOutcomeEntry,\r\n TransferOutcomesPage,\r\n TransferOutcomeSource,\r\n} from '../core/types.js';\r\nimport type {\r\n AgentsDirectory,\r\n CallDetail,\r\n CallHandler,\r\n CallHistoryEntry,\r\n CallOutcome,\r\n ContactsPage,\r\n ContactsSource,\r\n HistorySource,\r\n} from '../ui/types.js';\r\n\r\nexport interface CoventiaSourcesConfig {\r\n /** Coventia backend base URL, e.g. `https://api.coventia.es`. */\r\n readonly baseUrl: string;\r\n readonly tokenSource: TokenSource;\r\n /**\r\n * Only for session-authenticated hosts. A voice token carries the organization in its claim and\r\n * the backend pins it from there, so a partner omits this and cannot point the SDK at a tenant\r\n * their token does not cover. See `CoventiaBackendConfig.organizationId`.\r\n */\r\n readonly organizationId?: string;\r\n /**\r\n * Where the phone routes live. `/phone` today; `/api/v1/phone` once the partner API ships (§6.6),\r\n * at which point only this string changes.\r\n */\r\n readonly basePath?: string;\r\n /**\r\n * **The one thing a provider implements.** Their contact list, searched on their own backend.\r\n *\r\n * Supplying this is enough for everything: it drives the Contactos tab, and caller ID is DERIVED\r\n * from it by searching for the incoming number (see `resolverFromContacts`). One source means the\r\n * Contactos tab, a live call and a history row can never disagree about who a number belongs to —\r\n * which they did when the list and the lookup were separate overrides.\r\n *\r\n * The matching happens on their backend, where the CRM is: we send the number as `search` and take\r\n * an exact digit match from the results.\r\n */\r\n readonly contacts?: ContactsSource;\r\n\r\n /**\r\n * Caller ID, only if searching the list is not how their CRM answers \"who is this number\".\r\n *\r\n * Rarely needed — `contacts` already derives it. Worth setting when a dedicated lookup is cheaper\r\n * than a search, or when the number lives on a record that search does not return.\r\n */\r\n readonly resolveContact?: (number: string) => Promise<Contact | null>;\r\n /** Overrides polling `/transfers/outcomes` — see `CoventiaBackendConfig.transferOutcomes`. */\r\n readonly transferOutcomes?: TransferOutcomeSource;\r\n /** Overrides the presence WebSocket — see `CoventiaBackendConfig.presence`. */\r\n readonly presence?: PresenceSource;\r\n readonly fetch?: typeof globalThis.fetch;\r\n /** Overridable for tests — a fake `WebSocket` constructor. Defaults to `globalThis.WebSocket`. */\r\n readonly WebSocket?: typeof globalThis.WebSocket;\r\n}\r\n\r\nexport interface CoventiaSources {\r\n /** The Contactos tab. A partner may replace this with their own; everything else cannot be. */\r\n readonly contacts: ContactsSource;\r\n /** The Historial tab. **Coventia's, always** — we store the calls and the recordings. */\r\n readonly history: HistorySource;\r\n /** Caller ID for an incoming number, for `SoftphoneConfig.resolveContact`. */\r\n readonly resolveContact: (number: string) => Promise<Contact | null>;\r\n /** The Historial tab's \"filter by person\" directory. Always Coventia's own org membership. */\r\n readonly listAgents: () => Promise<AgentsDirectory>;\r\n /** Phase A (transfer outcome relay) — polled by the transfer watch, never by UI directly. */\r\n readonly transferOutcomes: TransferOutcomeSource;\r\n /** Phase B (presence-gated transfer picker) — opened by `presence-socket.ts`, never by UI directly. */\r\n readonly presence: PresenceSource;\r\n}\r\n\r\n/** What `/agents` returns. */\r\ninterface AgentRow {\r\n readonly user_id: string;\r\n readonly name: string | null;\r\n}\r\n\r\n/** What `/webrtc/contacts` returns. **The only place `wa_id` is named in the whole SDK.** */\r\ninterface WhatsAppContactRow {\r\n readonly wa_id: string;\r\n readonly name: string | null;\r\n}\r\n\r\n/**\r\n * `wa_id` → `Contact`.\r\n *\r\n * `id` IS the `wa_id`, and that leaks nothing: the backend serves callable contacts only\r\n * (`wa_id: /^[0-9]+$/`), so the value is literally the phone digits and carries nothing `phone` does\r\n * not. What the boundary removes is the SHAPE — no `custom_username`/`username` coalescing, no\r\n * WhatsApp field names past this function.\r\n */\r\nconst toContact = (row: WhatsAppContactRow): Contact => ({\r\n id: row.wa_id,\r\n name: row.name,\r\n phone: `+${row.wa_id}`,\r\n});\r\n\r\n/** One hop in `handled_by_chain`, as Mongo stores it — no `name` (see `HandledByChainRow`'s own note). */\r\ninterface HandledByChainRow {\r\n readonly ext?: string;\r\n readonly user_id?: string | null;\r\n}\r\n\r\n/** The same hop, enriched by `GET /calls/:id` with a resolved display name. */\r\ninterface EnrichedHandledByChainRow extends HandledByChainRow {\r\n readonly name?: string | null;\r\n}\r\n\r\n/** A row from `/calls`. The backend spreads raw Mongo docs, so every read below is defensive. */\r\ninterface CallRow {\r\n readonly _id?: string;\r\n /** The PBX's vocabulary: `incoming` | `outgoing`. NOT the SDK's `inbound`/`outbound`. */\r\n readonly direction?: string;\r\n readonly caller_id?: string;\r\n readonly called_did?: string;\r\n readonly destination?: string;\r\n readonly disposition?: string;\r\n readonly duration?: number;\r\n readonly created_at?: string | number;\r\n readonly recording_count?: number;\r\n readonly handled_by_chain?: readonly HandledByChainRow[];\r\n /** The transfer route as bare extensions — populated even when `handled_by_chain` isn't (older\r\n * docs, written before the chain existed). */\r\n readonly extensions?: readonly string[];\r\n readonly internal?: string;\r\n readonly last_internal?: string;\r\n}\r\n\r\nconst toDirection = (raw: string | undefined): 'inbound' | 'outbound' =>\r\n raw === 'outgoing' ? 'outbound' : 'inbound';\r\n\r\n/** Which field holds the other party depends on the direction. */\r\nconst otherParty = (row: CallRow): string =>\r\n toDirection(row.direction) === 'outbound'\r\n ? (row.called_did ?? row.destination ?? '')\r\n : (row.caller_id ?? '');\r\n\r\n/**\r\n * Who handled the call, in order. Prefers the real chain (`ext` + resolved `user_id`); falls back to\r\n * bare extensions ONLY when there is no chain at all — a doc written before `handled_by_chain` existed.\r\n * Unifies both shapes into one `CallHandler[]` so the UI never has to know which source it came from:\r\n * a bare extension is just a handler with `userId: null, name: null`.\r\n */\r\nfunction toHandledBy(row: CallRow, chain: readonly EnrichedHandledByChainRow[] = row.handled_by_chain ?? []): readonly CallHandler[] {\r\n if (chain.length > 0) {\r\n return chain\r\n .filter((h): h is EnrichedHandledByChainRow & { ext: string } => Boolean(h?.ext))\r\n .map((h) => ({ extension: h.ext, userId: h.user_id ?? null, name: h.name ?? null }));\r\n }\r\n const bare = row.extensions?.length ? row.extensions : [row.internal, row.last_internal];\r\n return [...new Set(bare.filter((e): e is string => Boolean(e)))].map((extension) => ({\r\n extension,\r\n userId: null,\r\n name: null,\r\n }));\r\n}\r\n\r\n/**\r\n * \"Not answered\" is ANY non-empty disposition other than `answered` — `no answer`, `busy`, `cancel`,\r\n * `failed`, `congestion`, and whatever Zadarma adds next.\r\n *\r\n * Open-ended rather than an allow-list, so an unrecognised value surfaces under \"Perdidas\" instead of\r\n * vanishing. **An EMPTY disposition counts as ANSWERED**, so a call of unknown status is not wrongly\r\n * flagged. Both halves are load-bearing.\r\n */\r\nconst isMissed = (row: CallRow): boolean => {\r\n const d = String(row.disposition ?? '').trim().toLowerCase();\r\n return d !== '' && d !== 'answered';\r\n};\r\n\r\n/**\r\n * The PBX's own vocabulary translated to the SDK's, for a call `isMissed`. Never returns Zadarma's\r\n * raw string — an unrecognised or future value falls into `'unavailable'` rather than leaking through\r\n * or being dropped, same open-ended reasoning `isMissed` itself documents.\r\n */\r\nconst toOutcome = (row: CallRow): CallOutcome => {\r\n switch (String(row.disposition ?? '').trim().toLowerCase()) {\r\n case 'no answer':\r\n return 'no-answer';\r\n case 'busy':\r\n return 'busy';\r\n case 'cancel':\r\n return 'cancelled';\r\n case 'failed':\r\n return 'failed';\r\n default:\r\n return 'unavailable';\r\n }\r\n};\r\n\r\n/**\r\n * Caller ID, derived from a contacts source: ask it for the number and take an exact match.\r\n *\r\n * This is why a provider only has to implement ONE function. Their search endpoint already knows how\r\n * to match a phone number — it is the same question the Contactos tab asks, with a different query —\r\n * and doing it on their backend is where the CRM actually lives.\r\n *\r\n * **Exact digit match only.** A search for a number can return partial or fuzzy hits, and naming a\r\n * caller after a near-miss is worse than showing the bare number: the user trusts the name.\r\n * `toDigits` on both sides so `+34 600 111 222` and `34600111222` compare equal.\r\n */\r\nexport function resolverFromContacts(\r\n contacts: ContactsSource,\r\n): (number: string) => Promise<Contact | null> {\r\n return async (number) => {\r\n const digits = toDigits(number);\r\n if (!digits) return null;\r\n // The number as the PBX reported it — their backend has the most to work with that way. `limit`\r\n // is small: we want one exact hit, not a page of near misses.\r\n const page = await contacts({ search: number, skip: 0, limit: 10 });\r\n return page.items.find((c) => toDigits(c.phone) === digits) ?? null;\r\n };\r\n}\r\n\r\nconst toDate = (raw: string | number | undefined): Date => {\r\n if (raw === undefined) return new Date(0);\r\n const d = new Date(raw);\r\n return Number.isNaN(d.getTime()) ? new Date(0) : d;\r\n};\r\n\r\n/** What `GET /transfers/outcomes` returns per entry — `transferOutcomeStore.js`'s own wire shape. */\r\ninterface TransferOutcomeRow {\r\n readonly id?: string;\r\n readonly target?: string;\r\n /** The backend's wire value; `'connected'` or anything else (`'not_connected'`) maps below. */\r\n readonly status?: string;\r\n readonly at?: string;\r\n}\r\n\r\n/** `not_connected` → `'not-connected'` (spec §4 D2's kebab-case, matching `HangupReason`) — the ONE\r\n * place that translation happens; open-ended rather than an allow-list, so an unrecognised status\r\n * fails safe as `'not-connected'` (the agent gets told, never silently nothing) rather than throwing. */\r\nconst toTransferOutcome = (row: TransferOutcomeRow): TransferOutcomeEntry => ({\r\n id: row.id ?? '',\r\n target: row.target ?? '',\r\n status: row.status === 'connected' ? 'connected' : 'not-connected',\r\n at: toDate(row.at),\r\n});\r\n\r\n/** What the presence WebSocket's `snapshot`/`delta` messages carry per agent — `presenceSocket.js`'s\r\n * own wire shape (`docs/specs/transfer-presence-picker.md` §4.3, plan §2). */\r\ninterface PresenceAgentRow {\r\n readonly userId?: string;\r\n readonly extension?: string;\r\n readonly name?: string | null;\r\n readonly state?: string;\r\n}\r\n\r\n/** Open-ended rather than an allow-list, matching `toTransferOutcome`'s own reasoning for an\r\n * unrecognised value: fails safe as `'desconectado'` (never wrongly reachable) rather than throwing. */\r\nconst toPresenceState = (raw: string | undefined): PresenceState =>\r\n raw === 'disponible' || raw === 'en_llamada' ? raw : 'desconectado';\r\n\r\nconst toPresenceEntry = (row: PresenceAgentRow): PresenceEntry => ({\r\n userId: row.userId ?? '',\r\n extension: row.extension ?? '',\r\n name: row.name ?? null,\r\n state: toPresenceState(row.state),\r\n});\r\n\r\n/** `POST /presence/ticket`'s response (`presenceTicketStore.js`'s own wire shape). */\r\ninterface PresenceTicketResponse {\r\n readonly success?: boolean;\r\n readonly ticket?: string;\r\n readonly expires_in?: number;\r\n}\r\n\r\n/** `https://api.coventia.es` → `wss://api.coventia.es`; `http://` → `ws://`. Scheme swapped, and any\r\n * trailing slash trimmed — same normalisation `createGet`/`createPost` apply to `baseUrl` internally,\r\n * needed again here since this function works from the raw config value, not their trimmed copy. */\r\nconst toWebSocketUrl = (httpUrl: string): string => httpUrl.replace(/^http/, 'ws').replace(/\\/+$/, '');\r\n\r\n/**\r\n * Coventia's phone service, as the sources the widget asks for.\r\n *\r\n * **This is the anti-corruption layer.** The backend speaks WhatsApp's data model; the SDK speaks\r\n * `Contact` and `CallHistoryEntry`. The translation happens here and nowhere else — enforced by\r\n * structure, since `core/` cannot reach `coventia/` and a partner never imports it.\r\n */\r\nexport function coventiaSources(config: CoventiaSourcesConfig): CoventiaSources {\r\n const get = createGet(config);\r\n const post = createPost(config);\r\n const base = config.basePath ?? '/phone';\r\n // Absent for a voice token, which already names the organization. Spread into every query below,\r\n // so omitting it removes the parameter entirely rather than sending `organization_id=undefined`.\r\n const org = config.organizationId ? { organization_id: config.organizationId } : {};\r\n\r\n const coventiaContacts: ContactsSource = async ({ search, skip, limit }): Promise<ContactsPage> => {\r\n const body = await get<{ contacts?: readonly WhatsAppContactRow[]; total?: number }>(\r\n `${base}/webrtc/contacts`,\r\n { ...org, search, skip, limit },\r\n );\r\n return { items: (body.contacts ?? []).map(toContact), total: body.total ?? 0 };\r\n };\r\n\r\n const coventiaResolve = async (number: string): Promise<Contact | null> => {\r\n const digits = toDigits(number);\r\n if (!digits) return null;\r\n const body = await get<{ name?: string | null }>(`${base}/webrtc/caller-name`, {\r\n ...org,\r\n number: digits,\r\n });\r\n // Re-applied on top of the backend's own rule: a contact saved as its own number resolves to a\r\n // \"name\" that is just the number reformatted, and showing that puts +34600111222 where a name\r\n // belongs — next to the number the widget is already displaying.\r\n const name = normalizeResolvedName(body.name ?? null, digits);\r\n return name === null ? null : { id: digits, name, phone: `+${digits}` };\r\n };\r\n\r\n const rawHistory: HistorySource = async ({\r\n skip,\r\n limit,\r\n missed,\r\n userId,\r\n }): Promise<readonly CallHistoryEntry[]> => {\r\n const body = await get<{ calls?: readonly CallRow[] }>(`${base}/calls`, {\r\n ...org,\r\n skip,\r\n limit,\r\n // Omitted rather than `'false'` — the backend only special-cases `missed=true`, so leaving it\r\n // out entirely for \"Todas\" is equivalent and one less thing for a param dump to explain.\r\n missed: missed ? 'true' : undefined,\r\n user_id: userId,\r\n });\r\n return (body.calls ?? []).map((row): CallHistoryEntry => {\r\n const id = row._id ?? '';\r\n const recorded = (row.recording_count ?? 0) > 0;\r\n const missedRow = isMissed(row);\r\n const handledBy = toHandledBy(row);\r\n return {\r\n id,\r\n direction: toDirection(row.direction),\r\n number: otherParty(row),\r\n // Filled by `withNameResolution` at the mount — deduped by digits and cached across pages.\r\n name: null,\r\n at: toDate(row.created_at),\r\n durationSec: row.duration ?? 0,\r\n missed: missedRow,\r\n ...(missedRow ? { outcome: toOutcome(row) } : {}),\r\n // The DID is only ever the ORG's own number, which only an inbound call dials — an outbound\r\n // row's `called_did` is the OTHER party (already `number` above); showing it again as \"DID\"\r\n // would just repeat the row under a wrong label.\r\n ...(toDirection(row.direction) === 'inbound' && row.called_did ? { did: row.called_did } : {}),\r\n recorded,\r\n handledBy,\r\n // The detail page's one enrichment fetch — signed recording URLs, plus this SAME chain with\r\n // names resolved. Skipped when there is nothing to enrich: minting signed URLs and resolving\r\n // names for a page of 30 calls nobody opens is pure waste.\r\n ...(recorded || handledBy.length > 0\r\n ? {\r\n loadDetail: async (): Promise<CallDetail> => {\r\n // Unlike `/calls` (rows spread flat into `calls: []`), `/calls/:id` wraps its one\r\n // result in `call` — `recordings.js`'s `res.json({ success, call: {...} })`.\r\n const detail = await get<{\r\n call?: {\r\n recording_urls?: readonly string[];\r\n handled_by_chain?: readonly EnrichedHandledByChainRow[];\r\n };\r\n }>(`${base}/calls/${encodeURIComponent(id)}`, org);\r\n return {\r\n recordingUrls: detail.call?.recording_urls ?? [],\r\n // `row`'s own bare-extension fields (read inside `toHandledBy` when the chain\r\n // passed here is empty) still work as the fallback — only the CHAIN comes from the\r\n // enriched response; the rest of `row` is unchanged.\r\n handledBy: toHandledBy(row, detail.call?.handled_by_chain ?? []),\r\n };\r\n },\r\n }\r\n : {}),\r\n };\r\n });\r\n };\r\n\r\n const coventiaListAgents = async (): Promise<AgentsDirectory> => {\r\n const body = await get<{ me?: AgentRow | null; people?: readonly AgentRow[] }>(\r\n `${base}/agents`,\r\n org,\r\n );\r\n const toAgent = (row: AgentRow) => ({ userId: row.user_id, name: row.name });\r\n return {\r\n me: body.me ? toAgent(body.me) : null,\r\n people: (body.people ?? []).map(toAgent),\r\n };\r\n };\r\n\r\n /**\r\n * Phase A (transfer outcome relay). `since` omitted on the caller's very first poll — that call\r\n * exists only to learn the starting `cursor`, per spec §5.3; this source is a thin, honest wrapper\r\n * around the endpoint and does not itself implement that \"discard the first page\" rule, which\r\n * belongs to the transfer watch (plan T5) that calls it.\r\n */\r\n const coventiaTransferOutcomes: TransferOutcomeSource = async ({ since }): Promise<TransferOutcomesPage> => {\r\n const body = await get<{ cursor?: string; outcomes?: readonly TransferOutcomeRow[] }>(\r\n `${base}/transfers/outcomes`,\r\n { ...org, since },\r\n );\r\n return {\r\n cursor: body.cursor ?? '0',\r\n outcomes: (body.outcomes ?? []).map(toTransferOutcome),\r\n };\r\n };\r\n\r\n /**\r\n * Phase B (presence-gated transfer picker). Mints a fresh, one-time ticket (never the real voice\r\n * token — spec §4.2's \"decided 2026-09-24: where the token travels\") and opens the WebSocket with\r\n * it. Called fresh on every (re)connect by `presence-socket.ts`, which owns retry/backoff — this\r\n * function is a single, honest attempt, the same division of labour `coventiaTransferOutcomes` has\r\n * with the transfer watch that calls IT.\r\n */\r\n const coventiaPresence: PresenceSource = (callbacks: PresenceCallbacks): Promise<PresenceConnection> => {\r\n const WS = config.WebSocket ?? globalThis.WebSocket;\r\n return new Promise<PresenceConnection>((resolve, reject) => {\r\n void (async () => {\r\n let ticket: string;\r\n try {\r\n const body = await post<PresenceTicketResponse>(`${base}/presence/ticket`, org);\r\n if (!body.ticket) throw new Error('presence ticket mint returned no ticket');\r\n ticket = body.ticket;\r\n } catch (error) {\r\n reject(error instanceof Error ? error : new Error(String(error)));\r\n return;\r\n }\r\n\r\n const wsUrl = `${toWebSocketUrl(config.baseUrl)}${base}/presence?ticket=${encodeURIComponent(ticket)}`;\r\n const ws = new WS(wsUrl);\r\n let settled = false;\r\n\r\n ws.onopen = () => {\r\n if (settled) return;\r\n settled = true;\r\n resolve({\r\n reportCallState: (onCall) => {\r\n if (ws.readyState !== WS.OPEN) return;\r\n ws.send(JSON.stringify({ type: 'call-state', onCall }));\r\n },\r\n bye: () => {\r\n if (ws.readyState !== WS.OPEN) return;\r\n ws.send(JSON.stringify({ type: 'bye' }));\r\n },\r\n close: () => {\r\n // Detached first — a deliberate close must never be reported to the caller as a drop\r\n // (`PresenceConnection.close`'s own contract).\r\n ws.onclose = null;\r\n ws.onerror = null;\r\n ws.close();\r\n },\r\n });\r\n };\r\n\r\n ws.onmessage = (event: MessageEvent) => {\r\n let data: unknown;\r\n try {\r\n data = JSON.parse(String(event.data));\r\n } catch {\r\n return; // not JSON — ignore rather than tear down the connection over one stray frame\r\n }\r\n const msg = data as {\r\n type?: string;\r\n agents?: readonly PresenceAgentRow[];\r\n agent?: PresenceAgentRow;\r\n };\r\n if (msg.type === 'snapshot') {\r\n callbacks.onSnapshot((msg.agents ?? []).map(toPresenceEntry));\r\n } else if (msg.type === 'delta' && msg.agent) {\r\n callbacks.onDelta(toPresenceEntry(msg.agent));\r\n }\r\n };\r\n\r\n ws.onclose = () => {\r\n if (!settled) {\r\n // Never actually opened (ticket refused, network refusal, ...) — indistinguishable from a\r\n // drop to `presence-socket.ts`, which is exactly why this rejects rather than calling\r\n // `onClose`: there is no `PresenceConnection` yet for that callback to be about.\r\n settled = true;\r\n reject(new Error('presence socket closed before opening'));\r\n return;\r\n }\r\n callbacks.onClose();\r\n };\r\n\r\n // A real WebSocket always follows `error` with `close` — nothing extra to do here, and a\r\n // browser gives an `error` event no further detail worth reporting anyway.\r\n ws.onerror = () => {};\r\n })();\r\n });\r\n };\r\n\r\n const contacts = config.contacts ?? coventiaContacts;\r\n\r\n /*\r\n * Caller ID follows the CONTACT LIST, not a separate setting.\r\n *\r\n * A provider who supplies `contacts` gets a resolver derived from it, so the tab, a live call and a\r\n * history row always name the same number the same way. Supplying only the list used to leave\r\n * caller ID on ours — the same person appearing twice under two names inside one widget.\r\n */\r\n const resolve =\r\n config.resolveContact ??\r\n (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);\r\n\r\n // Names, resolved once per distinct number and cached across pages — a call log is the same handful\r\n // of numbers over and over. Wrapped HERE rather than left to the caller: the raw rows carry\r\n // `name: null`, so a caller who forgets this gets a history of bare numbers and no error.\r\n const history = withNameResolution(rawHistory, resolve);\r\n\r\n const transferOutcomes = config.transferOutcomes ?? coventiaTransferOutcomes;\r\n const presence = config.presence ?? coventiaPresence;\r\n\r\n return {\r\n contacts,\r\n history,\r\n resolveContact: resolve,\r\n listAgents: coventiaListAgents,\r\n transferOutcomes,\r\n presence,\r\n };\r\n}\r\n"]}
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as SoftphoneState, k as SoftphoneError, e as CallOptions, H as HangupReason, l as SoftphoneErrorCode, p as TokenSource } from '../call-
|
|
2
|
-
import { C as CallEngine, E as EngineCall } from '../engine-
|
|
1
|
+
import { o as SoftphoneState, k as SoftphoneError, e as CallOptions, H as HangupReason, l as SoftphoneErrorCode, p as TokenSource } from '../call-VKq1I8yP.cjs';
|
|
2
|
+
import { C as CallEngine, E as EngineCall } from '../engine-BlQBo2Ys.cjs';
|
|
3
3
|
|
|
4
4
|
interface SimulateIncomingOptions {
|
|
5
5
|
readonly from: string;
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as SoftphoneState, k as SoftphoneError, e as CallOptions, H as HangupReason, l as SoftphoneErrorCode, p as TokenSource } from '../call-
|
|
2
|
-
import { C as CallEngine, E as EngineCall } from '../engine-
|
|
1
|
+
import { o as SoftphoneState, k as SoftphoneError, e as CallOptions, H as HangupReason, l as SoftphoneErrorCode, p as TokenSource } from '../call-VKq1I8yP.js';
|
|
2
|
+
import { C as CallEngine, E as EngineCall } from '../engine-C0BOkIcW.js';
|
|
3
3
|
|
|
4
4
|
interface SimulateIncomingOptions {
|
|
5
5
|
readonly from: string;
|