@cosmoops/connect-sdk 0.43.0 → 0.44.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,51 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.44.0
7
+
8
+ ### `ConnectCall.dialledAt`: was the person's phone rung?
9
+
10
+ `unansweredReason: "cancelled"` covers two calls that need opposite handling:
11
+ your user hanging up while the person's phone rang, and your user giving up
12
+ before anybody was rung. The first left a missed call on the person's phone;
13
+ the second never happened. Nothing on the call told them apart, so an app
14
+ reading a call back after hang-up had to skip both and wait for the webhook.
15
+
16
+ - **`dialledAt`** on `ConnectCall` (from `calls.get()` and `calls.list()`) is
17
+ when Connect asked the carrier to ring the person, on a `member` call. Set
18
+ means a cancelled call was a missed one - file it as missed, exactly as
19
+ `call.missed` would. `null` means nobody was rung - file nothing. Connect's
20
+ webhooks already follow the same rule.
21
+ - It is on the record before any hang-up can be, so it never needs a retry.
22
+ `null` on agent calls and on member calls placed before this release.
23
+
24
+ Nothing to wire if you file calls from `call.completed` / `call.missed` only.
25
+
26
+ ### `calls.list({ placedByApplication: true })`: the calls your UI placed
27
+
28
+ A call placed with `<ConnectCallButton>` leaves from the workspace's own
29
+ number, which is assigned to no application - so `calls.list()` never
30
+ returned it, and an app whose webhook was delivered to another deployment had
31
+ no way to find out the call happened. `placedByApplication: true` (with an
32
+ optional `workspaceId`) lists exactly those calls, newest first: reconcile a
33
+ call log from it.
34
+
35
+ ### Call recording, from inside your app
36
+
37
+ `connect.calls.getCallSettings({ asUser })` and
38
+ `connect.calls.setCallRecording({ recordingEnabled, asUser })` read and switch
39
+ the workspace's call recording - the same switch as Connect's Calling screen.
40
+ `canManage` is the member's own `channel:manage`; draw the switch disabled
41
+ when it is false, because a save is refused `403`. The change applies to calls
42
+ started afterwards and is audited against the member. Needs `calls:place`,
43
+ which a key placing calls already holds.
44
+
45
+ ### The "Call ended" card shows the minutes left
46
+
47
+ After a call, `<ConnectCallProvider>` re-reads the calling balance and the
48
+ card now says how many minutes are left once that read lands, staying a few
49
+ seconds longer to show it. Nothing to wire.
50
+
6
51
  ## 0.43.0
7
52
 
8
53
  ### Recorded calls: transcript, summary, intent and action items
@@ -29,6 +74,19 @@ minute to the call - nothing to wire on your side to offer it.
29
74
 
30
75
  ## 0.42.0
31
76
 
77
+ ### `connect.whatsapp` - your own approved templates, to a workspace's customers
78
+
79
+ - **`whatsapp.send({ workspaceId, template, to, values, consent })`** sends one
80
+ of your application's Meta-approved templates - named by slug, resolved by
81
+ Connect as `app_<yourApplicationKey>_<slug>` - through Connect's WhatsApp
82
+ sender. Needs the `whatsapp:send` scope.
83
+ - **`whatsapp.template(slug)`** answers `live` and the `variables` a send must
84
+ carry. Offer WhatsApp on a screen only when `live` is true.
85
+ - `consent.collectedVia` records an opt-in your application collected, only
86
+ when Connect has none for the number. A number that opted out stays opted
87
+ out: the send fails with `opted_out`.
88
+
89
+
32
90
  ### Ask AI explains failures instead of printing them
33
91
 
34
92
  Nothing to wire - update and the panel behaves better on a bad day.
@@ -57,10 +57,15 @@ function useConnectCall() {
57
57
  }
58
58
  /** How long the SDK may take to sign in before the attempt is abandoned. */
59
59
  const LOGIN_TIMEOUT_MS = 15_000;
60
- /** How long the "Call ended" card stays before the bar goes away. */
60
+ /** How long the "Call ended" card stays once it is showing the minutes left. */
61
61
  const ENDED_CARD_MS = 4_000;
62
62
  /** How long after a call to re-read the balance - the carrier's report settles it. */
63
63
  const AVAILABILITY_SETTLE_MS = 3_000;
64
+ /**
65
+ * The longest the card waits for that re-read before going anyway - a read
66
+ * that failed must not leave a finished call on screen.
67
+ */
68
+ const ENDED_CARD_MAX_MS = AVAILABILITY_SETTLE_MS + 6_000;
64
69
  function messageFromCause(cause) {
65
70
  if (typeof cause === "string" && cause.trim())
66
71
  return cause.trim();
@@ -86,6 +91,8 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
86
91
  const targetRef = (0, react_1.useRef)(null);
87
92
  const callIdRef = (0, react_1.useRef)(null);
88
93
  const endedRef = (0, react_1.useRef)(false);
94
+ /** Set when a call ends; the next balance read is the one the card shows. */
95
+ const awaitingBalanceRef = (0, react_1.useRef)(false);
89
96
  const dismissTimer = (0, react_1.useRef)(null);
90
97
  // Read inside SDK event handlers wired once, so they always see the latest.
91
98
  const onCallEndedRef = (0, react_1.useRef)(onCallEnded);
@@ -118,15 +125,32 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
118
125
  : current);
119
126
  if (dismissTimer.current)
120
127
  clearTimeout(dismissTimer.current);
121
- dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
128
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MAX_MS);
122
129
  // The charge lands a moment after the line drops, so the minutes shown
123
- // are re-read once it has had time to.
130
+ // are re-read once it has had time to - and the card waits for that read
131
+ // (see the effect below) rather than showing what was left before the call.
132
+ awaitingBalanceRef.current = true;
124
133
  setTimeout(() => refreshRef.current(), AVAILABILITY_SETTLE_MS);
125
134
  if (target) {
126
135
  target.onEnded?.({ callId });
127
136
  onCallEndedRef.current?.({ callId, phone: target.phone });
128
137
  }
129
138
  }, []);
139
+ /*
140
+ * The re-read after a call has landed: put what is left on the "Call ended"
141
+ * card, and give it its own few seconds from here, so the number is on
142
+ * screen long enough to read.
143
+ */
144
+ (0, react_1.useEffect)(() => {
145
+ if (!awaitingBalanceRef.current || !availability)
146
+ return;
147
+ awaitingBalanceRef.current = false;
148
+ const minutesLeft = availability.availableMinutes;
149
+ setCall((current) => (current?.phase === "ended" ? { ...current, minutesLeft } : current));
150
+ if (dismissTimer.current)
151
+ clearTimeout(dismissTimer.current);
152
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
153
+ }, [availability]);
130
154
  const wire = (0, react_1.useCallback)((client) => {
131
155
  const ringing = () => setCall((current) => (current ? { ...current, phase: "ringing" } : current));
132
156
  client.on("onCalling", ringing);
@@ -200,6 +224,7 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
200
224
  if (dismissTimer.current)
201
225
  clearTimeout(dismissTimer.current);
202
226
  targetRef.current = target;
227
+ awaitingBalanceRef.current = false;
203
228
  callIdRef.current = null;
204
229
  placedRef.current = false;
205
230
  endedRef.current = false;
@@ -212,6 +237,7 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
212
237
  endedAfterSeconds: null,
213
238
  muted: false,
214
239
  notice: null,
240
+ minutesLeft: null,
215
241
  });
216
242
  try {
217
243
  const client = await signIn();
@@ -383,7 +409,15 @@ function ConnectActiveCallBar({ call, onHangUp, onToggleMute, className, }) {
383
409
  : call.phase === "ended" && call.endedAfterSeconds !== null
384
410
  ? `Call ended · ${formatSeconds(call.endedAfterSeconds)}`
385
411
  : PHASE_LABEL[call.phase];
386
- return ((0, jsx_runtime_1.jsx)("div", { role: "status", "aria-live": "polite", className: (0, cn_js_1.cn)("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-medium", children: call.label }), (0, jsx_runtime_1.jsxs)("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] })] }), live && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? (0, jsx_runtime_1.jsx)(lucide_react_1.MicOff, { className: "size-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Mic, { className: "size-4" }) }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOff, { className: "size-4" }) })] }))] }) }));
412
+ const remaining = call.phase === "ended" ? minutesLeftLabel(call.minutesLeft) : null;
413
+ return ((0, jsx_runtime_1.jsx)("div", { role: "status", "aria-live": "polite", className: (0, cn_js_1.cn)("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-medium", children: call.label }), (0, jsx_runtime_1.jsxs)("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] }), remaining && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted tabular-nums", children: remaining })] }), live && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? (0, jsx_runtime_1.jsx)(lucide_react_1.MicOff, { className: "size-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Mic, { className: "size-4" }) }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOff, { className: "size-4" }) })] }))] }) }));
414
+ }
415
+ /** "42 min of calling left", once the post-call balance read has landed. */
416
+ function minutesLeftLabel(minutes) {
417
+ if (minutes === null)
418
+ return null;
419
+ const whole = Math.max(0, Math.floor(minutes));
420
+ return whole === 0 ? "No calling minutes left" : `${whole} min of calling left`;
387
421
  }
388
422
  /** Seconds since `since`, ticking once a second. `0` while there is nothing to time. */
389
423
  function useElapsedSeconds(since) {
@@ -183,4 +183,4 @@ class ConnectClient {
183
183
  }
184
184
  exports.ConnectClient = ConnectClient;
185
185
  /** Kept in step with `package.json` by `sdk.test.ts`. */
186
- exports.SDK_VERSION = "0.43.0";
186
+ exports.SDK_VERSION = "0.44.0";
@@ -31,6 +31,8 @@ class CallsResource {
31
31
  path: "/calls",
32
32
  query: {
33
33
  channelId: input.channelId,
34
+ placedByApplication: input.placedByApplication ? "true" : undefined,
35
+ workspaceId: input.workspaceId,
34
36
  cursor: input.cursor,
35
37
  limit: input.limit === undefined ? undefined : String(input.limit),
36
38
  },
@@ -78,6 +80,30 @@ class CallsResource {
78
80
  headers: (0, acting_as_js_1.actingAs)(input.asUser),
79
81
  });
80
82
  }
83
+ /**
84
+ * Whether the workspace records its member calls, and whether this member
85
+ * may change that. The same switch as Connect's Calling screen.
86
+ */
87
+ getCallSettings(input) {
88
+ return (0, http_js_1.request)(this.transport, {
89
+ method: "GET",
90
+ path: "/calls/settings",
91
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
92
+ });
93
+ }
94
+ /**
95
+ * Switches recording on or off for calls started from now. Throws
96
+ * `ConnectError` 403 when the member lacks `channel:manage`; the change is
97
+ * audited against them. Answers the settings as they now stand.
98
+ */
99
+ setCallRecording(input) {
100
+ return (0, http_js_1.request)(this.transport, {
101
+ method: "PUT",
102
+ path: "/calls/settings",
103
+ json: { recordingEnabled: input.recordingEnabled },
104
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
105
+ });
106
+ }
81
107
  /** A carrier token the member's browser signs in to the phone network with. */
82
108
  getBrowserCallingToken(input) {
83
109
  return (0, http_js_1.request)(this.transport, {
@@ -17,10 +17,15 @@ export function useConnectCall() {
17
17
  }
18
18
  /** How long the SDK may take to sign in before the attempt is abandoned. */
19
19
  const LOGIN_TIMEOUT_MS = 15_000;
20
- /** How long the "Call ended" card stays before the bar goes away. */
20
+ /** How long the "Call ended" card stays once it is showing the minutes left. */
21
21
  const ENDED_CARD_MS = 4_000;
22
22
  /** How long after a call to re-read the balance - the carrier's report settles it. */
23
23
  const AVAILABILITY_SETTLE_MS = 3_000;
24
+ /**
25
+ * The longest the card waits for that re-read before going anyway - a read
26
+ * that failed must not leave a finished call on screen.
27
+ */
28
+ const ENDED_CARD_MAX_MS = AVAILABILITY_SETTLE_MS + 6_000;
24
29
  function messageFromCause(cause) {
25
30
  if (typeof cause === "string" && cause.trim())
26
31
  return cause.trim();
@@ -46,6 +51,8 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
46
51
  const targetRef = useRef(null);
47
52
  const callIdRef = useRef(null);
48
53
  const endedRef = useRef(false);
54
+ /** Set when a call ends; the next balance read is the one the card shows. */
55
+ const awaitingBalanceRef = useRef(false);
49
56
  const dismissTimer = useRef(null);
50
57
  // Read inside SDK event handlers wired once, so they always see the latest.
51
58
  const onCallEndedRef = useRef(onCallEnded);
@@ -78,15 +85,32 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
78
85
  : current);
79
86
  if (dismissTimer.current)
80
87
  clearTimeout(dismissTimer.current);
81
- dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
88
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MAX_MS);
82
89
  // The charge lands a moment after the line drops, so the minutes shown
83
- // are re-read once it has had time to.
90
+ // are re-read once it has had time to - and the card waits for that read
91
+ // (see the effect below) rather than showing what was left before the call.
92
+ awaitingBalanceRef.current = true;
84
93
  setTimeout(() => refreshRef.current(), AVAILABILITY_SETTLE_MS);
85
94
  if (target) {
86
95
  target.onEnded?.({ callId });
87
96
  onCallEndedRef.current?.({ callId, phone: target.phone });
88
97
  }
89
98
  }, []);
99
+ /*
100
+ * The re-read after a call has landed: put what is left on the "Call ended"
101
+ * card, and give it its own few seconds from here, so the number is on
102
+ * screen long enough to read.
103
+ */
104
+ useEffect(() => {
105
+ if (!awaitingBalanceRef.current || !availability)
106
+ return;
107
+ awaitingBalanceRef.current = false;
108
+ const minutesLeft = availability.availableMinutes;
109
+ setCall((current) => (current?.phase === "ended" ? { ...current, minutesLeft } : current));
110
+ if (dismissTimer.current)
111
+ clearTimeout(dismissTimer.current);
112
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
113
+ }, [availability]);
90
114
  const wire = useCallback((client) => {
91
115
  const ringing = () => setCall((current) => (current ? { ...current, phase: "ringing" } : current));
92
116
  client.on("onCalling", ringing);
@@ -160,6 +184,7 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
160
184
  if (dismissTimer.current)
161
185
  clearTimeout(dismissTimer.current);
162
186
  targetRef.current = target;
187
+ awaitingBalanceRef.current = false;
163
188
  callIdRef.current = null;
164
189
  placedRef.current = false;
165
190
  endedRef.current = false;
@@ -172,6 +197,7 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
172
197
  endedAfterSeconds: null,
173
198
  muted: false,
174
199
  notice: null,
200
+ minutesLeft: null,
175
201
  });
176
202
  try {
177
203
  const client = await signIn();
@@ -343,7 +369,15 @@ export function ConnectActiveCallBar({ call, onHangUp, onToggleMute, className,
343
369
  : call.phase === "ended" && call.endedAfterSeconds !== null
344
370
  ? `Call ended · ${formatSeconds(call.endedAfterSeconds)}`
345
371
  : PHASE_LABEL[call.phase];
346
- return (_jsx("div", { role: "status", "aria-live": "polite", className: cn("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: _jsxs("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: _jsx(PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: call.label }), _jsxs("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] })] }), live && (_jsxs(_Fragment, { children: [_jsx(Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? _jsx(MicOff, { className: "size-4" }) : _jsx(Mic, { className: "size-4" }) }), _jsx(Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: _jsx(PhoneOff, { className: "size-4" }) })] }))] }) }));
372
+ const remaining = call.phase === "ended" ? minutesLeftLabel(call.minutesLeft) : null;
373
+ return (_jsx("div", { role: "status", "aria-live": "polite", className: cn("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: _jsxs("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: _jsx(PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: call.label }), _jsxs("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] }), remaining && _jsx("p", { className: "truncate text-xs text-muted tabular-nums", children: remaining })] }), live && (_jsxs(_Fragment, { children: [_jsx(Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? _jsx(MicOff, { className: "size-4" }) : _jsx(Mic, { className: "size-4" }) }), _jsx(Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: _jsx(PhoneOff, { className: "size-4" }) })] }))] }) }));
374
+ }
375
+ /** "42 min of calling left", once the post-call balance read has landed. */
376
+ function minutesLeftLabel(minutes) {
377
+ if (minutes === null)
378
+ return null;
379
+ const whole = Math.max(0, Math.floor(minutes));
380
+ return whole === 0 ? "No calling minutes left" : `${whole} min of calling left`;
347
381
  }
348
382
  /** Seconds since `since`, ticking once a second. `0` while there is nothing to time. */
349
383
  function useElapsedSeconds(since) {
@@ -179,4 +179,4 @@ export class ConnectClient {
179
179
  }
180
180
  }
181
181
  /** Kept in step with `package.json` by `sdk.test.ts`. */
182
- export const SDK_VERSION = "0.43.0";
182
+ export const SDK_VERSION = "0.44.0";
@@ -28,6 +28,8 @@ export class CallsResource {
28
28
  path: "/calls",
29
29
  query: {
30
30
  channelId: input.channelId,
31
+ placedByApplication: input.placedByApplication ? "true" : undefined,
32
+ workspaceId: input.workspaceId,
31
33
  cursor: input.cursor,
32
34
  limit: input.limit === undefined ? undefined : String(input.limit),
33
35
  },
@@ -75,6 +77,30 @@ export class CallsResource {
75
77
  headers: actingAs(input.asUser),
76
78
  });
77
79
  }
80
+ /**
81
+ * Whether the workspace records its member calls, and whether this member
82
+ * may change that. The same switch as Connect's Calling screen.
83
+ */
84
+ getCallSettings(input) {
85
+ return request(this.transport, {
86
+ method: "GET",
87
+ path: "/calls/settings",
88
+ headers: actingAs(input.asUser),
89
+ });
90
+ }
91
+ /**
92
+ * Switches recording on or off for calls started from now. Throws
93
+ * `ConnectError` 403 when the member lacks `channel:manage`; the change is
94
+ * audited against them. Answers the settings as they now stand.
95
+ */
96
+ setCallRecording(input) {
97
+ return request(this.transport, {
98
+ method: "PUT",
99
+ path: "/calls/settings",
100
+ json: { recordingEnabled: input.recordingEnabled },
101
+ headers: actingAs(input.asUser),
102
+ });
103
+ }
78
104
  /** A carrier token the member's browser signs in to the phone network with. */
79
105
  getBrowserCallingToken(input) {
80
106
  return request(this.transport, {
@@ -45,6 +45,12 @@ interface ActiveCall {
45
45
  endedAfterSeconds: number | null;
46
46
  muted: boolean;
47
47
  notice: string | null;
48
+ /**
49
+ * Calling minutes left once this call has been charged - filled on the
50
+ * "Call ended" card by the balance re-read that follows it, and `null`
51
+ * until that read lands, so the card never shows the pre-call figure.
52
+ */
53
+ minutesLeft: number | null;
48
54
  }
49
55
  /** The number to ring, and how to name the person on the in-call bar. */
50
56
  export interface ConnectCallTarget {
@@ -141,4 +141,4 @@ export declare class ConnectClient {
141
141
  constructor(options: ConnectClientOptions);
142
142
  }
143
143
  /** Kept in step with `package.json` by `sdk.test.ts`. */
144
- export declare const SDK_VERSION = "0.43.0";
144
+ export declare const SDK_VERSION = "0.44.0";
@@ -1,8 +1,17 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { BrowserCallingToken, CallingAvailability, CallDetail, CallsPage, StartedBrowserCall } from "../../types.js";
2
+ import type { BrowserCallingToken, CallingAvailability, CallSettings, CallDetail, CallsPage, StartedBrowserCall } from "../../types.js";
3
3
  export interface ListCallsInput {
4
4
  /** Narrow to one channel. A channel belonging to another application simply matches nothing. */
5
5
  channelId?: string;
6
+ /**
7
+ * `true` for the member calls your UI placed (`<ConnectCallButton>`) instead
8
+ * of the calls on your channels. Those leave from the workspace's own
9
+ * number, so the default list never contains them - this is the list to
10
+ * reconcile a timeline from when a webhook went to another deployment.
11
+ */
12
+ placedByApplication?: boolean;
13
+ /** With `placedByApplication`, one workspace's calls. */
14
+ workspaceId?: string;
6
15
  /** From a previous page's `nextCursor`. */
7
16
  cursor?: string;
8
17
  /** 1-100, default 50. */
@@ -50,6 +59,22 @@ export declare class CallsResource {
50
59
  getCallingAvailability(input: {
51
60
  asUser: string;
52
61
  }): Promise<CallingAvailability>;
62
+ /**
63
+ * Whether the workspace records its member calls, and whether this member
64
+ * may change that. The same switch as Connect's Calling screen.
65
+ */
66
+ getCallSettings(input: {
67
+ asUser: string;
68
+ }): Promise<CallSettings>;
69
+ /**
70
+ * Switches recording on or off for calls started from now. Throws
71
+ * `ConnectError` 403 when the member lacks `channel:manage`; the change is
72
+ * audited against them. Answers the settings as they now stand.
73
+ */
74
+ setCallRecording(input: {
75
+ recordingEnabled: boolean;
76
+ asUser: string;
77
+ }): Promise<CallSettings>;
53
78
  /** A carrier token the member's browser signs in to the phone network with. */
54
79
  getBrowserCallingToken(input: {
55
80
  asUser: string;
@@ -2858,6 +2858,14 @@ export interface ConnectCall {
2858
2858
  * nobody answered.
2859
2859
  */
2860
2860
  talkSeconds: number | null;
2861
+ /**
2862
+ * When Connect asked the carrier to ring the person called, on a `member`
2863
+ * call. Tells apart the two calls `unansweredReason: "cancelled"` covers:
2864
+ * set when the member hung up while it rang (the person sees a missed call -
2865
+ * Connect sends `call.missed`), `null` when they gave up before anyone was
2866
+ * rung (no call happened, and no event is sent).
2867
+ */
2868
+ dialledAt: string | null;
2861
2869
  /** Why nobody answered - set only when `status` is `unanswered`. */
2862
2870
  unansweredReason: CallUnansweredReason | (string & {}) | null;
2863
2871
  /**
@@ -2902,6 +2910,18 @@ export interface StartedBrowserCall {
2902
2910
  * - `available`.
2903
2911
  */
2904
2912
  export type CallingAvailabilityStatus = "available" | "no_permission" | "no_number" | "number_pending" | "no_balance";
2913
+ /**
2914
+ * The workspace's calling settings, as the signed-in member sees them -
2915
+ * `connect.calls.getCallSettings()`.
2916
+ */
2917
+ export interface CallSettings {
2918
+ /** Whether member calls are recorded, transcribed and read (`call.analyzed`). */
2919
+ recordingEnabled: boolean;
2920
+ /** Paise, pre-tax, recording adds to every billed minute - show it beside the switch. */
2921
+ recordingRatePaisePerMinute: number;
2922
+ /** Whether this member may change it - their `channel:manage`. Draw the switch disabled when not. */
2923
+ canManage: boolean;
2924
+ }
2905
2925
  export interface CallingAvailability {
2906
2926
  status: CallingAvailabilityStatus;
2907
2927
  /** The workspace's business number, E.164, when it holds one. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",