@osmapi/osmtalk-sdk 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -28
- package/dist/index.d.mts +97 -12
- package/dist/index.d.ts +97 -12
- package/dist/index.js +215 -36
- package/dist/index.mjs +213 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Official TypeScript / JavaScript SDK for the [osmTalk](https://osmtalk.com) voice AI platform.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/@osmapi/osmtalk-sdk)
|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
8
|
npm install @osmapi/osmtalk-sdk
|
|
7
9
|
# or: pnpm add @osmapi/osmtalk-sdk
|
|
8
10
|
```
|
|
9
11
|
|
|
12
|
+
Works in **Node 18+, Deno, Bun, Cloudflare Workers, and browsers**.
|
|
13
|
+
|
|
10
14
|
## Quick start
|
|
11
15
|
|
|
12
16
|
```ts
|
|
@@ -34,11 +38,25 @@ console.log("Call started:", call.callId);
|
|
|
34
38
|
| Resource | Operations |
|
|
35
39
|
|---|---|
|
|
36
40
|
| `client.agents` | `list`, `get`, `create`, `update`, `delete`, `connect`, `publishVersion`, `listVersions`, `getVersion`, `rollbackToVersion` |
|
|
37
|
-
| `client.calls` | `list`, `get`, `outbound`, `end`, `transfer
|
|
41
|
+
| `client.calls` | `list`, `get`, `outbound`, `end`, `transfer`, **`waitUntilEnded`** |
|
|
38
42
|
| `client.campaigns` | `list`, `get`, `create`, `update`, `delete`, `start`, `pause`, `resume`, `stop`, `report`, `uploadLeadsCsv`, `uploadLeads`, `listLeads` |
|
|
39
43
|
| `client.dnc` | `list`, `add`, `bulkAdd`, `remove` |
|
|
40
44
|
| `client.eval` | `simulate`, `createTestCase`, `listTestCases`, `runTestCase`, `runAll`, `listRuns`, `getRun` |
|
|
41
45
|
| `client.settings` | `get`, `getStorage`, `updateStorage`, `getWebhook`, `updateWebhook`, `getCompliance`, `updateCompliance` |
|
|
46
|
+
| `client.platform` | `getRates`, `listProviders`, `getPresets`, `getModelHealth`, `getTemplates`, `getTemplate`, `saveTemplate`, `deleteTemplate` |
|
|
47
|
+
|
|
48
|
+
Plus the standalone helpers `verifyWebhookSignature` / `verifyWebhookSignatureAsync` (see below).
|
|
49
|
+
|
|
50
|
+
## What's new in 0.3.0
|
|
51
|
+
|
|
52
|
+
- **Auto-retry** on 5xx, 429, and network errors with `Retry-After` honored and exponential backoff. No more hand-rolling retry wrappers.
|
|
53
|
+
- **`client.calls.waitUntilEnded(callId)`** — one-line polling helper for the "place call → wait → get result" pattern.
|
|
54
|
+
- **`AbortSignal`** support on every method via `RequestOptions.signal`.
|
|
55
|
+
- **`User-Agent`** header sent automatically.
|
|
56
|
+
- **Per-org request override** via `RequestOptions.organizationId`.
|
|
57
|
+
- **`OsmtalkError.isRetryable` / `.isClientError` / `.retryAttempts`** for cleaner error branching.
|
|
58
|
+
|
|
59
|
+
Full version history: [CHANGELOG.md](./CHANGELOG.md).
|
|
42
60
|
|
|
43
61
|
## Examples
|
|
44
62
|
|
|
@@ -68,6 +86,45 @@ const report = await client.campaigns.report(camp.id);
|
|
|
68
86
|
console.log(report.counts.byStatus);
|
|
69
87
|
```
|
|
70
88
|
|
|
89
|
+
### Wait for a call to finish (without writing a poll loop)
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const { callId } = await client.calls.outbound({
|
|
93
|
+
agentId: "agent_xxx",
|
|
94
|
+
phoneNumberId: "pn_xxx",
|
|
95
|
+
destination: "+919876543210",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Default: poll every 5s, give up after 30 minutes. All configurable.
|
|
99
|
+
const final = await client.calls.waitUntilEnded(callId, {
|
|
100
|
+
pollIntervalMs: 5_000,
|
|
101
|
+
timeoutMs: 15 * 60 * 1000,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log("Final status:", final.status);
|
|
105
|
+
console.log("Duration: ", final.durationSeconds, "s");
|
|
106
|
+
console.log("Disposition: ", final.disposition);
|
|
107
|
+
console.log("Recording: ", final.recordingUrl);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For production, prefer webhooks — see the receiver example below.
|
|
111
|
+
|
|
112
|
+
### Cancel an in-flight request
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const ctrl = new AbortController();
|
|
116
|
+
setTimeout(() => ctrl.abort(), 2_000);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await client.agents.list({ signal: ctrl.signal });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (ctrl.signal.aborted) console.log("Cancelled by us");
|
|
122
|
+
else throw err;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`signal`, `timeoutMs`, and `organizationId` are accepted on every method via the trailing `RequestOptions` argument.
|
|
127
|
+
|
|
71
128
|
### Publish a new agent version and A/B test
|
|
72
129
|
|
|
73
130
|
```ts
|
|
@@ -75,7 +132,12 @@ console.log(report.counts.byStatus);
|
|
|
75
132
|
const v2 = await client.agents.publishVersion("agent_xxx", { label: "Tighter qualifier" });
|
|
76
133
|
|
|
77
134
|
// Call with v2 explicitly
|
|
78
|
-
await client.calls.outbound({
|
|
135
|
+
await client.calls.outbound({
|
|
136
|
+
agentId: "agent_xxx",
|
|
137
|
+
phoneNumberId: "pn_xxx",
|
|
138
|
+
destination: "+919876543210",
|
|
139
|
+
agentVersion: v2.version,
|
|
140
|
+
});
|
|
79
141
|
```
|
|
80
142
|
|
|
81
143
|
### Simulate before going live
|
|
@@ -91,33 +153,54 @@ for (const turn of sim.transcript) {
|
|
|
91
153
|
}
|
|
92
154
|
```
|
|
93
155
|
|
|
94
|
-
###
|
|
156
|
+
### Verify webhooks (Node, sync)
|
|
95
157
|
|
|
96
158
|
```ts
|
|
97
|
-
import crypto from "node:crypto";
|
|
98
159
|
import express from "express";
|
|
160
|
+
import { verifyWebhookSignature } from "@osmapi/osmtalk-sdk";
|
|
99
161
|
|
|
100
162
|
const app = express();
|
|
101
|
-
|
|
163
|
+
// IMPORTANT: use express.raw() — NOT express.json(). The signature was
|
|
164
|
+
// computed over the exact bytes; re-serialized JSON will not match.
|
|
165
|
+
app.use("/webhooks/osmtalk", express.raw({ type: "application/json" }));
|
|
102
166
|
|
|
103
167
|
app.post("/webhooks/osmtalk", (req, res) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return res.sendStatus(401);
|
|
112
|
-
}
|
|
168
|
+
const ok = verifyWebhookSignature(
|
|
169
|
+
req.body,
|
|
170
|
+
req.header("x-osmtalk-signature"),
|
|
171
|
+
process.env.OSMTALK_WEBHOOK_SECRET!,
|
|
172
|
+
);
|
|
173
|
+
if (!ok) return res.status(401).end();
|
|
174
|
+
|
|
113
175
|
const event = JSON.parse(req.body.toString());
|
|
114
|
-
if (event.event === "
|
|
115
|
-
console.log("
|
|
176
|
+
if (event.event === "call.completed") {
|
|
177
|
+
console.log("Call ended:", event.call.id, event.analysis?.disposition);
|
|
116
178
|
}
|
|
117
179
|
res.json({ ok: true });
|
|
118
180
|
});
|
|
119
181
|
```
|
|
120
182
|
|
|
183
|
+
### Verify webhooks in Workers / Deno / Bun (async, WebCrypto)
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { verifyWebhookSignatureAsync } from "@osmapi/osmtalk-sdk";
|
|
187
|
+
|
|
188
|
+
export default {
|
|
189
|
+
async fetch(req: Request) {
|
|
190
|
+
const raw = await req.text();
|
|
191
|
+
const ok = await verifyWebhookSignatureAsync(
|
|
192
|
+
raw,
|
|
193
|
+
req.headers.get("x-osmtalk-signature"),
|
|
194
|
+
env.OSMTALK_WEBHOOK_SECRET,
|
|
195
|
+
);
|
|
196
|
+
if (!ok) return new Response("invalid signature", { status: 401 });
|
|
197
|
+
const event = JSON.parse(raw);
|
|
198
|
+
// …handle event
|
|
199
|
+
return new Response("ok");
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
```
|
|
203
|
+
|
|
121
204
|
## Error handling
|
|
122
205
|
|
|
123
206
|
```ts
|
|
@@ -128,32 +211,61 @@ try {
|
|
|
128
211
|
} catch (err) {
|
|
129
212
|
if (err instanceof OsmtalkError) {
|
|
130
213
|
console.log("HTTP", err.status, err.body);
|
|
214
|
+
console.log("Retries attempted:", err.retryAttempts);
|
|
215
|
+
if (err.isRetryable) console.log("Server might recover — try again later.");
|
|
216
|
+
if (err.isClientError) console.log("Bad input — check err.body.details.");
|
|
131
217
|
} else {
|
|
132
218
|
throw err;
|
|
133
219
|
}
|
|
134
220
|
}
|
|
135
221
|
```
|
|
136
222
|
|
|
137
|
-
| Status | Meaning |
|
|
138
|
-
|
|
139
|
-
| 400 | Validation — `err.body.details` has zod field errors |
|
|
140
|
-
| 401 | Bad API key |
|
|
141
|
-
| 402 | Insufficient credits |
|
|
142
|
-
| 404 | Resource not found |
|
|
143
|
-
|
|
|
144
|
-
|
|
|
223
|
+
| Status | Meaning | `OsmtalkError` flag |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| 400 | Validation — `err.body.details` has zod field errors | `isClientError` |
|
|
226
|
+
| 401 | Bad API key | `isClientError` |
|
|
227
|
+
| 402 | Insufficient credits | `isClientError` |
|
|
228
|
+
| 404 | Resource not found | `isClientError` |
|
|
229
|
+
| 408 | Request timeout | `isRetryable` |
|
|
230
|
+
| 429 | Concurrency or rate limit | `isRetryable` |
|
|
231
|
+
| 5xx | Server error / provider outage | `isRetryable` |
|
|
232
|
+
|
|
233
|
+
The SDK already auto-retries 408/429/5xx and network errors twice by default. Mutating requests (POST/PUT/DELETE) are only retried when you pass `idempotencyKey` so the SDK never silently double-charges.
|
|
145
234
|
|
|
146
235
|
## Options
|
|
147
236
|
|
|
148
237
|
```ts
|
|
149
238
|
new Osmtalk({
|
|
150
|
-
apiKey: "
|
|
151
|
-
baseUrl: "https://api.osmtalk.com",
|
|
152
|
-
timeoutMs: 30_000,
|
|
153
|
-
|
|
239
|
+
apiKey: "osm_live_…",
|
|
240
|
+
baseUrl: "https://api.osmtalk.com", // default
|
|
241
|
+
timeoutMs: 30_000, // per-request, 0 to disable
|
|
242
|
+
maxRetries: 2, // auto-retry count for 5xx/429
|
|
243
|
+
retryInitialDelayMs: 250, // doubles per retry, jittered
|
|
244
|
+
organizationId: "org_xxx", // for multi-org accounts
|
|
245
|
+
defaultHeaders: { "X-Trace-Id": "…" },// added to every request
|
|
246
|
+
fetch: customFetch, // optional, defaults to global fetch
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Per-request overrides:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
await client.calls.outbound(input, {
|
|
254
|
+
idempotencyKey: `dest-${destination}-${date}`,
|
|
255
|
+
signal: controller.signal,
|
|
256
|
+
timeoutMs: 60_000,
|
|
257
|
+
organizationId: "org_yyy",
|
|
154
258
|
});
|
|
155
259
|
```
|
|
156
260
|
|
|
261
|
+
## Runnable examples
|
|
262
|
+
|
|
263
|
+
See [github.com/osm-API/osmtalk-examples](https://github.com/osm-API/osmtalk-examples) for three end-to-end projects:
|
|
264
|
+
|
|
265
|
+
1. **Personalized outbound call** — dynamic per-user prompts
|
|
266
|
+
2. **Bulk campaign from CSV** — scale to thousands
|
|
267
|
+
3. **Verified webhook receiver** — close the loop with `verifyWebhookSignature`
|
|
268
|
+
|
|
157
269
|
## License
|
|
158
270
|
|
|
159
271
|
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -10,12 +10,49 @@
|
|
|
10
10
|
* dynamicVariables: { first_name: "Arjun" },
|
|
11
11
|
* });
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Current SDK version, sent in the User-Agent header and useful for
|
|
15
|
+
* runtime version checks (e.g. logging which SDK build is talking to the
|
|
16
|
+
* API, or feature-detecting against minimum versions in shared code).
|
|
17
|
+
*/
|
|
18
|
+
declare const SDK_VERSION = "0.3.1";
|
|
13
19
|
interface OsmtalkOptions {
|
|
14
20
|
apiKey: string;
|
|
15
21
|
baseUrl?: string;
|
|
22
|
+
/** Per-request timeout. Default 30s. Set `0` to disable. */
|
|
16
23
|
timeoutMs?: number;
|
|
17
24
|
/** Custom fetch implementation (e.g. for testing). */
|
|
18
25
|
fetch?: typeof fetch;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum auto-retries on transient failures (5xx, 429, network
|
|
28
|
+
* errors). Default 2. Set to 0 to disable. Mutating requests (POST,
|
|
29
|
+
* PUT, DELETE) are only retried when an `idempotencyKey` is set on
|
|
30
|
+
* the call — otherwise a retry could double-charge or double-place
|
|
31
|
+
* a call.
|
|
32
|
+
*/
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Initial retry delay in ms. Doubled on each subsequent retry.
|
|
36
|
+
* Default 250ms → 500 → 1000. Server `Retry-After` headers take
|
|
37
|
+
* precedence when present.
|
|
38
|
+
*/
|
|
39
|
+
retryInitialDelayMs?: number;
|
|
40
|
+
/** Extra headers added to every request (e.g. observability IDs). */
|
|
41
|
+
defaultHeaders?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Org ID to send as `X-Organization-Id`. For users in multiple orgs;
|
|
44
|
+
* defaults to the API key's primary org if omitted.
|
|
45
|
+
*/
|
|
46
|
+
organizationId?: string;
|
|
47
|
+
}
|
|
48
|
+
interface RequestOptions {
|
|
49
|
+
idempotencyKey?: string;
|
|
50
|
+
/** AbortSignal for caller-side cancellation. Combined with the SDK's timeout signal. */
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
/** Per-call override of the global timeout. */
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
/** Per-call override of the org ID. */
|
|
55
|
+
organizationId?: string;
|
|
19
56
|
}
|
|
20
57
|
interface DynamicVariables {
|
|
21
58
|
[key: string]: string | number | boolean;
|
|
@@ -141,15 +178,36 @@ interface OsmtalkErrorBody {
|
|
|
141
178
|
declare class OsmtalkError extends Error {
|
|
142
179
|
readonly status: number;
|
|
143
180
|
readonly body: OsmtalkErrorBody;
|
|
144
|
-
|
|
181
|
+
/** Number of retry attempts the SDK made before giving up. */
|
|
182
|
+
readonly retryAttempts: number;
|
|
183
|
+
constructor(status: number, body: OsmtalkErrorBody, retryAttempts?: number);
|
|
184
|
+
/** True for status codes the server might recover from on retry. */
|
|
185
|
+
get isRetryable(): boolean;
|
|
186
|
+
/** True for client mistakes (bad input, bad auth). */
|
|
187
|
+
get isClientError(): boolean;
|
|
145
188
|
}
|
|
146
189
|
declare class HttpClient {
|
|
147
190
|
private readonly baseUrl;
|
|
148
191
|
private readonly apiKey;
|
|
149
192
|
private readonly timeoutMs;
|
|
193
|
+
private readonly maxRetries;
|
|
194
|
+
private readonly retryInitialDelayMs;
|
|
150
195
|
private readonly fetchImpl;
|
|
196
|
+
private readonly defaultHeaders;
|
|
197
|
+
private readonly organizationId;
|
|
198
|
+
private readonly userAgent;
|
|
151
199
|
constructor(opts: OsmtalkOptions);
|
|
152
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Combine the caller's AbortSignal with the SDK's per-request timeout
|
|
202
|
+
* signal so EITHER firing cancels the fetch. We can't use
|
|
203
|
+
* `AbortSignal.any` (Node 18 doesn't have it), so we wire it manually.
|
|
204
|
+
*/
|
|
205
|
+
private buildAbortSignal;
|
|
206
|
+
/** Parse the body once for both success and error paths. */
|
|
207
|
+
private static parseBody;
|
|
208
|
+
/** How long to wait before the next retry. Honors Retry-After. */
|
|
209
|
+
private retryDelay;
|
|
210
|
+
request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>, idempotencyKey?: string, options?: RequestOptions): Promise<T>;
|
|
153
211
|
}
|
|
154
212
|
declare class AgentsResource {
|
|
155
213
|
private readonly http;
|
|
@@ -199,27 +257,54 @@ declare class AgentsResource {
|
|
|
199
257
|
callId: string;
|
|
200
258
|
}>;
|
|
201
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Call statuses that mean "no more state changes are coming" — the
|
|
262
|
+
* record is final and safe to consume. `waitUntilEnded()` polls until
|
|
263
|
+
* the call's status matches one of these.
|
|
264
|
+
*
|
|
265
|
+
* Exported so downstream code can mirror the SDK's definition of "done"
|
|
266
|
+
* without copy-pasting strings (e.g. when reacting to webhook events or
|
|
267
|
+
* filtering call lists in a dashboard).
|
|
268
|
+
*/
|
|
269
|
+
declare const TERMINAL_CALL_STATUSES: readonly ["completed", "failed", "ended", "cancelled"];
|
|
202
270
|
declare class CallsResource {
|
|
203
271
|
private readonly http;
|
|
204
272
|
constructor(http: HttpClient);
|
|
205
|
-
list(): Promise<CallRecord[]>;
|
|
206
|
-
get(id: string): Promise<CallRecord>;
|
|
273
|
+
list(opts?: RequestOptions): Promise<CallRecord[]>;
|
|
274
|
+
get(id: string, opts?: RequestOptions): Promise<CallRecord>;
|
|
207
275
|
/**
|
|
208
|
-
* Place an outbound call.
|
|
209
|
-
*
|
|
276
|
+
* Place an outbound call. Pass `idempotencyKey` so a retry within 24h
|
|
277
|
+
* returns the same response instead of placing a duplicate call —
|
|
278
|
+
* required if you want this call to be retried on transient failures.
|
|
210
279
|
*/
|
|
211
|
-
outbound(input: CallStartRequest, opts?: {
|
|
212
|
-
idempotencyKey?: string;
|
|
213
|
-
}): Promise<{
|
|
280
|
+
outbound(input: CallStartRequest, opts?: RequestOptions): Promise<{
|
|
214
281
|
callId: string;
|
|
215
282
|
roomName: string;
|
|
216
283
|
}>;
|
|
217
|
-
end(id: string): Promise<{
|
|
284
|
+
end(id: string, opts?: RequestOptions): Promise<{
|
|
218
285
|
success: true;
|
|
219
286
|
}>;
|
|
220
|
-
transfer(id: string, destination: string, summary?: string): Promise<{
|
|
287
|
+
transfer(id: string, destination: string, summary?: string, opts?: RequestOptions): Promise<{
|
|
221
288
|
success: true;
|
|
222
289
|
}>;
|
|
290
|
+
/**
|
|
291
|
+
* Poll a call until it reaches a terminal status (`completed`,
|
|
292
|
+
* `failed`, `ended`, `cancelled`) and return the final record.
|
|
293
|
+
*
|
|
294
|
+
* Saves consumers from writing the same loop in every script. Use
|
|
295
|
+
* webhooks instead in production — this is fine for scripts, demos,
|
|
296
|
+
* and one-off jobs but consumes API quota on every poll.
|
|
297
|
+
*
|
|
298
|
+
* @param opts.pollIntervalMs - default 5s
|
|
299
|
+
* @param opts.timeoutMs - default 30min (rejects with
|
|
300
|
+
* `OsmtalkError` 408 on timeout)
|
|
301
|
+
* @param opts.signal - abort externally
|
|
302
|
+
*/
|
|
303
|
+
waitUntilEnded(id: string, opts?: {
|
|
304
|
+
pollIntervalMs?: number;
|
|
305
|
+
timeoutMs?: number;
|
|
306
|
+
signal?: AbortSignal;
|
|
307
|
+
}): Promise<CallRecord>;
|
|
223
308
|
}
|
|
224
309
|
declare class PlatformResource {
|
|
225
310
|
private readonly http;
|
|
@@ -517,4 +602,4 @@ declare class Osmtalk {
|
|
|
517
602
|
constructor(opts: OsmtalkOptions);
|
|
518
603
|
}
|
|
519
604
|
|
|
520
|
-
export { type AgentRecord, type AgentTemplateResult, type AssistantOverride, type CallRecord, type CallStartRequest, type CampaignCreateRequest, type CampaignRecord, type DynamicVariables, type LeadRow, Osmtalk, OsmtalkError, type OsmtalkErrorBody, type OsmtalkOptions, type PresetCostEstimate, type PresetWithCost, type ProviderHealth, Osmtalk as default, verifyWebhookSignature, verifyWebhookSignatureAsync };
|
|
605
|
+
export { type AgentRecord, type AgentTemplateResult, type AssistantOverride, type CallRecord, type CallStartRequest, type CampaignCreateRequest, type CampaignRecord, type DynamicVariables, type LeadRow, Osmtalk, OsmtalkError, type OsmtalkErrorBody, type OsmtalkOptions, type PresetCostEstimate, type PresetWithCost, type ProviderHealth, type RequestOptions, SDK_VERSION, TERMINAL_CALL_STATUSES, Osmtalk as default, verifyWebhookSignature, verifyWebhookSignatureAsync };
|
package/dist/index.d.ts
CHANGED
|
@@ -10,12 +10,49 @@
|
|
|
10
10
|
* dynamicVariables: { first_name: "Arjun" },
|
|
11
11
|
* });
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Current SDK version, sent in the User-Agent header and useful for
|
|
15
|
+
* runtime version checks (e.g. logging which SDK build is talking to the
|
|
16
|
+
* API, or feature-detecting against minimum versions in shared code).
|
|
17
|
+
*/
|
|
18
|
+
declare const SDK_VERSION = "0.3.1";
|
|
13
19
|
interface OsmtalkOptions {
|
|
14
20
|
apiKey: string;
|
|
15
21
|
baseUrl?: string;
|
|
22
|
+
/** Per-request timeout. Default 30s. Set `0` to disable. */
|
|
16
23
|
timeoutMs?: number;
|
|
17
24
|
/** Custom fetch implementation (e.g. for testing). */
|
|
18
25
|
fetch?: typeof fetch;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum auto-retries on transient failures (5xx, 429, network
|
|
28
|
+
* errors). Default 2. Set to 0 to disable. Mutating requests (POST,
|
|
29
|
+
* PUT, DELETE) are only retried when an `idempotencyKey` is set on
|
|
30
|
+
* the call — otherwise a retry could double-charge or double-place
|
|
31
|
+
* a call.
|
|
32
|
+
*/
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Initial retry delay in ms. Doubled on each subsequent retry.
|
|
36
|
+
* Default 250ms → 500 → 1000. Server `Retry-After` headers take
|
|
37
|
+
* precedence when present.
|
|
38
|
+
*/
|
|
39
|
+
retryInitialDelayMs?: number;
|
|
40
|
+
/** Extra headers added to every request (e.g. observability IDs). */
|
|
41
|
+
defaultHeaders?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Org ID to send as `X-Organization-Id`. For users in multiple orgs;
|
|
44
|
+
* defaults to the API key's primary org if omitted.
|
|
45
|
+
*/
|
|
46
|
+
organizationId?: string;
|
|
47
|
+
}
|
|
48
|
+
interface RequestOptions {
|
|
49
|
+
idempotencyKey?: string;
|
|
50
|
+
/** AbortSignal for caller-side cancellation. Combined with the SDK's timeout signal. */
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
/** Per-call override of the global timeout. */
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
/** Per-call override of the org ID. */
|
|
55
|
+
organizationId?: string;
|
|
19
56
|
}
|
|
20
57
|
interface DynamicVariables {
|
|
21
58
|
[key: string]: string | number | boolean;
|
|
@@ -141,15 +178,36 @@ interface OsmtalkErrorBody {
|
|
|
141
178
|
declare class OsmtalkError extends Error {
|
|
142
179
|
readonly status: number;
|
|
143
180
|
readonly body: OsmtalkErrorBody;
|
|
144
|
-
|
|
181
|
+
/** Number of retry attempts the SDK made before giving up. */
|
|
182
|
+
readonly retryAttempts: number;
|
|
183
|
+
constructor(status: number, body: OsmtalkErrorBody, retryAttempts?: number);
|
|
184
|
+
/** True for status codes the server might recover from on retry. */
|
|
185
|
+
get isRetryable(): boolean;
|
|
186
|
+
/** True for client mistakes (bad input, bad auth). */
|
|
187
|
+
get isClientError(): boolean;
|
|
145
188
|
}
|
|
146
189
|
declare class HttpClient {
|
|
147
190
|
private readonly baseUrl;
|
|
148
191
|
private readonly apiKey;
|
|
149
192
|
private readonly timeoutMs;
|
|
193
|
+
private readonly maxRetries;
|
|
194
|
+
private readonly retryInitialDelayMs;
|
|
150
195
|
private readonly fetchImpl;
|
|
196
|
+
private readonly defaultHeaders;
|
|
197
|
+
private readonly organizationId;
|
|
198
|
+
private readonly userAgent;
|
|
151
199
|
constructor(opts: OsmtalkOptions);
|
|
152
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Combine the caller's AbortSignal with the SDK's per-request timeout
|
|
202
|
+
* signal so EITHER firing cancels the fetch. We can't use
|
|
203
|
+
* `AbortSignal.any` (Node 18 doesn't have it), so we wire it manually.
|
|
204
|
+
*/
|
|
205
|
+
private buildAbortSignal;
|
|
206
|
+
/** Parse the body once for both success and error paths. */
|
|
207
|
+
private static parseBody;
|
|
208
|
+
/** How long to wait before the next retry. Honors Retry-After. */
|
|
209
|
+
private retryDelay;
|
|
210
|
+
request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>, idempotencyKey?: string, options?: RequestOptions): Promise<T>;
|
|
153
211
|
}
|
|
154
212
|
declare class AgentsResource {
|
|
155
213
|
private readonly http;
|
|
@@ -199,27 +257,54 @@ declare class AgentsResource {
|
|
|
199
257
|
callId: string;
|
|
200
258
|
}>;
|
|
201
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Call statuses that mean "no more state changes are coming" — the
|
|
262
|
+
* record is final and safe to consume. `waitUntilEnded()` polls until
|
|
263
|
+
* the call's status matches one of these.
|
|
264
|
+
*
|
|
265
|
+
* Exported so downstream code can mirror the SDK's definition of "done"
|
|
266
|
+
* without copy-pasting strings (e.g. when reacting to webhook events or
|
|
267
|
+
* filtering call lists in a dashboard).
|
|
268
|
+
*/
|
|
269
|
+
declare const TERMINAL_CALL_STATUSES: readonly ["completed", "failed", "ended", "cancelled"];
|
|
202
270
|
declare class CallsResource {
|
|
203
271
|
private readonly http;
|
|
204
272
|
constructor(http: HttpClient);
|
|
205
|
-
list(): Promise<CallRecord[]>;
|
|
206
|
-
get(id: string): Promise<CallRecord>;
|
|
273
|
+
list(opts?: RequestOptions): Promise<CallRecord[]>;
|
|
274
|
+
get(id: string, opts?: RequestOptions): Promise<CallRecord>;
|
|
207
275
|
/**
|
|
208
|
-
* Place an outbound call.
|
|
209
|
-
*
|
|
276
|
+
* Place an outbound call. Pass `idempotencyKey` so a retry within 24h
|
|
277
|
+
* returns the same response instead of placing a duplicate call —
|
|
278
|
+
* required if you want this call to be retried on transient failures.
|
|
210
279
|
*/
|
|
211
|
-
outbound(input: CallStartRequest, opts?: {
|
|
212
|
-
idempotencyKey?: string;
|
|
213
|
-
}): Promise<{
|
|
280
|
+
outbound(input: CallStartRequest, opts?: RequestOptions): Promise<{
|
|
214
281
|
callId: string;
|
|
215
282
|
roomName: string;
|
|
216
283
|
}>;
|
|
217
|
-
end(id: string): Promise<{
|
|
284
|
+
end(id: string, opts?: RequestOptions): Promise<{
|
|
218
285
|
success: true;
|
|
219
286
|
}>;
|
|
220
|
-
transfer(id: string, destination: string, summary?: string): Promise<{
|
|
287
|
+
transfer(id: string, destination: string, summary?: string, opts?: RequestOptions): Promise<{
|
|
221
288
|
success: true;
|
|
222
289
|
}>;
|
|
290
|
+
/**
|
|
291
|
+
* Poll a call until it reaches a terminal status (`completed`,
|
|
292
|
+
* `failed`, `ended`, `cancelled`) and return the final record.
|
|
293
|
+
*
|
|
294
|
+
* Saves consumers from writing the same loop in every script. Use
|
|
295
|
+
* webhooks instead in production — this is fine for scripts, demos,
|
|
296
|
+
* and one-off jobs but consumes API quota on every poll.
|
|
297
|
+
*
|
|
298
|
+
* @param opts.pollIntervalMs - default 5s
|
|
299
|
+
* @param opts.timeoutMs - default 30min (rejects with
|
|
300
|
+
* `OsmtalkError` 408 on timeout)
|
|
301
|
+
* @param opts.signal - abort externally
|
|
302
|
+
*/
|
|
303
|
+
waitUntilEnded(id: string, opts?: {
|
|
304
|
+
pollIntervalMs?: number;
|
|
305
|
+
timeoutMs?: number;
|
|
306
|
+
signal?: AbortSignal;
|
|
307
|
+
}): Promise<CallRecord>;
|
|
223
308
|
}
|
|
224
309
|
declare class PlatformResource {
|
|
225
310
|
private readonly http;
|
|
@@ -517,4 +602,4 @@ declare class Osmtalk {
|
|
|
517
602
|
constructor(opts: OsmtalkOptions);
|
|
518
603
|
}
|
|
519
604
|
|
|
520
|
-
export { type AgentRecord, type AgentTemplateResult, type AssistantOverride, type CallRecord, type CallStartRequest, type CampaignCreateRequest, type CampaignRecord, type DynamicVariables, type LeadRow, Osmtalk, OsmtalkError, type OsmtalkErrorBody, type OsmtalkOptions, type PresetCostEstimate, type PresetWithCost, type ProviderHealth, Osmtalk as default, verifyWebhookSignature, verifyWebhookSignatureAsync };
|
|
605
|
+
export { type AgentRecord, type AgentTemplateResult, type AssistantOverride, type CallRecord, type CallStartRequest, type CampaignCreateRequest, type CampaignRecord, type DynamicVariables, type LeadRow, Osmtalk, OsmtalkError, type OsmtalkErrorBody, type OsmtalkOptions, type PresetCostEstimate, type PresetWithCost, type ProviderHealth, type RequestOptions, SDK_VERSION, TERMINAL_CALL_STATUSES, Osmtalk as default, verifyWebhookSignature, verifyWebhookSignatureAsync };
|
package/dist/index.js
CHANGED
|
@@ -22,68 +22,196 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Osmtalk: () => Osmtalk,
|
|
24
24
|
OsmtalkError: () => OsmtalkError,
|
|
25
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
26
|
+
TERMINAL_CALL_STATUSES: () => TERMINAL_CALL_STATUSES,
|
|
25
27
|
default: () => index_default,
|
|
26
28
|
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
27
29
|
verifyWebhookSignatureAsync: () => verifyWebhookSignatureAsync
|
|
28
30
|
});
|
|
29
31
|
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
var SDK_VERSION = "0.3.1";
|
|
30
33
|
var OsmtalkError = class extends Error {
|
|
31
34
|
status;
|
|
32
35
|
body;
|
|
33
|
-
|
|
36
|
+
/** Number of retry attempts the SDK made before giving up. */
|
|
37
|
+
retryAttempts;
|
|
38
|
+
constructor(status, body, retryAttempts = 0) {
|
|
34
39
|
super(body?.error || body?.message || `osmTalk API error: ${status}`);
|
|
35
40
|
this.name = "OsmtalkError";
|
|
36
41
|
this.status = status;
|
|
37
42
|
this.body = body;
|
|
43
|
+
this.retryAttempts = retryAttempts;
|
|
38
44
|
}
|
|
45
|
+
/** True for status codes the server might recover from on retry. */
|
|
46
|
+
get isRetryable() {
|
|
47
|
+
return this.status === 408 || this.status === 429 || this.status >= 500;
|
|
48
|
+
}
|
|
49
|
+
/** True for client mistakes (bad input, bad auth). */
|
|
50
|
+
get isClientError() {
|
|
51
|
+
return this.status >= 400 && this.status < 500;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function runtimeTag() {
|
|
55
|
+
const g = globalThis;
|
|
56
|
+
if (g.Deno?.version?.deno) return `deno/${g.Deno.version.deno}`;
|
|
57
|
+
if (g.Bun?.version) return `bun/${g.Bun.version}`;
|
|
58
|
+
if (g.process?.versions?.node) return `node/${g.process.versions.node}`;
|
|
59
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) return "browser";
|
|
60
|
+
return "unknown";
|
|
61
|
+
}
|
|
62
|
+
var STATUS_TEXT = {
|
|
63
|
+
408: "Request Timeout",
|
|
64
|
+
429: "Too Many Requests",
|
|
65
|
+
500: "Internal Server Error",
|
|
66
|
+
502: "Bad Gateway",
|
|
67
|
+
503: "Service Unavailable",
|
|
68
|
+
504: "Gateway Timeout"
|
|
39
69
|
};
|
|
40
|
-
var
|
|
70
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
71
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
72
|
+
var HttpClient = class _HttpClient {
|
|
41
73
|
baseUrl;
|
|
42
74
|
apiKey;
|
|
43
75
|
timeoutMs;
|
|
76
|
+
maxRetries;
|
|
77
|
+
retryInitialDelayMs;
|
|
44
78
|
fetchImpl;
|
|
79
|
+
defaultHeaders;
|
|
80
|
+
organizationId;
|
|
81
|
+
userAgent;
|
|
45
82
|
constructor(opts) {
|
|
46
83
|
this.apiKey = opts.apiKey;
|
|
47
84
|
this.baseUrl = (opts.baseUrl ?? "https://api.osmtalk.com").replace(/\/$/, "");
|
|
48
85
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
86
|
+
this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
|
|
87
|
+
this.retryInitialDelayMs = Math.max(0, opts.retryInitialDelayMs ?? 250);
|
|
88
|
+
this.defaultHeaders = opts.defaultHeaders ?? {};
|
|
89
|
+
this.organizationId = opts.organizationId;
|
|
90
|
+
this.userAgent = `@osmapi/osmtalk-sdk/${SDK_VERSION} ${runtimeTag()}`;
|
|
49
91
|
this.fetchImpl = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : void 0);
|
|
50
|
-
if (!this.fetchImpl)
|
|
92
|
+
if (!this.fetchImpl) {
|
|
93
|
+
throw new Error("No fetch implementation available \u2014 provide one in OsmtalkOptions.fetch");
|
|
94
|
+
}
|
|
51
95
|
if (!opts.apiKey) throw new Error("apiKey is required");
|
|
52
96
|
}
|
|
53
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Combine the caller's AbortSignal with the SDK's per-request timeout
|
|
99
|
+
* signal so EITHER firing cancels the fetch. We can't use
|
|
100
|
+
* `AbortSignal.any` (Node 18 doesn't have it), so we wire it manually.
|
|
101
|
+
*/
|
|
102
|
+
buildAbortSignal(externalSignal, timeoutMs) {
|
|
54
103
|
const ctrl = new AbortController();
|
|
55
|
-
const
|
|
104
|
+
const onAbort = () => ctrl.abort();
|
|
105
|
+
if (externalSignal) {
|
|
106
|
+
if (externalSignal.aborted) ctrl.abort();
|
|
107
|
+
else externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
108
|
+
}
|
|
109
|
+
const timer = timeoutMs > 0 ? setTimeout(() => ctrl.abort(new Error("Request timed out")), timeoutMs) : null;
|
|
110
|
+
return {
|
|
111
|
+
signal: ctrl.signal,
|
|
112
|
+
cancel: () => {
|
|
113
|
+
if (timer) clearTimeout(timer);
|
|
114
|
+
if (externalSignal) externalSignal.removeEventListener("abort", onAbort);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Parse the body once for both success and error paths. */
|
|
119
|
+
static async parseBody(res) {
|
|
120
|
+
const text = await res.text();
|
|
121
|
+
if (!text) return null;
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(text);
|
|
124
|
+
} catch {
|
|
125
|
+
return text;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** How long to wait before the next retry. Honors Retry-After. */
|
|
129
|
+
retryDelay(attempt, res) {
|
|
130
|
+
if (res) {
|
|
131
|
+
const retryAfter = res.headers.get("retry-after");
|
|
132
|
+
if (retryAfter) {
|
|
133
|
+
const secs = Number(retryAfter);
|
|
134
|
+
if (Number.isFinite(secs) && secs >= 0) return Math.min(secs * 1e3, 3e4);
|
|
135
|
+
const when = Date.parse(retryAfter);
|
|
136
|
+
if (!Number.isNaN(when)) return Math.max(0, Math.min(when - Date.now(), 3e4));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const base = this.retryInitialDelayMs * 2 ** attempt;
|
|
140
|
+
const jitter = Math.floor(Math.random() * (base / 4));
|
|
141
|
+
return Math.min(base + jitter, 3e4);
|
|
142
|
+
}
|
|
143
|
+
async request(method, path, body, extraHeaders, idempotencyKey, options) {
|
|
144
|
+
const reqOrgId = options?.organizationId ?? this.organizationId;
|
|
145
|
+
const reqTimeout = options?.timeoutMs ?? this.timeoutMs;
|
|
56
146
|
const headers = {
|
|
57
147
|
Authorization: `Bearer ${this.apiKey}`,
|
|
148
|
+
"User-Agent": this.userAgent,
|
|
149
|
+
Accept: "application/json",
|
|
58
150
|
...body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
59
151
|
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {},
|
|
152
|
+
...reqOrgId ? { "X-Organization-Id": reqOrgId } : {},
|
|
153
|
+
...this.defaultHeaders,
|
|
60
154
|
...extraHeaders ?? {}
|
|
61
155
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
clearTimeout(timer);
|
|
72
|
-
}
|
|
73
|
-
const text = await res.text();
|
|
74
|
-
const parsed = text ? (() => {
|
|
156
|
+
const serializedBody = body === void 0 ? void 0 : typeof body === "string" ? body : JSON.stringify(body);
|
|
157
|
+
const canRetryMutation = SAFE_METHODS.has(method.toUpperCase()) || Boolean(idempotencyKey);
|
|
158
|
+
const url = `${this.baseUrl}${path}`;
|
|
159
|
+
let lastErr;
|
|
160
|
+
let lastRes = null;
|
|
161
|
+
const maxAttempts = this.maxRetries + 1;
|
|
162
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
163
|
+
const { signal, cancel } = this.buildAbortSignal(options?.signal, reqTimeout);
|
|
164
|
+
let res = null;
|
|
75
165
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
166
|
+
res = await this.fetchImpl(url, {
|
|
167
|
+
method,
|
|
168
|
+
headers,
|
|
169
|
+
body: serializedBody,
|
|
170
|
+
signal
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
lastErr = err;
|
|
174
|
+
cancel();
|
|
175
|
+
if (options?.signal?.aborted) throw err;
|
|
176
|
+
if (attempt < maxAttempts - 1 && canRetryMutation) {
|
|
177
|
+
await sleep(this.retryDelay(attempt, null));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
cancel();
|
|
183
|
+
if (res.ok) {
|
|
184
|
+
return await _HttpClient.parseBody(res);
|
|
79
185
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
186
|
+
const retryable = RETRYABLE_STATUSES.has(res.status);
|
|
187
|
+
if (retryable && canRetryMutation && attempt < maxAttempts - 1) {
|
|
188
|
+
try {
|
|
189
|
+
await res.text();
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
await sleep(this.retryDelay(attempt, res));
|
|
193
|
+
lastRes = res;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const parsed = await _HttpClient.parseBody(res);
|
|
197
|
+
throw new OsmtalkError(
|
|
198
|
+
res.status,
|
|
199
|
+
parsed ?? { error: STATUS_TEXT[res.status] ?? `HTTP ${res.status}` },
|
|
200
|
+
attempt
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (lastRes) {
|
|
204
|
+
const parsed = await _HttpClient.parseBody(lastRes);
|
|
205
|
+
throw new OsmtalkError(
|
|
206
|
+
lastRes.status,
|
|
207
|
+
parsed ?? { error: STATUS_TEXT[lastRes.status] ?? "Server error" },
|
|
208
|
+
maxAttempts - 1
|
|
209
|
+
);
|
|
83
210
|
}
|
|
84
|
-
|
|
211
|
+
throw lastErr ?? new Error("osmTalk SDK: request failed without details");
|
|
85
212
|
}
|
|
86
213
|
};
|
|
214
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
87
215
|
var AgentsResource = class {
|
|
88
216
|
constructor(http) {
|
|
89
217
|
this.http = http;
|
|
@@ -139,20 +267,22 @@ var AgentsResource = class {
|
|
|
139
267
|
);
|
|
140
268
|
}
|
|
141
269
|
};
|
|
270
|
+
var TERMINAL_CALL_STATUSES = ["completed", "failed", "ended", "cancelled"];
|
|
142
271
|
var CallsResource = class {
|
|
143
272
|
constructor(http) {
|
|
144
273
|
this.http = http;
|
|
145
274
|
}
|
|
146
275
|
http;
|
|
147
|
-
list() {
|
|
148
|
-
return this.http.request("GET", "/api/calls");
|
|
276
|
+
list(opts) {
|
|
277
|
+
return this.http.request("GET", "/api/calls", void 0, void 0, void 0, opts);
|
|
149
278
|
}
|
|
150
|
-
get(id) {
|
|
151
|
-
return this.http.request("GET", `/api/calls/${id}
|
|
279
|
+
get(id, opts) {
|
|
280
|
+
return this.http.request("GET", `/api/calls/${id}`, void 0, void 0, void 0, opts);
|
|
152
281
|
}
|
|
153
282
|
/**
|
|
154
|
-
* Place an outbound call.
|
|
155
|
-
*
|
|
283
|
+
* Place an outbound call. Pass `idempotencyKey` so a retry within 24h
|
|
284
|
+
* returns the same response instead of placing a duplicate call —
|
|
285
|
+
* required if you want this call to be retried on transient failures.
|
|
156
286
|
*/
|
|
157
287
|
outbound(input, opts) {
|
|
158
288
|
return this.http.request(
|
|
@@ -160,14 +290,61 @@ var CallsResource = class {
|
|
|
160
290
|
"/api/calls/outbound",
|
|
161
291
|
input,
|
|
162
292
|
void 0,
|
|
163
|
-
opts?.idempotencyKey
|
|
293
|
+
opts?.idempotencyKey,
|
|
294
|
+
opts
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
end(id, opts) {
|
|
298
|
+
return this.http.request(
|
|
299
|
+
"POST",
|
|
300
|
+
`/api/calls/${id}/end`,
|
|
301
|
+
void 0,
|
|
302
|
+
void 0,
|
|
303
|
+
opts?.idempotencyKey,
|
|
304
|
+
opts
|
|
164
305
|
);
|
|
165
306
|
}
|
|
166
|
-
|
|
167
|
-
return this.http.request(
|
|
307
|
+
transfer(id, destination, summary, opts) {
|
|
308
|
+
return this.http.request(
|
|
309
|
+
"POST",
|
|
310
|
+
`/api/calls/${id}/transfer`,
|
|
311
|
+
{ destination, summary },
|
|
312
|
+
void 0,
|
|
313
|
+
opts?.idempotencyKey,
|
|
314
|
+
opts
|
|
315
|
+
);
|
|
168
316
|
}
|
|
169
|
-
|
|
170
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Poll a call until it reaches a terminal status (`completed`,
|
|
319
|
+
* `failed`, `ended`, `cancelled`) and return the final record.
|
|
320
|
+
*
|
|
321
|
+
* Saves consumers from writing the same loop in every script. Use
|
|
322
|
+
* webhooks instead in production — this is fine for scripts, demos,
|
|
323
|
+
* and one-off jobs but consumes API quota on every poll.
|
|
324
|
+
*
|
|
325
|
+
* @param opts.pollIntervalMs - default 5s
|
|
326
|
+
* @param opts.timeoutMs - default 30min (rejects with
|
|
327
|
+
* `OsmtalkError` 408 on timeout)
|
|
328
|
+
* @param opts.signal - abort externally
|
|
329
|
+
*/
|
|
330
|
+
async waitUntilEnded(id, opts) {
|
|
331
|
+
const pollInterval = Math.max(1e3, opts?.pollIntervalMs ?? 5e3);
|
|
332
|
+
const totalTimeout = opts?.timeoutMs ?? 30 * 60 * 1e3;
|
|
333
|
+
const deadline = Date.now() + totalTimeout;
|
|
334
|
+
const terminal = new Set(TERMINAL_CALL_STATUSES);
|
|
335
|
+
while (true) {
|
|
336
|
+
if (opts?.signal?.aborted) {
|
|
337
|
+
throw new OsmtalkError(0, { error: "Aborted by caller" });
|
|
338
|
+
}
|
|
339
|
+
const call = await this.get(id, { signal: opts?.signal });
|
|
340
|
+
if (terminal.has(call.status)) return call;
|
|
341
|
+
if (Date.now() >= deadline) {
|
|
342
|
+
throw new OsmtalkError(408, {
|
|
343
|
+
error: `waitUntilEnded: call ${id} did not reach a terminal state within ${totalTimeout}ms (last status: ${call.status})`
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
await sleep(Math.min(pollInterval, Math.max(0, deadline - Date.now())));
|
|
347
|
+
}
|
|
171
348
|
}
|
|
172
349
|
};
|
|
173
350
|
var PlatformResource = class {
|
|
@@ -445,6 +622,8 @@ var index_default = Osmtalk;
|
|
|
445
622
|
0 && (module.exports = {
|
|
446
623
|
Osmtalk,
|
|
447
624
|
OsmtalkError,
|
|
625
|
+
SDK_VERSION,
|
|
626
|
+
TERMINAL_CALL_STATUSES,
|
|
448
627
|
verifyWebhookSignature,
|
|
449
628
|
verifyWebhookSignatureAsync
|
|
450
629
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,61 +1,187 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
var SDK_VERSION = "0.3.1";
|
|
2
3
|
var OsmtalkError = class extends Error {
|
|
3
4
|
status;
|
|
4
5
|
body;
|
|
5
|
-
|
|
6
|
+
/** Number of retry attempts the SDK made before giving up. */
|
|
7
|
+
retryAttempts;
|
|
8
|
+
constructor(status, body, retryAttempts = 0) {
|
|
6
9
|
super(body?.error || body?.message || `osmTalk API error: ${status}`);
|
|
7
10
|
this.name = "OsmtalkError";
|
|
8
11
|
this.status = status;
|
|
9
12
|
this.body = body;
|
|
13
|
+
this.retryAttempts = retryAttempts;
|
|
10
14
|
}
|
|
15
|
+
/** True for status codes the server might recover from on retry. */
|
|
16
|
+
get isRetryable() {
|
|
17
|
+
return this.status === 408 || this.status === 429 || this.status >= 500;
|
|
18
|
+
}
|
|
19
|
+
/** True for client mistakes (bad input, bad auth). */
|
|
20
|
+
get isClientError() {
|
|
21
|
+
return this.status >= 400 && this.status < 500;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function runtimeTag() {
|
|
25
|
+
const g = globalThis;
|
|
26
|
+
if (g.Deno?.version?.deno) return `deno/${g.Deno.version.deno}`;
|
|
27
|
+
if (g.Bun?.version) return `bun/${g.Bun.version}`;
|
|
28
|
+
if (g.process?.versions?.node) return `node/${g.process.versions.node}`;
|
|
29
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) return "browser";
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
var STATUS_TEXT = {
|
|
33
|
+
408: "Request Timeout",
|
|
34
|
+
429: "Too Many Requests",
|
|
35
|
+
500: "Internal Server Error",
|
|
36
|
+
502: "Bad Gateway",
|
|
37
|
+
503: "Service Unavailable",
|
|
38
|
+
504: "Gateway Timeout"
|
|
11
39
|
};
|
|
12
|
-
var
|
|
40
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
41
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
42
|
+
var HttpClient = class _HttpClient {
|
|
13
43
|
baseUrl;
|
|
14
44
|
apiKey;
|
|
15
45
|
timeoutMs;
|
|
46
|
+
maxRetries;
|
|
47
|
+
retryInitialDelayMs;
|
|
16
48
|
fetchImpl;
|
|
49
|
+
defaultHeaders;
|
|
50
|
+
organizationId;
|
|
51
|
+
userAgent;
|
|
17
52
|
constructor(opts) {
|
|
18
53
|
this.apiKey = opts.apiKey;
|
|
19
54
|
this.baseUrl = (opts.baseUrl ?? "https://api.osmtalk.com").replace(/\/$/, "");
|
|
20
55
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
56
|
+
this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
|
|
57
|
+
this.retryInitialDelayMs = Math.max(0, opts.retryInitialDelayMs ?? 250);
|
|
58
|
+
this.defaultHeaders = opts.defaultHeaders ?? {};
|
|
59
|
+
this.organizationId = opts.organizationId;
|
|
60
|
+
this.userAgent = `@osmapi/osmtalk-sdk/${SDK_VERSION} ${runtimeTag()}`;
|
|
21
61
|
this.fetchImpl = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : void 0);
|
|
22
|
-
if (!this.fetchImpl)
|
|
62
|
+
if (!this.fetchImpl) {
|
|
63
|
+
throw new Error("No fetch implementation available \u2014 provide one in OsmtalkOptions.fetch");
|
|
64
|
+
}
|
|
23
65
|
if (!opts.apiKey) throw new Error("apiKey is required");
|
|
24
66
|
}
|
|
25
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Combine the caller's AbortSignal with the SDK's per-request timeout
|
|
69
|
+
* signal so EITHER firing cancels the fetch. We can't use
|
|
70
|
+
* `AbortSignal.any` (Node 18 doesn't have it), so we wire it manually.
|
|
71
|
+
*/
|
|
72
|
+
buildAbortSignal(externalSignal, timeoutMs) {
|
|
26
73
|
const ctrl = new AbortController();
|
|
27
|
-
const
|
|
74
|
+
const onAbort = () => ctrl.abort();
|
|
75
|
+
if (externalSignal) {
|
|
76
|
+
if (externalSignal.aborted) ctrl.abort();
|
|
77
|
+
else externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
78
|
+
}
|
|
79
|
+
const timer = timeoutMs > 0 ? setTimeout(() => ctrl.abort(new Error("Request timed out")), timeoutMs) : null;
|
|
80
|
+
return {
|
|
81
|
+
signal: ctrl.signal,
|
|
82
|
+
cancel: () => {
|
|
83
|
+
if (timer) clearTimeout(timer);
|
|
84
|
+
if (externalSignal) externalSignal.removeEventListener("abort", onAbort);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Parse the body once for both success and error paths. */
|
|
89
|
+
static async parseBody(res) {
|
|
90
|
+
const text = await res.text();
|
|
91
|
+
if (!text) return null;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(text);
|
|
94
|
+
} catch {
|
|
95
|
+
return text;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** How long to wait before the next retry. Honors Retry-After. */
|
|
99
|
+
retryDelay(attempt, res) {
|
|
100
|
+
if (res) {
|
|
101
|
+
const retryAfter = res.headers.get("retry-after");
|
|
102
|
+
if (retryAfter) {
|
|
103
|
+
const secs = Number(retryAfter);
|
|
104
|
+
if (Number.isFinite(secs) && secs >= 0) return Math.min(secs * 1e3, 3e4);
|
|
105
|
+
const when = Date.parse(retryAfter);
|
|
106
|
+
if (!Number.isNaN(when)) return Math.max(0, Math.min(when - Date.now(), 3e4));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const base = this.retryInitialDelayMs * 2 ** attempt;
|
|
110
|
+
const jitter = Math.floor(Math.random() * (base / 4));
|
|
111
|
+
return Math.min(base + jitter, 3e4);
|
|
112
|
+
}
|
|
113
|
+
async request(method, path, body, extraHeaders, idempotencyKey, options) {
|
|
114
|
+
const reqOrgId = options?.organizationId ?? this.organizationId;
|
|
115
|
+
const reqTimeout = options?.timeoutMs ?? this.timeoutMs;
|
|
28
116
|
const headers = {
|
|
29
117
|
Authorization: `Bearer ${this.apiKey}`,
|
|
118
|
+
"User-Agent": this.userAgent,
|
|
119
|
+
Accept: "application/json",
|
|
30
120
|
...body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
31
121
|
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {},
|
|
122
|
+
...reqOrgId ? { "X-Organization-Id": reqOrgId } : {},
|
|
123
|
+
...this.defaultHeaders,
|
|
32
124
|
...extraHeaders ?? {}
|
|
33
125
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
clearTimeout(timer);
|
|
44
|
-
}
|
|
45
|
-
const text = await res.text();
|
|
46
|
-
const parsed = text ? (() => {
|
|
126
|
+
const serializedBody = body === void 0 ? void 0 : typeof body === "string" ? body : JSON.stringify(body);
|
|
127
|
+
const canRetryMutation = SAFE_METHODS.has(method.toUpperCase()) || Boolean(idempotencyKey);
|
|
128
|
+
const url = `${this.baseUrl}${path}`;
|
|
129
|
+
let lastErr;
|
|
130
|
+
let lastRes = null;
|
|
131
|
+
const maxAttempts = this.maxRetries + 1;
|
|
132
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
133
|
+
const { signal, cancel } = this.buildAbortSignal(options?.signal, reqTimeout);
|
|
134
|
+
let res = null;
|
|
47
135
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
136
|
+
res = await this.fetchImpl(url, {
|
|
137
|
+
method,
|
|
138
|
+
headers,
|
|
139
|
+
body: serializedBody,
|
|
140
|
+
signal
|
|
141
|
+
});
|
|
142
|
+
} catch (err) {
|
|
143
|
+
lastErr = err;
|
|
144
|
+
cancel();
|
|
145
|
+
if (options?.signal?.aborted) throw err;
|
|
146
|
+
if (attempt < maxAttempts - 1 && canRetryMutation) {
|
|
147
|
+
await sleep(this.retryDelay(attempt, null));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
cancel();
|
|
153
|
+
if (res.ok) {
|
|
154
|
+
return await _HttpClient.parseBody(res);
|
|
51
155
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
156
|
+
const retryable = RETRYABLE_STATUSES.has(res.status);
|
|
157
|
+
if (retryable && canRetryMutation && attempt < maxAttempts - 1) {
|
|
158
|
+
try {
|
|
159
|
+
await res.text();
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
await sleep(this.retryDelay(attempt, res));
|
|
163
|
+
lastRes = res;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const parsed = await _HttpClient.parseBody(res);
|
|
167
|
+
throw new OsmtalkError(
|
|
168
|
+
res.status,
|
|
169
|
+
parsed ?? { error: STATUS_TEXT[res.status] ?? `HTTP ${res.status}` },
|
|
170
|
+
attempt
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (lastRes) {
|
|
174
|
+
const parsed = await _HttpClient.parseBody(lastRes);
|
|
175
|
+
throw new OsmtalkError(
|
|
176
|
+
lastRes.status,
|
|
177
|
+
parsed ?? { error: STATUS_TEXT[lastRes.status] ?? "Server error" },
|
|
178
|
+
maxAttempts - 1
|
|
179
|
+
);
|
|
55
180
|
}
|
|
56
|
-
|
|
181
|
+
throw lastErr ?? new Error("osmTalk SDK: request failed without details");
|
|
57
182
|
}
|
|
58
183
|
};
|
|
184
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
59
185
|
var AgentsResource = class {
|
|
60
186
|
constructor(http) {
|
|
61
187
|
this.http = http;
|
|
@@ -111,20 +237,22 @@ var AgentsResource = class {
|
|
|
111
237
|
);
|
|
112
238
|
}
|
|
113
239
|
};
|
|
240
|
+
var TERMINAL_CALL_STATUSES = ["completed", "failed", "ended", "cancelled"];
|
|
114
241
|
var CallsResource = class {
|
|
115
242
|
constructor(http) {
|
|
116
243
|
this.http = http;
|
|
117
244
|
}
|
|
118
245
|
http;
|
|
119
|
-
list() {
|
|
120
|
-
return this.http.request("GET", "/api/calls");
|
|
246
|
+
list(opts) {
|
|
247
|
+
return this.http.request("GET", "/api/calls", void 0, void 0, void 0, opts);
|
|
121
248
|
}
|
|
122
|
-
get(id) {
|
|
123
|
-
return this.http.request("GET", `/api/calls/${id}
|
|
249
|
+
get(id, opts) {
|
|
250
|
+
return this.http.request("GET", `/api/calls/${id}`, void 0, void 0, void 0, opts);
|
|
124
251
|
}
|
|
125
252
|
/**
|
|
126
|
-
* Place an outbound call.
|
|
127
|
-
*
|
|
253
|
+
* Place an outbound call. Pass `idempotencyKey` so a retry within 24h
|
|
254
|
+
* returns the same response instead of placing a duplicate call —
|
|
255
|
+
* required if you want this call to be retried on transient failures.
|
|
128
256
|
*/
|
|
129
257
|
outbound(input, opts) {
|
|
130
258
|
return this.http.request(
|
|
@@ -132,14 +260,61 @@ var CallsResource = class {
|
|
|
132
260
|
"/api/calls/outbound",
|
|
133
261
|
input,
|
|
134
262
|
void 0,
|
|
135
|
-
opts?.idempotencyKey
|
|
263
|
+
opts?.idempotencyKey,
|
|
264
|
+
opts
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
end(id, opts) {
|
|
268
|
+
return this.http.request(
|
|
269
|
+
"POST",
|
|
270
|
+
`/api/calls/${id}/end`,
|
|
271
|
+
void 0,
|
|
272
|
+
void 0,
|
|
273
|
+
opts?.idempotencyKey,
|
|
274
|
+
opts
|
|
136
275
|
);
|
|
137
276
|
}
|
|
138
|
-
|
|
139
|
-
return this.http.request(
|
|
277
|
+
transfer(id, destination, summary, opts) {
|
|
278
|
+
return this.http.request(
|
|
279
|
+
"POST",
|
|
280
|
+
`/api/calls/${id}/transfer`,
|
|
281
|
+
{ destination, summary },
|
|
282
|
+
void 0,
|
|
283
|
+
opts?.idempotencyKey,
|
|
284
|
+
opts
|
|
285
|
+
);
|
|
140
286
|
}
|
|
141
|
-
|
|
142
|
-
|
|
287
|
+
/**
|
|
288
|
+
* Poll a call until it reaches a terminal status (`completed`,
|
|
289
|
+
* `failed`, `ended`, `cancelled`) and return the final record.
|
|
290
|
+
*
|
|
291
|
+
* Saves consumers from writing the same loop in every script. Use
|
|
292
|
+
* webhooks instead in production — this is fine for scripts, demos,
|
|
293
|
+
* and one-off jobs but consumes API quota on every poll.
|
|
294
|
+
*
|
|
295
|
+
* @param opts.pollIntervalMs - default 5s
|
|
296
|
+
* @param opts.timeoutMs - default 30min (rejects with
|
|
297
|
+
* `OsmtalkError` 408 on timeout)
|
|
298
|
+
* @param opts.signal - abort externally
|
|
299
|
+
*/
|
|
300
|
+
async waitUntilEnded(id, opts) {
|
|
301
|
+
const pollInterval = Math.max(1e3, opts?.pollIntervalMs ?? 5e3);
|
|
302
|
+
const totalTimeout = opts?.timeoutMs ?? 30 * 60 * 1e3;
|
|
303
|
+
const deadline = Date.now() + totalTimeout;
|
|
304
|
+
const terminal = new Set(TERMINAL_CALL_STATUSES);
|
|
305
|
+
while (true) {
|
|
306
|
+
if (opts?.signal?.aborted) {
|
|
307
|
+
throw new OsmtalkError(0, { error: "Aborted by caller" });
|
|
308
|
+
}
|
|
309
|
+
const call = await this.get(id, { signal: opts?.signal });
|
|
310
|
+
if (terminal.has(call.status)) return call;
|
|
311
|
+
if (Date.now() >= deadline) {
|
|
312
|
+
throw new OsmtalkError(408, {
|
|
313
|
+
error: `waitUntilEnded: call ${id} did not reach a terminal state within ${totalTimeout}ms (last status: ${call.status})`
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
await sleep(Math.min(pollInterval, Math.max(0, deadline - Date.now())));
|
|
317
|
+
}
|
|
143
318
|
}
|
|
144
319
|
};
|
|
145
320
|
var PlatformResource = class {
|
|
@@ -416,6 +591,8 @@ var index_default = Osmtalk;
|
|
|
416
591
|
export {
|
|
417
592
|
Osmtalk,
|
|
418
593
|
OsmtalkError,
|
|
594
|
+
SDK_VERSION,
|
|
595
|
+
TERMINAL_CALL_STATUSES,
|
|
419
596
|
index_default as default,
|
|
420
597
|
verifyWebhookSignature,
|
|
421
598
|
verifyWebhookSignatureAsync
|