@link-assistant/agent 0.0.9 → 0.0.12
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/EXAMPLES.md +36 -0
- package/MODELS.md +72 -24
- package/README.md +59 -2
- package/TOOLS.md +20 -0
- package/package.json +35 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +469 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +144 -119
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +39 -24
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +346 -199
- package/src/json-standard/index.ts +67 -51
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Global } from '../global';
|
|
4
|
+
import { Log } from '../util/log';
|
|
5
|
+
import z from 'zod';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Claude OAuth Module
|
|
9
|
+
*
|
|
10
|
+
* Implements OAuth 2.0 with PKCE for Claude authentication.
|
|
11
|
+
* This allows using Claude Pro/Max subscription for API access.
|
|
12
|
+
*
|
|
13
|
+
* OAuth Flow:
|
|
14
|
+
* 1. Generate PKCE code verifier and challenge
|
|
15
|
+
* 2. Open browser to authorization URL
|
|
16
|
+
* 3. User authenticates and authorizes
|
|
17
|
+
* 4. Receive authorization code via redirect
|
|
18
|
+
* 5. Exchange code for tokens
|
|
19
|
+
* 6. Store tokens in ~/.claude/.credentials.json
|
|
20
|
+
*
|
|
21
|
+
* References:
|
|
22
|
+
* - Claude Code CLI uses the same OAuth flow
|
|
23
|
+
* - https://github.com/grll/claude-code-login for implementation details
|
|
24
|
+
*/
|
|
25
|
+
export namespace ClaudeOAuth {
|
|
26
|
+
const log = Log.create({ service: 'claude-oauth' });
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* OAuth Configuration
|
|
30
|
+
* These values are from the Claude Code CLI OAuth implementation
|
|
31
|
+
*/
|
|
32
|
+
export const Config = {
|
|
33
|
+
// OAuth endpoints
|
|
34
|
+
authorizationUrl: 'https://claude.ai/oauth/authorize',
|
|
35
|
+
tokenUrl: 'https://console.anthropic.com/v1/oauth/token',
|
|
36
|
+
redirectUri: 'https://console.anthropic.com/oauth/code/callback',
|
|
37
|
+
|
|
38
|
+
// OAuth client - this is the same client ID used by Claude Code CLI
|
|
39
|
+
clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
40
|
+
|
|
41
|
+
// Requested scopes for API access
|
|
42
|
+
scopes: ['org:create_api_key', 'user:profile', 'user:inference'],
|
|
43
|
+
|
|
44
|
+
// API beta header for OAuth authentication
|
|
45
|
+
betaHeader: 'oauth-2025-04-20',
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Schema for OAuth credentials stored in ~/.claude/.credentials.json
|
|
50
|
+
*/
|
|
51
|
+
export const Credentials = z.object({
|
|
52
|
+
claudeAiOauth: z
|
|
53
|
+
.object({
|
|
54
|
+
accessToken: z.string(),
|
|
55
|
+
refreshToken: z.string(),
|
|
56
|
+
expiresAt: z.number(),
|
|
57
|
+
scopes: z.array(z.string()).optional(),
|
|
58
|
+
subscriptionType: z.string().optional(),
|
|
59
|
+
rateLimitTier: z.string().optional(),
|
|
60
|
+
})
|
|
61
|
+
.optional(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type Credentials = z.infer<typeof Credentials>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Schema for OAuth state file (temporary, during auth flow)
|
|
68
|
+
*/
|
|
69
|
+
export const OAuthState = z.object({
|
|
70
|
+
state: z.string(),
|
|
71
|
+
codeVerifier: z.string(),
|
|
72
|
+
expiresAt: z.number(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export type OAuthState = z.infer<typeof OAuthState>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Schema for token response from OAuth server
|
|
79
|
+
*/
|
|
80
|
+
export const TokenResponse = z.object({
|
|
81
|
+
access_token: z.string(),
|
|
82
|
+
refresh_token: z.string(),
|
|
83
|
+
expires_in: z.number(),
|
|
84
|
+
token_type: z.string(),
|
|
85
|
+
scope: z.string().optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export type TokenResponse = z.infer<typeof TokenResponse>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Paths for credential and state storage
|
|
92
|
+
*/
|
|
93
|
+
const claudeDir = path.join(Global.Path.home, '.claude');
|
|
94
|
+
const credentialsPath = path.join(claudeDir, '.credentials.json');
|
|
95
|
+
const statePath = path.join(claudeDir, '.oauth_state.json');
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate a cryptographically secure random string
|
|
99
|
+
*/
|
|
100
|
+
function generateRandomString(length: number): string {
|
|
101
|
+
return crypto.randomBytes(length).toString('base64url');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate PKCE code challenge from code verifier
|
|
106
|
+
*/
|
|
107
|
+
function generateCodeChallenge(verifier: string): string {
|
|
108
|
+
return crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate authorization URL with PKCE parameters
|
|
113
|
+
*
|
|
114
|
+
* @returns Object containing the authorization URL and state data to save
|
|
115
|
+
*/
|
|
116
|
+
export function generateAuthUrl(): { url: string; state: OAuthState } {
|
|
117
|
+
// Generate PKCE values
|
|
118
|
+
const codeVerifier = generateRandomString(32);
|
|
119
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
120
|
+
const state = generateRandomString(32);
|
|
121
|
+
|
|
122
|
+
// Build authorization URL
|
|
123
|
+
const params = new URLSearchParams({
|
|
124
|
+
client_id: Config.clientId,
|
|
125
|
+
redirect_uri: Config.redirectUri,
|
|
126
|
+
response_type: 'code',
|
|
127
|
+
scope: Config.scopes.join(' '),
|
|
128
|
+
state,
|
|
129
|
+
code_challenge: codeChallenge,
|
|
130
|
+
code_challenge_method: 'S256',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const url = `${Config.authorizationUrl}?${params.toString()}`;
|
|
134
|
+
|
|
135
|
+
// State expires in 10 minutes
|
|
136
|
+
const oauthState: OAuthState = {
|
|
137
|
+
state,
|
|
138
|
+
codeVerifier,
|
|
139
|
+
expiresAt: Date.now() + 10 * 60 * 1000,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return { url, state: oauthState };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Save OAuth state for later code exchange
|
|
147
|
+
*/
|
|
148
|
+
export async function saveState(state: OAuthState): Promise<void> {
|
|
149
|
+
await Bun.write(statePath, JSON.stringify(state, null, 2));
|
|
150
|
+
log.info('saved oauth state', {
|
|
151
|
+
expiresAt: new Date(state.expiresAt).toISOString(),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Load saved OAuth state
|
|
157
|
+
*/
|
|
158
|
+
export async function loadState(): Promise<OAuthState | undefined> {
|
|
159
|
+
try {
|
|
160
|
+
const file = Bun.file(statePath);
|
|
161
|
+
if (!(await file.exists())) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const content = await file.json();
|
|
165
|
+
const parsed = OAuthState.parse(content);
|
|
166
|
+
|
|
167
|
+
if (parsed.expiresAt < Date.now()) {
|
|
168
|
+
log.warn('oauth state expired');
|
|
169
|
+
await clearState();
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return parsed;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
log.error('failed to load oauth state', { error });
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Clear saved OAuth state
|
|
182
|
+
*/
|
|
183
|
+
export async function clearState(): Promise<void> {
|
|
184
|
+
try {
|
|
185
|
+
const file = Bun.file(statePath);
|
|
186
|
+
if (await file.exists()) {
|
|
187
|
+
await Bun.write(statePath, '');
|
|
188
|
+
// Delete the file
|
|
189
|
+
const fs = await import('fs/promises');
|
|
190
|
+
await fs.unlink(statePath).catch(() => {});
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
log.error('failed to clear oauth state', { error });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Exchange authorization code for tokens
|
|
199
|
+
*
|
|
200
|
+
* @param code - Authorization code from OAuth callback
|
|
201
|
+
* @param codeVerifier - PKCE code verifier
|
|
202
|
+
* @returns Token response from OAuth server
|
|
203
|
+
*/
|
|
204
|
+
export async function exchangeCode(
|
|
205
|
+
code: string,
|
|
206
|
+
codeVerifier: string
|
|
207
|
+
): Promise<TokenResponse> {
|
|
208
|
+
const body = new URLSearchParams({
|
|
209
|
+
grant_type: 'authorization_code',
|
|
210
|
+
client_id: Config.clientId,
|
|
211
|
+
redirect_uri: Config.redirectUri,
|
|
212
|
+
code,
|
|
213
|
+
code_verifier: codeVerifier,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
log.info('exchanging authorization code for tokens');
|
|
217
|
+
|
|
218
|
+
const response = await fetch(Config.tokenUrl, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
222
|
+
},
|
|
223
|
+
body: body.toString(),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
const error = await response.text();
|
|
228
|
+
log.error('token exchange failed', { status: response.status, error });
|
|
229
|
+
throw new Error(`Token exchange failed: ${response.status} ${error}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const data = await response.json();
|
|
233
|
+
return TokenResponse.parse(data);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Save credentials to ~/.claude/.credentials.json
|
|
238
|
+
*
|
|
239
|
+
* @param tokens - Token response from OAuth server
|
|
240
|
+
*/
|
|
241
|
+
export async function saveCredentials(tokens: TokenResponse): Promise<void> {
|
|
242
|
+
// Ensure .claude directory exists
|
|
243
|
+
const fs = await import('fs/promises');
|
|
244
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
245
|
+
|
|
246
|
+
// Load existing credentials if any
|
|
247
|
+
let existing: Credentials = {};
|
|
248
|
+
try {
|
|
249
|
+
const file = Bun.file(credentialsPath);
|
|
250
|
+
if (await file.exists()) {
|
|
251
|
+
existing = await file.json();
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
// Ignore errors reading existing credentials
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Update with new OAuth credentials
|
|
258
|
+
const credentials: Credentials = {
|
|
259
|
+
...existing,
|
|
260
|
+
claudeAiOauth: {
|
|
261
|
+
accessToken: tokens.access_token,
|
|
262
|
+
refreshToken: tokens.refresh_token,
|
|
263
|
+
expiresAt: Date.now() + tokens.expires_in * 1000,
|
|
264
|
+
scopes: tokens.scope?.split(' '),
|
|
265
|
+
subscriptionType: 'unknown', // Will be populated from API response
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
await Bun.write(credentialsPath, JSON.stringify(credentials, null, 2));
|
|
270
|
+
log.info('saved credentials', {
|
|
271
|
+
expiresAt: new Date(credentials.claudeAiOauth!.expiresAt).toISOString(),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get stored OAuth credentials
|
|
277
|
+
*
|
|
278
|
+
* @returns OAuth credentials if available, undefined otherwise
|
|
279
|
+
*/
|
|
280
|
+
export async function getCredentials(): Promise<
|
|
281
|
+
Credentials['claudeAiOauth'] | undefined
|
|
282
|
+
> {
|
|
283
|
+
try {
|
|
284
|
+
const file = Bun.file(credentialsPath);
|
|
285
|
+
if (!(await file.exists())) {
|
|
286
|
+
log.info('credentials file not found', { path: credentialsPath });
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const content = await file.json();
|
|
291
|
+
const parsed = Credentials.parse(content);
|
|
292
|
+
|
|
293
|
+
if (!parsed.claudeAiOauth) {
|
|
294
|
+
log.info('no claudeAiOauth credentials found');
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check if token is expired
|
|
299
|
+
if (parsed.claudeAiOauth.expiresAt < Date.now()) {
|
|
300
|
+
log.warn('token expired', {
|
|
301
|
+
expiresAt: new Date(parsed.claudeAiOauth.expiresAt).toISOString(),
|
|
302
|
+
});
|
|
303
|
+
// TODO: Implement token refresh using refreshToken
|
|
304
|
+
// For now, user needs to re-authenticate
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
log.info('loaded oauth credentials', {
|
|
308
|
+
subscriptionType: parsed.claudeAiOauth.subscriptionType,
|
|
309
|
+
scopes: parsed.claudeAiOauth.scopes,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return parsed.claudeAiOauth;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
log.error('failed to read credentials', { error });
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get access token for API requests
|
|
321
|
+
*/
|
|
322
|
+
export async function getAccessToken(): Promise<string | undefined> {
|
|
323
|
+
const creds = await getCredentials();
|
|
324
|
+
return creds?.accessToken;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check if OAuth credentials are available and valid
|
|
329
|
+
*/
|
|
330
|
+
export async function isAuthenticated(): Promise<boolean> {
|
|
331
|
+
const creds = await getCredentials();
|
|
332
|
+
if (!creds?.accessToken) return false;
|
|
333
|
+
|
|
334
|
+
// Check if not expired (with 5 minute buffer)
|
|
335
|
+
return creds.expiresAt > Date.now() + 5 * 60 * 1000;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Complete the OAuth flow by exchanging the authorization code
|
|
340
|
+
*
|
|
341
|
+
* @param code - Authorization code from OAuth callback
|
|
342
|
+
* @returns Success status
|
|
343
|
+
*/
|
|
344
|
+
export async function completeAuth(code: string): Promise<boolean> {
|
|
345
|
+
const state = await loadState();
|
|
346
|
+
if (!state) {
|
|
347
|
+
log.error('no oauth state found - please start login flow first');
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const tokens = await exchangeCode(code, state.codeVerifier);
|
|
353
|
+
await saveCredentials(tokens);
|
|
354
|
+
await clearState();
|
|
355
|
+
log.info('authentication completed successfully');
|
|
356
|
+
return true;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
log.error('failed to complete authentication', { error });
|
|
359
|
+
await clearState();
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Refresh the access token using the refresh token
|
|
366
|
+
*/
|
|
367
|
+
export async function refreshToken(): Promise<boolean> {
|
|
368
|
+
const creds = await getCredentials();
|
|
369
|
+
if (!creds?.refreshToken) {
|
|
370
|
+
log.error('no refresh token available');
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const body = new URLSearchParams({
|
|
375
|
+
grant_type: 'refresh_token',
|
|
376
|
+
client_id: Config.clientId,
|
|
377
|
+
refresh_token: creds.refreshToken,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
log.info('refreshing access token');
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const response = await fetch(Config.tokenUrl, {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
headers: {
|
|
386
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
387
|
+
},
|
|
388
|
+
body: body.toString(),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (!response.ok) {
|
|
392
|
+
const error = await response.text();
|
|
393
|
+
log.error('token refresh failed', { status: response.status, error });
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const tokens = TokenResponse.parse(await response.json());
|
|
398
|
+
await saveCredentials(tokens);
|
|
399
|
+
log.info('token refreshed successfully');
|
|
400
|
+
return true;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
log.error('failed to refresh token', { error });
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create a custom fetch function that uses OAuth Bearer token authentication
|
|
409
|
+
*/
|
|
410
|
+
export function createAuthenticatedFetch(accessToken: string): typeof fetch {
|
|
411
|
+
return async (url, init) => {
|
|
412
|
+
const headers = new Headers(init?.headers);
|
|
413
|
+
// Remove x-api-key if present and add Authorization Bearer
|
|
414
|
+
headers.delete('x-api-key');
|
|
415
|
+
headers.set('Authorization', `Bearer ${accessToken}`);
|
|
416
|
+
// Add OAuth beta header
|
|
417
|
+
const existingBeta = headers.get('anthropic-beta');
|
|
418
|
+
if (existingBeta) {
|
|
419
|
+
headers.set('anthropic-beta', `${Config.betaHeader},${existingBeta}`);
|
|
420
|
+
} else {
|
|
421
|
+
headers.set('anthropic-beta', Config.betaHeader);
|
|
422
|
+
}
|
|
423
|
+
return fetch(url, { ...init, headers });
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
package/src/auth/index.ts
CHANGED
|
@@ -1,64 +1,66 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { Global } from
|
|
3
|
-
import fs from
|
|
4
|
-
import z from
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { Global } from '../global';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import z from 'zod';
|
|
5
5
|
|
|
6
6
|
export namespace Auth {
|
|
7
7
|
export const Oauth = z
|
|
8
8
|
.object({
|
|
9
|
-
type: z.literal(
|
|
9
|
+
type: z.literal('oauth'),
|
|
10
10
|
refresh: z.string(),
|
|
11
11
|
access: z.string(),
|
|
12
12
|
expires: z.number(),
|
|
13
13
|
enterpriseUrl: z.string().optional(),
|
|
14
14
|
})
|
|
15
|
-
.meta({ ref:
|
|
15
|
+
.meta({ ref: 'OAuth' });
|
|
16
16
|
|
|
17
17
|
export const Api = z
|
|
18
18
|
.object({
|
|
19
|
-
type: z.literal(
|
|
19
|
+
type: z.literal('api'),
|
|
20
20
|
key: z.string(),
|
|
21
21
|
})
|
|
22
|
-
.meta({ ref:
|
|
22
|
+
.meta({ ref: 'ApiAuth' });
|
|
23
23
|
|
|
24
24
|
export const WellKnown = z
|
|
25
25
|
.object({
|
|
26
|
-
type: z.literal(
|
|
26
|
+
type: z.literal('wellknown'),
|
|
27
27
|
key: z.string(),
|
|
28
28
|
token: z.string(),
|
|
29
29
|
})
|
|
30
|
-
.meta({ ref:
|
|
30
|
+
.meta({ ref: 'WellKnownAuth' });
|
|
31
31
|
|
|
32
|
-
export const Info = z
|
|
33
|
-
|
|
32
|
+
export const Info = z
|
|
33
|
+
.discriminatedUnion('type', [Oauth, Api, WellKnown])
|
|
34
|
+
.meta({ ref: 'Auth' });
|
|
35
|
+
export type Info = z.infer<typeof Info>;
|
|
34
36
|
|
|
35
|
-
const filepath = path.join(Global.Path.data,
|
|
37
|
+
const filepath = path.join(Global.Path.data, 'auth.json');
|
|
36
38
|
|
|
37
39
|
export async function get(providerID: string) {
|
|
38
|
-
const file = Bun.file(filepath)
|
|
40
|
+
const file = Bun.file(filepath);
|
|
39
41
|
return file
|
|
40
42
|
.json()
|
|
41
43
|
.catch(() => ({}))
|
|
42
|
-
.then((x) => x[providerID] as Info | undefined)
|
|
44
|
+
.then((x) => x[providerID] as Info | undefined);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export async function all(): Promise<Record<string, Info>> {
|
|
46
|
-
const file = Bun.file(filepath)
|
|
47
|
-
return file.json().catch(() => ({}))
|
|
48
|
+
const file = Bun.file(filepath);
|
|
49
|
+
return file.json().catch(() => ({}));
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export async function set(key: string, info: Info) {
|
|
51
|
-
const file = Bun.file(filepath)
|
|
52
|
-
const data = await all()
|
|
53
|
-
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
|
|
54
|
-
await fs.chmod(file.name!, 0o600)
|
|
53
|
+
const file = Bun.file(filepath);
|
|
54
|
+
const data = await all();
|
|
55
|
+
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2));
|
|
56
|
+
await fs.chmod(file.name!, 0o600);
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export async function remove(key: string) {
|
|
58
|
-
const file = Bun.file(filepath)
|
|
59
|
-
const data = await all()
|
|
60
|
-
delete data[key]
|
|
61
|
-
await Bun.write(file, JSON.stringify(data, null, 2))
|
|
62
|
-
await fs.chmod(file.name!, 0o600)
|
|
60
|
+
const file = Bun.file(filepath);
|
|
61
|
+
const data = await all();
|
|
62
|
+
delete data[key];
|
|
63
|
+
await Bun.write(file, JSON.stringify(data, null, 2));
|
|
64
|
+
await fs.chmod(file.name!, 0o600);
|
|
63
65
|
}
|
|
64
66
|
}
|