@coventia/softphone 0.1.0 → 0.2.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 (54) hide show
  1. package/dist/{call-BTYlT2SU.d.cts → call-DUVnDxsz.d.cts} +152 -5
  2. package/dist/{call-BTYlT2SU.d.ts → call-DUVnDxsz.d.ts} +152 -5
  3. package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
  4. package/dist/chunk-7R6LOOAI.js.map +1 -0
  5. package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
  6. package/dist/chunk-LMJXX4YM.js.map +1 -0
  7. package/dist/{chunk-5BGEHMPP.js → chunk-YWI6EQYS.js} +40 -7
  8. package/dist/chunk-YWI6EQYS.js.map +1 -0
  9. package/dist/element/index.d.ts +154 -5
  10. package/dist/element/index.js +512 -152
  11. package/dist/element/index.js.map +1 -1
  12. package/dist/{engine-D9nbeY08.d.cts → engine-CDjkrXHY.d.cts} +1 -1
  13. package/dist/{engine-Doa6Brkm.d.ts → engine-EPCKf-Bl.d.ts} +1 -1
  14. package/dist/index.cjs +396 -14
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +5 -5
  17. package/dist/index.d.ts +5 -5
  18. package/dist/index.js +256 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/react/index.cjs +15 -0
  21. package/dist/react/index.cjs.map +1 -1
  22. package/dist/react/index.d.cts +14 -3
  23. package/dist/react/index.d.ts +14 -3
  24. package/dist/react/index.js +2 -2
  25. package/dist/{softphone-DhuhcKCy.d.cts → softphone-BtdgZWkm.d.ts} +17 -2
  26. package/dist/{softphone-DHmhotbf.d.ts → softphone-DHG9MTNg.d.cts} +17 -2
  27. package/dist/softphone.css +395 -1
  28. package/dist/{sources-N4BOEDRL.js → sources-5L2R4HTV.js} +104 -5
  29. package/dist/sources-5L2R4HTV.js.map +1 -0
  30. package/dist/testing/index.cjs +41 -5
  31. package/dist/testing/index.cjs.map +1 -1
  32. package/dist/testing/index.d.cts +8 -2
  33. package/dist/testing/index.d.ts +8 -2
  34. package/dist/testing/index.js +41 -5
  35. package/dist/testing/index.js.map +1 -1
  36. package/dist/ui/index.cjs +490 -139
  37. package/dist/ui/index.cjs.map +1 -1
  38. package/dist/ui/index.d.cts +2 -2
  39. package/dist/ui/index.d.ts +2 -2
  40. package/dist/ui/index.js +478 -141
  41. package/dist/ui/index.js.map +1 -1
  42. package/dist/zadarma/index.cjs +38 -5
  43. package/dist/zadarma/index.cjs.map +1 -1
  44. package/dist/zadarma/index.d.cts +2 -2
  45. package/dist/zadarma/index.d.ts +2 -2
  46. package/dist/zadarma/index.js +1 -1
  47. package/dist/zadarma-engine-4VOPIBXG.js +5 -0
  48. package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-4VOPIBXG.js.map} +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-3V2L6UTK.js.map +0 -1
  51. package/dist/chunk-5BGEHMPP.js.map +0 -1
  52. package/dist/chunk-N2N2EQQJ.js.map +0 -1
  53. package/dist/sources-N4BOEDRL.js.map +0 -1
  54. package/dist/zadarma-engine-DN2SZSQ3.js +0 -5
@@ -45,7 +45,7 @@ var ApiError = class extends Error {
45
45
  this.status = status;
46
46
  }
47
47
  };
48
- function createGet(config) {
48
+ function createRequest(config, method) {
49
49
  const doFetch = config.fetch ? config.fetch : (...args) => globalThis.fetch(...args);
50
50
  const base = config.baseUrl.replace(/\/+$/, "");
51
51
  async function attempt(path, params, signal) {
@@ -56,6 +56,7 @@ function createGet(config) {
56
56
  }
57
57
  const query = qs.toString();
58
58
  const res = await doFetch(`${base}${path}${query ? `?${query}` : ""}`, {
59
+ method,
59
60
  // The RAW token, no `Bearer ` prefix — see `VoiceToken.token` (SPEC §6.1).
60
61
  headers: { Authorization: token },
61
62
  ...signal ? { signal } : {}
@@ -68,7 +69,7 @@ function createGet(config) {
68
69
  }
69
70
  return body;
70
71
  }
71
- return async function get(path, params = {}, signal) {
72
+ return async function request(path, params = {}, signal) {
72
73
  try {
73
74
  return await attempt(path, params, signal);
74
75
  } catch (error) {
@@ -78,6 +79,12 @@ function createGet(config) {
78
79
  }
79
80
  };
80
81
  }
82
+ function createGet(config) {
83
+ return createRequest(config, "GET");
84
+ }
85
+ function createPost(config) {
86
+ return createRequest(config, "POST");
87
+ }
81
88
 
82
89
  // src/coventia/sources.ts
83
90
  var toContact = (row) => ({
@@ -129,8 +136,23 @@ var toDate = (raw) => {
129
136
  const d = new Date(raw);
130
137
  return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
131
138
  };
139
+ var toTransferOutcome = (row) => ({
140
+ id: row.id ?? "",
141
+ target: row.target ?? "",
142
+ status: row.status === "connected" ? "connected" : "not-connected",
143
+ at: toDate(row.at)
144
+ });
145
+ var toPresenceState = (raw) => raw === "disponible" || raw === "en_llamada" ? raw : "desconectado";
146
+ var toPresenceEntry = (row) => ({
147
+ userId: row.userId ?? "",
148
+ extension: row.extension ?? "",
149
+ name: row.name ?? null,
150
+ state: toPresenceState(row.state)
151
+ });
152
+ var toWebSocketUrl = (httpUrl) => httpUrl.replace(/^http/, "ws").replace(/\/+$/, "");
132
153
  function coventiaSources(config) {
133
154
  const get = createGet(config);
155
+ const post = createPost(config);
134
156
  const base = config.basePath ?? "/phone";
135
157
  const org = config.organizationId ? { organization_id: config.organizationId } : {};
136
158
  const coventiaContacts = async ({ search, skip, limit }) => {
@@ -215,12 +237,89 @@ function coventiaSources(config) {
215
237
  people: (body.people ?? []).map(toAgent)
216
238
  };
217
239
  };
240
+ const coventiaTransferOutcomes = async ({ since }) => {
241
+ const body = await get(
242
+ `${base}/transfers/outcomes`,
243
+ { ...org, since }
244
+ );
245
+ return {
246
+ cursor: body.cursor ?? "0",
247
+ outcomes: (body.outcomes ?? []).map(toTransferOutcome)
248
+ };
249
+ };
250
+ const coventiaPresence = (callbacks) => {
251
+ const WS = config.WebSocket ?? globalThis.WebSocket;
252
+ return new Promise((resolve2, reject) => {
253
+ void (async () => {
254
+ let ticket;
255
+ try {
256
+ const body = await post(`${base}/presence/ticket`, org);
257
+ if (!body.ticket) throw new Error("presence ticket mint returned no ticket");
258
+ ticket = body.ticket;
259
+ } catch (error) {
260
+ reject(error instanceof Error ? error : new Error(String(error)));
261
+ return;
262
+ }
263
+ const wsUrl = `${toWebSocketUrl(config.baseUrl)}${base}/presence?ticket=${encodeURIComponent(ticket)}`;
264
+ const ws = new WS(wsUrl);
265
+ let settled = false;
266
+ ws.onopen = () => {
267
+ if (settled) return;
268
+ settled = true;
269
+ resolve2({
270
+ reportCallState: (onCall) => {
271
+ if (ws.readyState !== WS.OPEN) return;
272
+ ws.send(JSON.stringify({ type: "call-state", onCall }));
273
+ },
274
+ close: () => {
275
+ ws.onclose = null;
276
+ ws.onerror = null;
277
+ ws.close();
278
+ }
279
+ });
280
+ };
281
+ ws.onmessage = (event) => {
282
+ let data;
283
+ try {
284
+ data = JSON.parse(String(event.data));
285
+ } catch {
286
+ return;
287
+ }
288
+ const msg = data;
289
+ if (msg.type === "snapshot") {
290
+ callbacks.onSnapshot((msg.agents ?? []).map(toPresenceEntry));
291
+ } else if (msg.type === "delta" && msg.agent) {
292
+ callbacks.onDelta(toPresenceEntry(msg.agent));
293
+ }
294
+ };
295
+ ws.onclose = () => {
296
+ if (!settled) {
297
+ settled = true;
298
+ reject(new Error("presence socket closed before opening"));
299
+ return;
300
+ }
301
+ callbacks.onClose();
302
+ };
303
+ ws.onerror = () => {
304
+ };
305
+ })();
306
+ });
307
+ };
218
308
  const contacts = config.contacts ?? coventiaContacts;
219
309
  const resolve = config.resolveContact ?? (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);
220
310
  const history = withNameResolution(rawHistory, resolve);
221
- return { contacts, history, resolveContact: resolve, listAgents: coventiaListAgents };
311
+ const transferOutcomes = config.transferOutcomes ?? coventiaTransferOutcomes;
312
+ const presence = config.presence ?? coventiaPresence;
313
+ return {
314
+ contacts,
315
+ history,
316
+ resolveContact: resolve,
317
+ listAgents: coventiaListAgents,
318
+ transferOutcomes,
319
+ presence
320
+ };
222
321
  }
223
322
 
224
323
  export { coventiaSources, resolverFromContacts };
225
- //# sourceMappingURL=sources-N4BOEDRL.js.map
226
- //# sourceMappingURL=sources-N4BOEDRL.js.map
324
+ //# sourceMappingURL=sources-5L2R4HTV.js.map
325
+ //# sourceMappingURL=sources-5L2R4HTV.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-5L2R4HTV.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 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"]}
@@ -165,14 +165,16 @@ function nextId(prefix) {
165
165
  return `${prefix}-${seq}`;
166
166
  }
167
167
  var MockEngineCall = class _MockEngineCall {
168
- constructor(id, direction, remoteNumber) {
168
+ constructor(id, direction, remoteNumber, onConsultCreated) {
169
169
  this.id = id;
170
170
  this.direction = direction;
171
171
  this.remoteNumber = remoteNumber;
172
+ this.onConsultCreated = onConsultCreated;
172
173
  }
173
174
  id;
174
175
  direction;
175
176
  remoteNumber;
177
+ onConsultCreated;
176
178
  #listeners = /* @__PURE__ */ new Set();
177
179
  #ended = false;
178
180
  onEvent(listener) {
@@ -214,15 +216,20 @@ var MockEngineCall = class _MockEngineCall {
214
216
  this.fire({ type: "ended", reason: "transferred" });
215
217
  }
216
218
  async startConsult(target) {
217
- return new _MockEngineCall(nextId("consult"), "outbound", target);
219
+ const consult = new _MockEngineCall(nextId("consult"), "outbound", target);
220
+ this.onConsultCreated?.(consult);
221
+ return consult;
218
222
  }
219
- async completeTransfer(_consult) {
223
+ async completeTransfer(consult) {
224
+ consult.fire({ type: "ended", reason: "transferred" });
220
225
  this.fire({ type: "ended", reason: "transferred" });
221
226
  }
222
227
  };
223
228
  var MockProvider = class {
224
229
  #state = "stopped";
225
230
  #current = null;
231
+ /** The consult leg from the current call's own `startConsult`, if any — see `simulateConsult*`. */
232
+ #currentConsult = null;
226
233
  #incoming = /* @__PURE__ */ new Set();
227
234
  #stateListeners = /* @__PURE__ */ new Set();
228
235
  #errorListeners = /* @__PURE__ */ new Set();
@@ -249,15 +256,21 @@ var MockProvider = class {
249
256
  return () => this.#errorListeners.delete(listener);
250
257
  }
251
258
  async placeCall(destination, _options) {
252
- const call = new MockEngineCall(nextId("out"), "outbound", destination);
259
+ const call = new MockEngineCall(nextId("out"), "outbound", destination, (c) => {
260
+ this.#currentConsult = c;
261
+ });
253
262
  this.#current = call;
263
+ this.#currentConsult = null;
254
264
  return call;
255
265
  }
256
266
  // ── The simulation surface (SPEC §5.7) ────────────────────────────────────
257
267
  /** An inbound INVITE lands. Fires the engine's `onIncoming` — the Softphone raises IncomingCall. */
258
268
  simulateIncoming(options) {
259
- const call = new MockEngineCall(nextId("in"), "inbound", options.from);
269
+ const call = new MockEngineCall(nextId("in"), "inbound", options.from, (c) => {
270
+ this.#currentConsult = c;
271
+ });
260
272
  this.#current = call;
273
+ this.#currentConsult = null;
261
274
  for (const listener of [...this.#incoming]) listener(call);
262
275
  }
263
276
  /** The remote answered our outbound call (or a provisional 180/183 first, if `progress` is set). */
@@ -310,6 +323,23 @@ var MockProvider = class {
310
323
  simulateNetworkRecover() {
311
324
  this.#requireCurrent("simulateNetworkRecover").fire({ type: "reconnected" });
312
325
  }
326
+ // ── The consult leg (attended transfer, SPEC §5.6) ─────────────────────────
327
+ //
328
+ // A `startConsult()` call creates a SECOND `MockEngineCall`, tracked separately from `#current` —
329
+ // the methods above all target the main call, so scripting the consult leg's own outcome (it rings,
330
+ // it's answered, or it fails on its own) needs its own surface rather than overloading `#current`.
331
+ /** The target of the current call's `startConsult` is ringing. Optional early media. */
332
+ simulateConsultProgress(kind = "ringing") {
333
+ this.#requireCurrentConsult("simulateConsultProgress").fire({ type: "progress", response: kind });
334
+ }
335
+ /** The consult target answered — the agent can now `completeTransfer()` or `cancelConsult()`. */
336
+ simulateConsultAnswer() {
337
+ this.#requireCurrentConsult("simulateConsultAnswer").fire({ type: "accepted" });
338
+ }
339
+ /** The consult leg ended on its own — busy, no answer, rejected, or a generic failure. */
340
+ simulateConsultHangup(reason = "no-answer") {
341
+ this.#requireCurrentConsult("simulateConsultHangup").fire({ type: "ended", reason });
342
+ }
313
343
  // ── internals ─────────────────────────────────────────────────────────────
314
344
  #setState(state) {
315
345
  if (state === this.#state) return;
@@ -322,6 +352,12 @@ var MockProvider = class {
322
352
  }
323
353
  return this.#current;
324
354
  }
355
+ #requireCurrentConsult(op) {
356
+ if (!this.#currentConsult || this.#currentConsult.ended) {
357
+ throw new Error(`MockProvider.${op}() called with no active consult leg.`);
358
+ }
359
+ return this.#currentConsult;
360
+ }
325
361
  };
326
362
 
327
363
  // src/testing/index.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/errors.ts","../../src/testing/mock.ts","../../src/testing/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * The error taxonomy.\r\n *\r\n * Two things make this worth more than `onError(Error)`:\r\n *\r\n * 1. **Fatal vs non-fatal, as a discriminated union.** A fatal error means the call (or the engine)\r\n * is over and the host must react. A non-fatal one is a warning the host may surface or ignore.\r\n * Collapsing them into one channel — which is what almost every SDK does — forces every\r\n * integrator to re-derive the distinction from string matching. Daily's split is the model.\r\n *\r\n * 2. **A coded catalogue with a `solution` per code.** Twilio and Telnyx both ship this and\r\n * integrators love it, because the support conversation becomes \"send me the code\" instead of\r\n * \"send me your logs\".\r\n *\r\n * The catalogue below is the SINGLE source of truth: `fatal` is derived from it at the type level,\r\n * so a code cannot be fatal in the table and non-fatal in a type, and adding a code without deciding\r\n * its severity is a compile error.\r\n */\r\n\r\ninterface ErrorSpec {\r\n readonly fatal: boolean;\r\n /** What went wrong, for a developer. */\r\n readonly message: string;\r\n /** What to do about it. Shown in logs and in our docs. */\r\n readonly solution: string;\r\n}\r\n\r\nexport const ERROR_CATALOGUE = {\r\n // ── Engine lifecycle ────────────────────────────────────────────────────────\r\n 'engine/duplicate-instance': {\r\n fatal: true,\r\n message: 'A Softphone instance is already running on this page.',\r\n solution:\r\n 'Construct one Softphone and share it. React StrictMode double-mounts in development — hold the instance above the tree, or in a ref, and call destroy() on unmount.',\r\n },\r\n 'engine/not-started': {\r\n fatal: false,\r\n message: 'The softphone has not been started.',\r\n solution: 'Await softphone.start() before placing or answering a call.',\r\n },\r\n 'engine/destroyed': {\r\n fatal: true,\r\n message: 'This Softphone has been destroyed and cannot be reused.',\r\n solution: 'Construct a new Softphone.',\r\n },\r\n\r\n // ── Auth ────────────────────────────────────────────────────────────────────\r\n 'auth/token-fetch-failed': {\r\n fatal: true,\r\n message: 'Could not obtain a voice token from the token endpoint.',\r\n solution:\r\n 'Check that the token endpoint is reachable and that the headers callback returns a valid host-app session token.',\r\n },\r\n 'auth/token-rejected': {\r\n fatal: true,\r\n message: 'The voice token was rejected.',\r\n solution:\r\n 'The token is expired, malformed, or scoped to a different organization. Mint a fresh one.',\r\n },\r\n\r\n // ── Media ───────────────────────────────────────────────────────────────────\r\n /*\r\n NONE of the four codes below are ever actually raised via `createError` — nothing in the SDK\r\n constructs a `SoftphoneError` with these codes today. Deleting them was the other option\r\n (SPEC discussion); they stay, commented, because the taxonomy and Spanish copy (`error-text.ts`)\r\n are worth keeping intact for whenever a real caller wants them, and a `SoftphoneErrorCode` union\r\n is a cheap thing to keep honest with a comment, expensive to keep churning as a breaking change.\r\n */\r\n 'media/permission-denied': {\r\n fatal: true,\r\n message: 'Microphone permission was denied.',\r\n solution:\r\n '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.',\r\n // NEVER RAISED. `MicPermissionGate` (ui/permission/) handles this condition today — it predates\r\n // this catalogue and has BETTER, browser-specific unblock instructions than a generic gate could\r\n // show. It does not import this catalogue at all; the two are unconnected on purpose.\r\n },\r\n 'media/no-input-device': {\r\n fatal: true,\r\n message: 'No microphone is available.',\r\n solution: 'Ask the user to connect a microphone, then retry.',\r\n // NEVER RAISED — same reason as `media/permission-denied` above: `MicPermissionGate` already\r\n // owns this.\r\n },\r\n 'media/webrtc-unsupported': {\r\n fatal: true,\r\n message: 'This browser does not expose the WebRTC APIs.',\r\n solution:\r\n 'Usually a privacy extension (uBlock Origin Extra, WebRTC Leak Prevent) disabling RTCPeerConnection — Twilio documents the same. Ask the user to disable it for this origin.',\r\n // NEVER RAISED — same reason again: `MicPermissionGate` already owns this.\r\n },\r\n 'media/playback-blocked': {\r\n fatal: false,\r\n message: 'The browser blocked audio playback (autoplay policy).',\r\n solution:\r\n 'Call softphone.startAudio() from a click handler. Until then the user cannot hear the caller, and an INCOMING RINGTONE WILL BE SILENT — this is how calls get missed.',\r\n /*\r\n NEVER RAISED — different reason from the other three. This is not superseded by an existing\r\n UI; the feature it describes (autoplay-recovery) is simply not built yet. `Softphone.canPlayAudio`\r\n / `startAudio()` (core/softphone.ts) are Phase 2 and currently no-op stubs — `canPlayAudio`\r\n always reports `true`. This is where that future work would report the block once it exists.\r\n */\r\n },\r\n\r\n // ── Network / signalling ────────────────────────────────────────────────────\r\n 'net/registration-failed': {\r\n fatal: true,\r\n message: 'SIP registration failed.',\r\n solution: 'Usually a stale credential. The engine refreshes and retries; if it persists, the extension may not exist.',\r\n },\r\n 'net/transport-failed': {\r\n fatal: true,\r\n message: 'The signalling transport could not be established.',\r\n solution: 'Check the CSP connect-src directive and any corporate proxy blocking WSS.',\r\n },\r\n 'net/ice-failed': {\r\n fatal: false,\r\n message: 'Media connectivity was lost; attempting an ICE restart.',\r\n solution:\r\n 'Usually a network handoff (wifi → cellular). The engine recovers on its own; surface a \"reconnecting\" banner rather than ending the call.',\r\n /*\r\n NEVER RAISED. The \"reconnecting\" banner this `solution` describes already exists — CallScreen\r\n shows \"Reconectando…\" directly off the call's own `reconnecting` STATE (`STATUS_LABEL` in\r\n views/CallScreen.tsx), which the SIP engine drives without going through the error channel at\r\n all. Raising this too would say the same fact twice through two different mechanisms. It would\r\n only earn its keep for host-side OBSERVABILITY — counting ICE restarts independently of the UI\r\n — which nobody has asked for yet.\r\n */\r\n },\r\n\r\n // ── Call ────────────────────────────────────────────────────────────────────\r\n 'call/not-answerable': {\r\n fatal: false,\r\n message: 'This call cannot be answered in its current state.',\r\n solution: 'Only a call in `ringing` may be answered. Check call.state first.',\r\n },\r\n 'call/busy': {\r\n fatal: false,\r\n message: 'The remote party is busy.',\r\n solution:\r\n 'Nothing to fix. Note SIP 486 is also what most endpoints send for a manual decline, so do not ' +\r\n 'label this \"busy\" alone in your UI — it is equally often \"did not answer\".',\r\n },\r\n 'call/no-answer': {\r\n fatal: false,\r\n message: 'The remote party did not answer.',\r\n solution: 'Nothing to fix. Surface it to the user.',\r\n },\r\n 'call/rejected': {\r\n fatal: false,\r\n message: 'The remote party rejected the call.',\r\n solution: 'Nothing to fix. Surface it to the user.',\r\n },\r\n 'call/missed': {\r\n fatal: false,\r\n message: 'An incoming call was cancelled before it was answered.',\r\n solution:\r\n 'Nothing to fix — the caller gave up while it rang. Surface it, and log it as a missed call.',\r\n },\r\n 'call/failed': {\r\n fatal: false,\r\n message: 'The call could not be completed.',\r\n solution:\r\n 'The catch-all for a call that never connected — a bad number, a rejected offer, a media ' +\r\n 'failure. `cause` carries the underlying SIP cause. Nothing is fatal: the phone still works.',\r\n },\r\n 'call/transfer-failed': {\r\n fatal: false,\r\n message: 'The transfer could not be completed.',\r\n solution: 'The original call is still up. Let the user retry or resume.',\r\n },\r\n 'call/unsupported': {\r\n fatal: false,\r\n message: 'The provider does not support this operation.',\r\n solution: 'Guard the UI on the operation before offering it.',\r\n },\r\n\r\n // ── Provider ────────────────────────────────────────────────────────────────\r\n 'provider/provision-failed': {\r\n fatal: true,\r\n message: 'The provider could not issue SIP credentials.',\r\n solution:\r\n 'The backend reached the carrier but got no usable credential. Check the org has a SIP extension provisioned.',\r\n },\r\n} as const satisfies Record<string, ErrorSpec>;\r\n\r\nexport type SoftphoneErrorCode = keyof typeof ERROR_CATALOGUE;\r\n\r\n/** Codes whose spec says `fatal: true`. Derived — cannot drift from the catalogue. */\r\nexport type FatalErrorCode = {\r\n [K in SoftphoneErrorCode]: (typeof ERROR_CATALOGUE)[K]['fatal'] extends true ? K : never;\r\n}[SoftphoneErrorCode];\r\n\r\nexport type NonFatalErrorCode = Exclude<SoftphoneErrorCode, FatalErrorCode>;\r\n\r\n/**\r\n * A fatal error: the call, or the engine, is over.\r\n *\r\n * `if (err.fatal)` narrows `err.code` to the fatal codes — which is the entire point of the split,\r\n * and the thing JSDoc could not have expressed (SPEC §2.2).\r\n */\r\nexport interface FatalSoftphoneError extends Error {\r\n readonly fatal: true;\r\n readonly code: FatalErrorCode;\r\n readonly solution: string;\r\n}\r\n\r\n/** A non-fatal error: a warning. The call, if any, continues. */\r\nexport interface NonFatalSoftphoneError extends Error {\r\n readonly fatal: false;\r\n readonly code: NonFatalErrorCode;\r\n readonly solution: string;\r\n}\r\n\r\nexport type SoftphoneError = FatalSoftphoneError | NonFatalSoftphoneError;\r\n\r\n/**\r\n * The one concrete error class. Not exported: the public contract is the `SoftphoneError` union and\r\n * the `code` on it, so nothing outside should be reaching for `instanceof`. It is a real `Error`\r\n * subclass, so `instanceof Error`, `.stack` and console formatting all behave as a host expects.\r\n */\r\nclass CatalogueError extends Error {\r\n readonly fatal: boolean;\r\n readonly code: SoftphoneErrorCode;\r\n readonly solution: string;\r\n\r\n constructor(code: SoftphoneErrorCode, cause?: unknown) {\r\n const spec: ErrorSpec = ERROR_CATALOGUE[code];\r\n // `cause` is only passed when there IS one: `{ cause: undefined }` would still define the\r\n // property, and an error carrying an explicit `undefined` cause reads as \"we looked and found\r\n // nothing\" rather than \"nothing was passed\".\r\n super(spec.message, cause === undefined ? undefined : { cause });\r\n this.name = 'SoftphoneError';\r\n this.code = code;\r\n this.fatal = spec.fatal;\r\n this.solution = spec.solution;\r\n }\r\n}\r\n\r\n/**\r\n * Is this already one of ours?\r\n *\r\n * Used where a layer catches whatever a lower one threw: a failure that already carries a catalogue\r\n * code should be reported AS that code, not flattened into the generic one for the layer that caught\r\n * it. Without this, a rejected voice token surfaces as \"the provider could not issue credentials\",\r\n * which is true and useless.\r\n *\r\n * Structural, not `instanceof` — the concrete class is not exported, and a duck-typed object with a\r\n * real code is as good as one of ours.\r\n */\r\nexport function isSoftphoneError(value: unknown): value is SoftphoneError {\r\n if (typeof value !== 'object' || value === null) return false;\r\n const candidate = value as { code?: unknown; fatal?: unknown };\r\n return typeof candidate.fatal === 'boolean' && (candidate.code as string) in ERROR_CATALOGUE;\r\n}\r\n\r\n/** Type guard, for hosts that would rather not rely on structural narrowing. */\r\nexport function isFatal(error: SoftphoneError): error is FatalSoftphoneError {\r\n return error.fatal;\r\n}\r\n\r\n/**\r\n * Construct a catalogue-backed error. **The only way the SDK raises one** — every `message` and\r\n * `solution` a host sees comes from the table above, so no call site invents error prose.\r\n *\r\n * The return type narrows on the code: `createError('call/busy')` is a `NonFatalSoftphoneError`, and\r\n * reading `.fatal` on it gives `false`, not `boolean`.\r\n */\r\nexport function createError<C extends SoftphoneErrorCode>(\r\n code: C,\r\n cause?: unknown,\r\n): C extends FatalErrorCode ? FatalSoftphoneError : NonFatalSoftphoneError {\r\n // The one cast in this module. A conditional return type cannot be verified from inside the\r\n // implementation — TypeScript has no way to prove the branch for an unresolved `C` — so the\r\n // guarantee rests on `fatal` being copied from the catalogue that DERIVES `FatalErrorCode`. The\r\n // test suite asserts that agreement for every code, which is the check the compiler cannot make.\r\n return new CatalogueError(code, cause) as C extends FatalErrorCode\r\n ? FatalSoftphoneError\r\n : NonFatalSoftphoneError;\r\n}\r\n","import type { CallEngine, EngineCall, EngineCallEvent } from '../core/engine.js';\r\nimport type { CallDirection, CallOptions, SoftphoneState } from '../core/types.js';\r\nimport type { HangupReason } from '../core/call.js';\r\nimport { createError, type SoftphoneError, type SoftphoneErrorCode } from '../core/errors.js';\r\n\r\n/**\r\n * MockProvider — a `CallEngine` that scripts a full call lifecycle with no Zadarma, no microphone, no\r\n * network and no real phone call (SPEC §5.7).\r\n *\r\n * It is DOUBLE-DUTY, and that is the point:\r\n * - the engine behind the playgrounds, so a full call (ring → answer → hold → transfer → hang up)\r\n * is drivable in a browser instantly;\r\n * - the test suite's fake.\r\n * The SAME object both ways, kept honest by the CallEngine contract suite — so what the playground\r\n * exercises is what the real engine must also satisfy.\r\n *\r\n * Everything is explicit and deterministic: nothing auto-advances on a timer. The host (a test, or a\r\n * playground button) decides when the remote answers, holds, drops the network, or hangs up. That is\r\n * what makes it usable as a test fake and as a demo without divergence.\r\n */\r\n\r\nlet seq = 0;\r\nfunction nextId(prefix: string): string {\r\n seq += 1;\r\n return `${prefix}-${seq}`;\r\n}\r\n\r\nclass MockEngineCall implements EngineCall {\r\n readonly #listeners = new Set<(event: EngineCallEvent) => void>();\r\n #ended = false;\r\n\r\n constructor(\r\n readonly id: string,\r\n readonly direction: CallDirection,\r\n readonly remoteNumber: string,\r\n ) {}\r\n\r\n onEvent(listener: (event: EngineCallEvent) => void): () => void {\r\n this.#listeners.add(listener);\r\n return () => this.#listeners.delete(listener);\r\n }\r\n\r\n /** @internal — fire a raw engine signal, asynchronously, as a real transport would. */\r\n fire(event: EngineCallEvent): void {\r\n if (this.#ended) return;\r\n if (event.type === 'ended') this.#ended = true;\r\n queueMicrotask(() => {\r\n for (const listener of [...this.#listeners]) listener(event);\r\n });\r\n }\r\n\r\n get ended(): boolean {\r\n return this.#ended;\r\n }\r\n\r\n // ── Commands (local side) ─────────────────────────────────────────────────\r\n async answer(): Promise<void> {\r\n // Inbound: the local user accepted. Media comes up.\r\n this.fire({ type: 'accepted' });\r\n }\r\n async reject(): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'rejected' });\r\n }\r\n async hangup(): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'local' });\r\n }\r\n mute(_muted: boolean): void {\r\n // The engine only carries the effect; Call owns the mute state + event.\r\n }\r\n async hold(): Promise<void> {\r\n this.fire({ type: 'held', originator: 'local' });\r\n }\r\n async unhold(): Promise<void> {\r\n this.fire({ type: 'unheld', originator: 'local' });\r\n }\r\n sendDigits(_digits: string): void {\r\n // A real engine sends RFC 2833 / SIP INFO; the mock has nothing to send to.\r\n }\r\n async transfer(_target: string): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'transferred' });\r\n }\r\n async startConsult(target: string): Promise<EngineCall> {\r\n return new MockEngineCall(nextId('consult'), 'outbound', target);\r\n }\r\n async completeTransfer(_consult: EngineCall): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'transferred' });\r\n }\r\n}\r\n\r\nexport interface SimulateIncomingOptions {\r\n readonly from: string;\r\n /**\r\n * No `displayName` here, deliberately. `EngineCall` carries only `remoteNumber` — the seam has no\r\n * display-name channel, because caller identity is resolved from CONTACTS, not from the SIP `From`\r\n * header (`CallImpl.setRemoteContact`). Until then `remote.displayName` IS the number.\r\n *\r\n * Accepting a name here would let consumers write passing tests for behaviour the real engine can\r\n * never produce. The mock must not outrun the seam it stands in for.\r\n */\r\n}\r\n\r\nexport class MockProvider implements CallEngine {\r\n #state: SoftphoneState = 'stopped';\r\n #current: MockEngineCall | null = null;\r\n readonly #incoming = new Set<(call: EngineCall) => void>();\r\n readonly #stateListeners = new Set<(state: SoftphoneState) => void>();\r\n readonly #errorListeners = new Set<(error: SoftphoneError) => void>();\r\n\r\n get state(): SoftphoneState {\r\n return this.#state;\r\n }\r\n\r\n async start(): Promise<void> {\r\n this.#setState('online');\r\n }\r\n async stop(): Promise<void> {\r\n this.#current = null;\r\n this.#setState('stopped');\r\n }\r\n\r\n onIncoming(listener: (call: EngineCall) => void): () => void {\r\n this.#incoming.add(listener);\r\n return () => this.#incoming.delete(listener);\r\n }\r\n onStateChange(listener: (state: SoftphoneState) => void): () => void {\r\n this.#stateListeners.add(listener);\r\n return () => this.#stateListeners.delete(listener);\r\n }\r\n onError(listener: (error: SoftphoneError) => void): () => void {\r\n this.#errorListeners.add(listener);\r\n return () => this.#errorListeners.delete(listener);\r\n }\r\n\r\n async placeCall(destination: string, _options?: CallOptions): Promise<EngineCall> {\r\n const call = new MockEngineCall(nextId('out'), 'outbound', destination);\r\n this.#current = call;\r\n return call;\r\n }\r\n\r\n // ── The simulation surface (SPEC §5.7) ────────────────────────────────────\r\n\r\n /** An inbound INVITE lands. Fires the engine's `onIncoming` — the Softphone raises IncomingCall. */\r\n simulateIncoming(options: SimulateIncomingOptions): void {\r\n const call = new MockEngineCall(nextId('in'), 'inbound', options.from);\r\n this.#current = call;\r\n for (const listener of [...this.#incoming]) listener(call);\r\n }\r\n\r\n /** The remote answered our outbound call (or a provisional 180/183 first, if `progress` is set). */\r\n simulateRemoteAnswer(): void {\r\n this.#requireCurrent('simulateRemoteAnswer').fire({ type: 'accepted' });\r\n }\r\n\r\n /** Outbound provisional response — remote is ringing. Optional early media. */\r\n simulateProgress(kind: 'ringing' | 'early' = 'ringing'): void {\r\n this.#requireCurrent('simulateProgress').fire({ type: 'progress', response: kind });\r\n }\r\n\r\n /**\r\n * Remote media arrives — what `ontrack` produces on a real call.\r\n *\r\n * Added after the SDK shipped with NO audio at all: `core` hands the stream out and the UI attaches\r\n * it, but nothing did, and no test could see that because the mock had no way to produce a stream.\r\n * The seam always carried one (`EngineCallEvent` has `{ type: 'stream' }`); only the driver was\r\n * missing — unlike `displayName`, which the seam genuinely cannot carry (see SimulateIncomingOptions).\r\n *\r\n * The default is a stand-in object: jsdom has no `MediaStream`, and every consumer only ever hands\r\n * this to `srcObject`.\r\n */\r\n simulateRemoteStream(stream: MediaStream = {} as MediaStream): void {\r\n this.#requireCurrent('simulateRemoteStream').fire({ type: 'stream', stream });\r\n }\r\n\r\n simulateRemoteHold(): void {\r\n this.#requireCurrent('simulateRemoteHold').fire({ type: 'held', originator: 'remote' });\r\n }\r\n simulateRemoteUnhold(): void {\r\n this.#requireCurrent('simulateRemoteUnhold').fire({ type: 'unheld', originator: 'remote' });\r\n }\r\n\r\n simulateRemoteHangup(reason: HangupReason = 'remote'): void {\r\n this.#requireCurrent('simulateRemoteHangup').fire({ type: 'ended', reason });\r\n }\r\n\r\n /**\r\n * Raise any catalogued failure, exactly as a real engine would.\r\n *\r\n * The point is the two TREATMENTS: pass a fatal code (`net/registration-failed`) and the widget\r\n * replaces the dialpad with a gate that cannot be dismissed; pass a non-fatal one (`call/missed`)\r\n * and it shows a muted line under the number that the next keypress clears. A playground wants\r\n * both on a button, and neither can be reached by driving a mock call to a happy ending.\r\n */\r\n simulateError(code: SoftphoneErrorCode, cause?: unknown): void {\r\n const error = createError(code, cause);\r\n for (const listener of [...this.#errorListeners]) listener(error);\r\n }\r\n\r\n /** Media lost — the engine begins an ICE restart. The call is NOT over. */\r\n simulateNetworkDrop(): void {\r\n this.#requireCurrent('simulateNetworkDrop').fire({ type: 'reconnecting' });\r\n }\r\n simulateNetworkRecover(): void {\r\n this.#requireCurrent('simulateNetworkRecover').fire({ type: 'reconnected' });\r\n }\r\n\r\n // ── internals ─────────────────────────────────────────────────────────────\r\n #setState(state: SoftphoneState): void {\r\n if (state === this.#state) return;\r\n this.#state = state;\r\n for (const listener of [...this.#stateListeners]) listener(state);\r\n }\r\n\r\n #requireCurrent(op: string): MockEngineCall {\r\n if (!this.#current || this.#current.ended) {\r\n throw new Error(`MockProvider.${op}() called with no active call.`);\r\n }\r\n return this.#current;\r\n }\r\n}\r\n","/**\r\n * @coventia/softphone/testing — MockProvider and test helpers.\r\n *\r\n * Its own entry point so it never lands in a consumer's production bundle. See SPEC §5.7.\r\n */\r\nimport type { TokenSource, VoiceToken } from '../core/types.js';\r\n\r\nexport { MockProvider } from './mock.js';\r\nexport type { SimulateIncomingOptions } from './mock.js';\r\n\r\n/**\r\n * A TokenSource that returns a fixed, far-future fake token.\r\n *\r\n * `SoftphoneConfig.tokenSource` is required (SPEC §6.1) because the real Zadarma path needs it — but\r\n * the mock engine never mints credentials, so tests and playgrounds need something to satisfy the\r\n * type without a backend. This is that something. Never ship it.\r\n */\r\nexport function mockTokenSource(): TokenSource {\r\n return {\r\n async getToken(): Promise<VoiceToken> {\r\n return {\r\n token: 'mock-voice-token',\r\n identity: 'mock-user',\r\n // Far future; the mock does not refresh.\r\n expiresAt: new Date(4102444800000), // 2100-01-01\r\n };\r\n },\r\n };\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/core/errors.ts","../../src/testing/mock.ts","../../src/testing/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * The error taxonomy.\r\n *\r\n * Two things make this worth more than `onError(Error)`:\r\n *\r\n * 1. **Fatal vs non-fatal, as a discriminated union.** A fatal error means the call (or the engine)\r\n * is over and the host must react. A non-fatal one is a warning the host may surface or ignore.\r\n * Collapsing them into one channel — which is what almost every SDK does — forces every\r\n * integrator to re-derive the distinction from string matching. Daily's split is the model.\r\n *\r\n * 2. **A coded catalogue with a `solution` per code.** Twilio and Telnyx both ship this and\r\n * integrators love it, because the support conversation becomes \"send me the code\" instead of\r\n * \"send me your logs\".\r\n *\r\n * The catalogue below is the SINGLE source of truth: `fatal` is derived from it at the type level,\r\n * so a code cannot be fatal in the table and non-fatal in a type, and adding a code without deciding\r\n * its severity is a compile error.\r\n */\r\n\r\ninterface ErrorSpec {\r\n readonly fatal: boolean;\r\n /** What went wrong, for a developer. */\r\n readonly message: string;\r\n /** What to do about it. Shown in logs and in our docs. */\r\n readonly solution: string;\r\n}\r\n\r\nexport const ERROR_CATALOGUE = {\r\n // ── Engine lifecycle ────────────────────────────────────────────────────────\r\n 'engine/duplicate-instance': {\r\n fatal: true,\r\n message: 'A Softphone instance is already running on this page.',\r\n solution:\r\n 'Construct one Softphone and share it. React StrictMode double-mounts in development — hold the instance above the tree, or in a ref, and call destroy() on unmount.',\r\n },\r\n 'engine/not-started': {\r\n fatal: false,\r\n message: 'The softphone has not been started.',\r\n solution: 'Await softphone.start() before placing or answering a call.',\r\n },\r\n 'engine/destroyed': {\r\n fatal: true,\r\n message: 'This Softphone has been destroyed and cannot be reused.',\r\n solution: 'Construct a new Softphone.',\r\n },\r\n\r\n // ── Auth ────────────────────────────────────────────────────────────────────\r\n 'auth/token-fetch-failed': {\r\n fatal: true,\r\n message: 'Could not obtain a voice token from the token endpoint.',\r\n solution:\r\n 'Check that the token endpoint is reachable and that the headers callback returns a valid host-app session token.',\r\n },\r\n 'auth/token-rejected': {\r\n fatal: true,\r\n message: 'The voice token was rejected.',\r\n solution:\r\n 'The token is expired, malformed, or scoped to a different organization. Mint a fresh one.',\r\n },\r\n\r\n // ── Media ───────────────────────────────────────────────────────────────────\r\n /*\r\n NONE of the four codes below are ever actually raised via `createError` — nothing in the SDK\r\n constructs a `SoftphoneError` with these codes today. Deleting them was the other option\r\n (SPEC discussion); they stay, commented, because the taxonomy and Spanish copy (`error-text.ts`)\r\n are worth keeping intact for whenever a real caller wants them, and a `SoftphoneErrorCode` union\r\n is a cheap thing to keep honest with a comment, expensive to keep churning as a breaking change.\r\n */\r\n 'media/permission-denied': {\r\n fatal: true,\r\n message: 'Microphone permission was denied.',\r\n solution:\r\n '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.',\r\n // NEVER RAISED. `MicPermissionGate` (ui/permission/) handles this condition today — it predates\r\n // this catalogue and has BETTER, browser-specific unblock instructions than a generic gate could\r\n // show. It does not import this catalogue at all; the two are unconnected on purpose.\r\n },\r\n 'media/no-input-device': {\r\n fatal: true,\r\n message: 'No microphone is available.',\r\n solution: 'Ask the user to connect a microphone, then retry.',\r\n // NEVER RAISED — same reason as `media/permission-denied` above: `MicPermissionGate` already\r\n // owns this.\r\n },\r\n 'media/webrtc-unsupported': {\r\n fatal: true,\r\n message: 'This browser does not expose the WebRTC APIs.',\r\n solution:\r\n 'Usually a privacy extension (uBlock Origin Extra, WebRTC Leak Prevent) disabling RTCPeerConnection — Twilio documents the same. Ask the user to disable it for this origin.',\r\n // NEVER RAISED — same reason again: `MicPermissionGate` already owns this.\r\n },\r\n 'media/playback-blocked': {\r\n fatal: false,\r\n message: 'The browser blocked audio playback (autoplay policy).',\r\n solution:\r\n 'Call softphone.startAudio() from a click handler. Until then the user cannot hear the caller, and an INCOMING RINGTONE WILL BE SILENT — this is how calls get missed.',\r\n /*\r\n NEVER RAISED — different reason from the other three. This is not superseded by an existing\r\n UI; the feature it describes (autoplay-recovery) is simply not built yet. `Softphone.canPlayAudio`\r\n / `startAudio()` (core/softphone.ts) are Phase 2 and currently no-op stubs — `canPlayAudio`\r\n always reports `true`. This is where that future work would report the block once it exists.\r\n */\r\n },\r\n\r\n // ── Network / signalling ────────────────────────────────────────────────────\r\n 'net/registration-failed': {\r\n fatal: true,\r\n message: 'SIP registration failed.',\r\n solution: 'Usually a stale credential. The engine refreshes and retries; if it persists, the extension may not exist.',\r\n },\r\n 'net/transport-failed': {\r\n fatal: true,\r\n message: 'The signalling transport could not be established.',\r\n solution: 'Check the CSP connect-src directive and any corporate proxy blocking WSS.',\r\n },\r\n 'net/ice-failed': {\r\n fatal: false,\r\n message: 'Media connectivity was lost; attempting an ICE restart.',\r\n solution:\r\n 'Usually a network handoff (wifi → cellular). The engine recovers on its own; surface a \"reconnecting\" banner rather than ending the call.',\r\n /*\r\n NEVER RAISED. The \"reconnecting\" banner this `solution` describes already exists — CallScreen\r\n shows \"Reconectando…\" directly off the call's own `reconnecting` STATE (`STATUS_LABEL` in\r\n views/CallScreen.tsx), which the SIP engine drives without going through the error channel at\r\n all. Raising this too would say the same fact twice through two different mechanisms. It would\r\n only earn its keep for host-side OBSERVABILITY — counting ICE restarts independently of the UI\r\n — which nobody has asked for yet.\r\n */\r\n },\r\n\r\n // ── Call ────────────────────────────────────────────────────────────────────\r\n 'call/not-answerable': {\r\n fatal: false,\r\n message: 'This call cannot be answered in its current state.',\r\n solution: 'Only a call in `ringing` may be answered. Check call.state first.',\r\n },\r\n 'call/busy': {\r\n fatal: false,\r\n message: 'The remote party is busy.',\r\n solution:\r\n 'Nothing to fix. Note SIP 486 is also what most endpoints send for a manual decline, so do not ' +\r\n 'label this \"busy\" alone in your UI — it is equally often \"did not answer\".',\r\n },\r\n 'call/no-answer': {\r\n fatal: false,\r\n message: 'The remote party did not answer.',\r\n solution: 'Nothing to fix. Surface it to the user.',\r\n },\r\n 'call/rejected': {\r\n fatal: false,\r\n message: 'The remote party rejected the call.',\r\n solution: 'Nothing to fix. Surface it to the user.',\r\n },\r\n 'call/missed': {\r\n fatal: false,\r\n message: 'An incoming call was cancelled before it was answered.',\r\n solution:\r\n 'Nothing to fix — the caller gave up while it rang. Surface it, and log it as a missed call.',\r\n },\r\n 'call/failed': {\r\n fatal: false,\r\n message: 'The call could not be completed.',\r\n solution:\r\n 'The catch-all for a call that never connected — a bad number, a rejected offer, a media ' +\r\n 'failure. `cause` carries the underlying SIP cause. Nothing is fatal: the phone still works.',\r\n },\r\n 'call/transfer-failed': {\r\n fatal: false,\r\n message: 'The transfer could not be completed.',\r\n solution: 'The original call is still up. Let the user retry or resume.',\r\n },\r\n 'call/unsupported': {\r\n fatal: false,\r\n message: 'The provider does not support this operation.',\r\n solution: 'Guard the UI on the operation before offering it.',\r\n },\r\n\r\n // ── Provider ────────────────────────────────────────────────────────────────\r\n 'provider/provision-failed': {\r\n fatal: true,\r\n message: 'The provider could not issue SIP credentials.',\r\n solution:\r\n 'The backend reached the carrier but got no usable credential. Check the org has a SIP extension provisioned.',\r\n },\r\n} as const satisfies Record<string, ErrorSpec>;\r\n\r\nexport type SoftphoneErrorCode = keyof typeof ERROR_CATALOGUE;\r\n\r\n/** Codes whose spec says `fatal: true`. Derived — cannot drift from the catalogue. */\r\nexport type FatalErrorCode = {\r\n [K in SoftphoneErrorCode]: (typeof ERROR_CATALOGUE)[K]['fatal'] extends true ? K : never;\r\n}[SoftphoneErrorCode];\r\n\r\nexport type NonFatalErrorCode = Exclude<SoftphoneErrorCode, FatalErrorCode>;\r\n\r\n/**\r\n * A fatal error: the call, or the engine, is over.\r\n *\r\n * `if (err.fatal)` narrows `err.code` to the fatal codes — which is the entire point of the split,\r\n * and the thing JSDoc could not have expressed (SPEC §2.2).\r\n */\r\nexport interface FatalSoftphoneError extends Error {\r\n readonly fatal: true;\r\n readonly code: FatalErrorCode;\r\n readonly solution: string;\r\n}\r\n\r\n/** A non-fatal error: a warning. The call, if any, continues. */\r\nexport interface NonFatalSoftphoneError extends Error {\r\n readonly fatal: false;\r\n readonly code: NonFatalErrorCode;\r\n readonly solution: string;\r\n}\r\n\r\nexport type SoftphoneError = FatalSoftphoneError | NonFatalSoftphoneError;\r\n\r\n/**\r\n * The one concrete error class. Not exported: the public contract is the `SoftphoneError` union and\r\n * the `code` on it, so nothing outside should be reaching for `instanceof`. It is a real `Error`\r\n * subclass, so `instanceof Error`, `.stack` and console formatting all behave as a host expects.\r\n */\r\nclass CatalogueError extends Error {\r\n readonly fatal: boolean;\r\n readonly code: SoftphoneErrorCode;\r\n readonly solution: string;\r\n\r\n constructor(code: SoftphoneErrorCode, cause?: unknown) {\r\n const spec: ErrorSpec = ERROR_CATALOGUE[code];\r\n // `cause` is only passed when there IS one: `{ cause: undefined }` would still define the\r\n // property, and an error carrying an explicit `undefined` cause reads as \"we looked and found\r\n // nothing\" rather than \"nothing was passed\".\r\n super(spec.message, cause === undefined ? undefined : { cause });\r\n this.name = 'SoftphoneError';\r\n this.code = code;\r\n this.fatal = spec.fatal;\r\n this.solution = spec.solution;\r\n }\r\n}\r\n\r\n/**\r\n * Is this already one of ours?\r\n *\r\n * Used where a layer catches whatever a lower one threw: a failure that already carries a catalogue\r\n * code should be reported AS that code, not flattened into the generic one for the layer that caught\r\n * it. Without this, a rejected voice token surfaces as \"the provider could not issue credentials\",\r\n * which is true and useless.\r\n *\r\n * Structural, not `instanceof` — the concrete class is not exported, and a duck-typed object with a\r\n * real code is as good as one of ours.\r\n */\r\nexport function isSoftphoneError(value: unknown): value is SoftphoneError {\r\n if (typeof value !== 'object' || value === null) return false;\r\n const candidate = value as { code?: unknown; fatal?: unknown };\r\n return typeof candidate.fatal === 'boolean' && (candidate.code as string) in ERROR_CATALOGUE;\r\n}\r\n\r\n/** Type guard, for hosts that would rather not rely on structural narrowing. */\r\nexport function isFatal(error: SoftphoneError): error is FatalSoftphoneError {\r\n return error.fatal;\r\n}\r\n\r\n/**\r\n * Construct a catalogue-backed error. **The only way the SDK raises one** — every `message` and\r\n * `solution` a host sees comes from the table above, so no call site invents error prose.\r\n *\r\n * The return type narrows on the code: `createError('call/busy')` is a `NonFatalSoftphoneError`, and\r\n * reading `.fatal` on it gives `false`, not `boolean`.\r\n */\r\nexport function createError<C extends SoftphoneErrorCode>(\r\n code: C,\r\n cause?: unknown,\r\n): C extends FatalErrorCode ? FatalSoftphoneError : NonFatalSoftphoneError {\r\n // The one cast in this module. A conditional return type cannot be verified from inside the\r\n // implementation — TypeScript has no way to prove the branch for an unresolved `C` — so the\r\n // guarantee rests on `fatal` being copied from the catalogue that DERIVES `FatalErrorCode`. The\r\n // test suite asserts that agreement for every code, which is the check the compiler cannot make.\r\n return new CatalogueError(code, cause) as C extends FatalErrorCode\r\n ? FatalSoftphoneError\r\n : NonFatalSoftphoneError;\r\n}\r\n","import type { CallEngine, EngineCall, EngineCallEvent } from '../core/engine.js';\r\nimport type { CallDirection, CallOptions, SoftphoneState } from '../core/types.js';\r\nimport type { HangupReason } from '../core/call.js';\r\nimport { createError, type SoftphoneError, type SoftphoneErrorCode } from '../core/errors.js';\r\n\r\n/**\r\n * MockProvider — a `CallEngine` that scripts a full call lifecycle with no Zadarma, no microphone, no\r\n * network and no real phone call (SPEC §5.7).\r\n *\r\n * It is DOUBLE-DUTY, and that is the point:\r\n * - the engine behind the playgrounds, so a full call (ring → answer → hold → transfer → hang up)\r\n * is drivable in a browser instantly;\r\n * - the test suite's fake.\r\n * The SAME object both ways, kept honest by the CallEngine contract suite — so what the playground\r\n * exercises is what the real engine must also satisfy.\r\n *\r\n * Everything is explicit and deterministic: nothing auto-advances on a timer. The host (a test, or a\r\n * playground button) decides when the remote answers, holds, drops the network, or hangs up. That is\r\n * what makes it usable as a test fake and as a demo without divergence.\r\n */\r\n\r\nlet seq = 0;\r\nfunction nextId(prefix: string): string {\r\n seq += 1;\r\n return `${prefix}-${seq}`;\r\n}\r\n\r\nclass MockEngineCall implements EngineCall {\r\n readonly #listeners = new Set<(event: EngineCallEvent) => void>();\r\n #ended = false;\r\n\r\n constructor(\r\n readonly id: string,\r\n readonly direction: CallDirection,\r\n readonly remoteNumber: string,\r\n /** Notified with the new leg when `startConsult` creates one — lets `MockProvider` script it. */\r\n private readonly onConsultCreated?: (consult: MockEngineCall) => void,\r\n ) {}\r\n\r\n onEvent(listener: (event: EngineCallEvent) => void): () => void {\r\n this.#listeners.add(listener);\r\n return () => this.#listeners.delete(listener);\r\n }\r\n\r\n /** @internal — fire a raw engine signal, asynchronously, as a real transport would. */\r\n fire(event: EngineCallEvent): void {\r\n if (this.#ended) return;\r\n if (event.type === 'ended') this.#ended = true;\r\n queueMicrotask(() => {\r\n for (const listener of [...this.#listeners]) listener(event);\r\n });\r\n }\r\n\r\n get ended(): boolean {\r\n return this.#ended;\r\n }\r\n\r\n // ── Commands (local side) ─────────────────────────────────────────────────\r\n async answer(): Promise<void> {\r\n // Inbound: the local user accepted. Media comes up.\r\n this.fire({ type: 'accepted' });\r\n }\r\n async reject(): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'rejected' });\r\n }\r\n async hangup(): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'local' });\r\n }\r\n mute(_muted: boolean): void {\r\n // The engine only carries the effect; Call owns the mute state + event.\r\n }\r\n async hold(): Promise<void> {\r\n this.fire({ type: 'held', originator: 'local' });\r\n }\r\n async unhold(): Promise<void> {\r\n this.fire({ type: 'unheld', originator: 'local' });\r\n }\r\n sendDigits(_digits: string): void {\r\n // A real engine sends RFC 2833 / SIP INFO; the mock has nothing to send to.\r\n }\r\n async transfer(_target: string): Promise<void> {\r\n this.fire({ type: 'ended', reason: 'transferred' });\r\n }\r\n async startConsult(target: string): Promise<EngineCall> {\r\n const consult = new MockEngineCall(nextId('consult'), 'outbound', target);\r\n this.onConsultCreated?.(consult);\r\n return consult;\r\n }\r\n async completeTransfer(consult: EngineCall): Promise<void> {\r\n // Both legs end — the real engine's REFER…Replaces bridges caller↔consult-target and drops both\r\n // the original session and the consult session (`call-impl.ts`'s own comment on `completeTransfer`).\r\n (consult as MockEngineCall).fire({ type: 'ended', reason: 'transferred' });\r\n this.fire({ type: 'ended', reason: 'transferred' });\r\n }\r\n}\r\n\r\nexport interface SimulateIncomingOptions {\r\n readonly from: string;\r\n /**\r\n * No `displayName` here, deliberately. `EngineCall` carries only `remoteNumber` — the seam has no\r\n * display-name channel, because caller identity is resolved from CONTACTS, not from the SIP `From`\r\n * header (`CallImpl.setRemoteContact`). Until then `remote.displayName` IS the number.\r\n *\r\n * Accepting a name here would let consumers write passing tests for behaviour the real engine can\r\n * never produce. The mock must not outrun the seam it stands in for.\r\n */\r\n}\r\n\r\nexport class MockProvider implements CallEngine {\r\n #state: SoftphoneState = 'stopped';\r\n #current: MockEngineCall | null = null;\r\n /** The consult leg from the current call's own `startConsult`, if any — see `simulateConsult*`. */\r\n #currentConsult: MockEngineCall | null = null;\r\n readonly #incoming = new Set<(call: EngineCall) => void>();\r\n readonly #stateListeners = new Set<(state: SoftphoneState) => void>();\r\n readonly #errorListeners = new Set<(error: SoftphoneError) => void>();\r\n\r\n get state(): SoftphoneState {\r\n return this.#state;\r\n }\r\n\r\n async start(): Promise<void> {\r\n this.#setState('online');\r\n }\r\n async stop(): Promise<void> {\r\n this.#current = null;\r\n this.#setState('stopped');\r\n }\r\n\r\n onIncoming(listener: (call: EngineCall) => void): () => void {\r\n this.#incoming.add(listener);\r\n return () => this.#incoming.delete(listener);\r\n }\r\n onStateChange(listener: (state: SoftphoneState) => void): () => void {\r\n this.#stateListeners.add(listener);\r\n return () => this.#stateListeners.delete(listener);\r\n }\r\n onError(listener: (error: SoftphoneError) => void): () => void {\r\n this.#errorListeners.add(listener);\r\n return () => this.#errorListeners.delete(listener);\r\n }\r\n\r\n async placeCall(destination: string, _options?: CallOptions): Promise<EngineCall> {\r\n const call = new MockEngineCall(nextId('out'), 'outbound', destination, (c) => {\r\n this.#currentConsult = c;\r\n });\r\n this.#current = call;\r\n this.#currentConsult = null;\r\n return call;\r\n }\r\n\r\n // ── The simulation surface (SPEC §5.7) ────────────────────────────────────\r\n\r\n /** An inbound INVITE lands. Fires the engine's `onIncoming` — the Softphone raises IncomingCall. */\r\n simulateIncoming(options: SimulateIncomingOptions): void {\r\n const call = new MockEngineCall(nextId('in'), 'inbound', options.from, (c) => {\r\n this.#currentConsult = c;\r\n });\r\n this.#current = call;\r\n this.#currentConsult = null;\r\n for (const listener of [...this.#incoming]) listener(call);\r\n }\r\n\r\n /** The remote answered our outbound call (or a provisional 180/183 first, if `progress` is set). */\r\n simulateRemoteAnswer(): void {\r\n this.#requireCurrent('simulateRemoteAnswer').fire({ type: 'accepted' });\r\n }\r\n\r\n /** Outbound provisional response — remote is ringing. Optional early media. */\r\n simulateProgress(kind: 'ringing' | 'early' = 'ringing'): void {\r\n this.#requireCurrent('simulateProgress').fire({ type: 'progress', response: kind });\r\n }\r\n\r\n /**\r\n * Remote media arrives — what `ontrack` produces on a real call.\r\n *\r\n * Added after the SDK shipped with NO audio at all: `core` hands the stream out and the UI attaches\r\n * it, but nothing did, and no test could see that because the mock had no way to produce a stream.\r\n * The seam always carried one (`EngineCallEvent` has `{ type: 'stream' }`); only the driver was\r\n * missing — unlike `displayName`, which the seam genuinely cannot carry (see SimulateIncomingOptions).\r\n *\r\n * The default is a stand-in object: jsdom has no `MediaStream`, and every consumer only ever hands\r\n * this to `srcObject`.\r\n */\r\n simulateRemoteStream(stream: MediaStream = {} as MediaStream): void {\r\n this.#requireCurrent('simulateRemoteStream').fire({ type: 'stream', stream });\r\n }\r\n\r\n simulateRemoteHold(): void {\r\n this.#requireCurrent('simulateRemoteHold').fire({ type: 'held', originator: 'remote' });\r\n }\r\n simulateRemoteUnhold(): void {\r\n this.#requireCurrent('simulateRemoteUnhold').fire({ type: 'unheld', originator: 'remote' });\r\n }\r\n\r\n simulateRemoteHangup(reason: HangupReason = 'remote'): void {\r\n this.#requireCurrent('simulateRemoteHangup').fire({ type: 'ended', reason });\r\n }\r\n\r\n /**\r\n * Raise any catalogued failure, exactly as a real engine would.\r\n *\r\n * The point is the two TREATMENTS: pass a fatal code (`net/registration-failed`) and the widget\r\n * replaces the dialpad with a gate that cannot be dismissed; pass a non-fatal one (`call/missed`)\r\n * and it shows a muted line under the number that the next keypress clears. A playground wants\r\n * both on a button, and neither can be reached by driving a mock call to a happy ending.\r\n */\r\n simulateError(code: SoftphoneErrorCode, cause?: unknown): void {\r\n const error = createError(code, cause);\r\n for (const listener of [...this.#errorListeners]) listener(error);\r\n }\r\n\r\n /** Media lost — the engine begins an ICE restart. The call is NOT over. */\r\n simulateNetworkDrop(): void {\r\n this.#requireCurrent('simulateNetworkDrop').fire({ type: 'reconnecting' });\r\n }\r\n simulateNetworkRecover(): void {\r\n this.#requireCurrent('simulateNetworkRecover').fire({ type: 'reconnected' });\r\n }\r\n\r\n // ── The consult leg (attended transfer, SPEC §5.6) ─────────────────────────\r\n //\r\n // A `startConsult()` call creates a SECOND `MockEngineCall`, tracked separately from `#current` —\r\n // the methods above all target the main call, so scripting the consult leg's own outcome (it rings,\r\n // it's answered, or it fails on its own) needs its own surface rather than overloading `#current`.\r\n\r\n /** The target of the current call's `startConsult` is ringing. Optional early media. */\r\n simulateConsultProgress(kind: 'ringing' | 'early' = 'ringing'): void {\r\n this.#requireCurrentConsult('simulateConsultProgress').fire({ type: 'progress', response: kind });\r\n }\r\n /** The consult target answered — the agent can now `completeTransfer()` or `cancelConsult()`. */\r\n simulateConsultAnswer(): void {\r\n this.#requireCurrentConsult('simulateConsultAnswer').fire({ type: 'accepted' });\r\n }\r\n /** The consult leg ended on its own — busy, no answer, rejected, or a generic failure. */\r\n simulateConsultHangup(reason: HangupReason = 'no-answer'): void {\r\n this.#requireCurrentConsult('simulateConsultHangup').fire({ type: 'ended', reason });\r\n }\r\n\r\n // ── internals ─────────────────────────────────────────────────────────────\r\n #setState(state: SoftphoneState): void {\r\n if (state === this.#state) return;\r\n this.#state = state;\r\n for (const listener of [...this.#stateListeners]) listener(state);\r\n }\r\n\r\n #requireCurrent(op: string): MockEngineCall {\r\n if (!this.#current || this.#current.ended) {\r\n throw new Error(`MockProvider.${op}() called with no active call.`);\r\n }\r\n return this.#current;\r\n }\r\n\r\n #requireCurrentConsult(op: string): MockEngineCall {\r\n if (!this.#currentConsult || this.#currentConsult.ended) {\r\n throw new Error(`MockProvider.${op}() called with no active consult leg.`);\r\n }\r\n return this.#currentConsult;\r\n }\r\n}\r\n","/**\r\n * @coventia/softphone/testing — MockProvider and test helpers.\r\n *\r\n * Its own entry point so it never lands in a consumer's production bundle. See SPEC §5.7.\r\n */\r\nimport type { TokenSource, VoiceToken } from '../core/types.js';\r\n\r\nexport { MockProvider } from './mock.js';\r\nexport type { SimulateIncomingOptions } from './mock.js';\r\n\r\n/**\r\n * A TokenSource that returns a fixed, far-future fake token.\r\n *\r\n * `SoftphoneConfig.tokenSource` is required (SPEC §6.1) because the real Zadarma path needs it — but\r\n * the mock engine never mints credentials, so tests and playgrounds need something to satisfy the\r\n * type without a backend. This is that something. Never ship it.\r\n */\r\nexport function mockTokenSource(): TokenSource {\r\n return {\r\n async getToken(): Promise<VoiceToken> {\r\n return {\r\n token: 'mock-voice-token',\r\n identity: 'mock-user',\r\n // Far future; the mock does not refresh.\r\n expiresAt: new Date(4102444800000), // 2100-01-01\r\n };\r\n },\r\n };\r\n}\r\n"]}
@@ -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-BTYlT2SU.cjs';
2
- import { C as CallEngine, E as EngineCall } from '../engine-D9nbeY08.cjs';
1
+ import { o as SoftphoneState, k as SoftphoneError, e as CallOptions, H as HangupReason, l as SoftphoneErrorCode, p as TokenSource } from '../call-DUVnDxsz.cjs';
2
+ import { C as CallEngine, E as EngineCall } from '../engine-CDjkrXHY.cjs';
3
3
 
4
4
  interface SimulateIncomingOptions {
5
5
  readonly from: string;
@@ -46,6 +46,12 @@ declare class MockProvider implements CallEngine {
46
46
  /** Media lost — the engine begins an ICE restart. The call is NOT over. */
47
47
  simulateNetworkDrop(): void;
48
48
  simulateNetworkRecover(): void;
49
+ /** The target of the current call's `startConsult` is ringing. Optional early media. */
50
+ simulateConsultProgress(kind?: 'ringing' | 'early'): void;
51
+ /** The consult target answered — the agent can now `completeTransfer()` or `cancelConsult()`. */
52
+ simulateConsultAnswer(): void;
53
+ /** The consult leg ended on its own — busy, no answer, rejected, or a generic failure. */
54
+ simulateConsultHangup(reason?: HangupReason): void;
49
55
  }
50
56
 
51
57
  /**
@@ -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-BTYlT2SU.js';
2
- import { C as CallEngine, E as EngineCall } from '../engine-Doa6Brkm.js';
1
+ import { o as SoftphoneState, k as SoftphoneError, e as CallOptions, H as HangupReason, l as SoftphoneErrorCode, p as TokenSource } from '../call-DUVnDxsz.js';
2
+ import { C as CallEngine, E as EngineCall } from '../engine-EPCKf-Bl.js';
3
3
 
4
4
  interface SimulateIncomingOptions {
5
5
  readonly from: string;
@@ -46,6 +46,12 @@ declare class MockProvider implements CallEngine {
46
46
  /** Media lost — the engine begins an ICE restart. The call is NOT over. */
47
47
  simulateNetworkDrop(): void;
48
48
  simulateNetworkRecover(): void;
49
+ /** The target of the current call's `startConsult` is ringing. Optional early media. */
50
+ simulateConsultProgress(kind?: 'ringing' | 'early'): void;
51
+ /** The consult target answered — the agent can now `completeTransfer()` or `cancelConsult()`. */
52
+ simulateConsultAnswer(): void;
53
+ /** The consult leg ended on its own — busy, no answer, rejected, or a generic failure. */
54
+ simulateConsultHangup(reason?: HangupReason): void;
49
55
  }
50
56
 
51
57
  /**