@tokenite/sdk 2.2.0 → 2.4.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/dist/client.d.ts CHANGED
@@ -40,7 +40,15 @@ 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
- * Supports optional suggested budget that pre-fills the consent screen.
43
+ *
44
+ * Pass `prompt: 'select_account'` on "Sign in with Tokenite" buttons
45
+ * that follow a sign-out — Tokenite shows a "Continue as <email>?"
46
+ * card with a "Use a different account" option instead of silently
47
+ * dropping the user back into the app. The user's existing spending
48
+ * limit is preserved (no budget re-entry):
49
+ * ```typescript
50
+ * res.redirect(tk.getAuthorizeUrl({ prompt: 'select_account' }));
51
+ * ```
44
52
  */
45
53
  getAuthorizeUrl: (options?: AuthorizeOptions) => string;
46
54
  /**
@@ -64,6 +72,15 @@ export declare const Tokenite: (config: TokeniteConfig) => {
64
72
  * body: JSON.stringify({ code }),
65
73
  * });
66
74
  * ```
75
+ *
76
+ * On a "Sign in with Tokenite" button shown after the user signed
77
+ * out, pass `prompt: 'select_account'` — Tokenite shows a "Continue
78
+ * as <email>?" card with a "Use a different account" option instead
79
+ * of silently re-authorizing. The user's existing spending limit is
80
+ * preserved:
81
+ * ```typescript
82
+ * const { code } = await tk.popup({ prompt: 'select_account' });
83
+ * ```
67
84
  */
68
85
  popup: (options?: PopupOptions) => Promise<PopupResult>;
69
86
  /**
package/dist/client.js CHANGED
@@ -92,12 +92,22 @@ export const Tokenite = (config) => {
92
92
  params.set('suggested_budget', String(options.suggestedBudget));
93
93
  if (options?.mode)
94
94
  params.set('mode', options.mode);
95
+ if (options?.prompt)
96
+ params.set('prompt', options.prompt);
95
97
  return `${baseUrl}/oauth/authorize?${params}`;
96
98
  };
97
99
  return {
98
100
  /**
99
101
  * Build the authorization URL for a full-page redirect.
100
- * Supports optional suggested budget that pre-fills the consent screen.
102
+ *
103
+ * Pass `prompt: 'select_account'` on "Sign in with Tokenite" buttons
104
+ * that follow a sign-out — Tokenite shows a "Continue as <email>?"
105
+ * card with a "Use a different account" option instead of silently
106
+ * dropping the user back into the app. The user's existing spending
107
+ * limit is preserved (no budget re-entry):
108
+ * ```typescript
109
+ * res.redirect(tk.getAuthorizeUrl({ prompt: 'select_account' }));
110
+ * ```
101
111
  */
102
112
  getAuthorizeUrl: (options) => buildAuthorizeUrl(options),
103
113
  /**
@@ -121,6 +131,15 @@ export const Tokenite = (config) => {
121
131
  * body: JSON.stringify({ code }),
122
132
  * });
123
133
  * ```
134
+ *
135
+ * On a "Sign in with Tokenite" button shown after the user signed
136
+ * out, pass `prompt: 'select_account'` — Tokenite shows a "Continue
137
+ * as <email>?" card with a "Use a different account" option instead
138
+ * of silently re-authorizing. The user's existing spending limit is
139
+ * preserved:
140
+ * ```typescript
141
+ * const { code } = await tk.popup({ prompt: 'select_account' });
142
+ * ```
124
143
  */
125
144
  popup: (options) => {
126
145
  const mode = options?.mode ?? 'iframe';
@@ -133,6 +152,7 @@ export const Tokenite = (config) => {
133
152
  const url = buildAuthorizeUrl({
134
153
  suggestedBudget: options?.suggestedBudget,
135
154
  mode: mode === 'window' ? 'popup' : 'iframe',
155
+ prompt: options?.prompt,
136
156
  });
137
157
  return mode === 'window'
138
158
  ? openWindowPopup(url, width, height, baseUrl, config.redirectUri)
@@ -299,17 +319,17 @@ const handleAuthMessage = (event, baseUrl, resolve, reject, cleanup) => {
299
319
  };
300
320
  const openIframeModal = (url, width, height, baseUrl) => {
301
321
  const overlay = document.createElement('div');
302
- overlay.style.cssText = `
303
- position: fixed; inset: 0; z-index: 999999;
304
- background: rgba(0,0,0,0.5); backdrop-filter: blur(2px);
305
- display: flex; align-items: center; justify-content: center;
322
+ overlay.style.cssText = `
323
+ position: fixed; inset: 0; z-index: 999999;
324
+ background: rgba(0,0,0,0.5); backdrop-filter: blur(2px);
325
+ display: flex; align-items: center; justify-content: center;
306
326
  `;
307
327
  const container = document.createElement('div');
308
- container.style.cssText = `
309
- background: white; border-radius: 12px; overflow: hidden;
310
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
311
- width: ${width}px; max-width: calc(100vw - 32px);
312
- height: ${height}px; max-height: calc(100vh - 32px);
328
+ container.style.cssText = `
329
+ background: white; border-radius: 12px; overflow: hidden;
330
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
331
+ width: ${width}px; max-width: calc(100vw - 32px);
332
+ height: ${height}px; max-height: calc(100vh - 32px);
313
333
  `;
314
334
  const iframe = document.createElement('iframe');
315
335
  iframe.src = url;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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, TopUpOptions, TopUpResult, CallWithRecoveryOptions, } from './types.js';
2
+ export type { TokeniteConfig, AuthorizeOptions, OAuthPrompt, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyUsage, ProxySuccess, ProxyError, ProxyResponse, ErrorSource, ProviderInfo, ModelInfo, TierInfo, AppInfo, UserInfo, AccessContext, TopUpOptions, TopUpResult, CallWithRecoveryOptions, } from './types.js';
3
3
  export { isProxyError, isProxySuccess } from './types.js';
4
4
  export { parseCallback } from './parse-callback.js';
5
5
  export type { CallbackResult, CallbackSuccess, CallbackError, CallbackReason, ParseCallbackOptions, } from './parse-callback.js';
package/dist/types.d.ts CHANGED
@@ -10,11 +10,46 @@ 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
+ * - `'select_account'` — **recommended for "Sign in with Tokenite"
19
+ * buttons that follow a sign-out.** Interrupts the silent re-auth
20
+ * that would otherwise drop the user straight back into your app:
21
+ * instead Tokenite shows a "Continue as <email>?" confirmation card
22
+ * with a "Use a different account" option. The user's existing
23
+ * spending limit is preserved — they aren't asked to re-set it.
24
+ * - `'consent'` — re-show the **full** consent screen (budget input
25
+ * and all), discarding any existing grant. Use this only when you
26
+ * want the user to actively re-set their budget; for the common
27
+ * "they signed out, now they're signing back in" case, prefer
28
+ * `'select_account'`.
29
+ * - `'login'` — request that the user re-authenticate. Today this
30
+ * behaves the same as `'consent'` on Tokenite (full session-cookie
31
+ * clear is a TODO); the consent screen still shows.
32
+ * - `'none'` — never show UI; fail if interaction is required.
33
+ * Currently treated as the default (silent reauth).
34
+ *
35
+ * The OAuth 2.0 spec allows a space-separated combination
36
+ * (e.g. `'login consent'`); for that, pass the raw string. The
37
+ * union above is just for autocomplete on the common values.
38
+ */
39
+ export type OAuthPrompt = 'login' | 'consent' | 'select_account' | 'none' | (string & {});
13
40
  export type AuthorizeOptions = {
14
41
  /** Custom state parameter for CSRF protection. Auto-generated if not provided. */
15
42
  readonly state?: string;
16
43
  /** Suggested budget amount (user can override on consent screen) */
17
44
  readonly suggestedBudget?: number;
45
+ /**
46
+ * OAuth `prompt` parameter. Most common use: pass `'select_account'`
47
+ * on "Sign in with Tokenite" buttons that follow a sign-out — it
48
+ * stops the silent reauth that would otherwise drop the user
49
+ * straight back into your app, and shows a "Continue as <email>?"
50
+ * confirmation card instead. See {@link OAuthPrompt}.
51
+ */
52
+ readonly prompt?: OAuthPrompt;
18
53
  };
19
54
  export type PopupOptions = {
20
55
  /** Suggested budget amount (user can override on consent screen) */
@@ -35,6 +70,14 @@ export type PopupOptions = {
35
70
  readonly width?: number;
36
71
  /** Modal/popup height in pixels. Default: 620 */
37
72
  readonly height?: number;
73
+ /**
74
+ * OAuth `prompt` parameter. Most common use: pass `'select_account'`
75
+ * on "Sign in with Tokenite" buttons that follow a sign-out — it
76
+ * stops the silent reauth that would otherwise drop the user
77
+ * straight back into your app, and shows a "Continue as <email>?"
78
+ * confirmation card instead. See {@link OAuthPrompt}.
79
+ */
80
+ readonly prompt?: OAuthPrompt;
38
81
  };
39
82
  export type PopupResult = {
40
83
  /**
@@ -207,6 +250,50 @@ export type ProviderInfo = {
207
250
  /** Whether the logo is a glyph/symbol or a full wordmark */
208
251
  readonly logoStyle: 'symbol' | 'wordmark';
209
252
  };
253
+ /**
254
+ * A model the access token may call, scoped to the app's model strategy
255
+ * and the holder's provider keys.
256
+ *
257
+ * - `servedBy` — every provider that hosts this model (catalog fact).
258
+ * - `callableNow` — the subset the holder can run *right now* (they hold
259
+ * a key for it). Empty means the model is visible but not yet usable —
260
+ * render it disabled, or prompt the user to add a key.
261
+ *
262
+ * Pass `slug` as the `model` field in `tk.call()`.
263
+ */
264
+ export type ModelInfo = {
265
+ /** Stable Tokenite model slug — pass this as `model` in tk.call() */
266
+ readonly slug: string;
267
+ /** Human-readable name, e.g. "Claude Haiku 4.5" */
268
+ readonly displayName: string;
269
+ /** The lab that built the model */
270
+ readonly creator: 'anthropic' | 'openai' | 'google' | 'grok';
271
+ /** Capability tiers this model satisfies (cheap / fast / smart / reasoning) */
272
+ readonly tiers: readonly string[];
273
+ /** Feature capabilities, e.g. "vision", "tools", "thinking" */
274
+ readonly capabilities: readonly string[];
275
+ /** Every provider that serves this model */
276
+ readonly servedBy: readonly Provider[];
277
+ /** Providers the holder can run it through right now (subset of servedBy) */
278
+ readonly callableNow: readonly Provider[];
279
+ /** Indicative price per million tokens */
280
+ readonly pricing: {
281
+ readonly inputPerMillion: number;
282
+ readonly outputPerMillion: number;
283
+ };
284
+ };
285
+ /**
286
+ * A provider-agnostic capability bucket. Use this for a "pick a speed /
287
+ * quality" UI where the user never sees a model name.
288
+ */
289
+ export type TierInfo = {
290
+ /** Tier id: "cheap" | "fast" | "smart" | "reasoning" */
291
+ readonly id: string;
292
+ /** Whether the holder can run at least one model in this tier */
293
+ readonly reachable: boolean;
294
+ /** A representative callable model slug for this tier, or null */
295
+ readonly recommendedModel: string | null;
296
+ };
210
297
  /** Summary of the app the access token belongs to */
211
298
  export type AppInfo = {
212
299
  readonly id: string;
@@ -231,5 +318,18 @@ export type AccessContext = {
231
318
  readonly app: AppInfo;
232
319
  readonly user: UserInfo;
233
320
  readonly providers: readonly ProviderInfo[];
321
+ /**
322
+ * Models the token may call — already filtered to the app's strategy
323
+ * and the user's keys. Render a picker from this; no need to maintain
324
+ * your own model list. Each entry's `callableNow` says whether it's
325
+ * usable now or needs a key.
326
+ */
327
+ readonly models: readonly ModelInfo[];
328
+ /**
329
+ * Provider-agnostic capability buckets. For a "pick a tier" UI where
330
+ * the user never sees a model name — `recommendedModel` gives you a
331
+ * concrete slug to pass to `tk.call()`.
332
+ */
333
+ readonly tiers: readonly TierInfo[];
234
334
  };
235
335
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,53 +1,54 @@
1
- {
2
- "name": "@tokenite/sdk",
3
- "version": "2.2.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
- "repository": {
37
- "type": "git",
38
- "url": "git+https://github.com/eran-broder/tokenite-sdk.git"
39
- },
40
- "homepage": "https://github.com/eran-broder/tokenite-sdk#readme",
41
- "bugs": {
42
- "url": "https://github.com/eran-broder/tokenite-sdk/issues"
43
- },
44
- "devDependencies": {
45
- "tsx": "^4.0.0",
46
- "typescript": "^5.0.0"
47
- },
48
- "scripts": {
49
- "build": "tsc && npm run generate-readme",
50
- "typecheck": "tsc --noEmit",
51
- "generate-readme": "npx tsx scripts/generate-readme.ts"
52
- }
53
- }
1
+ {
2
+ "name": "@tokenite/sdk",
3
+ "version": "2.4.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
+ "scripts": {
22
+ "build": "tsc && npm run generate-readme",
23
+ "typecheck": "tsc --noEmit",
24
+ "generate-readme": "npx tsx scripts/generate-readme.ts",
25
+ "prepack": "tsc && npm run generate-readme && node -e \"import('node:fs/promises').then(async fs => { const files = await fs.readdir('dist', { recursive: true }); for (const f of files) if (f.endsWith('.map') || f.endsWith('.tsbuildinfo')) await fs.rm('dist/' + f); })\""
26
+ },
27
+ "keywords": [
28
+ "tokenite",
29
+ "llm",
30
+ "ai",
31
+ "proxy",
32
+ "oauth",
33
+ "byok",
34
+ "anthropic",
35
+ "openai",
36
+ "google"
37
+ ],
38
+ "license": "MIT",
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/eran-broder/tokenite-sdk.git"
45
+ },
46
+ "homepage": "https://github.com/eran-broder/tokenite-sdk#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/eran-broder/tokenite-sdk/issues"
49
+ },
50
+ "devDependencies": {
51
+ "tsx": "^4.0.0",
52
+ "typescript": "^5.0.0"
53
+ }
54
+ }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=client.test.d.ts.map
@@ -1,66 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { TokenWallet } from './client.js';
3
- const tw = TokenWallet({
4
- clientId: 'test-app-id',
5
- clientSecret: 'test-secret',
6
- redirectUri: 'https://myapp.com/callback',
7
- baseUrl: 'https://tokenwallet.ai',
8
- proxyUrl: 'https://api.tokenwallet.ai',
9
- });
10
- describe('TokenWallet', () => {
11
- describe('getAuthorizeUrl', () => {
12
- it('builds correct OAuth URL', () => {
13
- const url = tw.getAuthorizeUrl({ state: 'abc123' });
14
- expect(url).toBe('https://tokenwallet.ai/oauth/authorize?client_id=test-app-id&redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback&response_type=code&state=abc123');
15
- });
16
- it('auto-generates state if not provided', () => {
17
- const url = tw.getAuthorizeUrl();
18
- expect(url).toContain('state=');
19
- expect(url).toContain('client_id=test-app-id');
20
- });
21
- it('generates different state each time', () => {
22
- const url1 = tw.getAuthorizeUrl();
23
- const url2 = tw.getAuthorizeUrl();
24
- const state1 = new URL(url1).searchParams.get('state');
25
- const state2 = new URL(url2).searchParams.get('state');
26
- expect(state1).not.toBe(state2);
27
- });
28
- it('includes suggested budget and period when provided', () => {
29
- const url = tw.getAuthorizeUrl({ state: 's1', suggestedBudget: 10, suggestedPeriod: 'weekly' });
30
- const parsed = new URL(url);
31
- expect(parsed.searchParams.get('suggested_budget')).toBe('10');
32
- expect(parsed.searchParams.get('suggested_period')).toBe('weekly');
33
- });
34
- it('omits budget params when not provided', () => {
35
- const url = tw.getAuthorizeUrl({ state: 's2' });
36
- const parsed = new URL(url);
37
- expect(parsed.searchParams.has('suggested_budget')).toBe(false);
38
- expect(parsed.searchParams.has('suggested_period')).toBe(false);
39
- });
40
- });
41
- describe('proxyUrl', () => {
42
- it('returns correct URL for anthropic', () => {
43
- expect(tw.proxyUrl('anthropic')).toBe('https://api.tokenwallet.ai/anthropic');
44
- });
45
- it('returns correct URL for openai', () => {
46
- expect(tw.proxyUrl('openai')).toBe('https://api.tokenwallet.ai/openai');
47
- });
48
- it('returns correct URL for google', () => {
49
- expect(tw.proxyUrl('google')).toBe('https://api.tokenwallet.ai/google');
50
- });
51
- });
52
- describe('exchangeCode', () => {
53
- it('throws if clientSecret is not set', async () => {
54
- const noSecret = TokenWallet({ clientId: 'x', redirectUri: 'http://x.com/cb' });
55
- await expect(noSecret.exchangeCode('code123')).rejects.toThrow('clientSecret is required');
56
- });
57
- });
58
- describe('defaults', () => {
59
- it('uses default base URL', () => {
60
- const client = TokenWallet({ clientId: 'x', redirectUri: 'http://x.com/cb' });
61
- expect(client.baseUrl).toBe('https://tokenwallet.ai');
62
- expect(client.proxyBase).toBe('https://api.tokenwallet.ai');
63
- });
64
- });
65
- });
66
- //# sourceMappingURL=client.test.js.map