@tokenite/sdk 1.0.1 → 2.0.1
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 -21
- package/README.md +438 -422
- package/dist/admin/types.d.ts +0 -6
- package/dist/client.d.ts +10 -5
- package/dist/client.js +52 -61
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.js +66 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +22 -8
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Eran Broder
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Eran Broder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,173 +1,174 @@
|
|
|
1
|
-
# @tokenite/sdk
|
|
2
|
-
|
|
3
|
-
One wallet for all your AI apps. Users store their Anthropic, OpenAI, and Google API keys in Tokenite, then grant your app metered access via OAuth. You pay nothing for AI usage.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @tokenite/sdk
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { Tokenite, isProxyError } from '@tokenite/sdk';
|
|
15
|
-
|
|
16
|
-
const tw = Tokenite({
|
|
17
|
-
clientId: 'your-app-id',
|
|
18
|
-
clientSecret: 'your-app-secret',
|
|
19
|
-
redirectUri: 'https://yourapp.com/callback',
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// 1. Redirect the user to the consent screen
|
|
23
|
-
app.get('/login', (req, res) => res.redirect(tk.getAuthorizeUrl()));
|
|
24
|
-
|
|
25
|
-
// 2. Exchange the OAuth code for an access token in your callback
|
|
26
|
-
app.get('/callback', async (req, res) => {
|
|
27
|
-
const { access_token } = await tk.exchangeCode(req.query.code as string);
|
|
28
|
-
req.session.tokeniteToken = access_token;
|
|
29
|
-
res.redirect('/');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// 3. Call the LLM through the proxy
|
|
33
|
-
app.post('/chat', async (req, res) => {
|
|
34
|
-
const result = await tk.call({
|
|
35
|
-
accessToken: req.session.tokeniteToken,
|
|
36
|
-
provider: 'anthropic',
|
|
37
|
-
path: '/v1/messages',
|
|
38
|
-
body: {
|
|
39
|
-
model: 'claude-3-5-sonnet-latest',
|
|
40
|
-
max_tokens: 1024,
|
|
41
|
-
messages: [{ role: 'user', content: req.body.prompt }],
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (isProxyError(result)) {
|
|
46
|
-
return res.status(400).json({ error: result.error });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
res.json({ model: result.model, usage: result.usage, data: result.data });
|
|
50
|
-
});
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
The same `tk.call(...)` works for OpenAI and Google — change `provider` and `path`:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
await tk.call({
|
|
57
|
-
accessToken,
|
|
58
|
-
provider: 'openai',
|
|
59
|
-
path: '/v1/chat/completions',
|
|
60
|
-
body: { model: 'gpt-4o', messages: [...] },
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
await tk.call({
|
|
64
|
-
accessToken,
|
|
65
|
-
provider: 'google',
|
|
66
|
-
path: '/v1beta/models/gemini-1.5-pro:generateContent',
|
|
67
|
-
body: { contents: [...] },
|
|
68
|
-
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Modal flow (single-page apps)
|
|
72
|
-
|
|
73
|
-
If your app is a SPA, open the consent screen in an iframe modal:
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
const { code } = await tk.popup({ suggestedBudget: 5 });
|
|
77
|
-
|
|
78
|
-
// Send the code to your backend, which calls tk.exchangeCode(code).
|
|
79
|
-
// The exchange requires clientSecret and must never run in browser code.
|
|
80
|
-
await fetch('/api/auth/exchange', {
|
|
81
|
-
method: 'POST',
|
|
82
|
-
body: JSON.stringify({ code }),
|
|
83
|
-
});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Managed agents (Anthropic)
|
|
87
|
-
|
|
88
|
-
Tokenite proxies Anthropic's [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) surface — the `/v1/agents`, `/v1/environments`, `/v1/sessions`, `/v1/vaults`, and `/v1/
|
|
89
|
-
|
|
90
|
-
**Explicit consent required.** Because agent sessions run long-lived, billable server-side work on Anthropic (`$0.08/hr` session runtime on top of tokens), the proxy rejects agent-surface calls unless the app was created with `allowsManagedAgents: true`. The flag sits alongside the other app fields on creation:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
await admin.apps.create({
|
|
94
|
-
name: 'Life Coach',
|
|
95
|
-
callbackUrl: 'https://lifecoach.ai/callback',
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
1
|
+
# @tokenite/sdk
|
|
2
|
+
|
|
3
|
+
One wallet for all your AI apps. Users store their Anthropic, OpenAI, and Google API keys in Tokenite, then grant your app metered access via OAuth. You pay nothing for AI usage.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tokenite/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Tokenite, isProxyError } from '@tokenite/sdk';
|
|
15
|
+
|
|
16
|
+
const tw = Tokenite({
|
|
17
|
+
clientId: 'your-app-id',
|
|
18
|
+
clientSecret: 'your-app-secret',
|
|
19
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 1. Redirect the user to the consent screen
|
|
23
|
+
app.get('/login', (req, res) => res.redirect(tk.getAuthorizeUrl()));
|
|
24
|
+
|
|
25
|
+
// 2. Exchange the OAuth code for an access token in your callback
|
|
26
|
+
app.get('/callback', async (req, res) => {
|
|
27
|
+
const { access_token } = await tk.exchangeCode(req.query.code as string);
|
|
28
|
+
req.session.tokeniteToken = access_token;
|
|
29
|
+
res.redirect('/');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 3. Call the LLM through the proxy
|
|
33
|
+
app.post('/chat', async (req, res) => {
|
|
34
|
+
const result = await tk.call({
|
|
35
|
+
accessToken: req.session.tokeniteToken,
|
|
36
|
+
provider: 'anthropic',
|
|
37
|
+
path: '/v1/messages',
|
|
38
|
+
body: {
|
|
39
|
+
model: 'claude-3-5-sonnet-latest',
|
|
40
|
+
max_tokens: 1024,
|
|
41
|
+
messages: [{ role: 'user', content: req.body.prompt }],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (isProxyError(result)) {
|
|
46
|
+
return res.status(400).json({ error: result.error });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
res.json({ model: result.model, usage: result.usage, data: result.data });
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The same `tk.call(...)` works for OpenAI and Google — change `provider` and `path`:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
await tk.call({
|
|
57
|
+
accessToken,
|
|
58
|
+
provider: 'openai',
|
|
59
|
+
path: '/v1/chat/completions',
|
|
60
|
+
body: { model: 'gpt-4o', messages: [...] },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await tk.call({
|
|
64
|
+
accessToken,
|
|
65
|
+
provider: 'google',
|
|
66
|
+
path: '/v1beta/models/gemini-1.5-pro:generateContent',
|
|
67
|
+
body: { contents: [...] },
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Modal flow (single-page apps)
|
|
72
|
+
|
|
73
|
+
If your app is a SPA, open the consent screen in an iframe modal:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const { code } = await tk.popup({ suggestedBudget: 5 });
|
|
77
|
+
|
|
78
|
+
// Send the code to your backend, which calls tk.exchangeCode(code).
|
|
79
|
+
// The exchange requires clientSecret and must never run in browser code.
|
|
80
|
+
await fetch('/api/auth/exchange', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
body: JSON.stringify({ code }),
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Managed agents (Anthropic)
|
|
87
|
+
|
|
88
|
+
Tokenite proxies Anthropic's [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) surface — the `/v1/agents`, `/v1/environments`, `/v1/sessions`, `/v1/vaults`, `/v1/files`, and `/v1/skills` endpoints — under the same BYOK model as Messages. The app points the Anthropic SDK at `tk.proxyUrl('anthropic')`; the proxy injects the right beta header per surface (`managed-agents-2026-04-01` for agents/environments/sessions/vaults, `files-api-2025-04-14` for files, `skills-2025-10-02` for skills) and forwards calls to the user's Anthropic org. Client-supplied `anthropic-beta` headers are passed through and merged with the auto-injected ones, so callers can opt into additional betas (e.g. multi-version skills) without losing the proxy-managed defaults.
|
|
89
|
+
|
|
90
|
+
**Explicit consent required.** Because agent sessions run long-lived, billable server-side work on Anthropic (`$0.08/hr` session runtime on top of tokens), the proxy rejects agent-surface calls unless the app was created with `allowsManagedAgents: true`. The flag sits alongside the other app fields on creation:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
await admin.apps.create({
|
|
94
|
+
name: 'Life Coach',
|
|
95
|
+
callbackUrl: 'https://lifecoach.ai/callback',
|
|
96
|
+
modelStrategy: 'models',
|
|
97
|
+
allowedModels: ['claude-opus-4-7', 'claude-sonnet-4-6'],
|
|
98
|
+
allowsManagedAgents: true, // ← opt in
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Without it, every agent endpoint returns `403 AGENT_SCOPE_MISSING`.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
106
|
+
|
|
107
|
+
const anthropic = new Anthropic({
|
|
108
|
+
apiKey: accessToken,
|
|
109
|
+
baseURL: tk.proxyUrl('anthropic'),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Provision once per user.
|
|
113
|
+
const agent = await anthropic.beta.agents.create({
|
|
114
|
+
name: 'Life Coach',
|
|
115
|
+
model: 'claude-haiku-4-5',
|
|
116
|
+
system: 'You are a terse life coach.',
|
|
117
|
+
tools: [{ type: 'agent_toolset_20260401' }],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Spin up a session.
|
|
121
|
+
const env = await anthropic.beta.environments.create({ name: 'prod' });
|
|
122
|
+
const session = await anthropic.beta.sessions.create({
|
|
123
|
+
agent: agent.id,
|
|
124
|
+
environment_id: env.id,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Send events, stream responses — all proxied.
|
|
128
|
+
await anthropic.beta.sessions.events.post(session.id, {
|
|
129
|
+
type: 'user.message', content: 'What should I focus on today?',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
for await (const event of anthropic.beta.sessions.events.stream(session.id)) {
|
|
133
|
+
// ...
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Archive when done. This is what triggers Tokenite to pull the final
|
|
137
|
+
// usage totals from Anthropic and debit tokens + runtime against the budget.
|
|
138
|
+
await anthropic.beta.sessions.archive(session.id);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Tokenite tracks each session for attribution and auto-terminates running sessions when the user revokes the connection — otherwise Anthropic would keep billing `$0.08/hr` for the session runtime on top of tokens. The proxy bills the user's wallet at archive time using Anthropic's reported cumulative usage and runtime, plus the standard token rate from the model's pricing.
|
|
142
|
+
|
|
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
|
+
|
|
145
|
+
### Streaming responses
|
|
146
|
+
|
|
147
|
+
`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(...)`:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
151
|
+
|
|
152
|
+
const anthropic = new Anthropic({
|
|
153
|
+
apiKey: accessToken,
|
|
154
|
+
baseURL: tk.proxyUrl('anthropic'),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const stream = await anthropic.messages.stream({
|
|
158
|
+
model: 'claude-3-5-sonnet-latest',
|
|
159
|
+
max_tokens: 1024,
|
|
160
|
+
messages: [{ role: 'user', content: 'Tell me a story.' }],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
for await (const event of stream) {
|
|
164
|
+
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
165
|
+
process.stdout.write(event.delta.text);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## API
|
|
171
|
+
|
|
171
172
|
<!-- GEN:API -->
|
|
172
173
|
### `.getAuthorizeUrl(options?: AuthorizeOptions) => string`
|
|
173
174
|
|
|
@@ -196,259 +197,274 @@ The Tokenite dashboard base URL
|
|
|
196
197
|
### `.proxyBase`: `string`
|
|
197
198
|
|
|
198
199
|
The Tokenite proxy base URL
|
|
199
|
-
<!-- /GEN:API -->
|
|
200
|
-
|
|
201
|
-
## Types
|
|
202
|
-
|
|
200
|
+
<!-- /GEN:API -->
|
|
201
|
+
|
|
202
|
+
## Types
|
|
203
|
+
|
|
203
204
|
<!-- GEN:TYPES -->
|
|
204
205
|
```typescript
|
|
205
|
-
export type TokeniteConfig = {
|
|
206
|
-
/** Your app's client ID (from the Tokenite dashboard) */
|
|
207
|
-
readonly clientId: string;
|
|
208
|
-
/** Your app's client secret — only needed for server-side code exchange */
|
|
209
|
-
readonly clientSecret?: string;
|
|
210
|
-
/** The URL Tokenite redirects back to after authorization */
|
|
211
|
-
readonly redirectUri: string;
|
|
212
|
-
/** Tokenite base URL. Default: https://tokenite.ai */
|
|
213
|
-
readonly baseUrl?: string;
|
|
214
|
-
/** Tokenite proxy URL. Default: https://api.tokenite.ai */
|
|
215
|
-
readonly proxyUrl?: string;
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
export type AuthorizeOptions = {
|
|
219
|
-
/** Custom state parameter for CSRF protection. Auto-generated if not provided. */
|
|
220
|
-
readonly state?: string;
|
|
221
|
-
/** Suggested budget amount (user can override on consent screen) */
|
|
222
|
-
readonly suggestedBudget?: number;
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
export type PopupOptions = {
|
|
226
|
-
/** Suggested budget amount (user can override on consent screen) */
|
|
227
|
-
readonly suggestedBudget?: number;
|
|
228
|
-
/**
|
|
229
|
-
* How to host the consent screen.
|
|
230
|
-
*
|
|
231
|
-
* - `'iframe'` (default) — overlay an iframe modal in the current
|
|
232
|
-
* window. Requires the dashboard to allow being framed by your
|
|
233
|
-
* origin (`Content-Security-Policy: frame-ancestors`). Cleaner UX
|
|
234
|
-
* but blocked by `X-Frame-Options: DENY`.
|
|
235
|
-
* - `'window'` — open a separate browser popup window via
|
|
236
|
-
* `window.open`. Works regardless of frame policy, but the user
|
|
237
|
-
* may be prompted by their popup blocker.
|
|
238
|
-
*/
|
|
239
|
-
readonly mode?: 'iframe' | 'window';
|
|
240
|
-
/** Modal/popup width in pixels. Default: 480 */
|
|
241
|
-
readonly width?: number;
|
|
242
|
-
/** Modal/popup height in pixels. Default: 620 */
|
|
243
|
-
readonly height?: number;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
export type PopupResult = {
|
|
247
|
-
/**
|
|
248
|
-
* OAuth authorization code returned by the consent screen.
|
|
249
|
-
* Send this to your backend, which exchanges it for an access token
|
|
250
|
-
* via `tk.exchangeCode(code)`. The exchange requires `clientSecret`
|
|
251
|
-
* and must never run in browser code.
|
|
252
|
-
*/
|
|
253
|
-
readonly code: string;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
export type TokenResponse = {
|
|
257
|
-
readonly access_token: string;
|
|
258
|
-
readonly token_type: string;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
export type Provider = 'anthropic' | 'openai' | 'google' | 'grok' | 'bedrock';
|
|
262
|
-
|
|
263
|
-
export type ProxyCallOptions = {
|
|
264
|
-
/** The user's Tokenite access token (returned by `tk.exchangeCode()`) */
|
|
265
|
-
readonly accessToken: string;
|
|
266
|
-
/** Which LLM provider to call */
|
|
267
|
-
readonly provider: Provider;
|
|
268
|
-
/** Path on the provider's API (e.g. `/v1/messages`, `/v1/chat/completions`) */
|
|
269
|
-
readonly path: string;
|
|
270
|
-
/** HTTP method. Default: `POST` */
|
|
271
|
-
readonly method?: string;
|
|
272
|
-
/** Request body — the vendor's request shape, JSON-serialised by the SDK */
|
|
273
|
-
readonly body: unknown;
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
// ─── Unified proxy response types ───
|
|
277
|
-
//
|
|
278
|
-
// Every non-streaming response from the Tokenite proxy returns one of
|
|
279
|
-
// these shapes. SDK consumers can always check for `.error` to distinguish
|
|
280
|
-
// success from failure — no vendor-specific parsing required.
|
|
281
|
-
|
|
282
|
-
/** Normalised token counts (identical across all providers) */
|
|
283
|
-
export type ProxyUsage = {
|
|
284
|
-
/** Number of tokens in the prompt / input */
|
|
285
|
-
readonly inputTokens: number;
|
|
286
|
-
/** Number of tokens in the completion / output */
|
|
287
|
-
readonly outputTokens: number;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Successful proxy response.
|
|
292
|
-
*
|
|
293
|
-
* `data` contains the original vendor response body (e.g. Anthropic's
|
|
294
|
-
* message object, OpenAI's chat completion, etc.). `provider`, `model`,
|
|
295
|
-
* and `usage` are extracted and normalised by the proxy so you don't
|
|
296
|
-
* need to parse vendor-specific fields.
|
|
297
|
-
*/
|
|
298
|
-
export type ProxySuccess = {
|
|
299
|
-
/** Which LLM provider handled the request */
|
|
300
|
-
readonly provider: Provider;
|
|
301
|
-
/** The model that generated the response */
|
|
302
|
-
readonly model: string;
|
|
303
|
-
/** Normalised token usage, or null if the provider didn't report it */
|
|
304
|
-
readonly usage: ProxyUsage | null;
|
|
305
|
-
/** The original, unmodified response body from the LLM provider */
|
|
306
|
-
readonly data: unknown;
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Where the error originated.
|
|
311
|
-
*
|
|
312
|
-
* - `"proxy"` — Tokenite rejected the request (auth, budget, config).
|
|
313
|
-
* - `"provider"` — The upstream LLM returned an error (rate limit, overload, etc.).
|
|
314
|
-
*/
|
|
315
|
-
export type ErrorSource = 'proxy' | 'provider';
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Error response (both proxy-level and provider-level errors share this shape).
|
|
319
|
-
*
|
|
320
|
-
* **Proxy error codes** (`source: "proxy"`):
|
|
321
|
-
* | Code | HTTP | Description |
|
|
322
|
-
* |---|---|---|
|
|
323
|
-
* | `TOKEN_INVALID` | 401 | Missing or invalid bearer token |
|
|
324
|
-
* | `TOKEN_REVOKED` | 401 | Access token was revoked by the user |
|
|
325
|
-
* | `TOKEN_SUSPENDED` | 403 | Access suspended by the user |
|
|
326
|
-
* | `TOKEN_EXPIRED` | 401 | Access token past its expiration date |
|
|
327
|
-
* | `BUDGET_EXCEEDED` | 402 | Spending reached the user-defined budget limit |
|
|
328
|
-
* | `PROVIDER_KEY_MISSING` | 402 | No API key or credits available for the provider |
|
|
329
|
-
* | `CREDITS_DEPLETED` | 402 | Insufficient platform credits |
|
|
330
|
-
* | `APP_NOT_FOUND` | 404 | Application not found |
|
|
331
|
-
* | `MODEL_NOT_ALLOWED` | 403 | Model not in the app's allowed models list |
|
|
332
|
-
*
|
|
333
|
-
* **Provider error codes** (`source: "provider"`):
|
|
334
|
-
* | Code | HTTP | Description |
|
|
335
|
-
* |---|---|---|
|
|
336
|
-
* | `RATE_LIMITED` | 429 | Upstream rate limit hit — retry after backoff |
|
|
337
|
-
* | `CONTEXT_LENGTH_EXCEEDED` | 400 | Request exceeds model's context window |
|
|
338
|
-
* | `INVALID_REQUEST` | 400 | Provider rejected the request as malformed |
|
|
339
|
-
* | `AUTHENTICATION_FAILED` | 401 | Provider rejected the API key |
|
|
340
|
-
* | `PROVIDER_OVERLOADED` | 503 | Provider temporarily overloaded |
|
|
341
|
-
* | `CONTENT_FILTERED` | 400 | Content blocked by safety filter |
|
|
342
|
-
* | `PROVIDER_TIMEOUT` | 504 | Provider did not respond in time |
|
|
343
|
-
* | `PROVIDER_ERROR` | 502 | Catch-all for other provider errors |
|
|
344
|
-
*/
|
|
345
|
-
export type ProxyError = {
|
|
346
|
-
readonly error: {
|
|
347
|
-
/** Machine-readable error code */
|
|
348
|
-
readonly code: string;
|
|
349
|
-
/** Human-readable description */
|
|
350
|
-
readonly message: string;
|
|
351
|
-
/** Where the error originated */
|
|
352
|
-
readonly source: ErrorSource;
|
|
353
|
-
/** Optional structured context (e.g. `retryAfter`, `provider`, `providerMessage`) */
|
|
354
|
-
readonly details?: Record<string, unknown>;
|
|
355
|
-
};
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
/** Discriminated union for all non-streaming proxy responses */
|
|
359
|
-
export type ProxyResponse = ProxySuccess | ProxyError;
|
|
360
|
-
|
|
361
|
-
/** Type guard: returns true if the response is an error */
|
|
362
|
-
export const isProxyError = (response: ProxyResponse): response is ProxyError =>
|
|
363
|
-
'error' in response;
|
|
364
|
-
|
|
365
|
-
/** Type guard: returns true if the response is a success */
|
|
366
|
-
export const isProxySuccess = (response: ProxyResponse): response is ProxySuccess =>
|
|
367
|
-
'provider' in response;
|
|
368
|
-
|
|
369
|
-
// ─── Access context (tk.
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
readonly
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
*/
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
206
|
+
export type TokeniteConfig = {
|
|
207
|
+
/** Your app's client ID (from the Tokenite dashboard) */
|
|
208
|
+
readonly clientId: string;
|
|
209
|
+
/** Your app's client secret — only needed for server-side code exchange */
|
|
210
|
+
readonly clientSecret?: string;
|
|
211
|
+
/** The URL Tokenite redirects back to after authorization */
|
|
212
|
+
readonly redirectUri: string;
|
|
213
|
+
/** Tokenite base URL. Default: https://tokenite.ai */
|
|
214
|
+
readonly baseUrl?: string;
|
|
215
|
+
/** Tokenite proxy URL. Default: https://api.tokenite.ai */
|
|
216
|
+
readonly proxyUrl?: string;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export type AuthorizeOptions = {
|
|
220
|
+
/** Custom state parameter for CSRF protection. Auto-generated if not provided. */
|
|
221
|
+
readonly state?: string;
|
|
222
|
+
/** Suggested budget amount (user can override on consent screen) */
|
|
223
|
+
readonly suggestedBudget?: number;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export type PopupOptions = {
|
|
227
|
+
/** Suggested budget amount (user can override on consent screen) */
|
|
228
|
+
readonly suggestedBudget?: number;
|
|
229
|
+
/**
|
|
230
|
+
* How to host the consent screen.
|
|
231
|
+
*
|
|
232
|
+
* - `'iframe'` (default) — overlay an iframe modal in the current
|
|
233
|
+
* window. Requires the dashboard to allow being framed by your
|
|
234
|
+
* origin (`Content-Security-Policy: frame-ancestors`). Cleaner UX
|
|
235
|
+
* but blocked by `X-Frame-Options: DENY`.
|
|
236
|
+
* - `'window'` — open a separate browser popup window via
|
|
237
|
+
* `window.open`. Works regardless of frame policy, but the user
|
|
238
|
+
* may be prompted by their popup blocker.
|
|
239
|
+
*/
|
|
240
|
+
readonly mode?: 'iframe' | 'window';
|
|
241
|
+
/** Modal/popup width in pixels. Default: 480 */
|
|
242
|
+
readonly width?: number;
|
|
243
|
+
/** Modal/popup height in pixels. Default: 620 */
|
|
244
|
+
readonly height?: number;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export type PopupResult = {
|
|
248
|
+
/**
|
|
249
|
+
* OAuth authorization code returned by the consent screen.
|
|
250
|
+
* Send this to your backend, which exchanges it for an access token
|
|
251
|
+
* via `tk.exchangeCode(code)`. The exchange requires `clientSecret`
|
|
252
|
+
* and must never run in browser code.
|
|
253
|
+
*/
|
|
254
|
+
readonly code: string;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export type TokenResponse = {
|
|
258
|
+
readonly access_token: string;
|
|
259
|
+
readonly token_type: string;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export type Provider = 'anthropic' | 'openai' | 'google' | 'grok' | 'bedrock';
|
|
263
|
+
|
|
264
|
+
export type ProxyCallOptions = {
|
|
265
|
+
/** The user's Tokenite access token (returned by `tk.exchangeCode()`) */
|
|
266
|
+
readonly accessToken: string;
|
|
267
|
+
/** Which LLM provider to call */
|
|
268
|
+
readonly provider: Provider;
|
|
269
|
+
/** Path on the provider's API (e.g. `/v1/messages`, `/v1/chat/completions`) */
|
|
270
|
+
readonly path: string;
|
|
271
|
+
/** HTTP method. Default: `POST` */
|
|
272
|
+
readonly method?: string;
|
|
273
|
+
/** Request body — the vendor's request shape, JSON-serialised by the SDK */
|
|
274
|
+
readonly body: unknown;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// ─── Unified proxy response types ───
|
|
278
|
+
//
|
|
279
|
+
// Every non-streaming response from the Tokenite proxy returns one of
|
|
280
|
+
// these shapes. SDK consumers can always check for `.error` to distinguish
|
|
281
|
+
// success from failure — no vendor-specific parsing required.
|
|
282
|
+
|
|
283
|
+
/** Normalised token counts (identical across all providers) */
|
|
284
|
+
export type ProxyUsage = {
|
|
285
|
+
/** Number of tokens in the prompt / input */
|
|
286
|
+
readonly inputTokens: number;
|
|
287
|
+
/** Number of tokens in the completion / output */
|
|
288
|
+
readonly outputTokens: number;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Successful proxy response.
|
|
293
|
+
*
|
|
294
|
+
* `data` contains the original vendor response body (e.g. Anthropic's
|
|
295
|
+
* message object, OpenAI's chat completion, etc.). `provider`, `model`,
|
|
296
|
+
* and `usage` are extracted and normalised by the proxy so you don't
|
|
297
|
+
* need to parse vendor-specific fields.
|
|
298
|
+
*/
|
|
299
|
+
export type ProxySuccess = {
|
|
300
|
+
/** Which LLM provider handled the request */
|
|
301
|
+
readonly provider: Provider;
|
|
302
|
+
/** The model that generated the response */
|
|
303
|
+
readonly model: string;
|
|
304
|
+
/** Normalised token usage, or null if the provider didn't report it */
|
|
305
|
+
readonly usage: ProxyUsage | null;
|
|
306
|
+
/** The original, unmodified response body from the LLM provider */
|
|
307
|
+
readonly data: unknown;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Where the error originated.
|
|
312
|
+
*
|
|
313
|
+
* - `"proxy"` — Tokenite rejected the request (auth, budget, config).
|
|
314
|
+
* - `"provider"` — The upstream LLM returned an error (rate limit, overload, etc.).
|
|
315
|
+
*/
|
|
316
|
+
export type ErrorSource = 'proxy' | 'provider';
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Error response (both proxy-level and provider-level errors share this shape).
|
|
320
|
+
*
|
|
321
|
+
* **Proxy error codes** (`source: "proxy"`):
|
|
322
|
+
* | Code | HTTP | Description |
|
|
323
|
+
* |---|---|---|
|
|
324
|
+
* | `TOKEN_INVALID` | 401 | Missing or invalid bearer token |
|
|
325
|
+
* | `TOKEN_REVOKED` | 401 | Access token was revoked by the user |
|
|
326
|
+
* | `TOKEN_SUSPENDED` | 403 | Access suspended by the user |
|
|
327
|
+
* | `TOKEN_EXPIRED` | 401 | Access token past its expiration date |
|
|
328
|
+
* | `BUDGET_EXCEEDED` | 402 | Spending reached the user-defined budget limit |
|
|
329
|
+
* | `PROVIDER_KEY_MISSING` | 402 | No API key or credits available for the provider |
|
|
330
|
+
* | `CREDITS_DEPLETED` | 402 | Insufficient platform credits |
|
|
331
|
+
* | `APP_NOT_FOUND` | 404 | Application not found |
|
|
332
|
+
* | `MODEL_NOT_ALLOWED` | 403 | Model not in the app's allowed models list |
|
|
333
|
+
*
|
|
334
|
+
* **Provider error codes** (`source: "provider"`):
|
|
335
|
+
* | Code | HTTP | Description |
|
|
336
|
+
* |---|---|---|
|
|
337
|
+
* | `RATE_LIMITED` | 429 | Upstream rate limit hit — retry after backoff |
|
|
338
|
+
* | `CONTEXT_LENGTH_EXCEEDED` | 400 | Request exceeds model's context window |
|
|
339
|
+
* | `INVALID_REQUEST` | 400 | Provider rejected the request as malformed |
|
|
340
|
+
* | `AUTHENTICATION_FAILED` | 401 | Provider rejected the API key |
|
|
341
|
+
* | `PROVIDER_OVERLOADED` | 503 | Provider temporarily overloaded |
|
|
342
|
+
* | `CONTENT_FILTERED` | 400 | Content blocked by safety filter |
|
|
343
|
+
* | `PROVIDER_TIMEOUT` | 504 | Provider did not respond in time |
|
|
344
|
+
* | `PROVIDER_ERROR` | 502 | Catch-all for other provider errors |
|
|
345
|
+
*/
|
|
346
|
+
export type ProxyError = {
|
|
347
|
+
readonly error: {
|
|
348
|
+
/** Machine-readable error code */
|
|
349
|
+
readonly code: string;
|
|
350
|
+
/** Human-readable description */
|
|
351
|
+
readonly message: string;
|
|
352
|
+
/** Where the error originated */
|
|
353
|
+
readonly source: ErrorSource;
|
|
354
|
+
/** Optional structured context (e.g. `retryAfter`, `provider`, `providerMessage`) */
|
|
355
|
+
readonly details?: Record<string, unknown>;
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/** Discriminated union for all non-streaming proxy responses */
|
|
360
|
+
export type ProxyResponse = ProxySuccess | ProxyError;
|
|
361
|
+
|
|
362
|
+
/** Type guard: returns true if the response is an error */
|
|
363
|
+
export const isProxyError = (response: ProxyResponse): response is ProxyError =>
|
|
364
|
+
'error' in response;
|
|
365
|
+
|
|
366
|
+
/** Type guard: returns true if the response is a success */
|
|
367
|
+
export const isProxySuccess = (response: ProxyResponse): response is ProxySuccess =>
|
|
368
|
+
'provider' in response;
|
|
369
|
+
|
|
370
|
+
// ─── Access context (tk.getAccessContext) ───
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Identity of the user who holds this access token.
|
|
374
|
+
*
|
|
375
|
+
* Use `id` as the stable key for per-user state in your app — it survives
|
|
376
|
+
* token refreshes, re-logins, and device switches. `email` is suitable for
|
|
377
|
+
* display in your UI; treat it as user-controlled and re-fetch on each
|
|
378
|
+
* session if you cache it.
|
|
379
|
+
*/
|
|
380
|
+
export type UserInfo = {
|
|
381
|
+
/** Stable Tokenite user id (UUID) */
|
|
382
|
+
readonly id: string;
|
|
383
|
+
/** The user's email address as registered with Tokenite */
|
|
384
|
+
readonly email: string;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/** Visual + identity metadata for a single provider */
|
|
388
|
+
export type ProviderInfo = {
|
|
389
|
+
/** Stable provider id (same value as the `Provider` union) */
|
|
390
|
+
readonly id: Provider;
|
|
391
|
+
/** Human-readable name, e.g. "Anthropic" */
|
|
392
|
+
readonly displayName: string;
|
|
393
|
+
/** Brand colour (hex string, e.g. "#d97706") */
|
|
394
|
+
readonly color: string;
|
|
395
|
+
/** Absolute URL to the provider's logo (PNG or SVG) */
|
|
396
|
+
readonly logoUrl: string;
|
|
397
|
+
/** Whether the logo is a glyph/symbol or a full wordmark */
|
|
398
|
+
readonly logoStyle: 'symbol' | 'wordmark';
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/** Summary of the app the access token belongs to */
|
|
402
|
+
export type AppInfo = {
|
|
403
|
+
readonly id: string;
|
|
404
|
+
readonly name: string;
|
|
405
|
+
readonly description: string | null;
|
|
406
|
+
readonly websiteUrl: string | null;
|
|
407
|
+
/** Absolute URL to the app's icon (PNG/SVG). null when the developer hasn't set one — render initials or a generic glyph. */
|
|
408
|
+
readonly iconUrl: string | null;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Full access context for a single access token: the app it belongs to,
|
|
413
|
+
* the user who holds it, and the providers it can call.
|
|
414
|
+
*
|
|
415
|
+
* `providers` lists only the providers the user has an active key for —
|
|
416
|
+
* exactly the set that will succeed through `tk.call()` (budget permitting).
|
|
417
|
+
*
|
|
418
|
+
* `user` identifies the human who holds the token. Use `user.id` as the
|
|
419
|
+
* stable key for any per-user state in your app — it survives token
|
|
420
|
+
* refreshes and re-logins, unlike the access token itself.
|
|
421
|
+
*/
|
|
422
|
+
export type AccessContext = {
|
|
423
|
+
readonly app: AppInfo;
|
|
424
|
+
readonly user: UserInfo;
|
|
425
|
+
readonly providers: readonly ProviderInfo[];
|
|
410
426
|
};
|
|
411
427
|
```
|
|
412
|
-
<!-- /GEN:TYPES -->
|
|
413
|
-
|
|
414
|
-
## Error Codes
|
|
415
|
-
|
|
416
|
-
Non-streaming proxy responses return a unified error envelope:
|
|
417
|
-
|
|
418
|
-
```json
|
|
419
|
-
{ "error": { "code": "...", "message": "...", "source": "proxy" | "provider", "details": { } } }
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
`source` distinguishes errors raised by the Tokenite proxy from errors forwarded from the upstream LLM. Use the `isProxyError` type guard to narrow a `ProxyResponse` before reading `error.code`.
|
|
423
|
-
|
|
424
|
-
### Proxy errors (`source: "proxy"`)
|
|
425
|
-
|
|
426
|
-
| Code | Status | Meaning |
|
|
427
|
-
|------|--------|---------|
|
|
428
|
-
| `TOKEN_INVALID` | 401 | Missing or unrecognized access token |
|
|
429
|
-
| `TOKEN_REVOKED` | 401 | User revoked access |
|
|
430
|
-
| `TOKEN_EXPIRED` | 401 | Token expired (30-day lifetime) |
|
|
431
|
-
| `TOKEN_SUSPENDED` | 403 | User temporarily suspended access |
|
|
432
|
-
| `BUDGET_EXCEEDED` | 402 | Spending limit reached |
|
|
433
|
-
| `PROVIDER_KEY_MISSING` | 402 | No API key or credits available for the provider |
|
|
434
|
-
| `CREDITS_DEPLETED` | 402 | Insufficient platform credits |
|
|
435
|
-
| `MODEL_NOT_ALLOWED` | 403 | Model not in your app's allowed list |
|
|
436
|
-
| `AGENT_SCOPE_MISSING` | 403 | App doesn't have the managed-agents scope (set `allowsManagedAgents: true` at app creation) |
|
|
437
|
-
| `APP_NOT_FOUND` | 404 | Application not found |
|
|
438
|
-
|
|
439
|
-
### Provider errors (`source: "provider"`)
|
|
440
|
-
|
|
441
|
-
| Code | Status | Meaning |
|
|
442
|
-
|------|--------|---------|
|
|
443
|
-
| `RATE_LIMITED` | 429 | Upstream rate limit hit — retry after backoff |
|
|
444
|
-
| `CONTEXT_LENGTH_EXCEEDED` | 400 | Request exceeds the model's context window |
|
|
445
|
-
| `INVALID_REQUEST` | 400 | Provider rejected the request as malformed |
|
|
446
|
-
| `CONTENT_FILTERED` | 400 | Content blocked by the provider's safety filter |
|
|
447
|
-
| `AUTHENTICATION_FAILED` | 401 | Provider rejected the API key |
|
|
448
|
-
| `PROVIDER_OVERLOADED` | 503 | Provider temporarily overloaded |
|
|
449
|
-
| `PROVIDER_TIMEOUT` | 504 | Provider did not respond in time |
|
|
450
|
-
| `PROVIDER_ERROR` | 502 | Catch-all for other provider errors |
|
|
451
|
-
|
|
452
|
-
## License
|
|
453
|
-
|
|
454
|
-
MIT
|
|
428
|
+
<!-- /GEN:TYPES -->
|
|
429
|
+
|
|
430
|
+
## Error Codes
|
|
431
|
+
|
|
432
|
+
Non-streaming proxy responses return a unified error envelope:
|
|
433
|
+
|
|
434
|
+
```json
|
|
435
|
+
{ "error": { "code": "...", "message": "...", "source": "proxy" | "provider", "details": { } } }
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
`source` distinguishes errors raised by the Tokenite proxy from errors forwarded from the upstream LLM. Use the `isProxyError` type guard to narrow a `ProxyResponse` before reading `error.code`.
|
|
439
|
+
|
|
440
|
+
### Proxy errors (`source: "proxy"`)
|
|
441
|
+
|
|
442
|
+
| Code | Status | Meaning |
|
|
443
|
+
|------|--------|---------|
|
|
444
|
+
| `TOKEN_INVALID` | 401 | Missing or unrecognized access token |
|
|
445
|
+
| `TOKEN_REVOKED` | 401 | User revoked access |
|
|
446
|
+
| `TOKEN_EXPIRED` | 401 | Token expired (30-day lifetime) |
|
|
447
|
+
| `TOKEN_SUSPENDED` | 403 | User temporarily suspended access |
|
|
448
|
+
| `BUDGET_EXCEEDED` | 402 | Spending limit reached |
|
|
449
|
+
| `PROVIDER_KEY_MISSING` | 402 | No API key or credits available for the provider |
|
|
450
|
+
| `CREDITS_DEPLETED` | 402 | Insufficient platform credits |
|
|
451
|
+
| `MODEL_NOT_ALLOWED` | 403 | Model not in your app's allowed list |
|
|
452
|
+
| `AGENT_SCOPE_MISSING` | 403 | App doesn't have the managed-agents scope (set `allowsManagedAgents: true` at app creation) |
|
|
453
|
+
| `APP_NOT_FOUND` | 404 | Application not found |
|
|
454
|
+
|
|
455
|
+
### Provider errors (`source: "provider"`)
|
|
456
|
+
|
|
457
|
+
| Code | Status | Meaning |
|
|
458
|
+
|------|--------|---------|
|
|
459
|
+
| `RATE_LIMITED` | 429 | Upstream rate limit hit — retry after backoff |
|
|
460
|
+
| `CONTEXT_LENGTH_EXCEEDED` | 400 | Request exceeds the model's context window |
|
|
461
|
+
| `INVALID_REQUEST` | 400 | Provider rejected the request as malformed |
|
|
462
|
+
| `CONTENT_FILTERED` | 400 | Content blocked by the provider's safety filter |
|
|
463
|
+
| `AUTHENTICATION_FAILED` | 401 | Provider rejected the API key |
|
|
464
|
+
| `PROVIDER_OVERLOADED` | 503 | Provider temporarily overloaded |
|
|
465
|
+
| `PROVIDER_TIMEOUT` | 504 | Provider did not respond in time |
|
|
466
|
+
| `PROVIDER_ERROR` | 502 | Catch-all for other provider errors |
|
|
467
|
+
|
|
468
|
+
## License
|
|
469
|
+
|
|
470
|
+
MIT
|
package/dist/admin/types.d.ts
CHANGED
|
@@ -6,9 +6,6 @@ export type AppRecord = {
|
|
|
6
6
|
readonly builderId: string;
|
|
7
7
|
readonly name: string;
|
|
8
8
|
readonly callbackUrl: string;
|
|
9
|
-
readonly requiredProviders: readonly Provider[];
|
|
10
|
-
readonly preferredProviders: readonly Provider[];
|
|
11
|
-
readonly allowSubstitution: boolean;
|
|
12
9
|
readonly allowedModels?: readonly string[];
|
|
13
10
|
readonly modelStrategy: ModelStrategy;
|
|
14
11
|
readonly requiredTier?: RequiredTier;
|
|
@@ -24,9 +21,6 @@ export type CreatedApp = AppRecord & {
|
|
|
24
21
|
export type CreateAppInput = {
|
|
25
22
|
readonly name: string;
|
|
26
23
|
readonly callbackUrl: string;
|
|
27
|
-
readonly requiredProviders: readonly Provider[];
|
|
28
|
-
readonly preferredProviders?: readonly Provider[];
|
|
29
|
-
readonly allowSubstitution?: boolean;
|
|
30
24
|
readonly allowedModels?: readonly string[];
|
|
31
25
|
readonly modelStrategy?: ModelStrategy;
|
|
32
26
|
readonly requiredTier?: RequiredTier;
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TokeniteConfig, AuthorizeOptions, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyResponse,
|
|
1
|
+
import type { TokeniteConfig, AuthorizeOptions, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyResponse, AccessContext } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Tokenite client.
|
|
4
4
|
*
|
|
@@ -91,21 +91,26 @@ export declare const Tokenite: (config: TokeniteConfig) => {
|
|
|
91
91
|
*/
|
|
92
92
|
call: (options: ProxyCallOptions) => Promise<ProxyResponse>;
|
|
93
93
|
/**
|
|
94
|
-
* Fetch the access context for an access token: which app it
|
|
95
|
-
* to
|
|
94
|
+
* Fetch the full access context for an access token: which app it
|
|
95
|
+
* belongs to, who holds the token, and which providers it can call.
|
|
96
96
|
*
|
|
97
97
|
* The returned `providers` list is exactly the set that will succeed
|
|
98
98
|
* through `tk.call()` (budget permitting). Use it to render a picker,
|
|
99
99
|
* gate UI, or detect that the user is missing a required provider.
|
|
100
100
|
*
|
|
101
|
+
* The returned `user` identifies the token holder. Use `user.id` as
|
|
102
|
+
* the stable key for any per-user state in your app — it survives
|
|
103
|
+
* token refreshes and re-logins, unlike the access token itself.
|
|
104
|
+
*
|
|
101
105
|
* ```typescript
|
|
102
|
-
* const { app, providers } = await tk.
|
|
106
|
+
* const { app, user, providers } = await tk.getAccessContext(accessToken);
|
|
107
|
+
* console.log(`Signed in as ${user.email}`);
|
|
103
108
|
* for (const p of providers) {
|
|
104
109
|
* console.log(p.displayName, p.logoUrl);
|
|
105
110
|
* }
|
|
106
111
|
* ```
|
|
107
112
|
*/
|
|
108
|
-
|
|
113
|
+
getAccessContext: (accessToken: string) => Promise<AccessContext>;
|
|
109
114
|
/**
|
|
110
115
|
* Get the proxy URL for a specific provider.
|
|
111
116
|
* Use as `baseURL` in a vendor SDK for streaming requests, which
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { extractErrorMessage } from './error.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// `tokenite.ai` is the marketing site. The dashboard (which hosts the
|
|
3
|
+
// OAuth consent screen and the API the SDK calls — /oauth/authorize,
|
|
4
|
+
// /api/oauth/token) lives at `app.tokenite.ai`. Older versions of this
|
|
5
|
+
// SDK pointed at `tokenite.ai` and only worked because that hostname
|
|
6
|
+
// was aliased to the dashboard CloudFront; the marketing-site move on
|
|
7
|
+
// 2026-05-06 broke that. Marketing CF now redirects /oauth/* and
|
|
8
|
+
// /api/* to app.tokenite.ai, so older SDK installs continue to work,
|
|
9
|
+
// but new installs should hit the dashboard directly.
|
|
10
|
+
const DEFAULT_BASE_URL = 'https://app.tokenite.ai';
|
|
11
|
+
const DEFAULT_PROXY_URL = 'https://proxy.tokenite.ai';
|
|
4
12
|
const IFRAME_WIDTH = 480;
|
|
5
13
|
const IFRAME_HEIGHT = 620;
|
|
6
14
|
/**
|
|
@@ -156,27 +164,32 @@ export const Tokenite = (config) => {
|
|
|
156
164
|
return (await response.json());
|
|
157
165
|
},
|
|
158
166
|
/**
|
|
159
|
-
* Fetch the access context for an access token: which app it
|
|
160
|
-
* to
|
|
167
|
+
* Fetch the full access context for an access token: which app it
|
|
168
|
+
* belongs to, who holds the token, and which providers it can call.
|
|
161
169
|
*
|
|
162
170
|
* The returned `providers` list is exactly the set that will succeed
|
|
163
171
|
* through `tk.call()` (budget permitting). Use it to render a picker,
|
|
164
172
|
* gate UI, or detect that the user is missing a required provider.
|
|
165
173
|
*
|
|
174
|
+
* The returned `user` identifies the token holder. Use `user.id` as
|
|
175
|
+
* the stable key for any per-user state in your app — it survives
|
|
176
|
+
* token refreshes and re-logins, unlike the access token itself.
|
|
177
|
+
*
|
|
166
178
|
* ```typescript
|
|
167
|
-
* const { app, providers } = await tk.
|
|
179
|
+
* const { app, user, providers } = await tk.getAccessContext(accessToken);
|
|
180
|
+
* console.log(`Signed in as ${user.email}`);
|
|
168
181
|
* for (const p of providers) {
|
|
169
182
|
* console.log(p.displayName, p.logoUrl);
|
|
170
183
|
* }
|
|
171
184
|
* ```
|
|
172
185
|
*/
|
|
173
|
-
|
|
174
|
-
const response = await fetch(`${proxyBase}/me
|
|
186
|
+
getAccessContext: async (accessToken) => {
|
|
187
|
+
const response = await fetch(`${proxyBase}/me`, {
|
|
175
188
|
headers: { 'authorization': `Bearer ${accessToken}` },
|
|
176
189
|
});
|
|
177
190
|
if (!response.ok) {
|
|
178
191
|
const body = await response.json().catch(() => null);
|
|
179
|
-
throw new Error(extractErrorMessage(body, `Failed to fetch
|
|
192
|
+
throw new Error(extractErrorMessage(body, `Failed to fetch access context (${response.status})`));
|
|
180
193
|
}
|
|
181
194
|
const data = (await response.json());
|
|
182
195
|
return {
|
|
@@ -203,25 +216,33 @@ const generateState = () => Array.from(crypto.getRandomValues(new Uint8Array(16)
|
|
|
203
216
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
204
217
|
.join('');
|
|
205
218
|
const absoluteUrl = (maybeRelative, base) => /^https?:\/\//i.test(maybeRelative) ? maybeRelative : `${base}${maybeRelative}`;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
const handleAuthMessage = (event, baseUrl, resolve, reject, cleanup) => {
|
|
220
|
+
if (event.origin !== baseUrl)
|
|
221
|
+
return;
|
|
222
|
+
const data = event.data;
|
|
223
|
+
if (data.type === 'tokenite:auth-success' && data.code) {
|
|
224
|
+
cleanup();
|
|
225
|
+
resolve({ code: data.code });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (data.type === 'tokenite:auth-error') {
|
|
229
|
+
cleanup();
|
|
230
|
+
reject(new Error(data.error ?? 'Authorization denied'));
|
|
231
|
+
}
|
|
232
|
+
};
|
|
212
233
|
const openIframeModal = (url, width, height, baseUrl) => {
|
|
213
234
|
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;
|
|
235
|
+
overlay.style.cssText = `
|
|
236
|
+
position: fixed; inset: 0; z-index: 999999;
|
|
237
|
+
background: rgba(0,0,0,0.5); backdrop-filter: blur(2px);
|
|
238
|
+
display: flex; align-items: center; justify-content: center;
|
|
218
239
|
`;
|
|
219
240
|
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);
|
|
241
|
+
container.style.cssText = `
|
|
242
|
+
background: white; border-radius: 12px; overflow: hidden;
|
|
243
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
244
|
+
width: ${width}px; max-width: calc(100vw - 32px);
|
|
245
|
+
height: ${height}px; max-height: calc(100vh - 32px);
|
|
225
246
|
`;
|
|
226
247
|
const iframe = document.createElement('iframe');
|
|
227
248
|
iframe.src = url;
|
|
@@ -231,23 +252,11 @@ const openIframeModal = (url, width, height, baseUrl) => {
|
|
|
231
252
|
overlay.appendChild(container);
|
|
232
253
|
document.body.appendChild(overlay);
|
|
233
254
|
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
255
|
const cleanup = () => {
|
|
248
256
|
window.removeEventListener('message', onMessage);
|
|
249
257
|
overlay.remove();
|
|
250
258
|
};
|
|
259
|
+
const onMessage = (event) => handleAuthMessage(event, baseUrl, resolve, reject, cleanup);
|
|
251
260
|
overlay.addEventListener('click', (e) => {
|
|
252
261
|
if (e.target === overlay) {
|
|
253
262
|
cleanup();
|
|
@@ -264,23 +273,13 @@ const openWindowPopup = (url, width, height, baseUrl, redirectUri) => {
|
|
|
264
273
|
if (!popup)
|
|
265
274
|
return Promise.reject(new Error('Popup blocked'));
|
|
266
275
|
return new Promise((resolve, reject) => {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
}
|
|
276
|
+
const cleanup = () => {
|
|
277
|
+
window.removeEventListener('message', onMessage);
|
|
278
|
+
clearInterval(poll);
|
|
279
|
+
if (!popup.closed)
|
|
280
|
+
popup.close();
|
|
279
281
|
};
|
|
280
|
-
|
|
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.
|
|
282
|
+
const onMessage = (event) => handleAuthMessage(event, baseUrl, resolve, reject, cleanup);
|
|
284
283
|
const poll = setInterval(() => {
|
|
285
284
|
if (popup.closed) {
|
|
286
285
|
cleanup();
|
|
@@ -297,16 +296,8 @@ const openWindowPopup = (url, width, height, baseUrl, redirectUri) => {
|
|
|
297
296
|
}
|
|
298
297
|
}
|
|
299
298
|
}
|
|
300
|
-
catch {
|
|
301
|
-
// cross-origin — keep polling
|
|
302
|
-
}
|
|
299
|
+
catch { /* cross-origin until navigation back to redirectUri */ }
|
|
303
300
|
}, 300);
|
|
304
|
-
const cleanup = () => {
|
|
305
|
-
window.removeEventListener('message', onMessage);
|
|
306
|
-
clearInterval(poll);
|
|
307
|
-
if (!popup.closed)
|
|
308
|
-
popup.close();
|
|
309
|
-
};
|
|
310
301
|
window.addEventListener('message', onMessage);
|
|
311
302
|
});
|
|
312
303
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
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,
|
|
2
|
+
export type { TokeniteConfig, AuthorizeOptions, PopupOptions, PopupResult, TokenResponse, Provider, ProxyCallOptions, ProxyUsage, ProxySuccess, ProxyError, ProxyResponse, ErrorSource, ProviderInfo, AppInfo, UserInfo, AccessContext, } from './types.js';
|
|
3
3
|
export { isProxyError, isProxySuccess } from './types.js';
|
|
4
4
|
export { extractErrorMessage } from './error.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/types.d.ts
CHANGED
|
@@ -140,6 +140,20 @@ export type ProxyResponse = ProxySuccess | ProxyError;
|
|
|
140
140
|
export declare const isProxyError: (response: ProxyResponse) => response is ProxyError;
|
|
141
141
|
/** Type guard: returns true if the response is a success */
|
|
142
142
|
export declare const isProxySuccess: (response: ProxyResponse) => response is ProxySuccess;
|
|
143
|
+
/**
|
|
144
|
+
* Identity of the user who holds this access token.
|
|
145
|
+
*
|
|
146
|
+
* Use `id` as the stable key for per-user state in your app — it survives
|
|
147
|
+
* token refreshes, re-logins, and device switches. `email` is suitable for
|
|
148
|
+
* display in your UI; treat it as user-controlled and re-fetch on each
|
|
149
|
+
* session if you cache it.
|
|
150
|
+
*/
|
|
151
|
+
export type UserInfo = {
|
|
152
|
+
/** Stable Tokenite user id (UUID) */
|
|
153
|
+
readonly id: string;
|
|
154
|
+
/** The user's email address as registered with Tokenite */
|
|
155
|
+
readonly email: string;
|
|
156
|
+
};
|
|
143
157
|
/** Visual + identity metadata for a single provider */
|
|
144
158
|
export type ProviderInfo = {
|
|
145
159
|
/** Stable provider id (same value as the `Provider` union) */
|
|
@@ -161,21 +175,21 @@ export type AppInfo = {
|
|
|
161
175
|
readonly websiteUrl: string | null;
|
|
162
176
|
/** Absolute URL to the app's icon (PNG/SVG). null when the developer hasn't set one — render initials or a generic glyph. */
|
|
163
177
|
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
178
|
};
|
|
171
179
|
/**
|
|
172
|
-
* Full access context for a single access token
|
|
180
|
+
* Full access context for a single access token: the app it belongs to,
|
|
181
|
+
* the user who holds it, and the providers it can call.
|
|
173
182
|
*
|
|
174
183
|
* `providers` lists only the providers the user has an active key for —
|
|
175
184
|
* exactly the set that will succeed through `tk.call()` (budget permitting).
|
|
185
|
+
*
|
|
186
|
+
* `user` identifies the human who holds the token. Use `user.id` as the
|
|
187
|
+
* stable key for any per-user state in your app — it survives token
|
|
188
|
+
* refreshes and re-logins, unlike the access token itself.
|
|
176
189
|
*/
|
|
177
|
-
export type
|
|
190
|
+
export type AccessContext = {
|
|
178
191
|
readonly app: AppInfo;
|
|
192
|
+
readonly user: UserInfo;
|
|
179
193
|
readonly providers: readonly ProviderInfo[];
|
|
180
194
|
};
|
|
181
195
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED