@coventia/softphone 0.1.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.
Files changed (69) hide show
  1. package/dist/3x2-2HQTJDKZ.js +6583 -0
  2. package/dist/3x2-2HQTJDKZ.js.map +1 -0
  3. package/dist/3x2-INKWPNLA.js +6585 -0
  4. package/dist/3x2-INKWPNLA.js.map +1 -0
  5. package/dist/call-BTYlT2SU.d.cts +680 -0
  6. package/dist/call-BTYlT2SU.d.ts +680 -0
  7. package/dist/chunk-2ZSOGGPO.js +169 -0
  8. package/dist/chunk-2ZSOGGPO.js.map +1 -0
  9. package/dist/chunk-3V2L6UTK.js +129 -0
  10. package/dist/chunk-3V2L6UTK.js.map +1 -0
  11. package/dist/chunk-5BGEHMPP.js +945 -0
  12. package/dist/chunk-5BGEHMPP.js.map +1 -0
  13. package/dist/chunk-5WRI5ZAA.js +29 -0
  14. package/dist/chunk-5WRI5ZAA.js.map +1 -0
  15. package/dist/chunk-KAXEMJ7E.js +11 -0
  16. package/dist/chunk-KAXEMJ7E.js.map +1 -0
  17. package/dist/chunk-MHOJHG5E.js +39 -0
  18. package/dist/chunk-MHOJHG5E.js.map +1 -0
  19. package/dist/chunk-MP7CSA24.js +299 -0
  20. package/dist/chunk-MP7CSA24.js.map +1 -0
  21. package/dist/chunk-N2N2EQQJ.js +29 -0
  22. package/dist/chunk-N2N2EQQJ.js.map +1 -0
  23. package/dist/element/index.d.ts +853 -0
  24. package/dist/element/index.js +9635 -0
  25. package/dist/element/index.js.map +1 -0
  26. package/dist/engine-D9nbeY08.d.cts +115 -0
  27. package/dist/engine-Doa6Brkm.d.ts +115 -0
  28. package/dist/index.cjs +2133 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +62 -0
  31. package/dist/index.d.ts +62 -0
  32. package/dist/index.js +630 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/min-64GIMWWL.js +5456 -0
  35. package/dist/min-64GIMWWL.js.map +1 -0
  36. package/dist/min-ATXYR23R.js +5458 -0
  37. package/dist/min-ATXYR23R.js.map +1 -0
  38. package/dist/react/index.cjs +138 -0
  39. package/dist/react/index.cjs.map +1 -0
  40. package/dist/react/index.d.cts +68 -0
  41. package/dist/react/index.d.ts +68 -0
  42. package/dist/react/index.js +4 -0
  43. package/dist/react/index.js.map +1 -0
  44. package/dist/softphone-DHmhotbf.d.ts +128 -0
  45. package/dist/softphone-DhuhcKCy.d.cts +128 -0
  46. package/dist/softphone.css +1923 -0
  47. package/dist/sources-N4BOEDRL.js +226 -0
  48. package/dist/sources-N4BOEDRL.js.map +1 -0
  49. package/dist/testing/index.cjs +345 -0
  50. package/dist/testing/index.cjs.map +1 -0
  51. package/dist/testing/index.d.cts +66 -0
  52. package/dist/testing/index.d.ts +66 -0
  53. package/dist/testing/index.js +186 -0
  54. package/dist/testing/index.js.map +1 -0
  55. package/dist/ui/index.cjs +15428 -0
  56. package/dist/ui/index.cjs.map +1 -0
  57. package/dist/ui/index.d.cts +188 -0
  58. package/dist/ui/index.d.ts +188 -0
  59. package/dist/ui/index.js +2256 -0
  60. package/dist/ui/index.js.map +1 -0
  61. package/dist/zadarma/index.cjs +1144 -0
  62. package/dist/zadarma/index.cjs.map +1 -0
  63. package/dist/zadarma/index.d.cts +86 -0
  64. package/dist/zadarma/index.d.ts +86 -0
  65. package/dist/zadarma/index.js +5 -0
  66. package/dist/zadarma/index.js.map +1 -0
  67. package/dist/zadarma-engine-DN2SZSQ3.js +5 -0
  68. package/dist/zadarma-engine-DN2SZSQ3.js.map +1 -0
  69. package/package.json +123 -0
@@ -0,0 +1,226 @@
1
+ import { invalidateToken } from './chunk-MHOJHG5E.js';
2
+
3
+ // src/sources.ts
4
+ function toDigits(number) {
5
+ return String(number ?? "").replace(/\D/g, "");
6
+ }
7
+ function normalizeResolvedName(raw, number) {
8
+ if (!raw) return null;
9
+ const trimmed = String(raw).trim();
10
+ if (!trimmed) return null;
11
+ const digits = toDigits(number);
12
+ if (!digits) return trimmed;
13
+ return toDigits(trimmed) === digits ? null : trimmed;
14
+ }
15
+ function withNameResolution(source, resolve) {
16
+ const cache = /* @__PURE__ */ new Map();
17
+ return async (params) => {
18
+ const entries = await source(params);
19
+ const wanted = [
20
+ ...new Set(entries.filter((e) => e.name === null).map((e) => toDigits(e.number)))
21
+ ].filter((d) => d !== "" && !cache.has(d));
22
+ await Promise.all(
23
+ wanted.map(async (digits) => {
24
+ try {
25
+ const contact = await resolve(digits);
26
+ cache.set(digits, normalizeResolvedName(contact?.name ?? null, digits));
27
+ } catch {
28
+ }
29
+ })
30
+ );
31
+ return entries.map((e) => {
32
+ if (e.name !== null) return e;
33
+ const resolved = cache.get(toDigits(e.number)) ?? null;
34
+ return resolved === null ? e : { ...e, name: resolved };
35
+ });
36
+ };
37
+ }
38
+
39
+ // src/coventia/api.ts
40
+ var ApiError = class extends Error {
41
+ status;
42
+ constructor(status, message) {
43
+ super(message);
44
+ this.name = "ApiError";
45
+ this.status = status;
46
+ }
47
+ };
48
+ function createGet(config) {
49
+ const doFetch = config.fetch ? config.fetch : (...args) => globalThis.fetch(...args);
50
+ const base = config.baseUrl.replace(/\/+$/, "");
51
+ async function attempt(path, params, signal) {
52
+ const { token } = await config.tokenSource.getToken(signal);
53
+ const qs = new URLSearchParams();
54
+ for (const [k, v] of Object.entries(params)) {
55
+ if (v !== void 0 && v !== "") qs.set(k, String(v));
56
+ }
57
+ const query = qs.toString();
58
+ const res = await doFetch(`${base}${path}${query ? `?${query}` : ""}`, {
59
+ // The RAW token, no `Bearer ` prefix — see `VoiceToken.token` (SPEC §6.1).
60
+ headers: { Authorization: token },
61
+ ...signal ? { signal } : {}
62
+ });
63
+ const body = await res.json().catch(() => null);
64
+ const message = body?.message ?? `${path} \u2014 HTTP ${res.status}`;
65
+ if (!res.ok) throw new ApiError(res.status, message);
66
+ if (body?.success === false) {
67
+ throw new ApiError(res.status, message);
68
+ }
69
+ return body;
70
+ }
71
+ return async function get(path, params = {}, signal) {
72
+ try {
73
+ return await attempt(path, params, signal);
74
+ } catch (error) {
75
+ if (!(error instanceof ApiError) || error.status !== 401) throw error;
76
+ invalidateToken(config.tokenSource);
77
+ return attempt(path, params, signal);
78
+ }
79
+ };
80
+ }
81
+
82
+ // src/coventia/sources.ts
83
+ var toContact = (row) => ({
84
+ id: row.wa_id,
85
+ name: row.name,
86
+ phone: `+${row.wa_id}`
87
+ });
88
+ var toDirection = (raw) => raw === "outgoing" ? "outbound" : "inbound";
89
+ var otherParty = (row) => toDirection(row.direction) === "outbound" ? row.called_did ?? row.destination ?? "" : row.caller_id ?? "";
90
+ function toHandledBy(row, chain = row.handled_by_chain ?? []) {
91
+ if (chain.length > 0) {
92
+ return chain.filter((h) => Boolean(h?.ext)).map((h) => ({ extension: h.ext, userId: h.user_id ?? null, name: h.name ?? null }));
93
+ }
94
+ const bare = row.extensions?.length ? row.extensions : [row.internal, row.last_internal];
95
+ return [...new Set(bare.filter((e) => Boolean(e)))].map((extension) => ({
96
+ extension,
97
+ userId: null,
98
+ name: null
99
+ }));
100
+ }
101
+ var isMissed = (row) => {
102
+ const d = String(row.disposition ?? "").trim().toLowerCase();
103
+ return d !== "" && d !== "answered";
104
+ };
105
+ var toOutcome = (row) => {
106
+ switch (String(row.disposition ?? "").trim().toLowerCase()) {
107
+ case "no answer":
108
+ return "no-answer";
109
+ case "busy":
110
+ return "busy";
111
+ case "cancel":
112
+ return "cancelled";
113
+ case "failed":
114
+ return "failed";
115
+ default:
116
+ return "unavailable";
117
+ }
118
+ };
119
+ function resolverFromContacts(contacts) {
120
+ return async (number) => {
121
+ const digits = toDigits(number);
122
+ if (!digits) return null;
123
+ const page = await contacts({ search: number, skip: 0, limit: 10 });
124
+ return page.items.find((c) => toDigits(c.phone) === digits) ?? null;
125
+ };
126
+ }
127
+ var toDate = (raw) => {
128
+ if (raw === void 0) return /* @__PURE__ */ new Date(0);
129
+ const d = new Date(raw);
130
+ return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
131
+ };
132
+ function coventiaSources(config) {
133
+ const get = createGet(config);
134
+ const base = config.basePath ?? "/phone";
135
+ const org = config.organizationId ? { organization_id: config.organizationId } : {};
136
+ const coventiaContacts = async ({ search, skip, limit }) => {
137
+ const body = await get(
138
+ `${base}/webrtc/contacts`,
139
+ { ...org, search, skip, limit }
140
+ );
141
+ return { items: (body.contacts ?? []).map(toContact), total: body.total ?? 0 };
142
+ };
143
+ const coventiaResolve = async (number) => {
144
+ const digits = toDigits(number);
145
+ if (!digits) return null;
146
+ const body = await get(`${base}/webrtc/caller-name`, {
147
+ ...org,
148
+ number: digits
149
+ });
150
+ const name = normalizeResolvedName(body.name ?? null, digits);
151
+ return name === null ? null : { id: digits, name, phone: `+${digits}` };
152
+ };
153
+ const rawHistory = async ({
154
+ skip,
155
+ limit,
156
+ missed,
157
+ userId
158
+ }) => {
159
+ const body = await get(`${base}/calls`, {
160
+ ...org,
161
+ skip,
162
+ limit,
163
+ // Omitted rather than `'false'` — the backend only special-cases `missed=true`, so leaving it
164
+ // out entirely for "Todas" is equivalent and one less thing for a param dump to explain.
165
+ missed: missed ? "true" : void 0,
166
+ user_id: userId
167
+ });
168
+ return (body.calls ?? []).map((row) => {
169
+ const id = row._id ?? "";
170
+ const recorded = (row.recording_count ?? 0) > 0;
171
+ const missedRow = isMissed(row);
172
+ const handledBy = toHandledBy(row);
173
+ return {
174
+ id,
175
+ direction: toDirection(row.direction),
176
+ number: otherParty(row),
177
+ // Filled by `withNameResolution` at the mount — deduped by digits and cached across pages.
178
+ name: null,
179
+ at: toDate(row.created_at),
180
+ durationSec: row.duration ?? 0,
181
+ missed: missedRow,
182
+ ...missedRow ? { outcome: toOutcome(row) } : {},
183
+ // The DID is only ever the ORG's own number, which only an inbound call dials — an outbound
184
+ // row's `called_did` is the OTHER party (already `number` above); showing it again as "DID"
185
+ // would just repeat the row under a wrong label.
186
+ ...toDirection(row.direction) === "inbound" && row.called_did ? { did: row.called_did } : {},
187
+ recorded,
188
+ handledBy,
189
+ // The detail page's one enrichment fetch — signed recording URLs, plus this SAME chain with
190
+ // names resolved. Skipped when there is nothing to enrich: minting signed URLs and resolving
191
+ // names for a page of 30 calls nobody opens is pure waste.
192
+ ...recorded || handledBy.length > 0 ? {
193
+ loadDetail: async () => {
194
+ const detail = await get(`${base}/calls/${encodeURIComponent(id)}`, org);
195
+ return {
196
+ recordingUrls: detail.call?.recording_urls ?? [],
197
+ // `row`'s own bare-extension fields (read inside `toHandledBy` when the chain
198
+ // passed here is empty) still work as the fallback — only the CHAIN comes from the
199
+ // enriched response; the rest of `row` is unchanged.
200
+ handledBy: toHandledBy(row, detail.call?.handled_by_chain ?? [])
201
+ };
202
+ }
203
+ } : {}
204
+ };
205
+ });
206
+ };
207
+ const coventiaListAgents = async () => {
208
+ const body = await get(
209
+ `${base}/agents`,
210
+ org
211
+ );
212
+ const toAgent = (row) => ({ userId: row.user_id, name: row.name });
213
+ return {
214
+ me: body.me ? toAgent(body.me) : null,
215
+ people: (body.people ?? []).map(toAgent)
216
+ };
217
+ };
218
+ const contacts = config.contacts ?? coventiaContacts;
219
+ const resolve = config.resolveContact ?? (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);
220
+ const history = withNameResolution(rawHistory, resolve);
221
+ return { contacts, history, resolveContact: resolve, listAgents: coventiaListAgents };
222
+ }
223
+
224
+ export { coventiaSources, resolverFromContacts };
225
+ //# sourceMappingURL=sources-N4BOEDRL.js.map
226
+ //# sourceMappingURL=sources-N4BOEDRL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sources.ts","../src/coventia/api.ts","../src/coventia/sources.ts"],"names":[],"mappings":"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","file":"sources-N4BOEDRL.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\nexport function createGet(config: {\r\n readonly baseUrl: string;\r\n readonly tokenSource: TokenSource;\r\n readonly fetch?: typeof globalThis.fetch;\r\n}) {\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 `get`.\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 // 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 get<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","import { normalizeResolvedName, toDigits, withNameResolution } from '../sources.js';\r\nimport { createGet } from './api.js';\r\nimport type { Contact, TokenSource } 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 readonly fetch?: typeof globalThis.fetch;\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}\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/**\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 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 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 return { contacts, history, resolveContact: resolve, listAgents: coventiaListAgents };\r\n}\r\n"]}
@@ -0,0 +1,345 @@
1
+ 'use strict';
2
+
3
+ // src/core/errors.ts
4
+ var ERROR_CATALOGUE = {
5
+ // ── Engine lifecycle ────────────────────────────────────────────────────────
6
+ "engine/duplicate-instance": {
7
+ fatal: true,
8
+ message: "A Softphone instance is already running on this page.",
9
+ solution: "Construct one Softphone and share it. React StrictMode double-mounts in development \u2014 hold the instance above the tree, or in a ref, and call destroy() on unmount."
10
+ },
11
+ "engine/not-started": {
12
+ fatal: false,
13
+ message: "The softphone has not been started.",
14
+ solution: "Await softphone.start() before placing or answering a call."
15
+ },
16
+ "engine/destroyed": {
17
+ fatal: true,
18
+ message: "This Softphone has been destroyed and cannot be reused.",
19
+ solution: "Construct a new Softphone."
20
+ },
21
+ // ── Auth ────────────────────────────────────────────────────────────────────
22
+ "auth/token-fetch-failed": {
23
+ fatal: true,
24
+ message: "Could not obtain a voice token from the token endpoint.",
25
+ solution: "Check that the token endpoint is reachable and that the headers callback returns a valid host-app session token."
26
+ },
27
+ "auth/token-rejected": {
28
+ fatal: true,
29
+ message: "The voice token was rejected.",
30
+ solution: "The token is expired, malformed, or scoped to a different organization. Mint a fresh one."
31
+ },
32
+ // ── Media ───────────────────────────────────────────────────────────────────
33
+ /*
34
+ NONE of the four codes below are ever actually raised via `createError` — nothing in the SDK
35
+ constructs a `SoftphoneError` with these codes today. Deleting them was the other option
36
+ (SPEC discussion); they stay, commented, because the taxonomy and Spanish copy (`error-text.ts`)
37
+ are worth keeping intact for whenever a real caller wants them, and a `SoftphoneErrorCode` union
38
+ is a cheap thing to keep honest with a comment, expensive to keep churning as a breaking change.
39
+ */
40
+ "media/permission-denied": {
41
+ fatal: true,
42
+ message: "Microphone permission was denied.",
43
+ solution: "The permission prompt names the TOP-LEVEL origin. Ask for it from a user gesture, and re-request via the browser UI if previously blocked."
44
+ // NEVER RAISED. `MicPermissionGate` (ui/permission/) handles this condition today — it predates
45
+ // this catalogue and has BETTER, browser-specific unblock instructions than a generic gate could
46
+ // show. It does not import this catalogue at all; the two are unconnected on purpose.
47
+ },
48
+ "media/no-input-device": {
49
+ fatal: true,
50
+ message: "No microphone is available.",
51
+ solution: "Ask the user to connect a microphone, then retry."
52
+ // NEVER RAISED — same reason as `media/permission-denied` above: `MicPermissionGate` already
53
+ // owns this.
54
+ },
55
+ "media/webrtc-unsupported": {
56
+ fatal: true,
57
+ message: "This browser does not expose the WebRTC APIs.",
58
+ solution: "Usually a privacy extension (uBlock Origin Extra, WebRTC Leak Prevent) disabling RTCPeerConnection \u2014 Twilio documents the same. Ask the user to disable it for this origin."
59
+ // NEVER RAISED — same reason again: `MicPermissionGate` already owns this.
60
+ },
61
+ "media/playback-blocked": {
62
+ fatal: false,
63
+ message: "The browser blocked audio playback (autoplay policy).",
64
+ solution: "Call softphone.startAudio() from a click handler. Until then the user cannot hear the caller, and an INCOMING RINGTONE WILL BE SILENT \u2014 this is how calls get missed."
65
+ /*
66
+ NEVER RAISED — different reason from the other three. This is not superseded by an existing
67
+ UI; the feature it describes (autoplay-recovery) is simply not built yet. `Softphone.canPlayAudio`
68
+ / `startAudio()` (core/softphone.ts) are Phase 2 and currently no-op stubs — `canPlayAudio`
69
+ always reports `true`. This is where that future work would report the block once it exists.
70
+ */
71
+ },
72
+ // ── Network / signalling ────────────────────────────────────────────────────
73
+ "net/registration-failed": {
74
+ fatal: true,
75
+ message: "SIP registration failed.",
76
+ solution: "Usually a stale credential. The engine refreshes and retries; if it persists, the extension may not exist."
77
+ },
78
+ "net/transport-failed": {
79
+ fatal: true,
80
+ message: "The signalling transport could not be established.",
81
+ solution: "Check the CSP connect-src directive and any corporate proxy blocking WSS."
82
+ },
83
+ "net/ice-failed": {
84
+ fatal: false,
85
+ message: "Media connectivity was lost; attempting an ICE restart.",
86
+ solution: 'Usually a network handoff (wifi \u2192 cellular). The engine recovers on its own; surface a "reconnecting" banner rather than ending the call.'
87
+ /*
88
+ NEVER RAISED. The "reconnecting" banner this `solution` describes already exists — CallScreen
89
+ shows "Reconectando…" directly off the call's own `reconnecting` STATE (`STATUS_LABEL` in
90
+ views/CallScreen.tsx), which the SIP engine drives without going through the error channel at
91
+ all. Raising this too would say the same fact twice through two different mechanisms. It would
92
+ only earn its keep for host-side OBSERVABILITY — counting ICE restarts independently of the UI
93
+ — which nobody has asked for yet.
94
+ */
95
+ },
96
+ // ── Call ────────────────────────────────────────────────────────────────────
97
+ "call/not-answerable": {
98
+ fatal: false,
99
+ message: "This call cannot be answered in its current state.",
100
+ solution: "Only a call in `ringing` may be answered. Check call.state first."
101
+ },
102
+ "call/busy": {
103
+ fatal: false,
104
+ message: "The remote party is busy.",
105
+ solution: 'Nothing to fix. Note SIP 486 is also what most endpoints send for a manual decline, so do not label this "busy" alone in your UI \u2014 it is equally often "did not answer".'
106
+ },
107
+ "call/no-answer": {
108
+ fatal: false,
109
+ message: "The remote party did not answer.",
110
+ solution: "Nothing to fix. Surface it to the user."
111
+ },
112
+ "call/rejected": {
113
+ fatal: false,
114
+ message: "The remote party rejected the call.",
115
+ solution: "Nothing to fix. Surface it to the user."
116
+ },
117
+ "call/missed": {
118
+ fatal: false,
119
+ message: "An incoming call was cancelled before it was answered.",
120
+ solution: "Nothing to fix \u2014 the caller gave up while it rang. Surface it, and log it as a missed call."
121
+ },
122
+ "call/failed": {
123
+ fatal: false,
124
+ message: "The call could not be completed.",
125
+ solution: "The catch-all for a call that never connected \u2014 a bad number, a rejected offer, a media failure. `cause` carries the underlying SIP cause. Nothing is fatal: the phone still works."
126
+ },
127
+ "call/transfer-failed": {
128
+ fatal: false,
129
+ message: "The transfer could not be completed.",
130
+ solution: "The original call is still up. Let the user retry or resume."
131
+ },
132
+ "call/unsupported": {
133
+ fatal: false,
134
+ message: "The provider does not support this operation.",
135
+ solution: "Guard the UI on the operation before offering it."
136
+ },
137
+ // ── Provider ────────────────────────────────────────────────────────────────
138
+ "provider/provision-failed": {
139
+ fatal: true,
140
+ message: "The provider could not issue SIP credentials.",
141
+ solution: "The backend reached the carrier but got no usable credential. Check the org has a SIP extension provisioned."
142
+ }
143
+ };
144
+ var CatalogueError = class extends Error {
145
+ fatal;
146
+ code;
147
+ solution;
148
+ constructor(code, cause) {
149
+ const spec = ERROR_CATALOGUE[code];
150
+ super(spec.message, cause === void 0 ? void 0 : { cause });
151
+ this.name = "SoftphoneError";
152
+ this.code = code;
153
+ this.fatal = spec.fatal;
154
+ this.solution = spec.solution;
155
+ }
156
+ };
157
+ function createError(code, cause) {
158
+ return new CatalogueError(code, cause);
159
+ }
160
+
161
+ // src/testing/mock.ts
162
+ var seq = 0;
163
+ function nextId(prefix) {
164
+ seq += 1;
165
+ return `${prefix}-${seq}`;
166
+ }
167
+ var MockEngineCall = class _MockEngineCall {
168
+ constructor(id, direction, remoteNumber) {
169
+ this.id = id;
170
+ this.direction = direction;
171
+ this.remoteNumber = remoteNumber;
172
+ }
173
+ id;
174
+ direction;
175
+ remoteNumber;
176
+ #listeners = /* @__PURE__ */ new Set();
177
+ #ended = false;
178
+ onEvent(listener) {
179
+ this.#listeners.add(listener);
180
+ return () => this.#listeners.delete(listener);
181
+ }
182
+ /** @internal — fire a raw engine signal, asynchronously, as a real transport would. */
183
+ fire(event) {
184
+ if (this.#ended) return;
185
+ if (event.type === "ended") this.#ended = true;
186
+ queueMicrotask(() => {
187
+ for (const listener of [...this.#listeners]) listener(event);
188
+ });
189
+ }
190
+ get ended() {
191
+ return this.#ended;
192
+ }
193
+ // ── Commands (local side) ─────────────────────────────────────────────────
194
+ async answer() {
195
+ this.fire({ type: "accepted" });
196
+ }
197
+ async reject() {
198
+ this.fire({ type: "ended", reason: "rejected" });
199
+ }
200
+ async hangup() {
201
+ this.fire({ type: "ended", reason: "local" });
202
+ }
203
+ mute(_muted) {
204
+ }
205
+ async hold() {
206
+ this.fire({ type: "held", originator: "local" });
207
+ }
208
+ async unhold() {
209
+ this.fire({ type: "unheld", originator: "local" });
210
+ }
211
+ sendDigits(_digits) {
212
+ }
213
+ async transfer(_target) {
214
+ this.fire({ type: "ended", reason: "transferred" });
215
+ }
216
+ async startConsult(target) {
217
+ return new _MockEngineCall(nextId("consult"), "outbound", target);
218
+ }
219
+ async completeTransfer(_consult) {
220
+ this.fire({ type: "ended", reason: "transferred" });
221
+ }
222
+ };
223
+ var MockProvider = class {
224
+ #state = "stopped";
225
+ #current = null;
226
+ #incoming = /* @__PURE__ */ new Set();
227
+ #stateListeners = /* @__PURE__ */ new Set();
228
+ #errorListeners = /* @__PURE__ */ new Set();
229
+ get state() {
230
+ return this.#state;
231
+ }
232
+ async start() {
233
+ this.#setState("online");
234
+ }
235
+ async stop() {
236
+ this.#current = null;
237
+ this.#setState("stopped");
238
+ }
239
+ onIncoming(listener) {
240
+ this.#incoming.add(listener);
241
+ return () => this.#incoming.delete(listener);
242
+ }
243
+ onStateChange(listener) {
244
+ this.#stateListeners.add(listener);
245
+ return () => this.#stateListeners.delete(listener);
246
+ }
247
+ onError(listener) {
248
+ this.#errorListeners.add(listener);
249
+ return () => this.#errorListeners.delete(listener);
250
+ }
251
+ async placeCall(destination, _options) {
252
+ const call = new MockEngineCall(nextId("out"), "outbound", destination);
253
+ this.#current = call;
254
+ return call;
255
+ }
256
+ // ── The simulation surface (SPEC §5.7) ────────────────────────────────────
257
+ /** An inbound INVITE lands. Fires the engine's `onIncoming` — the Softphone raises IncomingCall. */
258
+ simulateIncoming(options) {
259
+ const call = new MockEngineCall(nextId("in"), "inbound", options.from);
260
+ this.#current = call;
261
+ for (const listener of [...this.#incoming]) listener(call);
262
+ }
263
+ /** The remote answered our outbound call (or a provisional 180/183 first, if `progress` is set). */
264
+ simulateRemoteAnswer() {
265
+ this.#requireCurrent("simulateRemoteAnswer").fire({ type: "accepted" });
266
+ }
267
+ /** Outbound provisional response — remote is ringing. Optional early media. */
268
+ simulateProgress(kind = "ringing") {
269
+ this.#requireCurrent("simulateProgress").fire({ type: "progress", response: kind });
270
+ }
271
+ /**
272
+ * Remote media arrives — what `ontrack` produces on a real call.
273
+ *
274
+ * Added after the SDK shipped with NO audio at all: `core` hands the stream out and the UI attaches
275
+ * it, but nothing did, and no test could see that because the mock had no way to produce a stream.
276
+ * The seam always carried one (`EngineCallEvent` has `{ type: 'stream' }`); only the driver was
277
+ * missing — unlike `displayName`, which the seam genuinely cannot carry (see SimulateIncomingOptions).
278
+ *
279
+ * The default is a stand-in object: jsdom has no `MediaStream`, and every consumer only ever hands
280
+ * this to `srcObject`.
281
+ */
282
+ simulateRemoteStream(stream = {}) {
283
+ this.#requireCurrent("simulateRemoteStream").fire({ type: "stream", stream });
284
+ }
285
+ simulateRemoteHold() {
286
+ this.#requireCurrent("simulateRemoteHold").fire({ type: "held", originator: "remote" });
287
+ }
288
+ simulateRemoteUnhold() {
289
+ this.#requireCurrent("simulateRemoteUnhold").fire({ type: "unheld", originator: "remote" });
290
+ }
291
+ simulateRemoteHangup(reason = "remote") {
292
+ this.#requireCurrent("simulateRemoteHangup").fire({ type: "ended", reason });
293
+ }
294
+ /**
295
+ * Raise any catalogued failure, exactly as a real engine would.
296
+ *
297
+ * The point is the two TREATMENTS: pass a fatal code (`net/registration-failed`) and the widget
298
+ * replaces the dialpad with a gate that cannot be dismissed; pass a non-fatal one (`call/missed`)
299
+ * and it shows a muted line under the number that the next keypress clears. A playground wants
300
+ * both on a button, and neither can be reached by driving a mock call to a happy ending.
301
+ */
302
+ simulateError(code, cause) {
303
+ const error = createError(code, cause);
304
+ for (const listener of [...this.#errorListeners]) listener(error);
305
+ }
306
+ /** Media lost — the engine begins an ICE restart. The call is NOT over. */
307
+ simulateNetworkDrop() {
308
+ this.#requireCurrent("simulateNetworkDrop").fire({ type: "reconnecting" });
309
+ }
310
+ simulateNetworkRecover() {
311
+ this.#requireCurrent("simulateNetworkRecover").fire({ type: "reconnected" });
312
+ }
313
+ // ── internals ─────────────────────────────────────────────────────────────
314
+ #setState(state) {
315
+ if (state === this.#state) return;
316
+ this.#state = state;
317
+ for (const listener of [...this.#stateListeners]) listener(state);
318
+ }
319
+ #requireCurrent(op) {
320
+ if (!this.#current || this.#current.ended) {
321
+ throw new Error(`MockProvider.${op}() called with no active call.`);
322
+ }
323
+ return this.#current;
324
+ }
325
+ };
326
+
327
+ // src/testing/index.ts
328
+ function mockTokenSource() {
329
+ return {
330
+ async getToken() {
331
+ return {
332
+ token: "mock-voice-token",
333
+ identity: "mock-user",
334
+ // Far future; the mock does not refresh.
335
+ expiresAt: /* @__PURE__ */ new Date(41024448e5)
336
+ // 2100-01-01
337
+ };
338
+ }
339
+ };
340
+ }
341
+
342
+ exports.MockProvider = MockProvider;
343
+ exports.mockTokenSource = mockTokenSource;
344
+ //# sourceMappingURL=index.cjs.map
345
+ //# sourceMappingURL=index.cjs.map