@tokenite/sdk 2.3.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
@@ -41,11 +41,13 @@ 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:
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):
47
49
  * ```typescript
48
- * res.redirect(tk.getAuthorizeUrl({ prompt: 'consent' }));
50
+ * res.redirect(tk.getAuthorizeUrl({ prompt: 'select_account' }));
49
51
  * ```
50
52
  */
51
53
  getAuthorizeUrl: (options?: AuthorizeOptions) => string;
@@ -71,11 +73,13 @@ export declare const Tokenite: (config: TokeniteConfig) => {
71
73
  * });
72
74
  * ```
73
75
  *
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:
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:
77
81
  * ```typescript
78
- * const { code } = await tk.popup({ prompt: 'consent' });
82
+ * const { code } = await tk.popup({ prompt: 'select_account' });
79
83
  * ```
80
84
  */
81
85
  popup: (options?: PopupOptions) => Promise<PopupResult>;
package/dist/client.js CHANGED
@@ -100,11 +100,13 @@ export const Tokenite = (config) => {
100
100
  /**
101
101
  * Build the authorization URL for a full-page redirect.
102
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:
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):
106
108
  * ```typescript
107
- * res.redirect(tk.getAuthorizeUrl({ prompt: 'consent' }));
109
+ * res.redirect(tk.getAuthorizeUrl({ prompt: 'select_account' }));
108
110
  * ```
109
111
  */
110
112
  getAuthorizeUrl: (options) => buildAuthorizeUrl(options),
@@ -130,11 +132,13 @@ export const Tokenite = (config) => {
130
132
  * });
131
133
  * ```
132
134
  *
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:
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:
136
140
  * ```typescript
137
- * const { code } = await tk.popup({ prompt: 'consent' });
141
+ * const { code } = await tk.popup({ prompt: 'select_account' });
138
142
  * ```
139
143
  */
140
144
  popup: (options) => {
@@ -315,17 +319,17 @@ const handleAuthMessage = (event, baseUrl, resolve, reject, cleanup) => {
315
319
  };
316
320
  const openIframeModal = (url, width, height, baseUrl) => {
317
321
  const overlay = document.createElement('div');
318
- overlay.style.cssText = `
319
- position: fixed; inset: 0; z-index: 999999;
320
- background: rgba(0,0,0,0.5); backdrop-filter: blur(2px);
321
- 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;
322
326
  `;
323
327
  const container = document.createElement('div');
324
- container.style.cssText = `
325
- background: white; border-radius: 12px; overflow: hidden;
326
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
327
- width: ${width}px; max-width: calc(100vw - 32px);
328
- 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);
329
333
  `;
330
334
  const iframe = document.createElement('iframe');
331
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, OAuthPrompt, 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
@@ -15,18 +15,20 @@ export type TokeniteConfig = {
15
15
  * re-prompt the user even when they have an existing session and/or
16
16
  * an existing grant for this app.
17
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.
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'`.
23
29
  * - `'login'` — request that the user re-authenticate. Today this
24
30
  * behaves the same as `'consent'` on Tokenite (full session-cookie
25
31
  * 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
32
  * - `'none'` — never show UI; fail if interaction is required.
31
33
  * Currently treated as the default (silent reauth).
32
34
  *
@@ -41,9 +43,11 @@ export type AuthorizeOptions = {
41
43
  /** Suggested budget amount (user can override on consent screen) */
42
44
  readonly suggestedBudget?: number;
43
45
  /**
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}.
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}.
47
51
  */
48
52
  readonly prompt?: OAuthPrompt;
49
53
  };
@@ -67,9 +71,11 @@ export type PopupOptions = {
67
71
  /** Modal/popup height in pixels. Default: 620 */
68
72
  readonly height?: number;
69
73
  /**
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}.
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}.
73
79
  */
74
80
  readonly prompt?: OAuthPrompt;
75
81
  };
@@ -244,6 +250,50 @@ export type ProviderInfo = {
244
250
  /** Whether the logo is a glyph/symbol or a full wordmark */
245
251
  readonly logoStyle: 'symbol' | 'wordmark';
246
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
+ };
247
297
  /** Summary of the app the access token belongs to */
248
298
  export type AppInfo = {
249
299
  readonly id: string;
@@ -268,5 +318,18 @@ export type AccessContext = {
268
318
  readonly app: AppInfo;
269
319
  readonly user: UserInfo;
270
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[];
271
334
  };
272
335
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,53 +1,54 @@
1
- {
2
- "name": "@tokenite/sdk",
3
- "version": "2.3.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