@ottocode/sdk 0.1.283 → 0.1.284

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.283",
3
+ "version": "0.1.284",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -82,6 +82,16 @@ export {
82
82
  type OpenAIOAuthResult,
83
83
  } from './openai-oauth.ts';
84
84
 
85
+ export {
86
+ authorizeXai,
87
+ exchangeXai,
88
+ refreshXaiToken,
89
+ openXaiAuthUrl,
90
+ readGrokCliAuth,
91
+ type XaiOAuthResult,
92
+ type XaiOAuthTokens,
93
+ } from './xai-oauth.ts';
94
+
85
95
  export {
86
96
  generateWallet,
87
97
  importWallet,
@@ -0,0 +1,401 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { createServer } from 'node:http';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ const XAI_OAUTH_ISSUER = 'https://auth.x.ai';
9
+ const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
10
+ const XAI_OAUTH_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
11
+ const XAI_OAUTH_SCOPE =
12
+ 'openid profile email offline_access grok-cli:access api:access';
13
+ const XAI_CALLBACK_HOST = '127.0.0.1';
14
+ const XAI_CALLBACK_PORT = 56121;
15
+ const XAI_CALLBACK_PATH = '/callback';
16
+ const XAI_REFRESH_SKEW_MS = 2 * 60 * 1000;
17
+ const XAI_GROK_CLI_AUTH_SCOPE_KEY = `${XAI_OAUTH_ISSUER}::${XAI_OAUTH_CLIENT_ID}`;
18
+ const XAI_GROK_CLI_LEGACY_AUTH_SCOPE_KEY = 'https://accounts.x.ai/sign-in';
19
+
20
+ type XaiDiscovery = {
21
+ authorization_endpoint: string;
22
+ token_endpoint: string;
23
+ };
24
+
25
+ type XaiTokenPayload = {
26
+ access_token?: string;
27
+ refresh_token?: string;
28
+ id_token?: string;
29
+ expires_in?: number;
30
+ scope?: string;
31
+ token_type?: string;
32
+ };
33
+
34
+ export type XaiOAuthTokens = {
35
+ access: string;
36
+ refresh: string;
37
+ expires: number;
38
+ idToken?: string;
39
+ scopes?: string;
40
+ };
41
+
42
+ export type XaiOAuthResult = {
43
+ url: string;
44
+ verifier: string;
45
+ redirectUri: string;
46
+ waitForCallback: () => Promise<string>;
47
+ close: () => void;
48
+ };
49
+
50
+ function generatePKCE() {
51
+ const verifier = randomBytes(32)
52
+ .toString('base64')
53
+ .replace(/\+/g, '-')
54
+ .replace(/\//g, '_')
55
+ .replace(/=/g, '');
56
+
57
+ const challenge = createHash('sha256')
58
+ .update(verifier)
59
+ .digest('base64')
60
+ .replace(/\+/g, '-')
61
+ .replace(/\//g, '_')
62
+ .replace(/=/g, '');
63
+
64
+ return { verifier, challenge };
65
+ }
66
+
67
+ function generateState() {
68
+ return randomBytes(32)
69
+ .toString('base64')
70
+ .replace(/\+/g, '-')
71
+ .replace(/\//g, '_')
72
+ .replace(/=/g, '');
73
+ }
74
+
75
+ function validateXaiEndpoint(url: string): string {
76
+ const parsed = new URL(url);
77
+ const host = parsed.hostname.toLowerCase();
78
+ if (
79
+ parsed.protocol !== 'https:' ||
80
+ (host !== 'x.ai' && !host.endsWith('.x.ai'))
81
+ ) {
82
+ throw new Error(`xAI OAuth discovery returned unexpected endpoint: ${url}`);
83
+ }
84
+ return url;
85
+ }
86
+
87
+ async function discoverXaiOAuth(): Promise<XaiDiscovery> {
88
+ const response = await fetch(XAI_OAUTH_DISCOVERY_URL, {
89
+ headers: { Accept: 'application/json' },
90
+ });
91
+ if (!response.ok) {
92
+ throw new Error(
93
+ `xAI OAuth discovery failed: ${response.status} ${await response.text()}`,
94
+ );
95
+ }
96
+ const data = (await response.json()) as Partial<XaiDiscovery>;
97
+ if (!data.authorization_endpoint || !data.token_endpoint) {
98
+ throw new Error('xAI OAuth discovery did not return auth/token endpoints');
99
+ }
100
+ return {
101
+ authorization_endpoint: validateXaiEndpoint(data.authorization_endpoint),
102
+ token_endpoint: validateXaiEndpoint(data.token_endpoint),
103
+ };
104
+ }
105
+
106
+ async function openBrowser(url: string) {
107
+ const platform = process.platform;
108
+ let command: string;
109
+
110
+ switch (platform) {
111
+ case 'darwin':
112
+ command = `open "${url}"`;
113
+ break;
114
+ case 'win32':
115
+ command = `start "${url}"`;
116
+ break;
117
+ default:
118
+ command = `xdg-open "${url}"`;
119
+ break;
120
+ }
121
+
122
+ return new Promise<void>((resolve, reject) => {
123
+ const child = spawn(command, [], { shell: true });
124
+ child.on('error', reject);
125
+ child.on('exit', (code) => {
126
+ if (code === 0) resolve();
127
+ else reject(new Error(`Failed to open browser (exit code ${code})`));
128
+ });
129
+ });
130
+ }
131
+
132
+ function parseExpiry(value: unknown): number | undefined {
133
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
134
+ if (typeof value !== 'string' || !value.trim()) return undefined;
135
+ const numeric = Number(value);
136
+ if (Number.isFinite(numeric)) return numeric;
137
+ const parsed = Date.parse(value);
138
+ return Number.isFinite(parsed) ? parsed : undefined;
139
+ }
140
+
141
+ function tokensFromPayload(
142
+ data: XaiTokenPayload,
143
+ fallbackRefresh = '',
144
+ ): XaiOAuthTokens {
145
+ if (!data.access_token) {
146
+ throw new Error('xAI token response did not include an access token');
147
+ }
148
+ const refresh = data.refresh_token || fallbackRefresh;
149
+ if (!refresh) {
150
+ throw new Error('xAI token response did not include a refresh token');
151
+ }
152
+ return {
153
+ access: data.access_token,
154
+ refresh,
155
+ expires:
156
+ Date.now() + (data.expires_in ?? 3600) * 1000 - XAI_REFRESH_SKEW_MS,
157
+ idToken: data.id_token,
158
+ scopes: data.scope,
159
+ };
160
+ }
161
+
162
+ async function exchangeXaiToken(
163
+ body: Record<string, string>,
164
+ ): Promise<XaiTokenPayload> {
165
+ const discovery = await discoverXaiOAuth();
166
+ const response = await fetch(discovery.token_endpoint, {
167
+ method: 'POST',
168
+ headers: {
169
+ Accept: 'application/json',
170
+ 'Content-Type': 'application/x-www-form-urlencoded',
171
+ },
172
+ body: new URLSearchParams(body).toString(),
173
+ });
174
+ if (!response.ok) {
175
+ throw new Error(
176
+ `xAI token request failed: ${response.status} ${await response.text()}`,
177
+ );
178
+ }
179
+ return (await response.json()) as XaiTokenPayload;
180
+ }
181
+
182
+ /** Start the xAI OAuth PKCE browser flow using a localhost callback. */
183
+ export async function authorizeXai(): Promise<XaiOAuthResult> {
184
+ const discovery = await discoverXaiOAuth();
185
+ const pkce = generatePKCE();
186
+ const state = generateState();
187
+ const nonce = generateState();
188
+ const redirectUri = `http://${XAI_CALLBACK_HOST}:${XAI_CALLBACK_PORT}${XAI_CALLBACK_PATH}`;
189
+
190
+ const params = new URLSearchParams({
191
+ response_type: 'code',
192
+ client_id: XAI_OAUTH_CLIENT_ID,
193
+ redirect_uri: redirectUri,
194
+ scope: XAI_OAUTH_SCOPE,
195
+ code_challenge: pkce.challenge,
196
+ code_challenge_method: 'S256',
197
+ state,
198
+ nonce,
199
+ });
200
+ const authUrl = `${discovery.authorization_endpoint}?${params.toString()}`;
201
+
202
+ let resolveCallback: (code: string) => void;
203
+ let rejectCallback: (error: Error) => void;
204
+ const callbackPromise = new Promise<string>((resolve, reject) => {
205
+ resolveCallback = resolve;
206
+ rejectCallback = reject;
207
+ });
208
+
209
+ const server = createServer((req, res) => {
210
+ const reqUrl = new URL(
211
+ req.url || '/',
212
+ `http://${XAI_CALLBACK_HOST}:${XAI_CALLBACK_PORT}`,
213
+ );
214
+
215
+ if (reqUrl.pathname !== XAI_CALLBACK_PATH) {
216
+ res.writeHead(404);
217
+ res.end('Not found');
218
+ return;
219
+ }
220
+
221
+ const code = reqUrl.searchParams.get('code');
222
+ const returnedState = reqUrl.searchParams.get('state');
223
+ const error = reqUrl.searchParams.get('error');
224
+ const errorDescription = reqUrl.searchParams.get('error_description');
225
+
226
+ if (error) {
227
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
228
+ res.end(
229
+ `<html><body><h1>xAI authentication failed</h1><p>${errorDescription || error}</p></body></html>`,
230
+ );
231
+ rejectCallback(
232
+ new Error(`xAI OAuth error: ${errorDescription || error}`),
233
+ );
234
+ return;
235
+ }
236
+
237
+ if (returnedState !== state) {
238
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
239
+ res.end(
240
+ '<html><body><h1>Invalid State</h1><p>State mismatch. Please try again.</p></body></html>',
241
+ );
242
+ rejectCallback(new Error('xAI OAuth state mismatch'));
243
+ return;
244
+ }
245
+
246
+ if (!code) {
247
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
248
+ res.end('<html><body><h1>Missing Code</h1></body></html>');
249
+ rejectCallback(new Error('No xAI authorization code received'));
250
+ return;
251
+ }
252
+
253
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
254
+ res.end(`
255
+ <html>
256
+ <head>
257
+ <title>otto - xAI Connected</title>
258
+ <style>
259
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #111827 0%, #374151 100%); color: white; }
260
+ .container { text-align: center; padding: 2rem; background: rgba(255,255,255,0.1); border-radius: 16px; backdrop-filter: blur(10px); }
261
+ .checkmark { font-size: 4rem; margin-bottom: 1rem; }
262
+ h1 { margin: 0 0 0.5rem 0; }
263
+ p { margin: 0; opacity: 0.9; }
264
+ </style>
265
+ </head>
266
+ <body>
267
+ <div class="container">
268
+ <div class="checkmark">✓</div>
269
+ <h1>xAI connected!</h1>
270
+ <p>You can close this window.</p>
271
+ </div>
272
+ <script>setTimeout(() => window.close(), 1500);</script>
273
+ </body>
274
+ </html>
275
+ `);
276
+ resolveCallback(code);
277
+ });
278
+
279
+ await new Promise<void>((resolve, reject) => {
280
+ server.on('error', (err: NodeJS.ErrnoException) => {
281
+ if (err.code === 'EADDRINUSE') {
282
+ reject(
283
+ new Error(
284
+ `Port ${XAI_CALLBACK_PORT} is already in use. Stop any running Grok/xAI OAuth flow and try again.`,
285
+ ),
286
+ );
287
+ } else {
288
+ reject(err);
289
+ }
290
+ });
291
+ server.listen(XAI_CALLBACK_PORT, XAI_CALLBACK_HOST, () => resolve());
292
+ });
293
+
294
+ const timeoutMs = 5 * 60 * 1000;
295
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
296
+ const timeoutPromise = new Promise<string>((_resolve, reject) => {
297
+ timeoutId = setTimeout(() => {
298
+ server.close();
299
+ reject(new Error('xAI OAuth callback timeout'));
300
+ }, timeoutMs);
301
+ });
302
+
303
+ const waitForCallback = () =>
304
+ Promise.race([callbackPromise, timeoutPromise]).finally(() => {
305
+ if (timeoutId) clearTimeout(timeoutId);
306
+ });
307
+
308
+ return {
309
+ url: authUrl,
310
+ verifier: pkce.verifier,
311
+ redirectUri,
312
+ waitForCallback,
313
+ close: () => {
314
+ if (timeoutId) clearTimeout(timeoutId);
315
+ server.close();
316
+ },
317
+ };
318
+ }
319
+
320
+ /** Exchange an xAI OAuth authorization code for access and refresh tokens. */
321
+ export async function exchangeXai(
322
+ code: string,
323
+ verifier: string,
324
+ ): Promise<XaiOAuthTokens> {
325
+ const redirectUri = `http://${XAI_CALLBACK_HOST}:${XAI_CALLBACK_PORT}${XAI_CALLBACK_PATH}`;
326
+ const data = await exchangeXaiToken({
327
+ grant_type: 'authorization_code',
328
+ code,
329
+ redirect_uri: redirectUri,
330
+ client_id: XAI_OAUTH_CLIENT_ID,
331
+ code_verifier: verifier,
332
+ });
333
+ return tokensFromPayload(data);
334
+ }
335
+
336
+ /** Refresh an xAI OAuth access token. */
337
+ export async function refreshXaiToken(
338
+ refreshToken: string,
339
+ ): Promise<XaiOAuthTokens> {
340
+ const data = await exchangeXaiToken({
341
+ grant_type: 'refresh_token',
342
+ refresh_token: refreshToken,
343
+ client_id: XAI_OAUTH_CLIENT_ID,
344
+ });
345
+ return tokensFromPayload(data, refreshToken);
346
+ }
347
+
348
+ /** Open the xAI OAuth authorization URL in the user's default browser. */
349
+ export async function openXaiAuthUrl(url: string) {
350
+ try {
351
+ await openBrowser(url);
352
+ return true;
353
+ } catch {
354
+ return false;
355
+ }
356
+ }
357
+
358
+ /** Read reusable OAuth credentials created by the official Grok CLI, if present. */
359
+ export function readGrokCliAuth(): XaiOAuthTokens | undefined {
360
+ const authPath = join(homedir(), '.grok', 'auth.json');
361
+ if (!existsSync(authPath)) return undefined;
362
+
363
+ try {
364
+ const data = JSON.parse(readFileSync(authPath, 'utf8')) as Record<
365
+ string,
366
+ Record<string, unknown>
367
+ >;
368
+ const oidc = data[XAI_GROK_CLI_AUTH_SCOPE_KEY];
369
+ if (oidc) {
370
+ const access = String(oidc.key || oidc.access_token || oidc.token || '');
371
+ if (access) {
372
+ return {
373
+ access,
374
+ refresh: String(oidc.refresh_token || oidc.refresh || ''),
375
+ expires:
376
+ (parseExpiry(oidc.expires_at) || Date.now() + 6 * 60 * 60 * 1000) -
377
+ XAI_REFRESH_SKEW_MS,
378
+ idToken: String(oidc.id_token || '') || undefined,
379
+ scopes: String(oidc.scope || '') || undefined,
380
+ };
381
+ }
382
+ }
383
+
384
+ const legacy = data[XAI_GROK_CLI_LEGACY_AUTH_SCOPE_KEY];
385
+ const legacyAccess = legacy
386
+ ? String(legacy.key || legacy.access_token || legacy.token || '')
387
+ : '';
388
+ if (legacyAccess) {
389
+ return {
390
+ access: legacyAccess,
391
+ refresh: String(legacy?.refresh_token || legacy?.refresh || ''),
392
+ expires:
393
+ parseExpiry(legacy?.expires_at || legacy?.expires) ||
394
+ Date.now() + 30 * 24 * 60 * 60 * 1000,
395
+ };
396
+ }
397
+ } catch {
398
+ return undefined;
399
+ }
400
+ return undefined;
401
+ }
@@ -196,8 +196,9 @@ export async function resolveModel(
196
196
 
197
197
  if (provider === 'xai') {
198
198
  return createXaiModel(model, {
199
- apiKey: config.apiKey,
199
+ apiKey: config.oauth?.access ?? config.apiKey,
200
200
  baseURL: config.baseURL,
201
+ useResponses: !!config.oauth,
201
202
  });
202
203
  }
203
204
 
package/src/index.ts CHANGED
@@ -191,6 +191,14 @@ export {
191
191
  exchangeOpenAIWeb,
192
192
  } from './auth/src/index.ts';
193
193
  export type { OpenAIOAuthResult } from './auth/src/index.ts';
194
+ export {
195
+ authorizeXai,
196
+ exchangeXai,
197
+ refreshXaiToken,
198
+ openXaiAuthUrl,
199
+ readGrokCliAuth,
200
+ } from './auth/src/index.ts';
201
+ export type { XaiOAuthResult, XaiOAuthTokens } from './auth/src/index.ts';
194
202
  export {
195
203
  generateWallet,
196
204
  importWallet,
@@ -4,12 +4,25 @@ import { catalog } from './catalog-merged.ts';
4
4
  export type XaiProviderConfig = {
5
5
  apiKey?: string;
6
6
  baseURL?: string;
7
+ useResponses?: boolean;
7
8
  };
8
9
 
10
+ function shouldUseXaiResponsesApi(model: string): boolean {
11
+ const normalized = model.toLowerCase().split('/').pop() || model;
12
+ return (
13
+ normalized === 'grok-4.3' ||
14
+ normalized === 'grok-build-0.1' ||
15
+ normalized.startsWith('grok-4.20-')
16
+ );
17
+ }
18
+
9
19
  export function createXaiModel(model: string, config?: XaiProviderConfig) {
10
20
  const entry = catalog.xai;
11
21
  const apiKey = config?.apiKey || process.env.XAI_API_KEY || '';
12
22
  const baseURL = config?.baseURL || entry?.api;
13
23
  const instance = createXai({ apiKey, baseURL });
24
+ if (config?.useResponses ?? shouldUseXaiResponsesApi(model)) {
25
+ return instance.responses(model);
26
+ }
14
27
  return instance(model);
15
28
  }