@voiflow/sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/cjs/client.js +43 -0
- package/dist/cjs/errors.js +49 -0
- package/dist/cjs/http.js +280 -0
- package/dist/cjs/index.js +17 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pagination.js +15 -0
- package/dist/cjs/resources.generated.js +573 -0
- package/dist/cjs/sse.js +46 -0
- package/dist/cjs/webhooks.js +46 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.js +18 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.js +43 -0
- package/dist/http.d.ts +66 -0
- package/dist/http.js +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pagination.d.ts +7 -0
- package/dist/pagination.js +12 -0
- package/dist/resources.generated.d.ts +1999 -0
- package/dist/resources.generated.js +544 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.js +43 -0
- package/dist/webhooks.d.ts +26 -0
- package/dist/webhooks.js +40 -0
- package/package.json +45 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VoiFlow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
-
#
|
|
1
|
+
# VoiFlow for TypeScript / Node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run your voice agents from your own backend: create businesses, set up agents, start calls, read transcripts and get webhooks.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @voiflow/sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Node 18 or newer. Works with `import` (ESM) and `require` (CommonJS). TypeScript types are included. No other dependencies.
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { VoiFlowClient } from "@voiflow/sdk";
|
|
15
|
+
|
|
16
|
+
const client = new VoiFlowClient({ apiKey: process.env.VOIFLOW_API_KEY! });
|
|
17
|
+
|
|
18
|
+
for await (const business of client.businesses.list()) console.log(business.id, business.name);
|
|
19
|
+
|
|
20
|
+
const call = await client.calls.create(
|
|
21
|
+
{ agent_id: "agent_id", contact_id: "contact_id", mission: "Confirm tomorrow's visit" },
|
|
22
|
+
{ businessId: "business_id" }, // only needed if your key covers more than one business
|
|
23
|
+
);
|
|
24
|
+
console.log(call.call_id, call.status); // "starting": the call is accepted, not yet connected
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Keys
|
|
28
|
+
|
|
29
|
+
Create keys in the Partner Portal under Developers. A `vf_test_` key works on test data and never places a real call or touches a live business. A `vf_live_` key acts on real businesses and real customers. Both are secret: keep them on your server. The client refuses to start in a browser; give browsers a short-lived token from `client.sessions.create` instead.
|
|
30
|
+
|
|
31
|
+
A key is either tied to one business or can manage all of yours. With a management key, pass `businessId` on each call (or create the business first with `businesses.create`).
|
|
32
|
+
|
|
33
|
+
## Lists
|
|
34
|
+
|
|
35
|
+
List methods return an async iterator and fetch the next page for you.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
for await (const call of client.calls.list({ days: "7" })) console.log(call.call_id);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Errors and retries
|
|
42
|
+
|
|
43
|
+
Failures throw `VoiFlowError` with `status`, `code`, `message`, `requestId` and `details`. A network failure or an HTTP 202 means the result is not known yet (`VoiFlowConnectionError`, `VoiFlowOperationPending`): do not start the action again under a new key. Every method that changes something takes `idempotencyKey`; if you leave it out, one is generated and reused on automatic retries, and it is on the error as `err.idempotencyKey` so you can retry the same action safely.
|
|
44
|
+
|
|
45
|
+
## Webhooks
|
|
46
|
+
|
|
47
|
+
Create an endpoint with `client.webhookEndpoints.create({ url, event_types })`. The secret is shown once. Events today: `call.started`, `call.completed`, `agent.published`. Check every delivery before you trust it, using the raw body bytes:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { verifyWebhookSignature, parseWebhookEvent } from "@voiflow/sdk";
|
|
51
|
+
|
|
52
|
+
verifyWebhookSignature(rawBody, req.headers["voiflow-signature"], secret); // throws if wrong or older than 5 minutes
|
|
53
|
+
const event = parseWebhookEvent(rawBody); // event.id stays the same on retries: use it to skip duplicates
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Before you place calls
|
|
57
|
+
|
|
58
|
+
Phone calls and browser voice sessions work once VoiFlow has connected a phone line to the business, which VoiFlow does for you during onboarding. Until then `calls.create` and `sessions.create` return a clear error saying so. Webhook endpoints must be public `https` addresses.
|
|
59
|
+
|
|
60
|
+
## What is in the client
|
|
61
|
+
|
|
62
|
+
| Group | Methods |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `businesses` | list, create, retrieve, update, readiness, settings, updateSettings |
|
|
65
|
+
| `agents` | list, create, retrieve, update, delete, publish, voiceOptions |
|
|
66
|
+
| `journeys` | list, create, retrieve, update, draft, saveDraft, createVersion, retrieveVersion, publish, impact |
|
|
67
|
+
| `calls` | list, create, retrieve, end, steer, transcript, recording, stream |
|
|
68
|
+
| `conversations` | list, create, retrieve, listMessages, sendMessage, takeover, resumeAi |
|
|
69
|
+
| `contacts` | list, create, retrieve, update, setDnc |
|
|
70
|
+
| `enquiries` | list, retrieve |
|
|
71
|
+
| `appointments`, `calendar` | appointments.list / create; calendar.availability / resources |
|
|
72
|
+
| `knowledge`, `files` | knowledge.list / create / update / publish; files.list / upload / retrieve / download |
|
|
73
|
+
| `projects`, `campaigns`, `runs` | projects.list / create; campaigns.list / create / retrieve / update / setStatus / stats; runs.create / retrieve / stats |
|
|
74
|
+
| `connections`, `integrations`, `channelAccounts`, `lines` | connections.list / create / retrieve / update / readiness; integrations.list; channelAccounts.list / create; lines.list / update |
|
|
75
|
+
| `aiWork` | list, retrieve, cancel, requeue |
|
|
76
|
+
| `webhookEndpoints`, `events` | webhookEndpoints.list / create / retrieve / update / delete / rotateSecret / listDeliveries; events.list / replay |
|
|
77
|
+
| `sessions`, `usage`, `limits`, `operations` | sessions.create; usage.get / costs; limits.get; operations.retrieve |
|
|
78
|
+
|
|
79
|
+
Upload a document: `await client.files.upload(bytes, { filename: "policy.pdf", contentType: "application/pdf" })`.
|
|
80
|
+
|
|
81
|
+
Field names are the same as in the API reference (`agent_id`, `contact_id`).
|
|
82
|
+
|
|
83
|
+
MIT licence.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.SIGNATURE_HEADER = exports.WebhookSignatureError = exports.parseWebhookEvent = exports.verifyWebhookSignature = exports.VoiFlowOperationPending = exports.VoiFlowConnectionError = exports.VoiFlowError = exports.HttpClient = exports.VoiFlowClient = void 0;
|
|
18
|
+
const http_js_1 = require("./http.js");
|
|
19
|
+
Object.defineProperty(exports, "HttpClient", { enumerable: true, get: function () { return http_js_1.HttpClient; } });
|
|
20
|
+
const resources_generated_js_1 = require("./resources.generated.js");
|
|
21
|
+
/** Server-side client. Every published /v1 resource group is a property (`client.agents`, `client.aiWork`, ...). */
|
|
22
|
+
class VoiFlowClient extends resources_generated_js_1.GeneratedResources {
|
|
23
|
+
transport;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
const http = new http_js_1.HttpClient(options);
|
|
26
|
+
super(http);
|
|
27
|
+
this.transport = http;
|
|
28
|
+
}
|
|
29
|
+
get environment() {
|
|
30
|
+
return this.transport.environment;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.VoiFlowClient = VoiFlowClient;
|
|
34
|
+
__exportStar(require("./resources.generated.js"), exports);
|
|
35
|
+
var errors_js_1 = require("./errors.js");
|
|
36
|
+
Object.defineProperty(exports, "VoiFlowError", { enumerable: true, get: function () { return errors_js_1.VoiFlowError; } });
|
|
37
|
+
Object.defineProperty(exports, "VoiFlowConnectionError", { enumerable: true, get: function () { return errors_js_1.VoiFlowConnectionError; } });
|
|
38
|
+
Object.defineProperty(exports, "VoiFlowOperationPending", { enumerable: true, get: function () { return errors_js_1.VoiFlowOperationPending; } });
|
|
39
|
+
var webhooks_js_1 = require("./webhooks.js");
|
|
40
|
+
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhooks_js_1.verifyWebhookSignature; } });
|
|
41
|
+
Object.defineProperty(exports, "parseWebhookEvent", { enumerable: true, get: function () { return webhooks_js_1.parseWebhookEvent; } });
|
|
42
|
+
Object.defineProperty(exports, "WebhookSignatureError", { enumerable: true, get: function () { return webhooks_js_1.WebhookSignatureError; } });
|
|
43
|
+
Object.defineProperty(exports, "SIGNATURE_HEADER", { enumerable: true, get: function () { return webhooks_js_1.SIGNATURE_HEADER; } });
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VoiFlowOperationPending = exports.VoiFlowConnectionError = exports.VoiFlowError = void 0;
|
|
4
|
+
/** Thrown for any non-2xx API response or a request that could not be sent. */
|
|
5
|
+
class VoiFlowError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
status;
|
|
8
|
+
requestId;
|
|
9
|
+
details;
|
|
10
|
+
operationId;
|
|
11
|
+
statusUrl;
|
|
12
|
+
/** The Idempotency-Key the failed call used; pass it back to retry the same logical operation. */
|
|
13
|
+
idempotencyKey;
|
|
14
|
+
constructor(status, body) {
|
|
15
|
+
super(body.message);
|
|
16
|
+
this.name = "VoiFlowError";
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.code = body.code;
|
|
19
|
+
this.requestId = body.request_id;
|
|
20
|
+
this.details = body.details;
|
|
21
|
+
this.operationId = body.operation_id;
|
|
22
|
+
this.statusUrl = body.status_url;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.VoiFlowError = VoiFlowError;
|
|
26
|
+
/** The request never reached the server or the response could not be parsed (unknown outcome). */
|
|
27
|
+
class VoiFlowConnectionError extends Error {
|
|
28
|
+
cause;
|
|
29
|
+
/** The Idempotency-Key the call used; pass it back to retry without repeating the action. */
|
|
30
|
+
idempotencyKey;
|
|
31
|
+
constructor(message, cause) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "VoiFlowConnectionError";
|
|
34
|
+
this.cause = cause;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.VoiFlowConnectionError = VoiFlowConnectionError;
|
|
38
|
+
/**
|
|
39
|
+
* HTTP 202: the server accepted the request but its outcome is not yet known. The action may still
|
|
40
|
+
* complete, so do not repeat it under a new Idempotency-Key — poll `operations.retrieve(operationId)`
|
|
41
|
+
* or retry with the same `idempotencyKey`.
|
|
42
|
+
*/
|
|
43
|
+
class VoiFlowOperationPending extends VoiFlowError {
|
|
44
|
+
constructor(body) {
|
|
45
|
+
super(202, body);
|
|
46
|
+
this.name = "VoiFlowOperationPending";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.VoiFlowOperationPending = VoiFlowOperationPending;
|
package/dist/cjs/http.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpClient = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
6
|
+
const DEFAULT_BASE_URL = "https://api.voiflow.ai/v1";
|
|
7
|
+
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
|
8
|
+
const REQUEST_ID_HEADER = "VoiFlow-Request-Id";
|
|
9
|
+
function isKeyValid(key) {
|
|
10
|
+
return key.startsWith("vf_live_") || key.startsWith("vf_test_");
|
|
11
|
+
}
|
|
12
|
+
function assertServerRuntime() {
|
|
13
|
+
// This SDK holds a secret vf_live_/vf_test_ key. A browser runtime (window + document) means
|
|
14
|
+
// the key would ship to every visitor; refuse to construct the client at all in that case.
|
|
15
|
+
const g = globalThis;
|
|
16
|
+
if (typeof g.window !== "undefined" && typeof g.document !== "undefined") {
|
|
17
|
+
throw new Error("The VoiFlow SDK holds a secret server credential and must not run in a browser. " +
|
|
18
|
+
"Use it from your backend and give the browser a short-lived session token instead.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class HttpClient {
|
|
22
|
+
apiKey;
|
|
23
|
+
baseUrl;
|
|
24
|
+
timeoutMs;
|
|
25
|
+
maxRetries;
|
|
26
|
+
fetchImpl;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
assertServerRuntime();
|
|
29
|
+
if (!isKeyValid(options.apiKey)) {
|
|
30
|
+
throw new Error("apiKey must start with vf_live_ or vf_test_; browser/public keys are not supported here.");
|
|
31
|
+
}
|
|
32
|
+
this.apiKey = options.apiKey;
|
|
33
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
34
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
35
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
36
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
37
|
+
}
|
|
38
|
+
get environment() {
|
|
39
|
+
return this.apiKey.startsWith("vf_live_") ? "live" : "test";
|
|
40
|
+
}
|
|
41
|
+
resolvedUrl(path, query) {
|
|
42
|
+
const url = new URL(this.baseUrl + path);
|
|
43
|
+
if (query) {
|
|
44
|
+
for (const [key, value] of Object.entries(query)) {
|
|
45
|
+
if (value !== undefined)
|
|
46
|
+
url.searchParams.set(key, String(value));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return url;
|
|
50
|
+
}
|
|
51
|
+
/** Opens an authenticated text/event-stream GET request and returns its raw body. */
|
|
52
|
+
async openStream(path, query, options = {}) {
|
|
53
|
+
const url = this.resolvedUrl(path, query);
|
|
54
|
+
const headers = {
|
|
55
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
56
|
+
Accept: "text/event-stream",
|
|
57
|
+
};
|
|
58
|
+
if (options.lastEventId)
|
|
59
|
+
headers["Last-Event-ID"] = options.lastEventId;
|
|
60
|
+
const response = await this.fetchImpl(url, { headers, signal: options.signal });
|
|
61
|
+
if (!response.ok || !response.body) {
|
|
62
|
+
const body = await parseErrorBody(response);
|
|
63
|
+
throw new errors_js_1.VoiFlowError(response.status, body);
|
|
64
|
+
}
|
|
65
|
+
return response.body;
|
|
66
|
+
}
|
|
67
|
+
/** A successful binary response (e.g. a recording or document download) is raw bytes with an
|
|
68
|
+
* arbitrary content-type, not the {data, request_id} JSON envelope — an error response still is. */
|
|
69
|
+
async requestBinary(method, path, options = {}) {
|
|
70
|
+
const url = this.resolvedUrl(path, options.query);
|
|
71
|
+
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
72
|
+
const response = await this.runWithRetry(method, url, headers, undefined, options);
|
|
73
|
+
const requestId = readRequestId(response, undefined);
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new errors_js_1.VoiFlowError(response.status, await parseErrorBody(response));
|
|
76
|
+
}
|
|
77
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
78
|
+
return { data, contentType: response.headers.get("content-type") ?? "application/octet-stream", requestId, status: response.status };
|
|
79
|
+
}
|
|
80
|
+
/** Uploads raw bytes with an explicit content-type instead of JSON-serialising an object —
|
|
81
|
+
* the gateway forwards the body/content-type verbatim for upload routes. */
|
|
82
|
+
async uploadBinary(method, path, content, contentType, options = {}) {
|
|
83
|
+
const url = this.resolvedUrl(path, options.query);
|
|
84
|
+
const headers = { Authorization: `Bearer ${this.apiKey}`, "Content-Type": contentType, Accept: "application/json" };
|
|
85
|
+
if (options.idempotencyKey)
|
|
86
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
87
|
+
const response = await this.runWithRetry(method, url, headers, content, { idempotencyKey: options.idempotencyKey, signal: options.signal });
|
|
88
|
+
return this.parseJsonResponse(response);
|
|
89
|
+
}
|
|
90
|
+
/** Multipart/form-data upload (a document plus optional JSON `metadata`). Idempotent: a stable key is generated once per call. */
|
|
91
|
+
async uploadMultipart(method, path, file, options = {}) {
|
|
92
|
+
const idempotencyKey = options.idempotencyKey ?? (0, node_crypto_1.randomUUID)();
|
|
93
|
+
const form = new FormData();
|
|
94
|
+
const blob = file instanceof Blob ? file : new Blob([file], { type: options.contentType ?? "application/octet-stream" });
|
|
95
|
+
form.append("file", blob, options.filename ?? "upload");
|
|
96
|
+
if (options.metadata)
|
|
97
|
+
form.append("metadata", JSON.stringify(options.metadata));
|
|
98
|
+
const url = this.resolvedUrl(path, { business_id: options.businessId });
|
|
99
|
+
// No Content-Type here: fetch sets multipart/form-data with the boundary itself.
|
|
100
|
+
const headers = {
|
|
101
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
102
|
+
Accept: "application/json",
|
|
103
|
+
"Idempotency-Key": idempotencyKey,
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
const response = await this.runWithRetry(method, url, headers, form, { idempotencyKey });
|
|
107
|
+
return await this.parseJsonResponse(response);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (err instanceof errors_js_1.VoiFlowError || err instanceof errors_js_1.VoiFlowConnectionError)
|
|
111
|
+
err.idempotencyKey = idempotencyKey;
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async request(method, path, options = {}) {
|
|
116
|
+
const idempotencyKey = options.idempotencyKey ?? (options.idempotent ? (0, node_crypto_1.randomUUID)() : undefined);
|
|
117
|
+
const url = this.resolvedUrl(path, options.query);
|
|
118
|
+
const headers = {
|
|
119
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
120
|
+
Accept: "application/json",
|
|
121
|
+
};
|
|
122
|
+
if (idempotencyKey)
|
|
123
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
124
|
+
let payload;
|
|
125
|
+
if (options.body !== undefined) {
|
|
126
|
+
headers["Content-Type"] = "application/json";
|
|
127
|
+
payload = JSON.stringify(options.body);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const response = await this.runWithRetry(method, url, headers, payload, { ...options, idempotencyKey });
|
|
131
|
+
return await this.parseJsonResponse(response);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (idempotencyKey && (err instanceof errors_js_1.VoiFlowError || err instanceof errors_js_1.VoiFlowConnectionError)) {
|
|
135
|
+
err.idempotencyKey = idempotencyKey;
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async parseJsonResponse(response) {
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new errors_js_1.VoiFlowError(response.status, await parseErrorBody(response));
|
|
143
|
+
}
|
|
144
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
145
|
+
return { data: undefined, requestId: readRequestId(response, undefined), status: response.status };
|
|
146
|
+
}
|
|
147
|
+
const parsed = (await response.json());
|
|
148
|
+
if (response.status === 202 && parsed.error && !("data" in parsed)) {
|
|
149
|
+
throw new errors_js_1.VoiFlowOperationPending({ ...parsed.error, request_id: parsed.error.request_id ?? readRequestId(response, undefined) });
|
|
150
|
+
}
|
|
151
|
+
const requestId = readRequestId(response, parsed?.request_id);
|
|
152
|
+
return { data: parsed, requestId, status: response.status };
|
|
153
|
+
}
|
|
154
|
+
async runWithRetry(method, url, headers, body, options) {
|
|
155
|
+
const isSafeRetry = method === "GET" || options.idempotencyKey !== undefined;
|
|
156
|
+
let attempt = 0;
|
|
157
|
+
let lastError;
|
|
158
|
+
while (attempt <= this.maxRetries) {
|
|
159
|
+
if (options.signal?.aborted) {
|
|
160
|
+
throw new errors_js_1.VoiFlowConnectionError("Request was cancelled.", options.signal.reason);
|
|
161
|
+
}
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
164
|
+
const cleanupAbortLink = options.signal ? linkSignals(options.signal, controller) : undefined;
|
|
165
|
+
try {
|
|
166
|
+
const response = await this.fetchImpl(url, { method, headers, body, signal: controller.signal });
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
cleanupAbortLink?.();
|
|
169
|
+
if (RETRYABLE_STATUS.has(response.status) && isSafeRetry && attempt < this.maxRetries) {
|
|
170
|
+
const waited = await waitForRetry(retryDelayMs(response, attempt), options.signal);
|
|
171
|
+
if (!waited)
|
|
172
|
+
throw new errors_js_1.VoiFlowConnectionError("Request was cancelled.", options.signal?.reason);
|
|
173
|
+
attempt += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
cleanupAbortLink?.();
|
|
181
|
+
if (err instanceof errors_js_1.VoiFlowError || err instanceof errors_js_1.VoiFlowConnectionError)
|
|
182
|
+
throw err;
|
|
183
|
+
if (options.signal?.aborted) {
|
|
184
|
+
throw new errors_js_1.VoiFlowConnectionError("Request was cancelled.", options.signal.reason);
|
|
185
|
+
}
|
|
186
|
+
lastError = err;
|
|
187
|
+
if (isSafeRetry && attempt < this.maxRetries) {
|
|
188
|
+
const waited = await waitForRetry(retryDelayMs(undefined, attempt), options.signal);
|
|
189
|
+
if (!waited)
|
|
190
|
+
throw new errors_js_1.VoiFlowConnectionError("Request was cancelled.", options.signal?.reason);
|
|
191
|
+
attempt += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
throw new errors_js_1.VoiFlowConnectionError("VoiFlow API request failed; outcome is unknown.", err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw new errors_js_1.VoiFlowConnectionError("VoiFlow API request exhausted retries.", lastError);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
exports.HttpClient = HttpClient;
|
|
201
|
+
function readRequestId(response, fromBody) {
|
|
202
|
+
// VoiFlow-Request-Id is a real response header on every /v1 response (success and error
|
|
203
|
+
// alike) — the only reliable source for binary/204 responses, which carry no JSON body.
|
|
204
|
+
return fromBody ?? response.headers.get(REQUEST_ID_HEADER) ?? undefined;
|
|
205
|
+
}
|
|
206
|
+
// The curated contract's error envelope is {error:{code,message,request_id,details?}}, but some
|
|
207
|
+
// routes still raise a plain FastAPI HTTPException ({detail:{code,message}} or {detail:"..."}),
|
|
208
|
+
// and parameter validation failures use FastAPI's own {detail:[{loc,msg,type},...]} shape. All
|
|
209
|
+
// three are observed on the real gateway; handle all three rather than assuming the curated one.
|
|
210
|
+
async function parseErrorBody(response) {
|
|
211
|
+
const requestId = response.headers.get(REQUEST_ID_HEADER) ?? undefined;
|
|
212
|
+
try {
|
|
213
|
+
const json = (await response.json());
|
|
214
|
+
if (json.error) {
|
|
215
|
+
return {
|
|
216
|
+
code: json.error.code,
|
|
217
|
+
message: json.error.message,
|
|
218
|
+
request_id: json.error.request_id ?? requestId,
|
|
219
|
+
details: json.error.details,
|
|
220
|
+
operation_id: json.error.operation_id,
|
|
221
|
+
status_url: json.error.status_url,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (typeof json.detail === "string") {
|
|
225
|
+
return { code: "http_error", message: json.detail, request_id: requestId };
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(json.detail)) {
|
|
228
|
+
return { code: "unprocessable", message: "the request was not valid", request_id: requestId, details: json.detail };
|
|
229
|
+
}
|
|
230
|
+
if (json.detail) {
|
|
231
|
+
return {
|
|
232
|
+
code: json.detail.code ?? "http_error",
|
|
233
|
+
message: json.detail.message ?? `Request failed with status ${response.status}`,
|
|
234
|
+
request_id: requestId,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// body was not JSON; fall through to generic error below
|
|
240
|
+
}
|
|
241
|
+
return { code: "unknown_error", message: `Request failed with status ${response.status}`, request_id: requestId };
|
|
242
|
+
}
|
|
243
|
+
function retryDelayMs(response, attempt) {
|
|
244
|
+
const retryAfter = response?.headers.get("Retry-After");
|
|
245
|
+
if (retryAfter) {
|
|
246
|
+
const seconds = Number(retryAfter);
|
|
247
|
+
if (!Number.isNaN(seconds))
|
|
248
|
+
return seconds * 1000;
|
|
249
|
+
const whenMs = Date.parse(retryAfter);
|
|
250
|
+
if (!Number.isNaN(whenMs))
|
|
251
|
+
return Math.max(0, whenMs - Date.now());
|
|
252
|
+
}
|
|
253
|
+
return 250 * 2 ** attempt;
|
|
254
|
+
}
|
|
255
|
+
/** Resolves true after the delay, or false immediately if the signal aborts first — so a
|
|
256
|
+
* cancelled request doesn't sit out a retry backoff it will never use. */
|
|
257
|
+
function waitForRetry(ms, signal) {
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
if (signal?.aborted) {
|
|
260
|
+
resolve(false);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const timer = setTimeout(() => {
|
|
264
|
+
signal?.removeEventListener("abort", onAbort);
|
|
265
|
+
resolve(true);
|
|
266
|
+
}, ms);
|
|
267
|
+
const onAbort = () => {
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
resolve(false);
|
|
270
|
+
};
|
|
271
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Aborts `controller` when `signal` aborts, and always removes its own listener afterward —
|
|
275
|
+
* each attempt gets its own link instead of accumulating listeners across retries. */
|
|
276
|
+
function linkSignals(signal, controller) {
|
|
277
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
278
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
279
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
280
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./client.js"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paginate = paginate;
|
|
4
|
+
/** Lazily walks every page of a cursor-paginated list endpoint. */
|
|
5
|
+
async function* paginate(fetchPage) {
|
|
6
|
+
let cursor;
|
|
7
|
+
while (true) {
|
|
8
|
+
const page = await fetchPage(cursor);
|
|
9
|
+
for (const item of page.data)
|
|
10
|
+
yield item;
|
|
11
|
+
if (!page.next_cursor)
|
|
12
|
+
return;
|
|
13
|
+
cursor = page.next_cursor;
|
|
14
|
+
}
|
|
15
|
+
}
|