@vuevox/sdk 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/README.md +117 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +78 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +12 -0
- package/dist/generated/schema.d.ts +200 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @vuevox/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the VueVox Developer API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vuevox/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Create API Credentials
|
|
12
|
+
|
|
13
|
+
In VueVox, open:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
Settings -> Developer API -> Manage API Clients
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Create a client, select the scopes it can request, and copy the `client_secret` immediately. VueVox shows each client secret only once.
|
|
20
|
+
|
|
21
|
+
Available scope:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
hello:read
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createVueVoxClient } from "@vuevox/sdk";
|
|
31
|
+
|
|
32
|
+
const vuevox = createVueVoxClient({
|
|
33
|
+
clientId: process.env.VUEVOX_CLIENT_ID!,
|
|
34
|
+
clientSecret: process.env.VUEVOX_CLIENT_SECRET!,
|
|
35
|
+
scope: "hello:read",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const hello = await vuevox.hello();
|
|
39
|
+
console.log(hello.message);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The SDK requests and caches a short-lived access token using client credentials, then sends it as a bearer token for API calls.
|
|
43
|
+
|
|
44
|
+
## Error Handling
|
|
45
|
+
|
|
46
|
+
API errors throw `VueVoxApiError`.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { VueVoxApiError, createVueVoxClient } from "@vuevox/sdk";
|
|
50
|
+
|
|
51
|
+
const vuevox = createVueVoxClient({
|
|
52
|
+
clientId: process.env.VUEVOX_CLIENT_ID!,
|
|
53
|
+
clientSecret: process.env.VUEVOX_CLIENT_SECRET!,
|
|
54
|
+
scope: "hello:read",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await vuevox.hello();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof VueVoxApiError) {
|
|
61
|
+
console.error(error.status, error.code, error.message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Common API error codes:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
missing_token
|
|
72
|
+
invalid_token
|
|
73
|
+
invalid_client
|
|
74
|
+
invalid_scope
|
|
75
|
+
insufficient_scope
|
|
76
|
+
rate_limited
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Token Behavior
|
|
80
|
+
|
|
81
|
+
The SDK:
|
|
82
|
+
|
|
83
|
+
- Requests tokens using client credentials.
|
|
84
|
+
- Caches the access token in memory.
|
|
85
|
+
- Refreshes the token before expiry.
|
|
86
|
+
- Never stores credentials or tokens on disk.
|
|
87
|
+
|
|
88
|
+
## Base URL
|
|
89
|
+
|
|
90
|
+
The SDK defaults to:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
https://api.vuevox.com
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If VueVox support gives you a custom API base URL, pass it explicitly:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const vuevox = createVueVoxClient({
|
|
100
|
+
baseUrl: "https://api.vuevox.com",
|
|
101
|
+
clientId: process.env.VUEVOX_CLIENT_ID!,
|
|
102
|
+
clientSecret: process.env.VUEVOX_CLIENT_SECRET!,
|
|
103
|
+
scope: "hello:read",
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Lower-Level Calls
|
|
108
|
+
|
|
109
|
+
For advanced integrations, `raw` exposes a typed lower-level API client.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const { data, error } = await vuevox.raw.GET("/v1/hello", {
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${await vuevox.getAccessToken()}`,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { components, paths } from "./generated/schema.js";
|
|
2
|
+
type HelloResponse = components["schemas"]["HelloResponse"];
|
|
3
|
+
export interface VueVoxClientOptions {
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
scope?: string | string[];
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export declare function createVueVoxClient(options: VueVoxClientOptions): {
|
|
11
|
+
getAccessToken: () => Promise<string>;
|
|
12
|
+
hello: () => Promise<HelloResponse>;
|
|
13
|
+
raw: import("openapi-fetch").Client<paths, `${string}/${string}`>;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import createClient from "openapi-fetch";
|
|
2
|
+
import { VueVoxApiError } from "./errors.js";
|
|
3
|
+
export function createVueVoxClient(options) {
|
|
4
|
+
const baseUrl = trimTrailingSlash(options.baseUrl ?? "https://api.vuevox.com");
|
|
5
|
+
const fetchFn = options.fetch ?? fetch;
|
|
6
|
+
const raw = createClient({ baseUrl, fetch: fetchFn });
|
|
7
|
+
let cachedToken = null;
|
|
8
|
+
async function getAccessToken() {
|
|
9
|
+
if (cachedToken && Date.now() < cachedToken.expiresAt - 30_000) {
|
|
10
|
+
return cachedToken.accessToken;
|
|
11
|
+
}
|
|
12
|
+
const response = await fetchFn(`${baseUrl}/oauth/token`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
grant_type: "client_credentials",
|
|
19
|
+
client_id: options.clientId,
|
|
20
|
+
client_secret: options.clientSecret,
|
|
21
|
+
scope: formatScope(options.scope),
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
const body = await parseJson(response);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const error = isErrorResponse(body) ? body.error : null;
|
|
27
|
+
throw new VueVoxApiError(response.status, error?.code ?? "token_request_failed", error?.message ?? "VueVox token request failed.", isErrorResponse(body) ? body : undefined);
|
|
28
|
+
}
|
|
29
|
+
if (!isTokenResponse(body)) {
|
|
30
|
+
throw new VueVoxApiError(response.status, "invalid_token_response", "VueVox returned an invalid token response.");
|
|
31
|
+
}
|
|
32
|
+
cachedToken = {
|
|
33
|
+
accessToken: body.access_token,
|
|
34
|
+
expiresAt: Date.now() + body.expires_in * 1000,
|
|
35
|
+
};
|
|
36
|
+
return cachedToken.accessToken;
|
|
37
|
+
}
|
|
38
|
+
async function hello() {
|
|
39
|
+
const accessToken = await getAccessToken();
|
|
40
|
+
const { data, error, response } = await raw.GET("/v1/hello", {
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `Bearer ${accessToken}`,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (error) {
|
|
46
|
+
throw new VueVoxApiError(response.status, error.error.code, error.error.message, error);
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
getAccessToken,
|
|
52
|
+
hello,
|
|
53
|
+
raw,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function formatScope(scope) {
|
|
57
|
+
if (Array.isArray(scope)) {
|
|
58
|
+
return scope.join(" ");
|
|
59
|
+
}
|
|
60
|
+
return scope;
|
|
61
|
+
}
|
|
62
|
+
function trimTrailingSlash(value) {
|
|
63
|
+
return value.replace(/\/+$/, "");
|
|
64
|
+
}
|
|
65
|
+
async function parseJson(response) {
|
|
66
|
+
try {
|
|
67
|
+
return (await response.json());
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function isTokenResponse(value) {
|
|
74
|
+
return Boolean(value && "access_token" in value);
|
|
75
|
+
}
|
|
76
|
+
function isErrorResponse(value) {
|
|
77
|
+
return Boolean(value && "error" in value);
|
|
78
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { components } from "./generated/schema.js";
|
|
2
|
+
export type VueVoxErrorResponse = components["schemas"]["ErrorResponse"];
|
|
3
|
+
export declare class VueVoxApiError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly response?: VueVoxErrorResponse;
|
|
7
|
+
constructor(status: number, code: string, message: string, response?: VueVoxErrorResponse);
|
|
8
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was auto-generated by openapi-typescript.
|
|
3
|
+
* Do not make direct changes to the file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface paths {
|
|
7
|
+
"/oauth/token": {
|
|
8
|
+
parameters: {
|
|
9
|
+
query?: never;
|
|
10
|
+
header?: never;
|
|
11
|
+
path?: never;
|
|
12
|
+
cookie?: never;
|
|
13
|
+
};
|
|
14
|
+
get?: never;
|
|
15
|
+
put?: never;
|
|
16
|
+
/**
|
|
17
|
+
* Create an access token
|
|
18
|
+
* @description Issues a short-lived bearer token using OAuth client credentials.
|
|
19
|
+
*/
|
|
20
|
+
post: operations["createAccessToken"];
|
|
21
|
+
delete?: never;
|
|
22
|
+
options?: never;
|
|
23
|
+
head?: never;
|
|
24
|
+
patch?: never;
|
|
25
|
+
trace?: never;
|
|
26
|
+
};
|
|
27
|
+
"/v1/hello": {
|
|
28
|
+
parameters: {
|
|
29
|
+
query?: never;
|
|
30
|
+
header?: never;
|
|
31
|
+
path?: never;
|
|
32
|
+
cookie?: never;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Return a hello world message
|
|
36
|
+
* @description Protected starter endpoint for validating developer API authentication and SDK wiring.
|
|
37
|
+
*/
|
|
38
|
+
get: operations["getHello"];
|
|
39
|
+
put?: never;
|
|
40
|
+
post?: never;
|
|
41
|
+
delete?: never;
|
|
42
|
+
options?: never;
|
|
43
|
+
head?: never;
|
|
44
|
+
patch?: never;
|
|
45
|
+
trace?: never;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export type webhooks = Record<string, never>;
|
|
49
|
+
export interface components {
|
|
50
|
+
schemas: {
|
|
51
|
+
TokenRequest: {
|
|
52
|
+
/** @enum {string} */
|
|
53
|
+
grant_type: "client_credentials";
|
|
54
|
+
client_id: string;
|
|
55
|
+
client_secret: string;
|
|
56
|
+
/**
|
|
57
|
+
* @description Space-separated scopes requested for the access token.
|
|
58
|
+
* @example hello:read
|
|
59
|
+
*/
|
|
60
|
+
scope?: string;
|
|
61
|
+
};
|
|
62
|
+
TokenResponse: {
|
|
63
|
+
access_token: string;
|
|
64
|
+
/** @enum {string} */
|
|
65
|
+
token_type: "Bearer";
|
|
66
|
+
/** @example 3600 */
|
|
67
|
+
expires_in: number;
|
|
68
|
+
/** @example hello:read */
|
|
69
|
+
scope: string;
|
|
70
|
+
};
|
|
71
|
+
HelloResponse: {
|
|
72
|
+
/** @example Hello world */
|
|
73
|
+
message: string;
|
|
74
|
+
};
|
|
75
|
+
ErrorResponse: {
|
|
76
|
+
error: components["schemas"]["Error"];
|
|
77
|
+
};
|
|
78
|
+
Error: {
|
|
79
|
+
/** @example invalid_token */
|
|
80
|
+
code: string;
|
|
81
|
+
/** @example Bearer token is invalid or expired. */
|
|
82
|
+
message: string;
|
|
83
|
+
/** @example req_123 */
|
|
84
|
+
requestId: string;
|
|
85
|
+
details?: {
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
responses: never;
|
|
91
|
+
parameters: never;
|
|
92
|
+
requestBodies: never;
|
|
93
|
+
headers: never;
|
|
94
|
+
pathItems: never;
|
|
95
|
+
}
|
|
96
|
+
export type $defs = Record<string, never>;
|
|
97
|
+
export interface operations {
|
|
98
|
+
createAccessToken: {
|
|
99
|
+
parameters: {
|
|
100
|
+
query?: never;
|
|
101
|
+
header?: never;
|
|
102
|
+
path?: never;
|
|
103
|
+
cookie?: never;
|
|
104
|
+
};
|
|
105
|
+
requestBody: {
|
|
106
|
+
content: {
|
|
107
|
+
"application/json": components["schemas"]["TokenRequest"];
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
responses: {
|
|
111
|
+
/** @description Access token issued. */
|
|
112
|
+
200: {
|
|
113
|
+
headers: {
|
|
114
|
+
[name: string]: unknown;
|
|
115
|
+
};
|
|
116
|
+
content: {
|
|
117
|
+
"application/json": components["schemas"]["TokenResponse"];
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
/** @description Requested scope is not allowed. */
|
|
121
|
+
400: {
|
|
122
|
+
headers: {
|
|
123
|
+
[name: string]: unknown;
|
|
124
|
+
};
|
|
125
|
+
content: {
|
|
126
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
/** @description Client authentication failed. */
|
|
130
|
+
401: {
|
|
131
|
+
headers: {
|
|
132
|
+
[name: string]: unknown;
|
|
133
|
+
};
|
|
134
|
+
content: {
|
|
135
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
/** @description Request body failed validation. */
|
|
139
|
+
422: {
|
|
140
|
+
headers: {
|
|
141
|
+
[name: string]: unknown;
|
|
142
|
+
};
|
|
143
|
+
content: {
|
|
144
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
getHello: {
|
|
150
|
+
parameters: {
|
|
151
|
+
query?: never;
|
|
152
|
+
header?: never;
|
|
153
|
+
path?: never;
|
|
154
|
+
cookie?: never;
|
|
155
|
+
};
|
|
156
|
+
requestBody?: never;
|
|
157
|
+
responses: {
|
|
158
|
+
/** @description Hello response. */
|
|
159
|
+
200: {
|
|
160
|
+
headers: {
|
|
161
|
+
[name: string]: unknown;
|
|
162
|
+
};
|
|
163
|
+
content: {
|
|
164
|
+
"application/json": components["schemas"]["HelloResponse"];
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
/** @description Bearer token is missing, invalid, or expired. */
|
|
168
|
+
401: {
|
|
169
|
+
headers: {
|
|
170
|
+
[name: string]: unknown;
|
|
171
|
+
};
|
|
172
|
+
content: {
|
|
173
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
/** @description Bearer token does not include the required scope. */
|
|
177
|
+
403: {
|
|
178
|
+
headers: {
|
|
179
|
+
[name: string]: unknown;
|
|
180
|
+
};
|
|
181
|
+
content: {
|
|
182
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
/** @description Per-client rate limit exceeded. */
|
|
186
|
+
429: {
|
|
187
|
+
headers: {
|
|
188
|
+
/** @description Seconds to wait before retrying. */
|
|
189
|
+
"Retry-After"?: string;
|
|
190
|
+
/** @description Request limit per minute for this API client. */
|
|
191
|
+
"X-RateLimit-Limit"?: string;
|
|
192
|
+
[name: string]: unknown;
|
|
193
|
+
};
|
|
194
|
+
content: {
|
|
195
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vuevox/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for the VueVox Developer API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"vuevox",
|
|
24
|
+
"sdk",
|
|
25
|
+
"api",
|
|
26
|
+
"typescript"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "rm -rf dist && tsc --outDir dist && mkdir -p dist/generated && cp src/generated/schema.d.ts dist/generated/schema.d.ts",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"prepack": "npm run build",
|
|
35
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
36
|
+
"publish:dry-run": "npm publish --dry-run --access public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"openapi-fetch": "^0.14.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"openapi-typescript": "^7.10.1",
|
|
43
|
+
"typescript": "^5.9.3"
|
|
44
|
+
}
|
|
45
|
+
}
|