@vuevox/sdk 0.3.0 → 0.5.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/README.md +389 -49
- package/dist/client.d.ts +83 -3
- package/dist/client.js +121 -39
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +7 -1
- package/dist/generated/schema.d.ts +531 -2
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -4,12 +4,15 @@ export function createVueVoxClient(options) {
|
|
|
4
4
|
const baseUrl = trimTrailingSlash(options.baseUrl ?? "https://api.vuevox.com");
|
|
5
5
|
const fetchFn = options.fetch ?? fetch;
|
|
6
6
|
const raw = createClient({ baseUrl, fetch: fetchFn });
|
|
7
|
+
const retries = options.retries ?? 0;
|
|
8
|
+
const retryBaseDelayMs = options.retryBaseDelayMs ?? 250;
|
|
9
|
+
const retryMaxDelayMs = options.retryMaxDelayMs ?? 2_000;
|
|
7
10
|
let cachedToken = null;
|
|
8
11
|
async function getAccessToken() {
|
|
9
12
|
if (cachedToken && Date.now() < cachedToken.expiresAt - 30_000) {
|
|
10
13
|
return cachedToken.accessToken;
|
|
11
14
|
}
|
|
12
|
-
const
|
|
15
|
+
const result = await requestJson("POST", "/oauth/token", {
|
|
13
16
|
method: "POST",
|
|
14
17
|
headers: {
|
|
15
18
|
"Content-Type": "application/json",
|
|
@@ -21,57 +24,95 @@ export function createVueVoxClient(options) {
|
|
|
21
24
|
scope: formatScope(options.scope),
|
|
22
25
|
}),
|
|
23
26
|
});
|
|
24
|
-
const body =
|
|
25
|
-
const requestId = getRequestId(response, body);
|
|
26
|
-
notifyResponse(options, "POST", "/oauth/token", response, requestId);
|
|
27
|
-
if (!response.ok) {
|
|
28
|
-
const error = isErrorResponse(body) ? body.error : null;
|
|
29
|
-
throw new VueVoxApiError(response.status, error?.code ?? "token_request_failed", error?.message ?? "VueVox token request failed.", isErrorResponse(body) ? body : undefined, requestId);
|
|
30
|
-
}
|
|
31
|
-
if (!isTokenResponse(body)) {
|
|
32
|
-
throw new VueVoxApiError(response.status, "invalid_token_response", "VueVox returned an invalid token response.", undefined, requestId);
|
|
33
|
-
}
|
|
27
|
+
const body = result.data;
|
|
34
28
|
cachedToken = {
|
|
35
29
|
accessToken: body.access_token,
|
|
36
30
|
expiresAt: Date.now() + body.expires_in * 1000,
|
|
37
31
|
};
|
|
38
32
|
return cachedToken.accessToken;
|
|
39
33
|
}
|
|
40
|
-
async function
|
|
41
|
-
|
|
42
|
-
const { data, error, response } = await raw.GET("/v1/hello", {
|
|
43
|
-
headers: {
|
|
44
|
-
Authorization: `Bearer ${accessToken}`,
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
const requestId = getRequestId(response, error);
|
|
48
|
-
notifyResponse(options, "GET", "/v1/hello", response, requestId);
|
|
49
|
-
if (error) {
|
|
50
|
-
throw new VueVoxApiError(response.status, error.error.code, error.error.message, error, requestId);
|
|
51
|
-
}
|
|
52
|
-
return withMetadata(data, response, requestId);
|
|
34
|
+
async function getHello() {
|
|
35
|
+
return apiGet("/v1/hello");
|
|
53
36
|
}
|
|
54
37
|
async function listSpaces(listOptions = {}) {
|
|
38
|
+
return apiGet("/v1/spaces", listOptions);
|
|
39
|
+
}
|
|
40
|
+
async function listAgents(listOptions = {}) {
|
|
41
|
+
return apiGet("/v1/agents", listOptions);
|
|
42
|
+
}
|
|
43
|
+
async function listCalls(listOptions = {}) {
|
|
44
|
+
return apiGet("/v1/calls", listOptions);
|
|
45
|
+
}
|
|
46
|
+
async function getCall(callId) {
|
|
47
|
+
return apiGet(`/v1/calls/${encodeURIComponent(callId)}`);
|
|
48
|
+
}
|
|
49
|
+
async function listLeads(listOptions = {}) {
|
|
50
|
+
return apiGet("/v1/leads", listOptions);
|
|
51
|
+
}
|
|
52
|
+
async function getLead(leadId) {
|
|
53
|
+
return apiGet(`/v1/leads/${encodeURIComponent(leadId)}`);
|
|
54
|
+
}
|
|
55
|
+
async function apiGet(path, query) {
|
|
55
56
|
const accessToken = await getAccessToken();
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
query: listOptions,
|
|
59
|
-
},
|
|
57
|
+
const result = await requestJson("GET", path, {
|
|
58
|
+
method: "GET",
|
|
60
59
|
headers: {
|
|
61
60
|
Authorization: `Bearer ${accessToken}`,
|
|
62
61
|
},
|
|
62
|
+
query,
|
|
63
63
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
return withMetadata(result.data, result.response, result.requestId);
|
|
65
|
+
}
|
|
66
|
+
async function requestJson(method, path, init) {
|
|
67
|
+
for (let attempt = 0;; attempt++) {
|
|
68
|
+
const response = await fetchFn(buildUrl(baseUrl, path, init.query), init);
|
|
69
|
+
const body = await parseJson(response);
|
|
70
|
+
const requestId = getRequestId(response, body);
|
|
71
|
+
const retryAfter = retryAfterSeconds(response);
|
|
72
|
+
notifyResponse(options, method, path, response, requestId, retryAfter);
|
|
73
|
+
if (response.ok) {
|
|
74
|
+
if (body === null || isErrorResponse(body)) {
|
|
75
|
+
throw new VueVoxApiError(response.status, "invalid_response", "VueVox returned an invalid response.", undefined, requestId, retryAfter);
|
|
76
|
+
}
|
|
77
|
+
return { data: body, requestId, response };
|
|
78
|
+
}
|
|
79
|
+
if (attempt < retries && shouldRetry(response.status)) {
|
|
80
|
+
await sleep(retryDelayMs(attempt, retryAfter));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const error = isErrorResponse(body) ? body.error : null;
|
|
84
|
+
throw new VueVoxApiError(response.status, error?.code ?? "api_request_failed", error?.message ?? "VueVox API request failed.", isErrorResponse(body) ? body : undefined, requestId, retryAfter);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function retryDelayMs(attempt, retryAfter) {
|
|
88
|
+
if (retryAfter !== undefined) {
|
|
89
|
+
return retryAfter * 1000;
|
|
68
90
|
}
|
|
69
|
-
return
|
|
91
|
+
return Math.min(retryBaseDelayMs * 2 ** attempt, retryMaxDelayMs);
|
|
70
92
|
}
|
|
71
93
|
return {
|
|
72
94
|
getAccessToken,
|
|
73
|
-
hello
|
|
74
|
-
|
|
95
|
+
hello: {
|
|
96
|
+
get: getHello,
|
|
97
|
+
},
|
|
98
|
+
spaces: {
|
|
99
|
+
list: listSpaces,
|
|
100
|
+
paginate: (listOptions = {}) => paginate(listSpaces, listOptions),
|
|
101
|
+
},
|
|
102
|
+
agents: {
|
|
103
|
+
list: listAgents,
|
|
104
|
+
paginate: (listOptions = {}) => paginate(listAgents, listOptions),
|
|
105
|
+
},
|
|
106
|
+
calls: {
|
|
107
|
+
list: listCalls,
|
|
108
|
+
get: getCall,
|
|
109
|
+
paginate: (listOptions = {}) => paginate(listCalls, listOptions),
|
|
110
|
+
},
|
|
111
|
+
leads: {
|
|
112
|
+
list: listLeads,
|
|
113
|
+
get: getLead,
|
|
114
|
+
paginate: (listOptions = {}) => paginate(listLeads, listOptions),
|
|
115
|
+
},
|
|
75
116
|
raw,
|
|
76
117
|
};
|
|
77
118
|
}
|
|
@@ -84,6 +125,15 @@ function formatScope(scope) {
|
|
|
84
125
|
function trimTrailingSlash(value) {
|
|
85
126
|
return value.replace(/\/+$/, "");
|
|
86
127
|
}
|
|
128
|
+
function buildUrl(baseUrl, path, query) {
|
|
129
|
+
const url = new URL(`${baseUrl}${path}`);
|
|
130
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
131
|
+
if ((typeof value === "string" || typeof value === "number") && value !== "") {
|
|
132
|
+
url.searchParams.set(key, String(value));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return url.toString();
|
|
136
|
+
}
|
|
87
137
|
async function parseJson(response) {
|
|
88
138
|
try {
|
|
89
139
|
return (await response.json());
|
|
@@ -93,16 +143,19 @@ async function parseJson(response) {
|
|
|
93
143
|
}
|
|
94
144
|
}
|
|
95
145
|
function isTokenResponse(value) {
|
|
96
|
-
return
|
|
146
|
+
return isObject(value) && "access_token" in value;
|
|
97
147
|
}
|
|
98
148
|
function isErrorResponse(value) {
|
|
99
|
-
return
|
|
149
|
+
return isObject(value) && "error" in value;
|
|
100
150
|
}
|
|
101
151
|
function getRequestId(response, body) {
|
|
102
152
|
return response.headers.get("X-Request-Id") ?? (isErrorBody(body) ? body.error.requestId : undefined);
|
|
103
153
|
}
|
|
104
154
|
function isErrorBody(value) {
|
|
105
|
-
return
|
|
155
|
+
return isObject(value) && "error" in value;
|
|
156
|
+
}
|
|
157
|
+
function isObject(value) {
|
|
158
|
+
return typeof value === "object" && value !== null;
|
|
106
159
|
}
|
|
107
160
|
function withMetadata(data, response, requestId) {
|
|
108
161
|
return {
|
|
@@ -111,11 +164,40 @@ function withMetadata(data, response, requestId) {
|
|
|
111
164
|
status: response.status,
|
|
112
165
|
};
|
|
113
166
|
}
|
|
114
|
-
function notifyResponse(options, method, path, response, requestId) {
|
|
167
|
+
function notifyResponse(options, method, path, response, requestId, retryAfter) {
|
|
115
168
|
options.onResponse?.({
|
|
116
169
|
method,
|
|
117
170
|
path,
|
|
118
171
|
requestId,
|
|
172
|
+
retryAfter,
|
|
119
173
|
status: response.status,
|
|
120
174
|
});
|
|
121
175
|
}
|
|
176
|
+
async function* paginate(list, listOptions) {
|
|
177
|
+
let cursor = listOptions.cursor;
|
|
178
|
+
do {
|
|
179
|
+
const response = await list({ ...listOptions, cursor });
|
|
180
|
+
for (const item of response.data.data) {
|
|
181
|
+
yield item;
|
|
182
|
+
}
|
|
183
|
+
cursor = response.data.pagination.nextCursor ?? undefined;
|
|
184
|
+
} while (cursor);
|
|
185
|
+
}
|
|
186
|
+
function shouldRetry(status) {
|
|
187
|
+
return [429, 500, 502, 503, 504].includes(status);
|
|
188
|
+
}
|
|
189
|
+
function retryAfterSeconds(response) {
|
|
190
|
+
const value = response.headers.get("Retry-After");
|
|
191
|
+
if (!value) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const seconds = Number(value);
|
|
195
|
+
if (Number.isFinite(seconds)) {
|
|
196
|
+
return Math.max(0, seconds);
|
|
197
|
+
}
|
|
198
|
+
const timestamp = Date.parse(value);
|
|
199
|
+
return Number.isNaN(timestamp) ? undefined : Math.max(0, Math.ceil((timestamp - Date.now()) / 1000));
|
|
200
|
+
}
|
|
201
|
+
function sleep(ms) {
|
|
202
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
203
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ export type VueVoxErrorResponse = components["schemas"]["ErrorResponse"];
|
|
|
3
3
|
export declare class VueVoxApiError extends Error {
|
|
4
4
|
readonly status: number;
|
|
5
5
|
readonly code: string;
|
|
6
|
+
readonly details?: Record<string, unknown>;
|
|
7
|
+
readonly isRateLimited: boolean;
|
|
6
8
|
readonly requestId?: string;
|
|
9
|
+
readonly retryAfter?: number;
|
|
7
10
|
readonly response?: VueVoxErrorResponse;
|
|
8
|
-
constructor(status: number, code: string, message: string, response?: VueVoxErrorResponse, requestId?: string);
|
|
11
|
+
constructor(status: number, code: string, message: string, response?: VueVoxErrorResponse, requestId?: string, retryAfter?: number);
|
|
9
12
|
}
|
package/dist/errors.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
export class VueVoxApiError extends Error {
|
|
2
2
|
status;
|
|
3
3
|
code;
|
|
4
|
+
details;
|
|
5
|
+
isRateLimited;
|
|
4
6
|
requestId;
|
|
7
|
+
retryAfter;
|
|
5
8
|
response;
|
|
6
|
-
constructor(status, code, message, response, requestId) {
|
|
9
|
+
constructor(status, code, message, response, requestId, retryAfter) {
|
|
7
10
|
super(message);
|
|
8
11
|
this.name = "VueVoxApiError";
|
|
9
12
|
this.status = status;
|
|
10
13
|
this.code = code;
|
|
14
|
+
this.details = response?.error.details;
|
|
15
|
+
this.isRateLimited = status === 429 || code === "rate_limited";
|
|
11
16
|
this.requestId = requestId ?? response?.error.requestId;
|
|
17
|
+
this.retryAfter = retryAfter;
|
|
12
18
|
this.response = response;
|
|
13
19
|
}
|
|
14
20
|
}
|