@ai-sdk/harness-claude-code 1.0.110 → 1.0.112

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": "@ai-sdk/harness-claude-code",
3
- "version": "1.0.110",
3
+ "version": "1.0.112",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,8 +26,8 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.106",
30
- "@ai-sdk/provider-utils": "5.0.38",
29
+ "@ai-sdk/harness": "1.0.108",
30
+ "@ai-sdk/provider-utils": "5.0.40",
31
31
  "ws": "^8.21.0"
32
32
  },
33
33
  "peerDependencies": {
@@ -17,6 +17,7 @@ export const CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
17
17
  'AI_GATEWAY_API_KEY',
18
18
  'ANTHROPIC_API_KEY',
19
19
  'ANTHROPIC_AUTH_TOKEN',
20
+ 'CLAUDE_CODE_OAUTH_TOKEN',
20
21
  ] as const;
21
22
 
22
23
  export function createClaudeCodeRequestTransformations({
@@ -154,8 +155,10 @@ function pickAnthropic({
154
155
  const helperKey = readApiKey();
155
156
  const apiKey = processEnv.ANTHROPIC_API_KEY ?? helperKey;
156
157
  const authToken = processEnv.ANTHROPIC_AUTH_TOKEN ?? helperKey;
158
+ const oauthToken = processEnv.CLAUDE_CODE_OAUTH_TOKEN;
157
159
  if (apiKey) env.ANTHROPIC_API_KEY = apiKey;
158
160
  if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken;
161
+ if (oauthToken) env.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
159
162
  const baseUrl = processEnv.ANTHROPIC_BASE_URL;
160
163
  if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl;
161
164
  return env;
@@ -59,9 +59,12 @@ import {
59
59
  CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
60
60
  createClaudeCodeRequestTransformations,
61
61
  resolveClaudeCodeAuthenticationMode,
62
- resolveClaudeCodeEnv,
63
62
  type ClaudeCodeAuthenticationMode,
64
63
  } from './claude-code-auth';
64
+ import {
65
+ createClaudeCodeSubscriptionRequestTransformations,
66
+ resolveClaudeCodeAuthentication,
67
+ } from './claude-code-subscription';
65
68
  import {
66
69
  outboundMessageSchema,
67
70
  type InboundMessage,
@@ -849,7 +852,9 @@ export function createClaudeCode(
849
852
  const authenticationMode = resolveClaudeCodeAuthenticationMode(
850
853
  settings.auth,
851
854
  );
852
- const resolvedAuthEnvironment = resolveClaudeCodeEnv(settings.auth);
855
+ const resolvedAuthEnvironment = await resolveClaudeCodeAuthentication({
856
+ auth: settings.auth,
857
+ });
853
858
  const claudeEnvironment = {
854
859
  ...resolvedAuthEnvironment,
855
860
  /*
@@ -888,11 +893,17 @@ export function createClaudeCode(
888
893
  ...claudeEnvironment,
889
894
  ...sandboxCredentialEnvironment,
890
895
  };
891
- const requestTransformations = createClaudeCodeRequestTransformations({
896
+ const transformationSources = {
892
897
  env: claudeEnvironment,
893
898
  sandboxEnv: sandboxClaudeEnvironment,
894
899
  auth: authenticationMode,
895
- });
900
+ };
901
+ const requestTransformations = [
902
+ ...createClaudeCodeRequestTransformations(transformationSources),
903
+ ...createClaudeCodeSubscriptionRequestTransformations(
904
+ transformationSources,
905
+ ),
906
+ ];
896
907
  if (requestTransformations.length > 0) {
897
908
  await sandboxSession.addRequestTransformations(
898
909
  requestTransformations,
@@ -0,0 +1,258 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import type {
6
+ HarnessV1RequestTransformation,
7
+ HarnessV1RequestTransformationSources,
8
+ } from '@ai-sdk/harness';
9
+ import {
10
+ createCredentialRequestTransformation,
11
+ isAccessTokenExpiringSoon,
12
+ isHarnessAuthenticationEnvironment,
13
+ isMacOS,
14
+ readMacOSKeychainPassword,
15
+ refreshOAuthAccessToken,
16
+ shouldResolveNativeSubscription,
17
+ } from '@ai-sdk/harness/utils';
18
+ import { isRecord, safeParseJSON } from '@ai-sdk/provider-utils';
19
+ import {
20
+ resolveClaudeCodeEnv,
21
+ type ClaudeCodeAuthenticationMode,
22
+ type ClaudeCodeResolvedAuthenticationMode,
23
+ type ResolveClaudeCodeEnvOptions,
24
+ } from './claude-code-auth';
25
+
26
+ const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
27
+ const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
28
+
29
+ export function createClaudeCodeSubscriptionRequestTransformations({
30
+ env: environment,
31
+ sandboxEnv: sandboxEnvironment,
32
+ auth: authenticationMode,
33
+ }: HarnessV1RequestTransformationSources<ClaudeCodeResolvedAuthenticationMode>): HarnessV1RequestTransformation[] {
34
+ if (
35
+ environment.CLAUDE_CODE_OAUTH_TOKEN == null ||
36
+ sandboxEnvironment.CLAUDE_CODE_OAUTH_TOKEN == null
37
+ ) {
38
+ return [];
39
+ }
40
+ return [
41
+ createCredentialRequestTransformation({
42
+ matchUrl:
43
+ authenticationMode === 'ai-gateway'
44
+ ? environment.ANTHROPIC_BASE_URL
45
+ : (environment.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'),
46
+ matchHeaders: {
47
+ Authorization: `Bearer ${sandboxEnvironment.CLAUDE_CODE_OAUTH_TOKEN}`,
48
+ },
49
+ transformHeaders: {
50
+ Authorization: `Bearer ${environment.CLAUDE_CODE_OAUTH_TOKEN}`,
51
+ },
52
+ }),
53
+ ];
54
+ }
55
+
56
+ export async function resolveClaudeCodeAuthentication({
57
+ auth,
58
+ processEnv = process.env,
59
+ options = {},
60
+ readSubscription = readClaudeCodeSubscription,
61
+ }: {
62
+ auth: ClaudeCodeAuthenticationMode | undefined;
63
+ processEnv?: Record<string, string | undefined>;
64
+ options?: ResolveClaudeCodeEnvOptions;
65
+ readSubscription?: () => Promise<Record<string, string> | undefined>;
66
+ }): Promise<Record<string, string>> {
67
+ const environmentWithoutHelper = resolveClaudeCodeEnv(auth, processEnv, {
68
+ readApiKeyHelper: () => undefined,
69
+ });
70
+ if (isHarnessAuthenticationEnvironment(auth)) {
71
+ return environmentWithoutHelper;
72
+ }
73
+ if (
74
+ !shouldResolveNativeSubscription({
75
+ auth,
76
+ env: environmentWithoutHelper,
77
+ hasDirectCredential:
78
+ environmentWithoutHelper.ANTHROPIC_API_KEY != null ||
79
+ environmentWithoutHelper.ANTHROPIC_AUTH_TOKEN != null ||
80
+ environmentWithoutHelper.CLAUDE_CODE_OAUTH_TOKEN != null,
81
+ })
82
+ ) {
83
+ return environmentWithoutHelper;
84
+ }
85
+
86
+ return (
87
+ (await readSubscription()) ??
88
+ resolveClaudeCodeEnv(auth, processEnv, options)
89
+ );
90
+ }
91
+
92
+ export async function readClaudeCodeSubscription({
93
+ env = process.env,
94
+ homeDirectory = homedir(),
95
+ platform = process.platform,
96
+ fetch,
97
+ }: {
98
+ env?: Record<string, string | undefined>;
99
+ homeDirectory?: string;
100
+ platform?: NodeJS.Platform;
101
+ fetch?: typeof globalThis.fetch;
102
+ } = {}): Promise<Record<string, string> | undefined> {
103
+ const configDirectory = resolve(
104
+ env.CLAUDE_CONFIG_DIR ?? join(homeDirectory, '.claude'),
105
+ );
106
+ const credentialPath = join(configDirectory, '.credentials.json');
107
+ const stored = await readClaudeCredentialStore({
108
+ credentialPath,
109
+ homeDirectory,
110
+ configDirectory,
111
+ platform,
112
+ });
113
+ if (stored == null) return undefined;
114
+ const oauth = getClaudeOAuthCredential({ value: stored.value });
115
+ if (oauth == null) return undefined;
116
+ const { accessToken, refreshToken, expiresAt } = oauth;
117
+
118
+ let resolvedAccessToken = accessToken;
119
+ if (isAccessTokenExpiringSoon({ expiresAt })) {
120
+ const refreshed = await refreshOAuthAccessToken({
121
+ tokenUrl: CLAUDE_TOKEN_URL,
122
+ clientId: CLAUDE_CLIENT_ID,
123
+ refreshToken,
124
+ requestFormat: 'json',
125
+ ...(fetch == null ? {} : { fetch }),
126
+ });
127
+ resolvedAccessToken = refreshed.accessToken;
128
+ await stored.write({
129
+ ...stored.value,
130
+ claudeAiOauth: {
131
+ ...oauth,
132
+ accessToken: refreshed.accessToken,
133
+ refreshToken: refreshed.refreshToken ?? refreshToken,
134
+ expiresAt: refreshed.expiresAt,
135
+ },
136
+ });
137
+ }
138
+
139
+ return {
140
+ CLAUDE_CODE_OAUTH_TOKEN: resolvedAccessToken,
141
+ ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
142
+ };
143
+ }
144
+
145
+ type ClaudeCredentialFile = {
146
+ readonly claudeAiOauth?: unknown;
147
+ readonly [key: string]: unknown;
148
+ };
149
+
150
+ type ClaudeOAuthCredential = Record<string, unknown> & {
151
+ readonly accessToken: string;
152
+ readonly refreshToken: string;
153
+ readonly expiresAt: number;
154
+ };
155
+
156
+ async function readClaudeCredentialStore({
157
+ credentialPath,
158
+ homeDirectory,
159
+ configDirectory,
160
+ platform,
161
+ }: {
162
+ credentialPath: string;
163
+ homeDirectory: string;
164
+ configDirectory: string;
165
+ platform: NodeJS.Platform;
166
+ }): Promise<
167
+ | {
168
+ value: ClaudeCredentialFile;
169
+ write(value: ClaudeCredentialFile): Promise<void>;
170
+ }
171
+ | undefined
172
+ > {
173
+ const text = await readFile(credentialPath, 'utf8').catch(() => undefined);
174
+ if (text != null) {
175
+ const value = await parseClaudeCredentialFile(text);
176
+ if (value != null && getClaudeOAuthCredential({ value }) != null) {
177
+ return {
178
+ value,
179
+ write: updated =>
180
+ writeClaudeCredentialFile({ credentialPath, value: updated }),
181
+ };
182
+ }
183
+ }
184
+
185
+ if (!isMacOS(platform)) return undefined;
186
+ if (configDirectory !== resolve(join(homeDirectory, '.claude'))) {
187
+ return undefined;
188
+ }
189
+ const service = 'Claude Code-credentials';
190
+ const keychainText = await readMacOSKeychainPassword({
191
+ service,
192
+ account: process.env.USER ?? '',
193
+ });
194
+ if (keychainText == null) return undefined;
195
+ const value = await parseClaudeCredentialFile(keychainText);
196
+ if (value == null) return undefined;
197
+ return {
198
+ value,
199
+ write: async updated => {
200
+ execFileSync(
201
+ '/usr/bin/security',
202
+ [
203
+ 'add-generic-password',
204
+ '-U',
205
+ '-s',
206
+ service,
207
+ '-a',
208
+ process.env.USER ?? '',
209
+ '-w',
210
+ JSON.stringify(updated),
211
+ ],
212
+ { stdio: 'ignore' },
213
+ );
214
+ },
215
+ };
216
+ }
217
+
218
+ function getClaudeOAuthCredential({
219
+ value,
220
+ }: {
221
+ value: ClaudeCredentialFile;
222
+ }): ClaudeOAuthCredential | undefined {
223
+ const oauth = value.claudeAiOauth;
224
+ if (
225
+ !isRecord(oauth) ||
226
+ typeof oauth.accessToken !== 'string' ||
227
+ typeof oauth.refreshToken !== 'string' ||
228
+ typeof oauth.expiresAt !== 'number'
229
+ ) {
230
+ return undefined;
231
+ }
232
+ return oauth as ClaudeOAuthCredential;
233
+ }
234
+
235
+ async function parseClaudeCredentialFile(
236
+ text: string,
237
+ ): Promise<ClaudeCredentialFile | undefined> {
238
+ const parsed = await safeParseJSON({ text });
239
+ return parsed.success && isRecord(parsed.value)
240
+ ? (parsed.value as ClaudeCredentialFile)
241
+ : undefined;
242
+ }
243
+
244
+ async function writeClaudeCredentialFile({
245
+ credentialPath,
246
+ value,
247
+ }: {
248
+ credentialPath: string;
249
+ value: ClaudeCredentialFile;
250
+ }): Promise<void> {
251
+ await mkdir(dirname(credentialPath), { recursive: true });
252
+ const temporaryPath = `${credentialPath}.${process.pid}.tmp`;
253
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, {
254
+ mode: 0o600,
255
+ });
256
+ await chmod(temporaryPath, 0o600);
257
+ await rename(temporaryPath, credentialPath);
258
+ }