@tokenite/sdk 2.0.1 → 2.3.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 +128 -1
- package/dist/admin/types.d.ts +1 -1
- package/dist/client.d.ts +46 -2
- package/dist/client.js +142 -15
- package/dist/index.d.ts +7 -1
- package/dist/index.js +3 -0
- package/dist/parse-callback.d.ts +88 -0
- package/dist/parse-callback.js +87 -0
- package/dist/protocol.d.ts +29 -0
- package/dist/protocol.js +23 -0
- package/dist/recovery.d.ts +13 -0
- package/dist/recovery.js +47 -0
- package/dist/types.d.ts +77 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,57 @@ Tokenite tracks each session for attribution and auto-terminates running session
|
|
|
142
142
|
|
|
143
143
|
Budget is checked pre-call but not enforced mid-session: a session that started under budget can run past it. Revoke is the user's hard kill switch.
|
|
144
144
|
|
|
145
|
+
### Handling the OAuth callback
|
|
146
|
+
|
|
147
|
+
When Tokenite redirects back to your `redirect_uri`, the URL has one of:
|
|
148
|
+
|
|
149
|
+
- `?code=...&state=...` — user approved; exchange the code for an access token.
|
|
150
|
+
- `?error=access_denied&error_description=user_denied&state=...` — user clicked **Cancel** on the consent screen.
|
|
151
|
+
- `?error=...&error_description=...&state=...` — any other OAuth failure.
|
|
152
|
+
- nothing (or just `state`) — user landed on your callback path without completing a real OAuth round-trip.
|
|
153
|
+
|
|
154
|
+
`parseCallback(input, options?)` turns the URL into a typed result so you can switch on it instead of hand-parsing strings. Pure function; works in any runtime (Node, Bun, edge, browser).
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { parseCallback } from '@tokenite/sdk';
|
|
158
|
+
|
|
159
|
+
// In your /api/auth/callback handler:
|
|
160
|
+
const result = parseCallback(req.url, { expectedState: sessionStoredState });
|
|
161
|
+
|
|
162
|
+
if (result.ok) {
|
|
163
|
+
const { access_token } = await tk.exchangeCode(result.code);
|
|
164
|
+
// ... establish your app's session, redirect to /
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
switch (result.reason) {
|
|
169
|
+
case 'user_denied':
|
|
170
|
+
// User clicked Cancel. Don't show a scary error — render a
|
|
171
|
+
// friendly "you cancelled" screen with a "Try again" button.
|
|
172
|
+
return renderCancelled();
|
|
173
|
+
case 'invalid_state':
|
|
174
|
+
// CSRF check failed (state mismatch). Force a fresh login flow.
|
|
175
|
+
return renderRetry('Your sign-in session expired. Please try again.');
|
|
176
|
+
case 'missing_code':
|
|
177
|
+
// User navigated to /callback directly (bookmark, browser back).
|
|
178
|
+
return redirectToHome();
|
|
179
|
+
case 'access_denied':
|
|
180
|
+
case 'oauth_error':
|
|
181
|
+
// Real OAuth failure. result.error + result.description have details.
|
|
182
|
+
return renderError(result);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`parseCallback` accepts:
|
|
187
|
+
|
|
188
|
+
- A full URL string: `parseCallback('https://yourapp.com/cb?code=…&state=…')`
|
|
189
|
+
- A path + query: `parseCallback(req.url)` (Node's `req.url` works directly)
|
|
190
|
+
- A query string: `parseCallback('?code=…&state=…')` or `parseCallback('code=…&state=…')`
|
|
191
|
+
- A `URL` object: `parseCallback(new URL(...))`
|
|
192
|
+
- A `URLSearchParams`: `parseCallback(params)`
|
|
193
|
+
|
|
194
|
+
`expectedState` is optional. When you provide it, a mismatch returns `{ ok: false, reason: 'invalid_state' }` even if a `code` is present — treating a stolen code paired with a forged state as a CSRF attempt.
|
|
195
|
+
|
|
145
196
|
### Streaming responses
|
|
146
197
|
|
|
147
198
|
`tk.call()` is for non-streaming requests. Streaming responses bypass the unified envelope and are forwarded as-is, so use any vendor SDK with `baseURL: tk.proxyUrl(...)`:
|
|
@@ -172,7 +223,7 @@ for await (const event of stream) {
|
|
|
172
223
|
<!-- GEN:API -->
|
|
173
224
|
### `.getAuthorizeUrl(options?: AuthorizeOptions) => string`
|
|
174
225
|
|
|
175
|
-
Build the authorization URL for a full-page redirect.
|
|
226
|
+
Build the authorization URL for a full-page redirect.
|
|
176
227
|
|
|
177
228
|
### `.popup(options?: PopupOptions) => Promise<PopupResult>`
|
|
178
229
|
|
|
@@ -216,11 +267,43 @@ export type TokeniteConfig = {
|
|
|
216
267
|
readonly proxyUrl?: string;
|
|
217
268
|
};
|
|
218
269
|
|
|
270
|
+
/**
|
|
271
|
+
* OAuth 2.0 / OIDC `prompt` parameter. Tells Tokenite whether to
|
|
272
|
+
* re-prompt the user even when they have an existing session and/or
|
|
273
|
+
* an existing grant for this app.
|
|
274
|
+
*
|
|
275
|
+
* - `'consent'` — re-show the consent screen even if the user has
|
|
276
|
+
* already authorized this app. Use this on "Sign in with Tokenite"
|
|
277
|
+
* buttons that follow a sign-out, so users don't silently
|
|
278
|
+
* re-authorize without a chance to pick a different account or
|
|
279
|
+
* cancel.
|
|
280
|
+
* - `'login'` — request that the user re-authenticate. Today this
|
|
281
|
+
* behaves the same as `'consent'` on Tokenite (full session-cookie
|
|
282
|
+
* clear is a TODO); the consent screen still shows.
|
|
283
|
+
* - `'select_account'` — show the account picker. Tokenite already
|
|
284
|
+
* does this automatically when the user has multiple accounts;
|
|
285
|
+
* passing it explicitly is a hint for future apps that always want
|
|
286
|
+
* the picker.
|
|
287
|
+
* - `'none'` — never show UI; fail if interaction is required.
|
|
288
|
+
* Currently treated as the default (silent reauth).
|
|
289
|
+
*
|
|
290
|
+
* The OAuth 2.0 spec allows a space-separated combination
|
|
291
|
+
* (e.g. `'login consent'`); for that, pass the raw string. The
|
|
292
|
+
* union above is just for autocomplete on the common values.
|
|
293
|
+
*/
|
|
294
|
+
export type OAuthPrompt = 'login' | 'consent' | 'select_account' | 'none' | (string & {});
|
|
295
|
+
|
|
219
296
|
export type AuthorizeOptions = {
|
|
220
297
|
/** Custom state parameter for CSRF protection. Auto-generated if not provided. */
|
|
221
298
|
readonly state?: string;
|
|
222
299
|
/** Suggested budget amount (user can override on consent screen) */
|
|
223
300
|
readonly suggestedBudget?: number;
|
|
301
|
+
/**
|
|
302
|
+
* OAuth `prompt` parameter. Most common use: pass `'consent'` to
|
|
303
|
+
* force a fresh consent screen on sign-in (prevents silent reauth
|
|
304
|
+
* after the user signed out of the app). See {@link OAuthPrompt}.
|
|
305
|
+
*/
|
|
306
|
+
readonly prompt?: OAuthPrompt;
|
|
224
307
|
};
|
|
225
308
|
|
|
226
309
|
export type PopupOptions = {
|
|
@@ -242,6 +325,12 @@ export type PopupOptions = {
|
|
|
242
325
|
readonly width?: number;
|
|
243
326
|
/** Modal/popup height in pixels. Default: 620 */
|
|
244
327
|
readonly height?: number;
|
|
328
|
+
/**
|
|
329
|
+
* OAuth `prompt` parameter. Most common use: pass `'consent'` to
|
|
330
|
+
* force a fresh consent screen on sign-in (prevents silent reauth
|
|
331
|
+
* after the user signed out of the app). See {@link OAuthPrompt}.
|
|
332
|
+
*/
|
|
333
|
+
readonly prompt?: OAuthPrompt;
|
|
245
334
|
};
|
|
246
335
|
|
|
247
336
|
export type PopupResult = {
|
|
@@ -254,6 +343,44 @@ export type PopupResult = {
|
|
|
254
343
|
readonly code: string;
|
|
255
344
|
};
|
|
256
345
|
|
|
346
|
+
export type TopUpOptions = {
|
|
347
|
+
/**
|
|
348
|
+
* How to host the top-up screen.
|
|
349
|
+
*
|
|
350
|
+
* - `'popup'` (default) — open in a separate browser window via
|
|
351
|
+
* `window.open`. The user completes the form; the SDK resolves
|
|
352
|
+
* when Tokenite posts back the new limit.
|
|
353
|
+
* - `'redirect'` — full-page navigation. The builder's app loses
|
|
354
|
+
* in-memory state and must reload from the callback URL.
|
|
355
|
+
*/
|
|
356
|
+
readonly mode?: 'popup' | 'redirect';
|
|
357
|
+
/** Pre-fill the new-limit field. Default: 2 × current limit. */
|
|
358
|
+
readonly suggestedAmount?: number;
|
|
359
|
+
/** Popup width in pixels. Default: 480 */
|
|
360
|
+
readonly width?: number;
|
|
361
|
+
/** Popup height in pixels. Default: 560 */
|
|
362
|
+
readonly height?: number;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export type TopUpResult =
|
|
366
|
+
| { readonly ok: true; readonly newLimit: number; readonly remaining: number }
|
|
367
|
+
| { readonly ok: false; readonly reason: 'cancelled' | 'popup-blocked' | 'closed' | 'redirected' };
|
|
368
|
+
|
|
369
|
+
export type CallWithRecoveryOptions = {
|
|
370
|
+
/**
|
|
371
|
+
* What to do when the proxy returns a recoverable funding error.
|
|
372
|
+
*
|
|
373
|
+
* - `'popup'` (default) — open Tokenite's top-up popup, then retry the
|
|
374
|
+
* call once on success.
|
|
375
|
+
* - `'redirect'` — full-page navigation; the call does not retry (the
|
|
376
|
+
* builder's app re-loads from the callback and re-invokes manually).
|
|
377
|
+
* - `'throw'` — disable recovery; surface the original error.
|
|
378
|
+
*/
|
|
379
|
+
readonly onFundingNeeded?: 'popup' | 'redirect' | 'throw';
|
|
380
|
+
/** Override the suggested top-up amount. */
|
|
381
|
+
readonly suggestedAmount?: number;
|
|
382
|
+
};
|
|
383
|
+
|
|
257
384
|
export type TokenResponse = {
|
|
258
385
|
readonly access_token: string;
|
|
259
386
|
readonly token_type: string;
|
package/dist/admin/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Provider } from '../types.js';
|
|
2
|
-
export type ModelStrategy = 'any' | 'tier' | 'models';
|
|
2
|
+
export type ModelStrategy = 'any' | 'tier' | 'models' | 'none';
|
|
3
3
|
export type RequiredTier = 'cheap' | 'fast' | 'smart' | 'reasoning';
|
|
4
4
|
export type AppRecord = {
|
|
5
5
|
readonly id: string;
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TokeniteConfig, AuthorizeOptions, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyResponse, AccessContext } from './types.js';
|
|
1
|
+
import type { TokeniteConfig, AuthorizeOptions, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyResponse, AccessContext, TopUpOptions, TopUpResult, CallWithRecoveryOptions } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Tokenite client.
|
|
4
4
|
*
|
|
@@ -40,7 +40,13 @@ import type { TokeniteConfig, AuthorizeOptions, PopupOptions, PopupResult, Token
|
|
|
40
40
|
export declare const Tokenite: (config: TokeniteConfig) => {
|
|
41
41
|
/**
|
|
42
42
|
* Build the authorization URL for a full-page redirect.
|
|
43
|
-
*
|
|
43
|
+
*
|
|
44
|
+
* Pass `prompt: 'consent'` on "Sign in with Tokenite" buttons that
|
|
45
|
+
* follow a sign-out, so the user sees the consent screen again
|
|
46
|
+
* instead of being silently re-authorized:
|
|
47
|
+
* ```typescript
|
|
48
|
+
* res.redirect(tk.getAuthorizeUrl({ prompt: 'consent' }));
|
|
49
|
+
* ```
|
|
44
50
|
*/
|
|
45
51
|
getAuthorizeUrl: (options?: AuthorizeOptions) => string;
|
|
46
52
|
/**
|
|
@@ -64,6 +70,13 @@ export declare const Tokenite: (config: TokeniteConfig) => {
|
|
|
64
70
|
* body: JSON.stringify({ code }),
|
|
65
71
|
* });
|
|
66
72
|
* ```
|
|
73
|
+
*
|
|
74
|
+
* Pass `prompt: 'consent'` to force a fresh consent screen — use on
|
|
75
|
+
* the "Sign in with Tokenite" button shown after the user signed out
|
|
76
|
+
* of your app, so they don't silently re-authorize:
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const { code } = await tk.popup({ prompt: 'consent' });
|
|
79
|
+
* ```
|
|
67
80
|
*/
|
|
68
81
|
popup: (options?: PopupOptions) => Promise<PopupResult>;
|
|
69
82
|
/**
|
|
@@ -117,6 +130,37 @@ export declare const Tokenite: (config: TokeniteConfig) => {
|
|
|
117
130
|
* bypass the unified envelope.
|
|
118
131
|
*/
|
|
119
132
|
proxyUrl: (provider: Provider) => string;
|
|
133
|
+
/**
|
|
134
|
+
* Open a Tokenite-hosted "raise budget" popup for this app.
|
|
135
|
+
*
|
|
136
|
+
* When `tk.call()` returns `BUDGET_EXCEEDED`, call this to let the
|
|
137
|
+
* user authorise a higher spending cap without leaving your app.
|
|
138
|
+
* The popup resolves once the new limit is committed. The access
|
|
139
|
+
* token does not change — only its budget ceiling.
|
|
140
|
+
*
|
|
141
|
+
* The user must be signed in to Tokenite in the same browser (the
|
|
142
|
+
* popup uses their session cookie). If not, the popup routes them
|
|
143
|
+
* through sign-in and returns afterwards.
|
|
144
|
+
*
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const r = await tk.call({ accessToken, provider: 'anthropic', ... });
|
|
147
|
+
* if (isProxyError(r) && r.error.code === 'BUDGET_EXCEEDED') {
|
|
148
|
+
* const top = await tk.topUp();
|
|
149
|
+
* if (top.ok) return tk.call({ accessToken, ... });
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
topUp: (options?: TopUpOptions) => Promise<TopUpResult>;
|
|
154
|
+
/**
|
|
155
|
+
* Wrap `tk.call()` with automatic recovery when the proxy returns a
|
|
156
|
+
* recoverable funding error (`BUDGET_EXCEEDED` by default). On a
|
|
157
|
+
* fundable error, opens the top-up popup and retries the call once
|
|
158
|
+
* if the user committed a new limit.
|
|
159
|
+
*
|
|
160
|
+
* The retry is bounded to one attempt — repeated funding failures
|
|
161
|
+
* surface back to the caller so misconfigured caps don't loop.
|
|
162
|
+
*/
|
|
163
|
+
callWithRecovery: (options: ProxyCallOptions, recovery?: CallWithRecoveryOptions) => Promise<ProxyResponse>;
|
|
120
164
|
/** The Tokenite dashboard base URL */
|
|
121
165
|
baseUrl: string;
|
|
122
166
|
/** The Tokenite proxy base URL */
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { AuthMessageType, TopupMessageType } from './protocol.js';
|
|
1
2
|
import { extractErrorMessage } from './error.js';
|
|
3
|
+
import { isProxyError } from './types.js';
|
|
4
|
+
import { isFundingError } from './recovery.js';
|
|
2
5
|
// `tokenite.ai` is the marketing site. The dashboard (which hosts the
|
|
3
6
|
// OAuth consent screen and the API the SDK calls — /oauth/authorize,
|
|
4
7
|
// /api/oauth/token) lives at `app.tokenite.ai`. Older versions of this
|
|
@@ -11,6 +14,8 @@ const DEFAULT_BASE_URL = 'https://app.tokenite.ai';
|
|
|
11
14
|
const DEFAULT_PROXY_URL = 'https://proxy.tokenite.ai';
|
|
12
15
|
const IFRAME_WIDTH = 480;
|
|
13
16
|
const IFRAME_HEIGHT = 620;
|
|
17
|
+
const TOPUP_WIDTH = 480;
|
|
18
|
+
const TOPUP_HEIGHT = 560;
|
|
14
19
|
/**
|
|
15
20
|
* Tokenite client.
|
|
16
21
|
*
|
|
@@ -52,6 +57,29 @@ const IFRAME_HEIGHT = 620;
|
|
|
52
57
|
export const Tokenite = (config) => {
|
|
53
58
|
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
54
59
|
const proxyBase = config.proxyUrl ?? DEFAULT_PROXY_URL;
|
|
60
|
+
const proxyCall = async (options) => {
|
|
61
|
+
const path = options.path.startsWith('/') ? options.path : `/${options.path}`;
|
|
62
|
+
const response = await fetch(`${proxyBase}/${options.provider}${path}`, {
|
|
63
|
+
method: options.method ?? 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'authorization': `Bearer ${options.accessToken}`,
|
|
66
|
+
'content-type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
69
|
+
});
|
|
70
|
+
return (await response.json());
|
|
71
|
+
};
|
|
72
|
+
const runTopUp = (options) => {
|
|
73
|
+
const mode = options?.mode ?? 'popup';
|
|
74
|
+
const width = options?.width ?? TOPUP_WIDTH;
|
|
75
|
+
const height = options?.height ?? TOPUP_HEIGHT;
|
|
76
|
+
const url = buildTopupUrl(baseUrl, config.clientId, mode, options?.suggestedAmount);
|
|
77
|
+
if (mode === 'redirect') {
|
|
78
|
+
window.location.href = url;
|
|
79
|
+
return Promise.resolve({ ok: false, reason: 'redirected' });
|
|
80
|
+
}
|
|
81
|
+
return openTopupPopup(url, width, height, baseUrl);
|
|
82
|
+
};
|
|
55
83
|
const buildAuthorizeUrl = (options) => {
|
|
56
84
|
const state = options?.state ?? generateState();
|
|
57
85
|
const params = new URLSearchParams({
|
|
@@ -64,12 +92,20 @@ export const Tokenite = (config) => {
|
|
|
64
92
|
params.set('suggested_budget', String(options.suggestedBudget));
|
|
65
93
|
if (options?.mode)
|
|
66
94
|
params.set('mode', options.mode);
|
|
95
|
+
if (options?.prompt)
|
|
96
|
+
params.set('prompt', options.prompt);
|
|
67
97
|
return `${baseUrl}/oauth/authorize?${params}`;
|
|
68
98
|
};
|
|
69
99
|
return {
|
|
70
100
|
/**
|
|
71
101
|
* Build the authorization URL for a full-page redirect.
|
|
72
|
-
*
|
|
102
|
+
*
|
|
103
|
+
* Pass `prompt: 'consent'` on "Sign in with Tokenite" buttons that
|
|
104
|
+
* follow a sign-out, so the user sees the consent screen again
|
|
105
|
+
* instead of being silently re-authorized:
|
|
106
|
+
* ```typescript
|
|
107
|
+
* res.redirect(tk.getAuthorizeUrl({ prompt: 'consent' }));
|
|
108
|
+
* ```
|
|
73
109
|
*/
|
|
74
110
|
getAuthorizeUrl: (options) => buildAuthorizeUrl(options),
|
|
75
111
|
/**
|
|
@@ -93,6 +129,13 @@ export const Tokenite = (config) => {
|
|
|
93
129
|
* body: JSON.stringify({ code }),
|
|
94
130
|
* });
|
|
95
131
|
* ```
|
|
132
|
+
*
|
|
133
|
+
* Pass `prompt: 'consent'` to force a fresh consent screen — use on
|
|
134
|
+
* the "Sign in with Tokenite" button shown after the user signed out
|
|
135
|
+
* of your app, so they don't silently re-authorize:
|
|
136
|
+
* ```typescript
|
|
137
|
+
* const { code } = await tk.popup({ prompt: 'consent' });
|
|
138
|
+
* ```
|
|
96
139
|
*/
|
|
97
140
|
popup: (options) => {
|
|
98
141
|
const mode = options?.mode ?? 'iframe';
|
|
@@ -105,6 +148,7 @@ export const Tokenite = (config) => {
|
|
|
105
148
|
const url = buildAuthorizeUrl({
|
|
106
149
|
suggestedBudget: options?.suggestedBudget,
|
|
107
150
|
mode: mode === 'window' ? 'popup' : 'iframe',
|
|
151
|
+
prompt: options?.prompt,
|
|
108
152
|
});
|
|
109
153
|
return mode === 'window'
|
|
110
154
|
? openWindowPopup(url, width, height, baseUrl, config.redirectUri)
|
|
@@ -151,18 +195,7 @@ export const Tokenite = (config) => {
|
|
|
151
195
|
* });
|
|
152
196
|
* ```
|
|
153
197
|
*/
|
|
154
|
-
call:
|
|
155
|
-
const path = options.path.startsWith('/') ? options.path : `/${options.path}`;
|
|
156
|
-
const response = await fetch(`${proxyBase}/${options.provider}${path}`, {
|
|
157
|
-
method: options.method ?? 'POST',
|
|
158
|
-
headers: {
|
|
159
|
-
'authorization': `Bearer ${options.accessToken}`,
|
|
160
|
-
'content-type': 'application/json',
|
|
161
|
-
},
|
|
162
|
-
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
163
|
-
});
|
|
164
|
-
return (await response.json());
|
|
165
|
-
},
|
|
198
|
+
call: proxyCall,
|
|
166
199
|
/**
|
|
167
200
|
* Fetch the full access context for an access token: which app it
|
|
168
201
|
* belongs to, who holds the token, and which providers it can call.
|
|
@@ -206,6 +239,56 @@ export const Tokenite = (config) => {
|
|
|
206
239
|
* bypass the unified envelope.
|
|
207
240
|
*/
|
|
208
241
|
proxyUrl: (provider) => `${proxyBase}/${provider}`,
|
|
242
|
+
/**
|
|
243
|
+
* Open a Tokenite-hosted "raise budget" popup for this app.
|
|
244
|
+
*
|
|
245
|
+
* When `tk.call()` returns `BUDGET_EXCEEDED`, call this to let the
|
|
246
|
+
* user authorise a higher spending cap without leaving your app.
|
|
247
|
+
* The popup resolves once the new limit is committed. The access
|
|
248
|
+
* token does not change — only its budget ceiling.
|
|
249
|
+
*
|
|
250
|
+
* The user must be signed in to Tokenite in the same browser (the
|
|
251
|
+
* popup uses their session cookie). If not, the popup routes them
|
|
252
|
+
* through sign-in and returns afterwards.
|
|
253
|
+
*
|
|
254
|
+
* ```typescript
|
|
255
|
+
* const r = await tk.call({ accessToken, provider: 'anthropic', ... });
|
|
256
|
+
* if (isProxyError(r) && r.error.code === 'BUDGET_EXCEEDED') {
|
|
257
|
+
* const top = await tk.topUp();
|
|
258
|
+
* if (top.ok) return tk.call({ accessToken, ... });
|
|
259
|
+
* }
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
topUp: runTopUp,
|
|
263
|
+
/**
|
|
264
|
+
* Wrap `tk.call()` with automatic recovery when the proxy returns a
|
|
265
|
+
* recoverable funding error (`BUDGET_EXCEEDED` by default). On a
|
|
266
|
+
* fundable error, opens the top-up popup and retries the call once
|
|
267
|
+
* if the user committed a new limit.
|
|
268
|
+
*
|
|
269
|
+
* The retry is bounded to one attempt — repeated funding failures
|
|
270
|
+
* surface back to the caller so misconfigured caps don't loop.
|
|
271
|
+
*/
|
|
272
|
+
callWithRecovery: async (options, recovery = {}) => {
|
|
273
|
+
const r = await proxyCall(options);
|
|
274
|
+
if (!isProxyError(r))
|
|
275
|
+
return r;
|
|
276
|
+
const klass = isFundingError(r.error);
|
|
277
|
+
if (!klass || klass.kind !== 'user-topup')
|
|
278
|
+
return r;
|
|
279
|
+
const onNeed = recovery.onFundingNeeded ?? 'popup';
|
|
280
|
+
if (onNeed === 'throw')
|
|
281
|
+
return r;
|
|
282
|
+
const top = await runTopUp({
|
|
283
|
+
mode: onNeed,
|
|
284
|
+
...(recovery.suggestedAmount ?? klass.suggestedAmount
|
|
285
|
+
? { suggestedAmount: (recovery.suggestedAmount ?? klass.suggestedAmount) }
|
|
286
|
+
: {}),
|
|
287
|
+
});
|
|
288
|
+
if (!top.ok)
|
|
289
|
+
return r;
|
|
290
|
+
return proxyCall(options);
|
|
291
|
+
},
|
|
209
292
|
/** The Tokenite dashboard base URL */
|
|
210
293
|
baseUrl,
|
|
211
294
|
/** The Tokenite proxy base URL */
|
|
@@ -220,12 +303,12 @@ const handleAuthMessage = (event, baseUrl, resolve, reject, cleanup) => {
|
|
|
220
303
|
if (event.origin !== baseUrl)
|
|
221
304
|
return;
|
|
222
305
|
const data = event.data;
|
|
223
|
-
if (data.type ===
|
|
306
|
+
if (data.type === AuthMessageType.Success && data.code) {
|
|
224
307
|
cleanup();
|
|
225
308
|
resolve({ code: data.code });
|
|
226
309
|
return;
|
|
227
310
|
}
|
|
228
|
-
if (data.type ===
|
|
311
|
+
if (data.type === AuthMessageType.Error) {
|
|
229
312
|
cleanup();
|
|
230
313
|
reject(new Error(data.error ?? 'Authorization denied'));
|
|
231
314
|
}
|
|
@@ -266,6 +349,50 @@ const openIframeModal = (url, width, height, baseUrl) => {
|
|
|
266
349
|
window.addEventListener('message', onMessage);
|
|
267
350
|
});
|
|
268
351
|
};
|
|
352
|
+
const buildTopupUrl = (baseUrl, clientId, mode, suggestedAmount) => {
|
|
353
|
+
const params = new URLSearchParams({ client_id: clientId });
|
|
354
|
+
if (mode === 'popup')
|
|
355
|
+
params.set('mode', 'popup');
|
|
356
|
+
if (suggestedAmount !== undefined)
|
|
357
|
+
params.set('suggested_amount', String(suggestedAmount));
|
|
358
|
+
return `${baseUrl}/oauth/topup?${params}`;
|
|
359
|
+
};
|
|
360
|
+
const openTopupPopup = (url, width, height, baseUrl) => {
|
|
361
|
+
const left = Math.round(window.screenX + (window.innerWidth - width) / 2);
|
|
362
|
+
const top = Math.round(window.screenY + (window.innerHeight - height) / 2);
|
|
363
|
+
const popup = window.open(url, 'tokenite-topup', `width=${width},height=${height},left=${left},top=${top},popup=yes`);
|
|
364
|
+
if (!popup)
|
|
365
|
+
return Promise.resolve({ ok: false, reason: 'popup-blocked' });
|
|
366
|
+
return new Promise((resolve) => {
|
|
367
|
+
const cleanup = () => {
|
|
368
|
+
window.removeEventListener('message', onMessage);
|
|
369
|
+
clearInterval(poll);
|
|
370
|
+
if (!popup.closed)
|
|
371
|
+
popup.close();
|
|
372
|
+
};
|
|
373
|
+
const onMessage = (event) => {
|
|
374
|
+
if (event.origin !== baseUrl)
|
|
375
|
+
return;
|
|
376
|
+
const data = event.data;
|
|
377
|
+
if (data.type === TopupMessageType.Success && typeof data.newLimit === 'number') {
|
|
378
|
+
cleanup();
|
|
379
|
+
resolve({ ok: true, newLimit: data.newLimit, remaining: data.remaining ?? 0 });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (data.type === TopupMessageType.Cancelled) {
|
|
383
|
+
cleanup();
|
|
384
|
+
resolve({ ok: false, reason: 'cancelled' });
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const poll = setInterval(() => {
|
|
388
|
+
if (popup.closed) {
|
|
389
|
+
cleanup();
|
|
390
|
+
resolve({ ok: false, reason: 'closed' });
|
|
391
|
+
}
|
|
392
|
+
}, 300);
|
|
393
|
+
window.addEventListener('message', onMessage);
|
|
394
|
+
});
|
|
395
|
+
};
|
|
269
396
|
const openWindowPopup = (url, width, height, baseUrl, redirectUri) => {
|
|
270
397
|
const left = Math.round(window.screenX + (window.innerWidth - width) / 2);
|
|
271
398
|
const top = Math.round(window.screenY + (window.innerHeight - height) / 2);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export { Tokenite } from './client.js';
|
|
2
|
-
export type { TokeniteConfig, AuthorizeOptions, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyUsage, ProxySuccess, ProxyError, ProxyResponse, ErrorSource, ProviderInfo, AppInfo, UserInfo, AccessContext, } from './types.js';
|
|
2
|
+
export type { TokeniteConfig, AuthorizeOptions, OAuthPrompt, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyUsage, ProxySuccess, ProxyError, ProxyResponse, ErrorSource, ProviderInfo, AppInfo, UserInfo, AccessContext, TopUpOptions, TopUpResult, CallWithRecoveryOptions, } from './types.js';
|
|
3
3
|
export { isProxyError, isProxySuccess } from './types.js';
|
|
4
|
+
export { parseCallback } from './parse-callback.js';
|
|
5
|
+
export type { CallbackResult, CallbackSuccess, CallbackError, CallbackReason, ParseCallbackOptions, } from './parse-callback.js';
|
|
6
|
+
export { isFundingError } from './recovery.js';
|
|
7
|
+
export type { RecoveryClass, RecoveryKind } from './recovery.js';
|
|
8
|
+
export { AuthMessageType, TopupMessageType, parseFundingDetails } from './protocol.js';
|
|
9
|
+
export type { AuthMessage, TopupMessage, FundingErrorDetails } from './protocol.js';
|
|
4
10
|
export { extractErrorMessage } from './error.js';
|
|
5
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { Tokenite } from './client.js';
|
|
2
2
|
export { isProxyError, isProxySuccess } from './types.js';
|
|
3
|
+
export { parseCallback } from './parse-callback.js';
|
|
4
|
+
export { isFundingError } from './recovery.js';
|
|
5
|
+
export { AuthMessageType, TopupMessageType, parseFundingDetails } from './protocol.js';
|
|
3
6
|
export { extractErrorMessage } from './error.js';
|
|
4
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the OAuth redirect callback that Tokenite sends to your app's
|
|
3
|
+
* `redirect_uri`. Returns a typed result the caller can switch on
|
|
4
|
+
* instead of hand-checking string params.
|
|
5
|
+
*
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { parseCallback } from '@tokenite/sdk';
|
|
8
|
+
*
|
|
9
|
+
* const result = parseCallback(req.url, { expectedState: storedState });
|
|
10
|
+
* if (result.ok) {
|
|
11
|
+
* const { code } = result;
|
|
12
|
+
* const { access_token } = await tk.exchangeCode(code);
|
|
13
|
+
* // ...
|
|
14
|
+
* } else {
|
|
15
|
+
* switch (result.reason) {
|
|
16
|
+
* case 'user_denied': return renderCancelled(); // user clicked Cancel
|
|
17
|
+
* case 'invalid_state': return renderRetry(); // CSRF check failed
|
|
18
|
+
* case 'missing_code': return renderRetry(); // user landed here directly
|
|
19
|
+
* default: return renderError(result); // other OAuth error
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Pure function — no side effects, no network, no SDK config needed.
|
|
25
|
+
* Works server-side (Node, edge runtimes) and in the browser.
|
|
26
|
+
*/
|
|
27
|
+
export type CallbackSuccess = {
|
|
28
|
+
readonly ok: true;
|
|
29
|
+
readonly code: string;
|
|
30
|
+
readonly state: string | null;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Why an OAuth callback didn't yield a usable authorization code.
|
|
34
|
+
*
|
|
35
|
+
* - `user_denied` — the end user clicked "Cancel" on the Tokenite
|
|
36
|
+
* consent screen. The most common non-success case. Render a
|
|
37
|
+
* friendly "you cancelled" UI with a "Try again" CTA, NOT a
|
|
38
|
+
* technical error.
|
|
39
|
+
*
|
|
40
|
+
* - `access_denied` — the server returned `error=access_denied`
|
|
41
|
+
* without the `user_denied` description. Generic access refusal
|
|
42
|
+
* (e.g. the app's `redirect_uri` didn't match, scope was refused
|
|
43
|
+
* by policy). Still recoverable with a retry, but the cause is
|
|
44
|
+
* server-side rather than the user's choice.
|
|
45
|
+
*
|
|
46
|
+
* - `invalid_state` — `expectedState` was provided to `parseCallback`
|
|
47
|
+
* and didn't match the returned `state` value. Either a stale
|
|
48
|
+
* session or a CSRF attempt. Always reject and force a fresh
|
|
49
|
+
* flow; never auto-retry without user interaction.
|
|
50
|
+
*
|
|
51
|
+
* - `missing_code` — neither `code` nor `error` was present in the
|
|
52
|
+
* callback URL. Usually means the user navigated to the callback
|
|
53
|
+
* path directly (e.g. via bookmark or browser back) rather than
|
|
54
|
+
* completing a real OAuth round-trip.
|
|
55
|
+
*
|
|
56
|
+
* - `oauth_error` — any other OAuth 2.0 error code (`invalid_request`,
|
|
57
|
+
* `unauthorized_client`, `server_error`, etc.). Inspect
|
|
58
|
+
* `error` / `description` for specifics.
|
|
59
|
+
*/
|
|
60
|
+
export type CallbackReason = 'user_denied' | 'access_denied' | 'invalid_state' | 'missing_code' | 'oauth_error';
|
|
61
|
+
export type CallbackError = {
|
|
62
|
+
readonly ok: false;
|
|
63
|
+
readonly reason: CallbackReason;
|
|
64
|
+
/** Raw OAuth error code from the URL, when present. */
|
|
65
|
+
readonly error?: string;
|
|
66
|
+
/** Raw `error_description` from the URL, when present. */
|
|
67
|
+
readonly description?: string;
|
|
68
|
+
/** The `state` value returned by Tokenite (may be null if the original flow didn't include one). */
|
|
69
|
+
readonly state: string | null;
|
|
70
|
+
};
|
|
71
|
+
export type CallbackResult = CallbackSuccess | CallbackError;
|
|
72
|
+
export type ParseCallbackOptions = {
|
|
73
|
+
/**
|
|
74
|
+
* If provided, compare against the returned `state` query param.
|
|
75
|
+
* Mismatch → `{ ok: false, reason: 'invalid_state' }`, even if a
|
|
76
|
+
* `code` is present (a code + bad state is treated as a CSRF
|
|
77
|
+
* attempt, not a success).
|
|
78
|
+
*/
|
|
79
|
+
readonly expectedState?: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Parse an OAuth callback. Accepts any of: a full URL string, a path
|
|
83
|
+
* with query string (e.g. Node's `req.url`), a `URL` object, a
|
|
84
|
+
* `URLSearchParams`, or a plain query string (with or without leading
|
|
85
|
+
* `?`).
|
|
86
|
+
*/
|
|
87
|
+
export declare const parseCallback: (input: string | URL | URLSearchParams, options?: ParseCallbackOptions) => CallbackResult;
|
|
88
|
+
//# sourceMappingURL=parse-callback.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the OAuth redirect callback that Tokenite sends to your app's
|
|
3
|
+
* `redirect_uri`. Returns a typed result the caller can switch on
|
|
4
|
+
* instead of hand-checking string params.
|
|
5
|
+
*
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { parseCallback } from '@tokenite/sdk';
|
|
8
|
+
*
|
|
9
|
+
* const result = parseCallback(req.url, { expectedState: storedState });
|
|
10
|
+
* if (result.ok) {
|
|
11
|
+
* const { code } = result;
|
|
12
|
+
* const { access_token } = await tk.exchangeCode(code);
|
|
13
|
+
* // ...
|
|
14
|
+
* } else {
|
|
15
|
+
* switch (result.reason) {
|
|
16
|
+
* case 'user_denied': return renderCancelled(); // user clicked Cancel
|
|
17
|
+
* case 'invalid_state': return renderRetry(); // CSRF check failed
|
|
18
|
+
* case 'missing_code': return renderRetry(); // user landed here directly
|
|
19
|
+
* default: return renderError(result); // other OAuth error
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Pure function — no side effects, no network, no SDK config needed.
|
|
25
|
+
* Works server-side (Node, edge runtimes) and in the browser.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Parse an OAuth callback. Accepts any of: a full URL string, a path
|
|
29
|
+
* with query string (e.g. Node's `req.url`), a `URL` object, a
|
|
30
|
+
* `URLSearchParams`, or a plain query string (with or without leading
|
|
31
|
+
* `?`).
|
|
32
|
+
*/
|
|
33
|
+
export const parseCallback = (input, options = {}) => {
|
|
34
|
+
const params = toSearchParams(input);
|
|
35
|
+
const code = params.get('code');
|
|
36
|
+
const state = params.get('state');
|
|
37
|
+
const error = params.get('error');
|
|
38
|
+
const description = params.get('error_description');
|
|
39
|
+
// State validation runs first: a returned code paired with a bad
|
|
40
|
+
// state is more suspicious than no code at all (could be a planted
|
|
41
|
+
// code from an attacker hijacking the callback). Always reject.
|
|
42
|
+
if (options.expectedState !== undefined && state !== options.expectedState) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
reason: 'invalid_state',
|
|
46
|
+
state,
|
|
47
|
+
...(error !== null ? { error } : {}),
|
|
48
|
+
...(description !== null ? { description } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (error !== null) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
reason: classifyError(error, description),
|
|
55
|
+
error,
|
|
56
|
+
state,
|
|
57
|
+
...(description !== null ? { description } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (code !== null && code !== '') {
|
|
61
|
+
return { ok: true, code, state };
|
|
62
|
+
}
|
|
63
|
+
return { ok: false, reason: 'missing_code', state };
|
|
64
|
+
};
|
|
65
|
+
const classifyError = (error, description) => {
|
|
66
|
+
if (error === 'access_denied' && description === 'user_denied')
|
|
67
|
+
return 'user_denied';
|
|
68
|
+
if (error === 'access_denied')
|
|
69
|
+
return 'access_denied';
|
|
70
|
+
return 'oauth_error';
|
|
71
|
+
};
|
|
72
|
+
const toSearchParams = (input) => {
|
|
73
|
+
if (input instanceof URLSearchParams)
|
|
74
|
+
return input;
|
|
75
|
+
if (input instanceof URL)
|
|
76
|
+
return input.searchParams;
|
|
77
|
+
const trimmed = input.trim();
|
|
78
|
+
// Bare query string ("?foo=bar" or "foo=bar")
|
|
79
|
+
if (trimmed.startsWith('?'))
|
|
80
|
+
return new URLSearchParams(trimmed.slice(1));
|
|
81
|
+
// Path + query ("/cb?foo=bar") or full URL ("https://x/cb?foo=bar")
|
|
82
|
+
const queryStart = trimmed.indexOf('?');
|
|
83
|
+
if (queryStart < 0)
|
|
84
|
+
return new URLSearchParams('');
|
|
85
|
+
return new URLSearchParams(trimmed.slice(queryStart + 1));
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=parse-callback.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const AuthMessageType: {
|
|
2
|
+
readonly Success: "tokenite:auth-success";
|
|
3
|
+
readonly Error: "tokenite:auth-error";
|
|
4
|
+
};
|
|
5
|
+
export declare const TopupMessageType: {
|
|
6
|
+
readonly Success: "tokenite:topup-success";
|
|
7
|
+
readonly Cancelled: "tokenite:topup-cancelled";
|
|
8
|
+
};
|
|
9
|
+
export type AuthMessage = {
|
|
10
|
+
readonly type: typeof AuthMessageType.Success;
|
|
11
|
+
readonly code: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly type: typeof AuthMessageType.Error;
|
|
14
|
+
readonly error: string;
|
|
15
|
+
};
|
|
16
|
+
export type TopupMessage = {
|
|
17
|
+
readonly type: typeof TopupMessageType.Success;
|
|
18
|
+
readonly newLimit: number;
|
|
19
|
+
readonly remaining: number;
|
|
20
|
+
} | {
|
|
21
|
+
readonly type: typeof TopupMessageType.Cancelled;
|
|
22
|
+
};
|
|
23
|
+
export type FundingErrorDetails = {
|
|
24
|
+
readonly remaining?: number;
|
|
25
|
+
readonly currentLimit?: number;
|
|
26
|
+
readonly currentSpent?: number;
|
|
27
|
+
};
|
|
28
|
+
export declare const parseFundingDetails: (details: unknown) => FundingErrorDetails | null;
|
|
29
|
+
//# sourceMappingURL=protocol.d.ts.map
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const AuthMessageType = {
|
|
2
|
+
Success: 'tokenite:auth-success',
|
|
3
|
+
Error: 'tokenite:auth-error',
|
|
4
|
+
};
|
|
5
|
+
export const TopupMessageType = {
|
|
6
|
+
Success: 'tokenite:topup-success',
|
|
7
|
+
Cancelled: 'tokenite:topup-cancelled',
|
|
8
|
+
};
|
|
9
|
+
const isRecord = (v) => typeof v === 'object' && v !== null;
|
|
10
|
+
const numericField = (obj, key) => {
|
|
11
|
+
const v = obj[key];
|
|
12
|
+
return typeof v === 'number' ? v : undefined;
|
|
13
|
+
};
|
|
14
|
+
export const parseFundingDetails = (details) => {
|
|
15
|
+
if (!isRecord(details))
|
|
16
|
+
return null;
|
|
17
|
+
return {
|
|
18
|
+
...(numericField(details, 'remaining') !== undefined ? { remaining: numericField(details, 'remaining') } : {}),
|
|
19
|
+
...(numericField(details, 'currentLimit') !== undefined ? { currentLimit: numericField(details, 'currentLimit') } : {}),
|
|
20
|
+
...(numericField(details, 'currentSpent') !== undefined ? { currentSpent: numericField(details, 'currentSpent') } : {}),
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProxyError } from './types.js';
|
|
2
|
+
export type RecoveryKind = 'user-topup' | 'add-key' | 'team-action' | 'sponsored-out';
|
|
3
|
+
export type RecoveryClass = {
|
|
4
|
+
readonly kind: RecoveryKind;
|
|
5
|
+
readonly actor: 'user' | 'team-owner' | 'builder';
|
|
6
|
+
readonly suggestedAmount?: number;
|
|
7
|
+
readonly currentLimit?: number;
|
|
8
|
+
readonly currentSpent?: number;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly dashboardPath: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const isFundingError: (error: ProxyError["error"]) => RecoveryClass | null;
|
|
13
|
+
//# sourceMappingURL=recovery.d.ts.map
|
package/dist/recovery.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parseFundingDetails } from './protocol.js';
|
|
2
|
+
const SUGGESTED_MULTIPLIER = 2;
|
|
3
|
+
export const isFundingError = (error) => {
|
|
4
|
+
switch (error.code) {
|
|
5
|
+
case 'BUDGET_EXCEEDED': {
|
|
6
|
+
const details = parseFundingDetails(error.details);
|
|
7
|
+
const currentLimit = details?.currentLimit;
|
|
8
|
+
const currentSpent = details?.currentSpent;
|
|
9
|
+
const suggestedAmount = currentLimit !== undefined
|
|
10
|
+
? Math.max(currentLimit * SUGGESTED_MULTIPLIER, currentLimit + 1)
|
|
11
|
+
: undefined;
|
|
12
|
+
return {
|
|
13
|
+
kind: 'user-topup',
|
|
14
|
+
actor: 'user',
|
|
15
|
+
...(currentLimit !== undefined ? { currentLimit } : {}),
|
|
16
|
+
...(currentSpent !== undefined ? { currentSpent } : {}),
|
|
17
|
+
...(suggestedAmount !== undefined ? { suggestedAmount } : {}),
|
|
18
|
+
message: 'Your per-app spending limit is exhausted. Raise it to continue.',
|
|
19
|
+
dashboardPath: '/oauth/topup',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
case 'POOL_EXHAUSTED':
|
|
23
|
+
return {
|
|
24
|
+
kind: 'team-action',
|
|
25
|
+
actor: 'team-owner',
|
|
26
|
+
message: 'Your account pool budget is exhausted. The account owner must raise it.',
|
|
27
|
+
dashboardPath: '/a/_/team/pool',
|
|
28
|
+
};
|
|
29
|
+
case 'PROVIDER_KEY_MISSING':
|
|
30
|
+
return {
|
|
31
|
+
kind: 'add-key',
|
|
32
|
+
actor: 'user',
|
|
33
|
+
message: 'No provider key on file. Add one to continue.',
|
|
34
|
+
dashboardPath: '/a/_/consumer/keys',
|
|
35
|
+
};
|
|
36
|
+
case 'SPONSORED_POOL_EXHAUSTED':
|
|
37
|
+
return {
|
|
38
|
+
kind: 'sponsored-out',
|
|
39
|
+
actor: 'builder',
|
|
40
|
+
message: 'This app\'s sponsored credits are exhausted. The builder must refill.',
|
|
41
|
+
dashboardPath: '/builder/apps',
|
|
42
|
+
};
|
|
43
|
+
default:
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=recovery.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -10,11 +10,42 @@ export type TokeniteConfig = {
|
|
|
10
10
|
/** Tokenite proxy URL. Default: https://api.tokenite.ai */
|
|
11
11
|
readonly proxyUrl?: string;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* OAuth 2.0 / OIDC `prompt` parameter. Tells Tokenite whether to
|
|
15
|
+
* re-prompt the user even when they have an existing session and/or
|
|
16
|
+
* an existing grant for this app.
|
|
17
|
+
*
|
|
18
|
+
* - `'consent'` — re-show the consent screen even if the user has
|
|
19
|
+
* already authorized this app. Use this on "Sign in with Tokenite"
|
|
20
|
+
* buttons that follow a sign-out, so users don't silently
|
|
21
|
+
* re-authorize without a chance to pick a different account or
|
|
22
|
+
* cancel.
|
|
23
|
+
* - `'login'` — request that the user re-authenticate. Today this
|
|
24
|
+
* behaves the same as `'consent'` on Tokenite (full session-cookie
|
|
25
|
+
* clear is a TODO); the consent screen still shows.
|
|
26
|
+
* - `'select_account'` — show the account picker. Tokenite already
|
|
27
|
+
* does this automatically when the user has multiple accounts;
|
|
28
|
+
* passing it explicitly is a hint for future apps that always want
|
|
29
|
+
* the picker.
|
|
30
|
+
* - `'none'` — never show UI; fail if interaction is required.
|
|
31
|
+
* Currently treated as the default (silent reauth).
|
|
32
|
+
*
|
|
33
|
+
* The OAuth 2.0 spec allows a space-separated combination
|
|
34
|
+
* (e.g. `'login consent'`); for that, pass the raw string. The
|
|
35
|
+
* union above is just for autocomplete on the common values.
|
|
36
|
+
*/
|
|
37
|
+
export type OAuthPrompt = 'login' | 'consent' | 'select_account' | 'none' | (string & {});
|
|
13
38
|
export type AuthorizeOptions = {
|
|
14
39
|
/** Custom state parameter for CSRF protection. Auto-generated if not provided. */
|
|
15
40
|
readonly state?: string;
|
|
16
41
|
/** Suggested budget amount (user can override on consent screen) */
|
|
17
42
|
readonly suggestedBudget?: number;
|
|
43
|
+
/**
|
|
44
|
+
* OAuth `prompt` parameter. Most common use: pass `'consent'` to
|
|
45
|
+
* force a fresh consent screen on sign-in (prevents silent reauth
|
|
46
|
+
* after the user signed out of the app). See {@link OAuthPrompt}.
|
|
47
|
+
*/
|
|
48
|
+
readonly prompt?: OAuthPrompt;
|
|
18
49
|
};
|
|
19
50
|
export type PopupOptions = {
|
|
20
51
|
/** Suggested budget amount (user can override on consent screen) */
|
|
@@ -35,6 +66,12 @@ export type PopupOptions = {
|
|
|
35
66
|
readonly width?: number;
|
|
36
67
|
/** Modal/popup height in pixels. Default: 620 */
|
|
37
68
|
readonly height?: number;
|
|
69
|
+
/**
|
|
70
|
+
* OAuth `prompt` parameter. Most common use: pass `'consent'` to
|
|
71
|
+
* force a fresh consent screen on sign-in (prevents silent reauth
|
|
72
|
+
* after the user signed out of the app). See {@link OAuthPrompt}.
|
|
73
|
+
*/
|
|
74
|
+
readonly prompt?: OAuthPrompt;
|
|
38
75
|
};
|
|
39
76
|
export type PopupResult = {
|
|
40
77
|
/**
|
|
@@ -45,6 +82,46 @@ export type PopupResult = {
|
|
|
45
82
|
*/
|
|
46
83
|
readonly code: string;
|
|
47
84
|
};
|
|
85
|
+
export type TopUpOptions = {
|
|
86
|
+
/**
|
|
87
|
+
* How to host the top-up screen.
|
|
88
|
+
*
|
|
89
|
+
* - `'popup'` (default) — open in a separate browser window via
|
|
90
|
+
* `window.open`. The user completes the form; the SDK resolves
|
|
91
|
+
* when Tokenite posts back the new limit.
|
|
92
|
+
* - `'redirect'` — full-page navigation. The builder's app loses
|
|
93
|
+
* in-memory state and must reload from the callback URL.
|
|
94
|
+
*/
|
|
95
|
+
readonly mode?: 'popup' | 'redirect';
|
|
96
|
+
/** Pre-fill the new-limit field. Default: 2 × current limit. */
|
|
97
|
+
readonly suggestedAmount?: number;
|
|
98
|
+
/** Popup width in pixels. Default: 480 */
|
|
99
|
+
readonly width?: number;
|
|
100
|
+
/** Popup height in pixels. Default: 560 */
|
|
101
|
+
readonly height?: number;
|
|
102
|
+
};
|
|
103
|
+
export type TopUpResult = {
|
|
104
|
+
readonly ok: true;
|
|
105
|
+
readonly newLimit: number;
|
|
106
|
+
readonly remaining: number;
|
|
107
|
+
} | {
|
|
108
|
+
readonly ok: false;
|
|
109
|
+
readonly reason: 'cancelled' | 'popup-blocked' | 'closed' | 'redirected';
|
|
110
|
+
};
|
|
111
|
+
export type CallWithRecoveryOptions = {
|
|
112
|
+
/**
|
|
113
|
+
* What to do when the proxy returns a recoverable funding error.
|
|
114
|
+
*
|
|
115
|
+
* - `'popup'` (default) — open Tokenite's top-up popup, then retry the
|
|
116
|
+
* call once on success.
|
|
117
|
+
* - `'redirect'` — full-page navigation; the call does not retry (the
|
|
118
|
+
* builder's app re-loads from the callback and re-invokes manually).
|
|
119
|
+
* - `'throw'` — disable recovery; surface the original error.
|
|
120
|
+
*/
|
|
121
|
+
readonly onFundingNeeded?: 'popup' | 'redirect' | 'throw';
|
|
122
|
+
/** Override the suggested top-up amount. */
|
|
123
|
+
readonly suggestedAmount?: number;
|
|
124
|
+
};
|
|
48
125
|
export type TokenResponse = {
|
|
49
126
|
readonly access_token: string;
|
|
50
127
|
readonly token_type: string;
|
package/package.json
CHANGED