@datalayer/agent-runtimes 0.0.5 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -22
- package/lib/components/chat/components/AgentDetails.d.ts +15 -2
- package/lib/components/chat/components/AgentDetails.js +9 -93
- package/lib/components/chat/components/AgentIdentity.d.ts +92 -0
- package/lib/components/chat/components/AgentIdentity.js +318 -0
- package/lib/components/chat/components/Chat.d.ts +24 -1
- package/lib/components/chat/components/Chat.js +41 -19
- package/lib/components/chat/components/ChatFloating.d.ts +6 -1
- package/lib/components/chat/components/ChatFloating.js +12 -6
- package/lib/components/chat/components/ContextDistribution.d.ts +47 -0
- package/lib/components/chat/components/ContextDistribution.js +146 -0
- package/lib/components/chat/components/ContextUsage.d.ts +33 -0
- package/lib/components/chat/components/ContextUsage.js +127 -0
- package/lib/components/chat/components/base/ChatBase.d.ts +51 -1
- package/lib/components/chat/components/base/ChatBase.js +278 -74
- package/lib/components/chat/components/display/ToolCallDisplay.d.ts +16 -2
- package/lib/components/chat/components/display/ToolCallDisplay.js +148 -6
- package/lib/components/chat/components/display/index.d.ts +1 -1
- package/lib/components/chat/components/display/index.js +1 -1
- package/lib/components/chat/components/elements/ChatInputPrompt.d.ts +12 -1
- package/lib/components/chat/components/elements/ChatInputPrompt.js +8 -3
- package/lib/components/chat/components/index.d.ts +3 -0
- package/lib/components/chat/components/index.js +3 -0
- package/lib/components/chat/components/parts/ToolPart.d.ts +1 -1
- package/lib/components/chat/components/parts/ToolPart.js +142 -6
- package/lib/components/chat/index.d.ts +1 -1
- package/lib/components/chat/index.js +1 -1
- package/lib/components/chat/protocols/A2AAdapter.d.ts +9 -0
- package/lib/components/chat/protocols/A2AAdapter.js +13 -2
- package/lib/components/chat/protocols/ACPAdapter.d.ts +9 -0
- package/lib/components/chat/protocols/ACPAdapter.js +13 -2
- package/lib/components/chat/protocols/AGUIAdapter.d.ts +9 -0
- package/lib/components/chat/protocols/AGUIAdapter.js +19 -1
- package/lib/components/chat/protocols/VercelAIAdapter.d.ts +7 -0
- package/lib/components/chat/protocols/VercelAIAdapter.js +19 -0
- package/lib/components/chat/types/execution.d.ts +78 -0
- package/lib/components/chat/types/execution.js +64 -0
- package/lib/components/chat/types/index.d.ts +1 -0
- package/lib/components/chat/types/index.js +1 -0
- package/lib/components/chat/types/protocol.d.ts +9 -0
- package/lib/components/ui/pagination.d.ts +2 -2
- package/lib/components/ui/pagination.js +4 -4
- package/lib/components/ui/resizable.d.ts +4 -4
- package/lib/components/ui/resizable.js +4 -4
- package/lib/examples/A2UiRestaurantExample.js +2 -2
- package/lib/examples/AgUiAgenticExample.js +2 -2
- package/lib/examples/AgUiBackendToolRenderingExample.js +2 -2
- package/lib/examples/AgUiHaikuGenUIExample.js +2 -2
- package/lib/examples/AgUiHumanInTheLoopExample.js +2 -2
- package/lib/examples/AgUiSharedStateExample.js +2 -2
- package/lib/examples/AgUiToolsBasedGenUIExample.js +2 -2
- package/lib/examples/AgentRuntimeCustomExample.js +2 -2
- package/lib/examples/AgentRuntimeLexical2Example.js +2 -1
- package/lib/examples/AgentRuntimeLexicalExample.js +5 -2
- package/lib/examples/AgentRuntimeLexicalSidebarExample.js +4 -2
- package/lib/examples/AgentRuntimeNotebookExample.js +1 -1
- package/lib/examples/AgentRuntimeStandaloneExample.js +2 -2
- package/lib/examples/AgentSpaceFormExample.d.ts +70 -2
- package/lib/examples/AgentSpaceFormExample.js +204 -35
- package/lib/examples/CopilotKitLexicalExample.js +2 -1
- package/lib/examples/components/AgentConfiguration.d.ts +37 -0
- package/lib/examples/components/AgentConfiguration.js +239 -8
- package/lib/examples/components/Header.d.ts +0 -2
- package/lib/examples/components/Header.js +2 -16
- package/lib/examples/components/LexicalEditor.js +2 -1
- package/lib/examples/components/MockFileBrowser.js +6 -2
- package/lib/examples/components/index.d.ts +0 -1
- package/lib/examples/components/index.js +0 -1
- package/lib/examples/example-selector.js +0 -1
- package/lib/examples/index.d.ts +0 -1
- package/lib/examples/index.js +0 -1
- package/lib/examples/lexical/editorConfig.d.ts +3 -2
- package/lib/examples/lexical/editorConfig.js +7 -1
- package/lib/examples/lexical/initial-content.json +2210 -0
- package/lib/examples/main.js +15 -1
- package/lib/identity/IdentityConnect.d.ts +90 -0
- package/lib/identity/IdentityConnect.js +316 -0
- package/lib/identity/OAuthCallback.d.ts +58 -0
- package/lib/identity/OAuthCallback.js +223 -0
- package/lib/identity/dcr.d.ts +257 -0
- package/lib/identity/dcr.js +282 -0
- package/lib/identity/identityStore.d.ts +72 -0
- package/lib/identity/identityStore.js +529 -0
- package/lib/identity/index.d.ts +46 -0
- package/lib/identity/index.js +17 -0
- package/lib/identity/pkce.d.ts +30 -0
- package/lib/identity/pkce.js +65 -0
- package/lib/identity/types.d.ts +293 -0
- package/lib/identity/types.js +73 -0
- package/lib/identity/useIdentity.d.ts +108 -0
- package/lib/identity/useIdentity.js +323 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/lib/utils.js +1 -1
- package/lib/renderers/a2ui/lib/utils.js +1 -1
- package/lib/runtime/index.d.ts +35 -0
- package/lib/runtime/index.js +40 -0
- package/lib/runtime/runtimeStore.d.ts +77 -0
- package/lib/runtime/runtimeStore.js +184 -0
- package/lib/runtime/types.d.ts +84 -0
- package/lib/runtime/types.js +15 -0
- package/lib/runtime/useAgentConnection.d.ts +46 -0
- package/lib/runtime/useAgentConnection.js +112 -0
- package/lib/runtime/useAgentRuntime.d.ts +94 -0
- package/lib/runtime/useAgentRuntime.js +125 -0
- package/lib/test-setup.d.ts +1 -1
- package/lib/test-setup.js +1 -0
- package/lib/tools/adapters/agent-runtimes/AgentRuntimesToolAdapter.js +32 -1
- package/lib/tools/adapters/agent-runtimes/lexicalHooks.d.ts +6 -0
- package/lib/tools/adapters/agent-runtimes/lexicalHooks.js +16 -17
- package/package.json +20 -7
- package/patches/@datalayer+jupyter-lexical+1.0.8.patch +11628 -0
- package/patches/@datalayer+jupyter-react+2.0.2.patch +5338 -0
- package/lib/examples/AgentSpaceHomeExample.d.ts +0 -8
- package/lib/examples/AgentSpaceHomeExample.js +0 -171
- package/lib/examples/components/AgentsDataTable.d.ts +0 -13
- package/lib/examples/components/AgentsDataTable.js +0 -74
- package/lib/examples/components/Rating.d.ts +0 -14
- package/lib/examples/components/Rating.js +0 -12
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Zustand store for identity management.
|
|
7
|
+
*
|
|
8
|
+
* Provides OAuth 2.1 identity management with PKCE support.
|
|
9
|
+
*
|
|
10
|
+
* @module identity/identityStore
|
|
11
|
+
*/
|
|
12
|
+
import { create } from 'zustand';
|
|
13
|
+
import { devtools, subscribeWithSelector, persist } from 'zustand/middleware';
|
|
14
|
+
import { GITHUB_PROVIDER, GOOGLE_PROVIDER, KAGGLE_PROVIDER } from './types';
|
|
15
|
+
import { generatePKCEPair, generateState } from './pkce';
|
|
16
|
+
/**
|
|
17
|
+
* Storage key for persisted identity data
|
|
18
|
+
*/
|
|
19
|
+
const STORAGE_KEY = 'datalayer-agent-identities';
|
|
20
|
+
/**
|
|
21
|
+
* Token exchange endpoint on the agent-runtimes server
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_TOKEN_EXCHANGE_ENDPOINT = '/api/v1/identity/oauth/token';
|
|
24
|
+
const DEFAULT_USER_INFO_ENDPOINT = '/api/v1/identity/oauth/userinfo';
|
|
25
|
+
const DEFAULT_REVOKE_ENDPOINT = '/api/v1/identity/oauth/revoke';
|
|
26
|
+
/**
|
|
27
|
+
* Initial state
|
|
28
|
+
*/
|
|
29
|
+
const initialState = {
|
|
30
|
+
identities: new Map(),
|
|
31
|
+
pendingAuthorization: null,
|
|
32
|
+
providerConfigs: new Map(),
|
|
33
|
+
isLoading: false,
|
|
34
|
+
error: null,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Build OAuth authorization URL with PKCE
|
|
38
|
+
*/
|
|
39
|
+
function buildAuthorizationUrl(config, scopes, state, codeChallenge) {
|
|
40
|
+
const params = new URLSearchParams({
|
|
41
|
+
client_id: config.clientId,
|
|
42
|
+
redirect_uri: config.redirectUri,
|
|
43
|
+
response_type: 'code',
|
|
44
|
+
scope: scopes.join(' '),
|
|
45
|
+
state: state,
|
|
46
|
+
code_challenge: codeChallenge,
|
|
47
|
+
code_challenge_method: 'S256',
|
|
48
|
+
...config.additionalParams,
|
|
49
|
+
});
|
|
50
|
+
return `${config.authorizationUrl}?${params.toString()}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create the identity store
|
|
54
|
+
*/
|
|
55
|
+
export const useIdentityStore = create()(devtools(subscribeWithSelector(persist((set, get) => ({
|
|
56
|
+
...initialState,
|
|
57
|
+
configureProvider: (config) => {
|
|
58
|
+
set(state => {
|
|
59
|
+
const newConfigs = new Map(state.providerConfigs);
|
|
60
|
+
newConfigs.set(config.provider, config);
|
|
61
|
+
return { providerConfigs: newConfigs };
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
startAuthorization: async (provider, scopes, options) => {
|
|
65
|
+
const state = get();
|
|
66
|
+
const config = state.providerConfigs.get(provider);
|
|
67
|
+
if (!config) {
|
|
68
|
+
throw new Error(`Provider ${provider} is not configured`);
|
|
69
|
+
}
|
|
70
|
+
// Generate PKCE pair
|
|
71
|
+
const { codeVerifier, codeChallenge } = await generatePKCEPair();
|
|
72
|
+
const stateParam = generateState();
|
|
73
|
+
const effectiveScopes = scopes || config.defaultScopes;
|
|
74
|
+
// Build authorization URL
|
|
75
|
+
const authUrl = buildAuthorizationUrl(config, effectiveScopes, stateParam, codeChallenge);
|
|
76
|
+
// Create pending authorization
|
|
77
|
+
const request = {
|
|
78
|
+
requestId: `auth-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
79
|
+
provider,
|
|
80
|
+
authUrl,
|
|
81
|
+
state: stateParam,
|
|
82
|
+
codeVerifier,
|
|
83
|
+
scopes: effectiveScopes,
|
|
84
|
+
requestedAt: Date.now(),
|
|
85
|
+
onComplete: options?.onComplete,
|
|
86
|
+
onError: options?.onError,
|
|
87
|
+
};
|
|
88
|
+
set({ pendingAuthorization: request, error: null });
|
|
89
|
+
return authUrl;
|
|
90
|
+
},
|
|
91
|
+
completeAuthorization: async (callback) => {
|
|
92
|
+
const state = get();
|
|
93
|
+
const pending = state.pendingAuthorization;
|
|
94
|
+
if (!pending) {
|
|
95
|
+
throw new Error('No pending authorization request');
|
|
96
|
+
}
|
|
97
|
+
// Verify state parameter (CSRF protection)
|
|
98
|
+
if (callback.state !== pending.state) {
|
|
99
|
+
const error = new Error('State mismatch - possible CSRF attack');
|
|
100
|
+
pending.onError?.(error);
|
|
101
|
+
set({ pendingAuthorization: null, error });
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
// Check for OAuth error
|
|
105
|
+
if (callback.error) {
|
|
106
|
+
const error = new Error(callback.errorDescription || callback.error);
|
|
107
|
+
pending.onError?.(error);
|
|
108
|
+
set({ pendingAuthorization: null, error });
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
set({ isLoading: true });
|
|
112
|
+
try {
|
|
113
|
+
const config = state.providerConfigs.get(pending.provider);
|
|
114
|
+
if (!config) {
|
|
115
|
+
throw new Error(`Provider ${pending.provider} is not configured`);
|
|
116
|
+
}
|
|
117
|
+
// Exchange code for token via backend proxy
|
|
118
|
+
// This keeps client_secret secure on the server
|
|
119
|
+
const baseUrl = config.redirectUri
|
|
120
|
+
.split('/')
|
|
121
|
+
.slice(0, 3)
|
|
122
|
+
.join('/');
|
|
123
|
+
const tokenResponse = await fetch(`${baseUrl}${DEFAULT_TOKEN_EXCHANGE_ENDPOINT}`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
provider: pending.provider,
|
|
128
|
+
code: callback.code,
|
|
129
|
+
code_verifier: pending.codeVerifier,
|
|
130
|
+
redirect_uri: config.redirectUri,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
if (!tokenResponse.ok) {
|
|
134
|
+
const errorData = await tokenResponse.json().catch(() => ({}));
|
|
135
|
+
throw new Error(errorData.detail ||
|
|
136
|
+
`Token exchange failed: ${tokenResponse.status}`);
|
|
137
|
+
}
|
|
138
|
+
const tokenData = await tokenResponse.json();
|
|
139
|
+
const token = {
|
|
140
|
+
accessToken: tokenData.access_token,
|
|
141
|
+
tokenType: tokenData.token_type || 'Bearer',
|
|
142
|
+
expiresAt: tokenData.expires_in
|
|
143
|
+
? Date.now() + tokenData.expires_in * 1000
|
|
144
|
+
: undefined,
|
|
145
|
+
refreshToken: tokenData.refresh_token,
|
|
146
|
+
scopes: pending.scopes,
|
|
147
|
+
};
|
|
148
|
+
// Fetch user info
|
|
149
|
+
let userInfo;
|
|
150
|
+
if (config.userInfoUrl) {
|
|
151
|
+
try {
|
|
152
|
+
const userInfoResponse = await fetch(`${baseUrl}${DEFAULT_USER_INFO_ENDPOINT}`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
provider: pending.provider,
|
|
157
|
+
access_token: token.accessToken,
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
if (userInfoResponse.ok) {
|
|
161
|
+
const userInfoData = await userInfoResponse.json();
|
|
162
|
+
// Map provider-specific user info fields to common format
|
|
163
|
+
// - GitHub: login, avatar_url, html_url
|
|
164
|
+
// - Google: sub (id), picture, profile
|
|
165
|
+
// - Others: username, id, etc.
|
|
166
|
+
userInfo = {
|
|
167
|
+
id: userInfoData.id?.toString() || userInfoData.sub,
|
|
168
|
+
username: userInfoData.login || userInfoData.username, // GitHub uses 'login'
|
|
169
|
+
name: userInfoData.name,
|
|
170
|
+
email: userInfoData.email,
|
|
171
|
+
avatarUrl: userInfoData.avatar_url || userInfoData.picture, // GitHub: avatar_url, Google: picture
|
|
172
|
+
profileUrl: userInfoData.html_url || userInfoData.profile, // GitHub: html_url, Google: profile
|
|
173
|
+
raw: userInfoData,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
console.warn('Failed to fetch user info:', e);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Create identity
|
|
182
|
+
const identity = {
|
|
183
|
+
provider: pending.provider,
|
|
184
|
+
displayName: config.displayName,
|
|
185
|
+
iconUrl: config.iconUrl,
|
|
186
|
+
scopes: pending.scopes,
|
|
187
|
+
isConnected: true,
|
|
188
|
+
connectedAt: Date.now(),
|
|
189
|
+
userInfo,
|
|
190
|
+
token,
|
|
191
|
+
};
|
|
192
|
+
// Update store
|
|
193
|
+
set(state => {
|
|
194
|
+
const newIdentities = new Map(state.identities);
|
|
195
|
+
newIdentities.set(pending.provider, identity);
|
|
196
|
+
return {
|
|
197
|
+
identities: newIdentities,
|
|
198
|
+
pendingAuthorization: null,
|
|
199
|
+
isLoading: false,
|
|
200
|
+
error: null,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
// Call completion callback
|
|
204
|
+
pending.onComplete?.(identity);
|
|
205
|
+
return identity;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
209
|
+
pending.onError?.(err);
|
|
210
|
+
set({ isLoading: false, error: err, pendingAuthorization: null });
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
cancelAuthorization: () => {
|
|
215
|
+
const pending = get().pendingAuthorization;
|
|
216
|
+
if (pending?.onError) {
|
|
217
|
+
pending.onError(new Error('Authorization cancelled'));
|
|
218
|
+
}
|
|
219
|
+
set({ pendingAuthorization: null });
|
|
220
|
+
},
|
|
221
|
+
connectWithToken: async (provider, token, options) => {
|
|
222
|
+
set({ isLoading: true, error: null });
|
|
223
|
+
try {
|
|
224
|
+
// Create token object (token-based auth doesn't expire)
|
|
225
|
+
const oauthToken = {
|
|
226
|
+
accessToken: token,
|
|
227
|
+
tokenType: 'Bearer',
|
|
228
|
+
scopes: [], // Token-based auth doesn't have scopes
|
|
229
|
+
};
|
|
230
|
+
// Provider display config
|
|
231
|
+
const displayConfig = {
|
|
232
|
+
kaggle: {
|
|
233
|
+
displayName: 'Kaggle',
|
|
234
|
+
iconUrl: 'https://www.kaggle.com/static/images/favicon.ico',
|
|
235
|
+
},
|
|
236
|
+
}[provider] || { displayName: provider, iconUrl: undefined };
|
|
237
|
+
// Create identity
|
|
238
|
+
const identity = {
|
|
239
|
+
provider,
|
|
240
|
+
authType: 'token',
|
|
241
|
+
displayName: options?.displayName || displayConfig.displayName,
|
|
242
|
+
iconUrl: options?.iconUrl || displayConfig.iconUrl,
|
|
243
|
+
scopes: [],
|
|
244
|
+
isConnected: true,
|
|
245
|
+
connectedAt: Date.now(),
|
|
246
|
+
userInfo: options?.userInfo,
|
|
247
|
+
token: oauthToken,
|
|
248
|
+
};
|
|
249
|
+
// Update store
|
|
250
|
+
set(state => {
|
|
251
|
+
const newIdentities = new Map(state.identities);
|
|
252
|
+
newIdentities.set(provider, identity);
|
|
253
|
+
return {
|
|
254
|
+
identities: newIdentities,
|
|
255
|
+
isLoading: false,
|
|
256
|
+
error: null,
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
return identity;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
263
|
+
set({ isLoading: false, error: err });
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
disconnect: async (provider) => {
|
|
268
|
+
const state = get();
|
|
269
|
+
const identity = state.identities.get(provider);
|
|
270
|
+
const config = state.providerConfigs.get(provider);
|
|
271
|
+
if (!identity || !identity.token) {
|
|
272
|
+
// Already disconnected
|
|
273
|
+
set(state => {
|
|
274
|
+
const newIdentities = new Map(state.identities);
|
|
275
|
+
newIdentities.delete(provider);
|
|
276
|
+
return { identities: newIdentities };
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// For token-based auth, just remove from store (no revocation)
|
|
281
|
+
if (identity.authType === 'token') {
|
|
282
|
+
set(state => {
|
|
283
|
+
const newIdentities = new Map(state.identities);
|
|
284
|
+
newIdentities.delete(provider);
|
|
285
|
+
return { identities: newIdentities };
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// Try to revoke token (OAuth only)
|
|
290
|
+
if (config?.revocationUrl) {
|
|
291
|
+
try {
|
|
292
|
+
const baseUrl = config.redirectUri
|
|
293
|
+
.split('/')
|
|
294
|
+
.slice(0, 3)
|
|
295
|
+
.join('/');
|
|
296
|
+
await fetch(`${baseUrl}${DEFAULT_REVOKE_ENDPOINT}`, {
|
|
297
|
+
method: 'POST',
|
|
298
|
+
headers: { 'Content-Type': 'application/json' },
|
|
299
|
+
body: JSON.stringify({
|
|
300
|
+
provider,
|
|
301
|
+
access_token: identity.token.accessToken,
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
console.warn('Failed to revoke token:', e);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Remove from store
|
|
310
|
+
set(state => {
|
|
311
|
+
const newIdentities = new Map(state.identities);
|
|
312
|
+
newIdentities.delete(provider);
|
|
313
|
+
return { identities: newIdentities };
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
refreshToken: async (provider) => {
|
|
317
|
+
const state = get();
|
|
318
|
+
const identity = state.identities.get(provider);
|
|
319
|
+
const config = state.providerConfigs.get(provider);
|
|
320
|
+
if (!identity || !identity.token?.refreshToken) {
|
|
321
|
+
throw new Error(`No refresh token available for ${provider}`);
|
|
322
|
+
}
|
|
323
|
+
if (!config) {
|
|
324
|
+
throw new Error(`Provider ${provider} is not configured`);
|
|
325
|
+
}
|
|
326
|
+
const baseUrl = config.redirectUri.split('/').slice(0, 3).join('/');
|
|
327
|
+
const response = await fetch(`${baseUrl}${DEFAULT_TOKEN_EXCHANGE_ENDPOINT}`, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
provider,
|
|
332
|
+
grant_type: 'refresh_token',
|
|
333
|
+
refresh_token: identity.token.refreshToken,
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
338
|
+
}
|
|
339
|
+
const tokenData = await response.json();
|
|
340
|
+
const newToken = {
|
|
341
|
+
accessToken: tokenData.access_token,
|
|
342
|
+
tokenType: tokenData.token_type || 'Bearer',
|
|
343
|
+
expiresAt: tokenData.expires_in
|
|
344
|
+
? Date.now() + tokenData.expires_in * 1000
|
|
345
|
+
: undefined,
|
|
346
|
+
refreshToken: tokenData.refresh_token || identity.token.refreshToken,
|
|
347
|
+
scopes: identity.scopes,
|
|
348
|
+
};
|
|
349
|
+
// Update identity with new token
|
|
350
|
+
set(state => {
|
|
351
|
+
const newIdentities = new Map(state.identities);
|
|
352
|
+
newIdentities.set(provider, {
|
|
353
|
+
...identity,
|
|
354
|
+
token: newToken,
|
|
355
|
+
});
|
|
356
|
+
return { identities: newIdentities };
|
|
357
|
+
});
|
|
358
|
+
return newToken;
|
|
359
|
+
},
|
|
360
|
+
getIdentity: (provider) => {
|
|
361
|
+
return get().identities.get(provider);
|
|
362
|
+
},
|
|
363
|
+
isConnected: (provider) => {
|
|
364
|
+
const identity = get().identities.get(provider);
|
|
365
|
+
return identity?.isConnected ?? false;
|
|
366
|
+
},
|
|
367
|
+
getToken: async (provider) => {
|
|
368
|
+
const state = get();
|
|
369
|
+
const identity = state.identities.get(provider);
|
|
370
|
+
if (!identity || !identity.token) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
// Check if token is expired or about to expire (5 min buffer)
|
|
374
|
+
const expiresAt = identity.token.expiresAt;
|
|
375
|
+
if (expiresAt && expiresAt - Date.now() < 5 * 60 * 1000) {
|
|
376
|
+
// Token is expired or expiring soon, try to refresh
|
|
377
|
+
if (identity.token.refreshToken) {
|
|
378
|
+
try {
|
|
379
|
+
return await get().refreshToken(provider);
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
console.warn('Token refresh failed:', e);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
return identity.token;
|
|
389
|
+
},
|
|
390
|
+
clearAll: () => {
|
|
391
|
+
set({
|
|
392
|
+
identities: new Map(),
|
|
393
|
+
pendingAuthorization: null,
|
|
394
|
+
error: null,
|
|
395
|
+
});
|
|
396
|
+
},
|
|
397
|
+
setError: (error) => {
|
|
398
|
+
set({ error });
|
|
399
|
+
},
|
|
400
|
+
}), {
|
|
401
|
+
name: STORAGE_KEY,
|
|
402
|
+
// Custom serialization for Maps
|
|
403
|
+
storage: {
|
|
404
|
+
getItem: name => {
|
|
405
|
+
const str = localStorage.getItem(name);
|
|
406
|
+
if (!str)
|
|
407
|
+
return null;
|
|
408
|
+
const data = JSON.parse(str);
|
|
409
|
+
// Check if we're in an OAuth callback (code and state in URL)
|
|
410
|
+
// If not, clear any stale pendingAuthorization from previous sessions
|
|
411
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
412
|
+
const isOAuthCallback = urlParams.has('code') && urlParams.has('state');
|
|
413
|
+
return {
|
|
414
|
+
state: {
|
|
415
|
+
...data.state,
|
|
416
|
+
identities: new Map(data.state.identities || []),
|
|
417
|
+
providerConfigs: new Map(data.state.providerConfigs || []),
|
|
418
|
+
// Only restore pendingAuthorization if we're in an OAuth callback
|
|
419
|
+
// Otherwise, clear it to prevent stale "Working..." state
|
|
420
|
+
pendingAuthorization: isOAuthCallback
|
|
421
|
+
? data.state.pendingAuthorization || null
|
|
422
|
+
: null,
|
|
423
|
+
isLoading: false,
|
|
424
|
+
error: null,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
setItem: (name, value) => {
|
|
429
|
+
const data = {
|
|
430
|
+
state: {
|
|
431
|
+
...value.state,
|
|
432
|
+
identities: Array.from(value.state.identities.entries()),
|
|
433
|
+
providerConfigs: Array.from(value.state.providerConfigs.entries()),
|
|
434
|
+
// Persist pending auth so callback can complete after redirect
|
|
435
|
+
pendingAuthorization: value.state.pendingAuthorization
|
|
436
|
+
? {
|
|
437
|
+
...value.state.pendingAuthorization,
|
|
438
|
+
// Don't persist callbacks (functions can't be serialized)
|
|
439
|
+
onComplete: undefined,
|
|
440
|
+
onError: undefined,
|
|
441
|
+
}
|
|
442
|
+
: null,
|
|
443
|
+
isLoading: false,
|
|
444
|
+
error: null,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
localStorage.setItem(name, JSON.stringify(data));
|
|
448
|
+
},
|
|
449
|
+
removeItem: name => localStorage.removeItem(name),
|
|
450
|
+
},
|
|
451
|
+
partialize: state => ({
|
|
452
|
+
identities: state.identities,
|
|
453
|
+
providerConfigs: state.providerConfigs,
|
|
454
|
+
pendingAuthorization: state.pendingAuthorization,
|
|
455
|
+
}),
|
|
456
|
+
})), { name: 'IdentityStore' }));
|
|
457
|
+
// === Selector Hooks ===
|
|
458
|
+
/**
|
|
459
|
+
* Get all connected identities
|
|
460
|
+
*/
|
|
461
|
+
export function useConnectedIdentities() {
|
|
462
|
+
return useIdentityStore(state => Array.from(state.identities.values()).filter(i => i.isConnected));
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get connected provider names
|
|
466
|
+
*/
|
|
467
|
+
export function useConnectedProviders() {
|
|
468
|
+
return useIdentityStore(state => Array.from(state.identities.entries())
|
|
469
|
+
.filter(([_, identity]) => identity.isConnected)
|
|
470
|
+
.map(([provider]) => provider));
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get identity for a specific provider
|
|
474
|
+
*/
|
|
475
|
+
export function useIdentity(provider) {
|
|
476
|
+
return useIdentityStore(state => state.identities.get(provider));
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Check if a provider is connected
|
|
480
|
+
*/
|
|
481
|
+
export function useIsProviderConnected(provider) {
|
|
482
|
+
return useIdentityStore(state => state.identities.get(provider)?.isConnected ?? false);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get pending authorization
|
|
486
|
+
*/
|
|
487
|
+
export function usePendingAuthorization() {
|
|
488
|
+
return useIdentityStore(state => state.pendingAuthorization);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Get identity loading state
|
|
492
|
+
*/
|
|
493
|
+
export function useIdentityLoading() {
|
|
494
|
+
return useIdentityStore(state => state.isLoading);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get identity error
|
|
498
|
+
*/
|
|
499
|
+
export function useIdentityError() {
|
|
500
|
+
return useIdentityStore(state => state.error);
|
|
501
|
+
}
|
|
502
|
+
// === Initialization Helpers ===
|
|
503
|
+
/**
|
|
504
|
+
* Configure built-in providers with client IDs
|
|
505
|
+
*/
|
|
506
|
+
export function configureBuiltinProviders(options) {
|
|
507
|
+
const store = useIdentityStore.getState();
|
|
508
|
+
if (options.github) {
|
|
509
|
+
store.configureProvider({
|
|
510
|
+
...GITHUB_PROVIDER,
|
|
511
|
+
clientId: options.github.clientId,
|
|
512
|
+
redirectUri: options.github.redirectUri,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (options.google) {
|
|
516
|
+
store.configureProvider({
|
|
517
|
+
...GOOGLE_PROVIDER,
|
|
518
|
+
clientId: options.google.clientId,
|
|
519
|
+
redirectUri: options.google.redirectUri,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
if (options.kaggle) {
|
|
523
|
+
store.configureProvider({
|
|
524
|
+
...KAGGLE_PROVIDER,
|
|
525
|
+
clientId: options.kaggle.clientId,
|
|
526
|
+
redirectUri: options.kaggle.redirectUri,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity module for OAuth 2.1 user-delegated access.
|
|
3
|
+
*
|
|
4
|
+
* Provides secure identity management for AI agents to access
|
|
5
|
+
* external services like GitHub, Google, Kaggle, etc.
|
|
6
|
+
*
|
|
7
|
+
* Built-in provider support:
|
|
8
|
+
* - **GitHub**: OAuth 2.1 with PKCE (requires client_secret server-side)
|
|
9
|
+
* - **Google**: OAuth 2.1 with PKCE and offline access
|
|
10
|
+
* - **Kaggle**: Standard OAuth 2.1 for MCP server access
|
|
11
|
+
*
|
|
12
|
+
* Provider-specific notes:
|
|
13
|
+
* - GitHub requires `client_secret` for token exchange even with PKCE
|
|
14
|
+
* - GitHub returns user info from `https://api.github.com/user` with `login`, `avatar_url` fields
|
|
15
|
+
* - Google supports pure PKCE without client_secret
|
|
16
|
+
* - All providers need backend proxy for token exchange due to CORS restrictions
|
|
17
|
+
*
|
|
18
|
+
* @module identity
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import { useIdentity } from '@datalayer/agent-runtimes';
|
|
23
|
+
*
|
|
24
|
+
* function MyComponent() {
|
|
25
|
+
* const { connect, disconnect, isConnected, identities } = useIdentity({
|
|
26
|
+
* providers: {
|
|
27
|
+
* github: { clientId: 'your-github-client-id' },
|
|
28
|
+
* },
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* return (
|
|
32
|
+
* <button onClick={() => connect('github', ['repo'])}>
|
|
33
|
+
* {isConnected('github') ? 'Disconnect GitHub' : 'Connect GitHub'}
|
|
34
|
+
* </button>
|
|
35
|
+
* );
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export type { OAuthProvider, OAuthToken, Identity, ProviderUserInfo, OAuthProviderConfig, AuthorizationRequest, AuthorizationCallback, IdentityState, IdentityActions, IdentityStore, AuthType, TokenProviderConfig, OAuthIdentityProviderConfig, TokenIdentityProviderConfig, IdentityProviderConfig, IdentityProvidersConfig, } from './types';
|
|
40
|
+
export { GITHUB_PROVIDER, GOOGLE_PROVIDER, KAGGLE_PROVIDER, KAGGLE_TOKEN_PROVIDER, } from './types';
|
|
41
|
+
export { generateCodeVerifier, generateCodeChallenge, generateState, generatePKCEPair, } from './pkce';
|
|
42
|
+
export { discoverAuthorizationServer, supportsDCR, registerClient, updateClientRegistration, deleteClientRegistration, getOrCreateDynamicClient, loadDynamicClient, saveDynamicClient, removeDynamicClient, getAllDynamicClients, clearAllDynamicClients, dynamicClientToProviderConfig, type AuthorizationServerMetadata, type ClientRegistrationRequest, type ClientRegistrationResponse, type ClientRegistrationError, type DynamicClient, } from './dcr';
|
|
43
|
+
export { useIdentityStore, useConnectedIdentities, useConnectedProviders, useIdentity as useIdentitySelector, useIsProviderConnected, usePendingAuthorization, useIdentityLoading, useIdentityError, configureBuiltinProviders, } from './identityStore';
|
|
44
|
+
export { useIdentity, type UseIdentityOptions, type UseIdentityReturn, } from './useIdentity';
|
|
45
|
+
export { IdentityButton, IdentityConnect, IdentityMenu, type IdentityButtonProps, type IdentityConnectProps, type IdentityMenuProps, } from './IdentityConnect';
|
|
46
|
+
export { OAuthCallback, type OAuthCallbackProps } from './OAuthCallback';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
export { GITHUB_PROVIDER, GOOGLE_PROVIDER, KAGGLE_PROVIDER, KAGGLE_TOKEN_PROVIDER, } from './types';
|
|
6
|
+
// PKCE utilities
|
|
7
|
+
export { generateCodeVerifier, generateCodeChallenge, generateState, generatePKCEPair, } from './pkce';
|
|
8
|
+
// Dynamic Client Registration (DCR)
|
|
9
|
+
export { discoverAuthorizationServer, supportsDCR, registerClient, updateClientRegistration, deleteClientRegistration, getOrCreateDynamicClient, loadDynamicClient, saveDynamicClient, removeDynamicClient, getAllDynamicClients, clearAllDynamicClients, dynamicClientToProviderConfig, } from './dcr';
|
|
10
|
+
// Store
|
|
11
|
+
export { useIdentityStore, useConnectedIdentities, useConnectedProviders, useIdentity as useIdentitySelector, useIsProviderConnected, usePendingAuthorization, useIdentityLoading, useIdentityError, configureBuiltinProviders, } from './identityStore';
|
|
12
|
+
// Main hook
|
|
13
|
+
export { useIdentity, } from './useIdentity';
|
|
14
|
+
// UI Components
|
|
15
|
+
export { IdentityButton, IdentityConnect, IdentityMenu, } from './IdentityConnect';
|
|
16
|
+
// OAuth Callback
|
|
17
|
+
export { OAuthCallback } from './OAuthCallback';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKCE (Proof Key for Code Exchange) utilities for OAuth 2.1.
|
|
3
|
+
*
|
|
4
|
+
* @module identity/pkce
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Generate a cryptographically random code verifier for PKCE.
|
|
8
|
+
* @param length - Length of the verifier (default: 64, min: 43, max: 128)
|
|
9
|
+
* @returns Base64URL-encoded code verifier
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateCodeVerifier(length?: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* Generate a code challenge from a code verifier using SHA-256.
|
|
14
|
+
* @param codeVerifier - The code verifier string
|
|
15
|
+
* @returns Base64URL-encoded SHA-256 hash of the verifier
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateCodeChallenge(codeVerifier: string): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Generate a random state parameter for CSRF protection.
|
|
20
|
+
* @returns Random state string
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateState(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Generate a PKCE pair (code verifier and challenge).
|
|
25
|
+
* @returns Object with codeVerifier and codeChallenge
|
|
26
|
+
*/
|
|
27
|
+
export declare function generatePKCEPair(): Promise<{
|
|
28
|
+
codeVerifier: string;
|
|
29
|
+
codeChallenge: string;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* PKCE (Proof Key for Code Exchange) utilities for OAuth 2.1.
|
|
7
|
+
*
|
|
8
|
+
* @module identity/pkce
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Generate a cryptographically random code verifier for PKCE.
|
|
12
|
+
* @param length - Length of the verifier (default: 64, min: 43, max: 128)
|
|
13
|
+
* @returns Base64URL-encoded code verifier
|
|
14
|
+
*/
|
|
15
|
+
export function generateCodeVerifier(length = 64) {
|
|
16
|
+
const validLength = Math.max(43, Math.min(128, length));
|
|
17
|
+
const array = new Uint8Array(validLength);
|
|
18
|
+
crypto.getRandomValues(array);
|
|
19
|
+
return base64UrlEncode(array);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate a code challenge from a code verifier using SHA-256.
|
|
23
|
+
* @param codeVerifier - The code verifier string
|
|
24
|
+
* @returns Base64URL-encoded SHA-256 hash of the verifier
|
|
25
|
+
*/
|
|
26
|
+
export async function generateCodeChallenge(codeVerifier) {
|
|
27
|
+
const encoder = new TextEncoder();
|
|
28
|
+
const data = encoder.encode(codeVerifier);
|
|
29
|
+
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
30
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate a random state parameter for CSRF protection.
|
|
34
|
+
* @returns Random state string
|
|
35
|
+
*/
|
|
36
|
+
export function generateState() {
|
|
37
|
+
const array = new Uint8Array(32);
|
|
38
|
+
crypto.getRandomValues(array);
|
|
39
|
+
return base64UrlEncode(array);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Base64URL encode a Uint8Array (no padding, URL-safe).
|
|
43
|
+
* @param data - Data to encode
|
|
44
|
+
* @returns Base64URL-encoded string
|
|
45
|
+
*/
|
|
46
|
+
function base64UrlEncode(data) {
|
|
47
|
+
// Convert to base64
|
|
48
|
+
let base64 = '';
|
|
49
|
+
const bytes = new Uint8Array(data);
|
|
50
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
51
|
+
base64 += String.fromCharCode(bytes[i]);
|
|
52
|
+
}
|
|
53
|
+
base64 = btoa(base64);
|
|
54
|
+
// Convert to base64url (URL-safe, no padding)
|
|
55
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate a PKCE pair (code verifier and challenge).
|
|
59
|
+
* @returns Object with codeVerifier and codeChallenge
|
|
60
|
+
*/
|
|
61
|
+
export async function generatePKCEPair() {
|
|
62
|
+
const codeVerifier = generateCodeVerifier();
|
|
63
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
64
|
+
return { codeVerifier, codeChallenge };
|
|
65
|
+
}
|