@tokenite/sdk 1.0.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 +454 -0
- package/dist/admin/accounts.d.ts +28 -0
- package/dist/admin/accounts.js +16 -0
- package/dist/admin/apps.d.ts +15 -0
- package/dist/admin/apps.js +9 -0
- package/dist/admin/auth.d.ts +13 -0
- package/dist/admin/auth.js +9 -0
- package/dist/admin/client.d.ts +22 -0
- package/dist/admin/client.js +59 -0
- package/dist/admin/connections.d.ts +19 -0
- package/dist/admin/connections.js +9 -0
- package/dist/admin/index.d.ts +88 -0
- package/dist/admin/index.js +22 -0
- package/dist/admin/keys.d.ts +10 -0
- package/dist/admin/keys.js +6 -0
- package/dist/admin/oauth.d.ts +7 -0
- package/dist/admin/oauth.js +15 -0
- package/dist/admin/spending.d.ts +6 -0
- package/dist/admin/spending.js +19 -0
- package/dist/admin/types.d.ts +197 -0
- package/dist/admin/types.js +2 -0
- package/dist/client.d.ts +120 -0
- package/dist/client.js +313 -0
- package/dist/error.d.ts +10 -0
- package/dist/error.js +18 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +181 -0
- package/dist/types.js +5 -0
- package/package.json +45 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { extractErrorMessage } from './error.js';
|
|
2
|
+
const DEFAULT_BASE_URL = 'https://tokenite.ai';
|
|
3
|
+
const DEFAULT_PROXY_URL = 'https://api.tokenite.ai';
|
|
4
|
+
const IFRAME_WIDTH = 480;
|
|
5
|
+
const IFRAME_HEIGHT = 620;
|
|
6
|
+
/**
|
|
7
|
+
* Tokenite client.
|
|
8
|
+
*
|
|
9
|
+
* Two ways to obtain an access token:
|
|
10
|
+
*
|
|
11
|
+
* **Modal (single-page apps)** — open an iframe consent screen and
|
|
12
|
+
* receive an OAuth authorization code. The code must be exchanged
|
|
13
|
+
* server-side because the exchange requires `clientSecret`:
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const { code } = await tk.popup({ suggestedBudget: 5 });
|
|
16
|
+
* await fetch('/api/auth/exchange', {
|
|
17
|
+
* method: 'POST',
|
|
18
|
+
* body: JSON.stringify({ code }),
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* **Redirect (server-side)** — classic OAuth bounce:
|
|
23
|
+
* ```typescript
|
|
24
|
+
* res.redirect(tk.getAuthorizeUrl());
|
|
25
|
+
* // ...later in /callback:
|
|
26
|
+
* const { access_token } = await tk.exchangeCode(req.query.code);
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Once you have the access token, call the LLM through the proxy:
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const result = await tk.call({
|
|
32
|
+
* accessToken,
|
|
33
|
+
* provider: 'anthropic',
|
|
34
|
+
* path: '/v1/messages',
|
|
35
|
+
* body: { model: 'claude-3-5-sonnet-latest', max_tokens: 1024, messages: [...] },
|
|
36
|
+
* });
|
|
37
|
+
* if (isProxyError(result)) console.error(result.error);
|
|
38
|
+
* else console.log(result.data);
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* For streaming responses, use a vendor SDK with `baseURL: tk.proxyUrl(...)`
|
|
42
|
+
* — streams bypass the unified envelope and are forwarded as-is.
|
|
43
|
+
*/
|
|
44
|
+
export const Tokenite = (config) => {
|
|
45
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
46
|
+
const proxyBase = config.proxyUrl ?? DEFAULT_PROXY_URL;
|
|
47
|
+
const buildAuthorizeUrl = (options) => {
|
|
48
|
+
const state = options?.state ?? generateState();
|
|
49
|
+
const params = new URLSearchParams({
|
|
50
|
+
client_id: config.clientId,
|
|
51
|
+
redirect_uri: config.redirectUri,
|
|
52
|
+
response_type: 'code',
|
|
53
|
+
state,
|
|
54
|
+
});
|
|
55
|
+
if (options?.suggestedBudget)
|
|
56
|
+
params.set('suggested_budget', String(options.suggestedBudget));
|
|
57
|
+
if (options?.mode)
|
|
58
|
+
params.set('mode', options.mode);
|
|
59
|
+
return `${baseUrl}/oauth/authorize?${params}`;
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
/**
|
|
63
|
+
* Build the authorization URL for a full-page redirect.
|
|
64
|
+
* Supports optional suggested budget that pre-fills the consent screen.
|
|
65
|
+
*/
|
|
66
|
+
getAuthorizeUrl: (options) => buildAuthorizeUrl(options),
|
|
67
|
+
/**
|
|
68
|
+
* Open the consent screen and resolve with an OAuth authorization
|
|
69
|
+
* code when the user approves. The code must then be exchanged
|
|
70
|
+
* server-side via `tk.exchangeCode(code)` (the exchange requires
|
|
71
|
+
* `clientSecret`, which must never run in browser code).
|
|
72
|
+
*
|
|
73
|
+
* Two presentation modes:
|
|
74
|
+
*
|
|
75
|
+
* - `mode: 'iframe'` (default) — overlay an iframe modal in the
|
|
76
|
+
* current window. Cleaner UX, but the consent screen's host must
|
|
77
|
+
* allow being framed (no `X-Frame-Options: DENY`).
|
|
78
|
+
* - `mode: 'window'` — open a separate browser popup window via
|
|
79
|
+
* `window.open`. Works regardless of frame policy.
|
|
80
|
+
*
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const { code } = await tk.popup({ suggestedBudget: 5 });
|
|
83
|
+
* await fetch('/api/auth/exchange', {
|
|
84
|
+
* method: 'POST',
|
|
85
|
+
* body: JSON.stringify({ code }),
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
popup: (options) => {
|
|
90
|
+
const mode = options?.mode ?? 'iframe';
|
|
91
|
+
const width = options?.width ?? IFRAME_WIDTH;
|
|
92
|
+
const height = options?.height ?? IFRAME_HEIGHT;
|
|
93
|
+
// The SDK's `window` mode maps to the dashboard's `popup` URL
|
|
94
|
+
// param (separate browser window with `window.opener`); the
|
|
95
|
+
// SDK's `iframe` mode maps to the dashboard's `iframe` URL param
|
|
96
|
+
// (postMessage to `window.parent`).
|
|
97
|
+
const url = buildAuthorizeUrl({
|
|
98
|
+
suggestedBudget: options?.suggestedBudget,
|
|
99
|
+
mode: mode === 'window' ? 'popup' : 'iframe',
|
|
100
|
+
});
|
|
101
|
+
return mode === 'window'
|
|
102
|
+
? openWindowPopup(url, width, height, baseUrl, config.redirectUri)
|
|
103
|
+
: openIframeModal(url, width, height, baseUrl);
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* Exchange an authorization code for an access token.
|
|
107
|
+
* Call this server-side in your callback handler.
|
|
108
|
+
* Requires clientSecret to be set in config.
|
|
109
|
+
*/
|
|
110
|
+
exchangeCode: async (code) => {
|
|
111
|
+
if (!config.clientSecret)
|
|
112
|
+
throw new Error('clientSecret is required for exchangeCode (server-side only)');
|
|
113
|
+
const response = await fetch(`${baseUrl}/api/oauth/token`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'content-type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
code,
|
|
118
|
+
client_id: config.clientId,
|
|
119
|
+
client_secret: config.clientSecret,
|
|
120
|
+
redirect_uri: config.redirectUri,
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const body = await response.json().catch(() => null);
|
|
125
|
+
throw new Error(extractErrorMessage(body, `Token exchange failed (${response.status})`));
|
|
126
|
+
}
|
|
127
|
+
return response.json();
|
|
128
|
+
},
|
|
129
|
+
/**
|
|
130
|
+
* Make an authenticated, non-streaming request through the proxy.
|
|
131
|
+
* Returns a unified envelope: `ProxySuccess` on success, `ProxyError`
|
|
132
|
+
* on failure. Narrow the result with `isProxyError` / `isProxySuccess`.
|
|
133
|
+
*
|
|
134
|
+
* For streaming responses, use a vendor SDK with `baseURL: tk.proxyUrl(...)`
|
|
135
|
+
* — streams bypass the envelope and are forwarded as-is.
|
|
136
|
+
*
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const result = await tk.call({
|
|
139
|
+
* accessToken,
|
|
140
|
+
* provider: 'anthropic',
|
|
141
|
+
* path: '/v1/messages',
|
|
142
|
+
* body: { model: 'claude-3-5-sonnet-latest', max_tokens: 1024, messages: [...] },
|
|
143
|
+
* });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
call: async (options) => {
|
|
147
|
+
const path = options.path.startsWith('/') ? options.path : `/${options.path}`;
|
|
148
|
+
const response = await fetch(`${proxyBase}/${options.provider}${path}`, {
|
|
149
|
+
method: options.method ?? 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
'authorization': `Bearer ${options.accessToken}`,
|
|
152
|
+
'content-type': 'application/json',
|
|
153
|
+
},
|
|
154
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
155
|
+
});
|
|
156
|
+
return (await response.json());
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* Fetch the access context for an access token: which app it belongs
|
|
160
|
+
* to and which providers the user has authorised it to call.
|
|
161
|
+
*
|
|
162
|
+
* The returned `providers` list is exactly the set that will succeed
|
|
163
|
+
* through `tk.call()` (budget permitting). Use it to render a picker,
|
|
164
|
+
* gate UI, or detect that the user is missing a required provider.
|
|
165
|
+
*
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const { app, providers } = await tk.getAllowedProviders(accessToken);
|
|
168
|
+
* for (const p of providers) {
|
|
169
|
+
* console.log(p.displayName, p.logoUrl);
|
|
170
|
+
* }
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
getAllowedProviders: async (accessToken) => {
|
|
174
|
+
const response = await fetch(`${proxyBase}/me/providers`, {
|
|
175
|
+
headers: { 'authorization': `Bearer ${accessToken}` },
|
|
176
|
+
});
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
const body = await response.json().catch(() => null);
|
|
179
|
+
throw new Error(extractErrorMessage(body, `Failed to fetch allowed providers (${response.status})`));
|
|
180
|
+
}
|
|
181
|
+
const data = (await response.json());
|
|
182
|
+
return {
|
|
183
|
+
...data,
|
|
184
|
+
providers: data.providers.map((p) => ({
|
|
185
|
+
...p,
|
|
186
|
+
logoUrl: absoluteUrl(p.logoUrl, baseUrl),
|
|
187
|
+
})),
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
/**
|
|
191
|
+
* Get the proxy URL for a specific provider.
|
|
192
|
+
* Use as `baseURL` in a vendor SDK for streaming requests, which
|
|
193
|
+
* bypass the unified envelope.
|
|
194
|
+
*/
|
|
195
|
+
proxyUrl: (provider) => `${proxyBase}/${provider}`,
|
|
196
|
+
/** The Tokenite dashboard base URL */
|
|
197
|
+
baseUrl,
|
|
198
|
+
/** The Tokenite proxy base URL */
|
|
199
|
+
proxyBase,
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
const generateState = () => Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
203
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
204
|
+
.join('');
|
|
205
|
+
const absoluteUrl = (maybeRelative, base) => /^https?:\/\//i.test(maybeRelative) ? maybeRelative : `${base}${maybeRelative}`;
|
|
206
|
+
// ─── Popup hosts ───
|
|
207
|
+
//
|
|
208
|
+
// Two transports for the consent screen, sharing the same postMessage
|
|
209
|
+
// protocol with the dashboard:
|
|
210
|
+
// { type: 'tokenite:auth-success', code }
|
|
211
|
+
// { type: 'tokenite:auth-error', error }
|
|
212
|
+
const openIframeModal = (url, width, height, baseUrl) => {
|
|
213
|
+
const overlay = document.createElement('div');
|
|
214
|
+
overlay.style.cssText = `
|
|
215
|
+
position: fixed; inset: 0; z-index: 999999;
|
|
216
|
+
background: rgba(0,0,0,0.5); backdrop-filter: blur(2px);
|
|
217
|
+
display: flex; align-items: center; justify-content: center;
|
|
218
|
+
`;
|
|
219
|
+
const container = document.createElement('div');
|
|
220
|
+
container.style.cssText = `
|
|
221
|
+
background: white; border-radius: 12px; overflow: hidden;
|
|
222
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
223
|
+
width: ${width}px; max-width: calc(100vw - 32px);
|
|
224
|
+
height: ${height}px; max-height: calc(100vh - 32px);
|
|
225
|
+
`;
|
|
226
|
+
const iframe = document.createElement('iframe');
|
|
227
|
+
iframe.src = url;
|
|
228
|
+
iframe.style.cssText = `width: 100%; height: 100%; border: none;`;
|
|
229
|
+
iframe.setAttribute('allow', 'clipboard-write');
|
|
230
|
+
container.appendChild(iframe);
|
|
231
|
+
overlay.appendChild(container);
|
|
232
|
+
document.body.appendChild(overlay);
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const onMessage = (event) => {
|
|
235
|
+
if (event.origin !== baseUrl)
|
|
236
|
+
return;
|
|
237
|
+
const data = event.data;
|
|
238
|
+
if (data.type === 'tokenite:auth-success' && data.code) {
|
|
239
|
+
cleanup();
|
|
240
|
+
resolve({ code: data.code });
|
|
241
|
+
}
|
|
242
|
+
if (data.type === 'tokenite:auth-error') {
|
|
243
|
+
cleanup();
|
|
244
|
+
reject(new Error(data.error ?? 'Authorization denied'));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const cleanup = () => {
|
|
248
|
+
window.removeEventListener('message', onMessage);
|
|
249
|
+
overlay.remove();
|
|
250
|
+
};
|
|
251
|
+
overlay.addEventListener('click', (e) => {
|
|
252
|
+
if (e.target === overlay) {
|
|
253
|
+
cleanup();
|
|
254
|
+
reject(new Error('Authorization cancelled'));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
window.addEventListener('message', onMessage);
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
const openWindowPopup = (url, width, height, baseUrl, redirectUri) => {
|
|
261
|
+
const left = Math.round(window.screenX + (window.innerWidth - width) / 2);
|
|
262
|
+
const top = Math.round(window.screenY + (window.innerHeight - height) / 2);
|
|
263
|
+
const popup = window.open(url, 'tokenite-auth', `width=${width},height=${height},left=${left},top=${top},popup=yes`);
|
|
264
|
+
if (!popup)
|
|
265
|
+
return Promise.reject(new Error('Popup blocked'));
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
const onMessage = (event) => {
|
|
268
|
+
if (event.origin !== baseUrl)
|
|
269
|
+
return;
|
|
270
|
+
const data = event.data;
|
|
271
|
+
if (data.type === 'tokenite:auth-success' && data.code) {
|
|
272
|
+
cleanup();
|
|
273
|
+
resolve({ code: data.code });
|
|
274
|
+
}
|
|
275
|
+
if (data.type === 'tokenite:auth-error') {
|
|
276
|
+
cleanup();
|
|
277
|
+
reject(new Error(data.error ?? 'Authorization denied'));
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
// Cross-origin popups can't postMessage until they navigate back to
|
|
281
|
+
// our origin (the redirect URI). As a fallback, poll for that
|
|
282
|
+
// navigation by trying to read the popup's URL — same-origin reads
|
|
283
|
+
// succeed, cross-origin throws and we keep polling.
|
|
284
|
+
const poll = setInterval(() => {
|
|
285
|
+
if (popup.closed) {
|
|
286
|
+
cleanup();
|
|
287
|
+
reject(new Error('Popup closed'));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const popupUrl = popup.location.href;
|
|
292
|
+
if (popupUrl.startsWith(redirectUri)) {
|
|
293
|
+
const code = new URL(popupUrl).searchParams.get('code');
|
|
294
|
+
if (code) {
|
|
295
|
+
cleanup();
|
|
296
|
+
resolve({ code });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// cross-origin — keep polling
|
|
302
|
+
}
|
|
303
|
+
}, 300);
|
|
304
|
+
const cleanup = () => {
|
|
305
|
+
window.removeEventListener('message', onMessage);
|
|
306
|
+
clearInterval(poll);
|
|
307
|
+
if (!popup.closed)
|
|
308
|
+
popup.close();
|
|
309
|
+
};
|
|
310
|
+
window.addEventListener('message', onMessage);
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
//# sourceMappingURL=client.js.map
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a human-readable error message from a Tokenite-style envelope.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard returns `{ error: { code, message, status?, details? } }`
|
|
5
|
+
* for structured errors and (rarely) `{ error: "literal message" }` from
|
|
6
|
+
* legacy paths. This helper unifies both shapes and falls back to a
|
|
7
|
+
* caller-supplied default when the body is missing or malformed.
|
|
8
|
+
*/
|
|
9
|
+
export declare const extractErrorMessage: (body: unknown, fallback: string) => string;
|
|
10
|
+
//# sourceMappingURL=error.d.ts.map
|
package/dist/error.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a human-readable error message from a Tokenite-style envelope.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard returns `{ error: { code, message, status?, details? } }`
|
|
5
|
+
* for structured errors and (rarely) `{ error: "literal message" }` from
|
|
6
|
+
* legacy paths. This helper unifies both shapes and falls back to a
|
|
7
|
+
* caller-supplied default when the body is missing or malformed.
|
|
8
|
+
*/
|
|
9
|
+
export const extractErrorMessage = (body, fallback) => {
|
|
10
|
+
const err = body?.error;
|
|
11
|
+
if (typeof err === 'string')
|
|
12
|
+
return err;
|
|
13
|
+
if (err && typeof err === 'object' && 'message' in err && typeof err.message === 'string') {
|
|
14
|
+
return err.message;
|
|
15
|
+
}
|
|
16
|
+
return fallback;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=error.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Tokenite } from './client.js';
|
|
2
|
+
export type { TokeniteConfig, AuthorizeOptions, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyUsage, ProxySuccess, ProxyError, ProxyResponse, ErrorSource, ProviderInfo, AppInfo, AllowedProvidersResponse, } from './types.js';
|
|
3
|
+
export { isProxyError, isProxySuccess } from './types.js';
|
|
4
|
+
export { extractErrorMessage } from './error.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export type TokeniteConfig = {
|
|
2
|
+
/** Your app's client ID (from the Tokenite dashboard) */
|
|
3
|
+
readonly clientId: string;
|
|
4
|
+
/** Your app's client secret — only needed for server-side code exchange */
|
|
5
|
+
readonly clientSecret?: string;
|
|
6
|
+
/** The URL Tokenite redirects back to after authorization */
|
|
7
|
+
readonly redirectUri: string;
|
|
8
|
+
/** Tokenite base URL. Default: https://tokenite.ai */
|
|
9
|
+
readonly baseUrl?: string;
|
|
10
|
+
/** Tokenite proxy URL. Default: https://api.tokenite.ai */
|
|
11
|
+
readonly proxyUrl?: string;
|
|
12
|
+
};
|
|
13
|
+
export type AuthorizeOptions = {
|
|
14
|
+
/** Custom state parameter for CSRF protection. Auto-generated if not provided. */
|
|
15
|
+
readonly state?: string;
|
|
16
|
+
/** Suggested budget amount (user can override on consent screen) */
|
|
17
|
+
readonly suggestedBudget?: number;
|
|
18
|
+
};
|
|
19
|
+
export type PopupOptions = {
|
|
20
|
+
/** Suggested budget amount (user can override on consent screen) */
|
|
21
|
+
readonly suggestedBudget?: number;
|
|
22
|
+
/**
|
|
23
|
+
* How to host the consent screen.
|
|
24
|
+
*
|
|
25
|
+
* - `'iframe'` (default) — overlay an iframe modal in the current
|
|
26
|
+
* window. Requires the dashboard to allow being framed by your
|
|
27
|
+
* origin (`Content-Security-Policy: frame-ancestors`). Cleaner UX
|
|
28
|
+
* but blocked by `X-Frame-Options: DENY`.
|
|
29
|
+
* - `'window'` — open a separate browser popup window via
|
|
30
|
+
* `window.open`. Works regardless of frame policy, but the user
|
|
31
|
+
* may be prompted by their popup blocker.
|
|
32
|
+
*/
|
|
33
|
+
readonly mode?: 'iframe' | 'window';
|
|
34
|
+
/** Modal/popup width in pixels. Default: 480 */
|
|
35
|
+
readonly width?: number;
|
|
36
|
+
/** Modal/popup height in pixels. Default: 620 */
|
|
37
|
+
readonly height?: number;
|
|
38
|
+
};
|
|
39
|
+
export type PopupResult = {
|
|
40
|
+
/**
|
|
41
|
+
* OAuth authorization code returned by the consent screen.
|
|
42
|
+
* Send this to your backend, which exchanges it for an access token
|
|
43
|
+
* via `tk.exchangeCode(code)`. The exchange requires `clientSecret`
|
|
44
|
+
* and must never run in browser code.
|
|
45
|
+
*/
|
|
46
|
+
readonly code: string;
|
|
47
|
+
};
|
|
48
|
+
export type TokenResponse = {
|
|
49
|
+
readonly access_token: string;
|
|
50
|
+
readonly token_type: string;
|
|
51
|
+
};
|
|
52
|
+
export type Provider = 'anthropic' | 'openai' | 'google' | 'grok' | 'bedrock';
|
|
53
|
+
export type ProxyCallOptions = {
|
|
54
|
+
/** The user's Tokenite access token (returned by `tk.exchangeCode()`) */
|
|
55
|
+
readonly accessToken: string;
|
|
56
|
+
/** Which LLM provider to call */
|
|
57
|
+
readonly provider: Provider;
|
|
58
|
+
/** Path on the provider's API (e.g. `/v1/messages`, `/v1/chat/completions`) */
|
|
59
|
+
readonly path: string;
|
|
60
|
+
/** HTTP method. Default: `POST` */
|
|
61
|
+
readonly method?: string;
|
|
62
|
+
/** Request body — the vendor's request shape, JSON-serialised by the SDK */
|
|
63
|
+
readonly body: unknown;
|
|
64
|
+
};
|
|
65
|
+
/** Normalised token counts (identical across all providers) */
|
|
66
|
+
export type ProxyUsage = {
|
|
67
|
+
/** Number of tokens in the prompt / input */
|
|
68
|
+
readonly inputTokens: number;
|
|
69
|
+
/** Number of tokens in the completion / output */
|
|
70
|
+
readonly outputTokens: number;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Successful proxy response.
|
|
74
|
+
*
|
|
75
|
+
* `data` contains the original vendor response body (e.g. Anthropic's
|
|
76
|
+
* message object, OpenAI's chat completion, etc.). `provider`, `model`,
|
|
77
|
+
* and `usage` are extracted and normalised by the proxy so you don't
|
|
78
|
+
* need to parse vendor-specific fields.
|
|
79
|
+
*/
|
|
80
|
+
export type ProxySuccess = {
|
|
81
|
+
/** Which LLM provider handled the request */
|
|
82
|
+
readonly provider: Provider;
|
|
83
|
+
/** The model that generated the response */
|
|
84
|
+
readonly model: string;
|
|
85
|
+
/** Normalised token usage, or null if the provider didn't report it */
|
|
86
|
+
readonly usage: ProxyUsage | null;
|
|
87
|
+
/** The original, unmodified response body from the LLM provider */
|
|
88
|
+
readonly data: unknown;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Where the error originated.
|
|
92
|
+
*
|
|
93
|
+
* - `"proxy"` — Tokenite rejected the request (auth, budget, config).
|
|
94
|
+
* - `"provider"` — The upstream LLM returned an error (rate limit, overload, etc.).
|
|
95
|
+
*/
|
|
96
|
+
export type ErrorSource = 'proxy' | 'provider';
|
|
97
|
+
/**
|
|
98
|
+
* Error response (both proxy-level and provider-level errors share this shape).
|
|
99
|
+
*
|
|
100
|
+
* **Proxy error codes** (`source: "proxy"`):
|
|
101
|
+
* | Code | HTTP | Description |
|
|
102
|
+
* |---|---|---|
|
|
103
|
+
* | `TOKEN_INVALID` | 401 | Missing or invalid bearer token |
|
|
104
|
+
* | `TOKEN_REVOKED` | 401 | Access token was revoked by the user |
|
|
105
|
+
* | `TOKEN_SUSPENDED` | 403 | Access suspended by the user |
|
|
106
|
+
* | `TOKEN_EXPIRED` | 401 | Access token past its expiration date |
|
|
107
|
+
* | `BUDGET_EXCEEDED` | 402 | Spending reached the user-defined budget limit |
|
|
108
|
+
* | `PROVIDER_KEY_MISSING` | 402 | No API key or credits available for the provider |
|
|
109
|
+
* | `CREDITS_DEPLETED` | 402 | Insufficient platform credits |
|
|
110
|
+
* | `APP_NOT_FOUND` | 404 | Application not found |
|
|
111
|
+
* | `MODEL_NOT_ALLOWED` | 403 | Model not in the app's allowed models list |
|
|
112
|
+
*
|
|
113
|
+
* **Provider error codes** (`source: "provider"`):
|
|
114
|
+
* | Code | HTTP | Description |
|
|
115
|
+
* |---|---|---|
|
|
116
|
+
* | `RATE_LIMITED` | 429 | Upstream rate limit hit — retry after backoff |
|
|
117
|
+
* | `CONTEXT_LENGTH_EXCEEDED` | 400 | Request exceeds model's context window |
|
|
118
|
+
* | `INVALID_REQUEST` | 400 | Provider rejected the request as malformed |
|
|
119
|
+
* | `AUTHENTICATION_FAILED` | 401 | Provider rejected the API key |
|
|
120
|
+
* | `PROVIDER_OVERLOADED` | 503 | Provider temporarily overloaded |
|
|
121
|
+
* | `CONTENT_FILTERED` | 400 | Content blocked by safety filter |
|
|
122
|
+
* | `PROVIDER_TIMEOUT` | 504 | Provider did not respond in time |
|
|
123
|
+
* | `PROVIDER_ERROR` | 502 | Catch-all for other provider errors |
|
|
124
|
+
*/
|
|
125
|
+
export type ProxyError = {
|
|
126
|
+
readonly error: {
|
|
127
|
+
/** Machine-readable error code */
|
|
128
|
+
readonly code: string;
|
|
129
|
+
/** Human-readable description */
|
|
130
|
+
readonly message: string;
|
|
131
|
+
/** Where the error originated */
|
|
132
|
+
readonly source: ErrorSource;
|
|
133
|
+
/** Optional structured context (e.g. `retryAfter`, `provider`, `providerMessage`) */
|
|
134
|
+
readonly details?: Record<string, unknown>;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
/** Discriminated union for all non-streaming proxy responses */
|
|
138
|
+
export type ProxyResponse = ProxySuccess | ProxyError;
|
|
139
|
+
/** Type guard: returns true if the response is an error */
|
|
140
|
+
export declare const isProxyError: (response: ProxyResponse) => response is ProxyError;
|
|
141
|
+
/** Type guard: returns true if the response is a success */
|
|
142
|
+
export declare const isProxySuccess: (response: ProxyResponse) => response is ProxySuccess;
|
|
143
|
+
/** Visual + identity metadata for a single provider */
|
|
144
|
+
export type ProviderInfo = {
|
|
145
|
+
/** Stable provider id (same value as the `Provider` union) */
|
|
146
|
+
readonly id: Provider;
|
|
147
|
+
/** Human-readable name, e.g. "Anthropic" */
|
|
148
|
+
readonly displayName: string;
|
|
149
|
+
/** Brand colour (hex string, e.g. "#d97706") */
|
|
150
|
+
readonly color: string;
|
|
151
|
+
/** Absolute URL to the provider's logo (PNG or SVG) */
|
|
152
|
+
readonly logoUrl: string;
|
|
153
|
+
/** Whether the logo is a glyph/symbol or a full wordmark */
|
|
154
|
+
readonly logoStyle: 'symbol' | 'wordmark';
|
|
155
|
+
};
|
|
156
|
+
/** Summary of the app the access token belongs to */
|
|
157
|
+
export type AppInfo = {
|
|
158
|
+
readonly id: string;
|
|
159
|
+
readonly name: string;
|
|
160
|
+
readonly description: string | null;
|
|
161
|
+
readonly websiteUrl: string | null;
|
|
162
|
+
/** Absolute URL to the app's icon (PNG/SVG). null when the developer hasn't set one — render initials or a generic glyph. */
|
|
163
|
+
readonly iconUrl: string | null;
|
|
164
|
+
/** Providers the app declares it needs */
|
|
165
|
+
readonly requiredProviders: readonly Provider[];
|
|
166
|
+
/** Fallback order when `allowSubstitution` is true */
|
|
167
|
+
readonly preferredProviders: readonly Provider[];
|
|
168
|
+
/** Whether the app accepts substitute providers when a required one isn't available */
|
|
169
|
+
readonly allowSubstitution: boolean;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Full access context for a single access token.
|
|
173
|
+
*
|
|
174
|
+
* `providers` lists only the providers the user has an active key for —
|
|
175
|
+
* exactly the set that will succeed through `tk.call()` (budget permitting).
|
|
176
|
+
*/
|
|
177
|
+
export type AllowedProvidersResponse = {
|
|
178
|
+
readonly app: AppInfo;
|
|
179
|
+
readonly providers: readonly ProviderInfo[];
|
|
180
|
+
};
|
|
181
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Type guard: returns true if the response is an error */
|
|
2
|
+
export const isProxyError = (response) => 'error' in response;
|
|
3
|
+
/** Type guard: returns true if the response is a success */
|
|
4
|
+
export const isProxySuccess = (response) => 'provider' in response;
|
|
5
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokenite/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SDK for integrating \"Login with Tokenite\" into your app. Your users bring their own AI tokens — you pay nothing.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./admin": {
|
|
12
|
+
"types": "./dist/admin/index.d.ts",
|
|
13
|
+
"import": "./dist/admin/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"tokenite",
|
|
23
|
+
"llm",
|
|
24
|
+
"ai",
|
|
25
|
+
"proxy",
|
|
26
|
+
"oauth",
|
|
27
|
+
"byok",
|
|
28
|
+
"anthropic",
|
|
29
|
+
"openai",
|
|
30
|
+
"google"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"tsx": "^4.0.0",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc && npm run generate-readme",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"generate-readme": "npx tsx scripts/generate-readme.ts"
|
|
44
|
+
}
|
|
45
|
+
}
|