@demicodes/provider-claude-code 0.3.3 → 0.4.0
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/dist/index.d.mts +42 -3
- package/dist/index.mjs +149 -17
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -39,11 +39,15 @@ interface ClaudeCodeAuthStore {
|
|
|
39
39
|
forceRefresh?: boolean;
|
|
40
40
|
}): Promise<ClaudeCodeOAuthAccess>;
|
|
41
41
|
}
|
|
42
|
+
/** Renews a pool oauth secret (wired to `refreshClaudeCodeSecret`; injectable in tests). */
|
|
43
|
+
type ClaudeCodeSecretRefresh = (secret: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
42
44
|
interface FileClaudeCodeAuthStoreOptions {
|
|
43
45
|
/** Optional path to oauth.json (pool entry). */
|
|
44
46
|
oauthFile?: string;
|
|
45
47
|
/** Prefer this token over env/keychain when set (tests / static). */
|
|
46
48
|
accessToken?: string | null;
|
|
49
|
+
/** Renews the oauth file when its access token nears expiry. */
|
|
50
|
+
refresh?: ClaudeCodeSecretRefresh;
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
49
53
|
* Resolves Claude OAuth: explicit token → oauth file → CLAUDE_CODE_OAUTH_TOKEN → keychain.
|
|
@@ -51,6 +55,7 @@ interface FileClaudeCodeAuthStoreOptions {
|
|
|
51
55
|
declare class FileClaudeCodeAuthStore implements ClaudeCodeAuthStore {
|
|
52
56
|
private readonly oauthFile;
|
|
53
57
|
private readonly accessToken;
|
|
58
|
+
private readonly refresh;
|
|
54
59
|
constructor(options?: FileClaudeCodeAuthStoreOptions);
|
|
55
60
|
status(): Promise<ProviderAuthState>;
|
|
56
61
|
resolveAccess(): Promise<ClaudeCodeOAuthAccess>;
|
|
@@ -115,10 +120,44 @@ declare function openClaudeCodeCredentialPool(options?: {
|
|
|
115
120
|
stateDir?: string;
|
|
116
121
|
}): FileCredentialPool;
|
|
117
122
|
declare function createClaudeCodeCredentials(pool: FileCredentialPool, authStore: ClaudeCodeAuthStore, options?: {
|
|
118
|
-
loginCommand?: string;
|
|
119
|
-
loginArgs?: string[];
|
|
120
123
|
quota?: ProviderQuota | null;
|
|
121
124
|
onActiveChange?: () => void;
|
|
125
|
+
/** Injectable fetch for the OAuth login flow (tests). */
|
|
126
|
+
loginFetch?: typeof fetch;
|
|
122
127
|
}): ProviderCredentials;
|
|
123
128
|
//#endregion
|
|
124
|
-
|
|
129
|
+
//#region src/login.d.ts
|
|
130
|
+
/** Pool-entry oauth.json shape (demi-owned; refreshable when refreshToken present). */
|
|
131
|
+
interface ClaudeCodeOAuthSecret {
|
|
132
|
+
accessToken: string;
|
|
133
|
+
refreshToken?: string | null;
|
|
134
|
+
/** ISO-8601 access token expiry. */
|
|
135
|
+
expiresAt?: string | null;
|
|
136
|
+
scopes?: string[] | null;
|
|
137
|
+
subscriptionType?: string | null;
|
|
138
|
+
rateLimitTier?: string | null;
|
|
139
|
+
emailAddress?: string | null;
|
|
140
|
+
[key: string]: unknown;
|
|
141
|
+
}
|
|
142
|
+
interface ClaudeCodeLoginOptions {
|
|
143
|
+
signal?: AbortSignal;
|
|
144
|
+
/** Fires once with the authorize URL; the flow then waits on promptForCode. */
|
|
145
|
+
onPending?: (pending: {
|
|
146
|
+
verificationUrl: string;
|
|
147
|
+
requiresCodeInput: true;
|
|
148
|
+
}) => void;
|
|
149
|
+
/** Collects the "code#state" string the vendor page shows after approval. */
|
|
150
|
+
promptForCode: () => Promise<string>;
|
|
151
|
+
fetch?: typeof fetch;
|
|
152
|
+
consoleBase?: string;
|
|
153
|
+
}
|
|
154
|
+
/** Runs the copy-back OAuth flow and returns a refreshable pool secret. */
|
|
155
|
+
declare function runClaudeCodeLogin(options: ClaudeCodeLoginOptions): Promise<ClaudeCodeOAuthSecret>;
|
|
156
|
+
/** Refreshes a pool secret in place; returns the renewed secret. */
|
|
157
|
+
declare function refreshClaudeCodeSecret(secret: ClaudeCodeOAuthSecret, options?: {
|
|
158
|
+
fetch?: typeof fetch;
|
|
159
|
+
consoleBase?: string;
|
|
160
|
+
signal?: AbortSignal;
|
|
161
|
+
}): Promise<ClaudeCodeOAuthSecret>;
|
|
162
|
+
//#endregion
|
|
163
|
+
export { type ClaudeCodeAuthStore, type ClaudeCodeLoginOptions, type ClaudeCodeModelCatalogOptions, type ClaudeCodeOAuthAccess, type ClaudeCodeOAuthSecret, type ClaudeCodeProviderOptions, type ClaudeCodeQuotaOptions, FileClaudeCodeAuthStore, PoolAwareClaudeCodeAuthStore, StaticClaudeCodeAuthStore, createClaudeCodeCredentials, createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, openClaudeCodeCredentialPool, refreshClaudeCodeSecret, resolveClaudeCodeOAuthAccess, resolveWireLogDir, runClaudeCodeLogin };
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { errorMessage, isRecord, nonEmptyString, numberOrNull, stringOrNull } from "@demicodes/utils";
|
|
2
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { applyModelPolicy, clampUsedPercent, createProviderQuota, defineProvider, numberHeader, severityFromUsedPercent, toolResultContentToText, unixSecondsToIso } from "@demicodes/provider";
|
|
4
4
|
import { execFile, spawn } from "node:child_process";
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import process$1 from "node:process";
|
|
8
|
-
import { FileCredentialPool, credentialIdFromIdentity
|
|
8
|
+
import { FileCredentialPool, credentialIdFromIdentity } from "@demicodes/provider/credentials-pool";
|
|
9
9
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
10
10
|
import { appendFileSync, mkdirSync, statSync } from "node:fs";
|
|
11
11
|
import { createInterface } from "node:readline";
|
|
@@ -202,15 +202,18 @@ function reasoningEfforts(value) {
|
|
|
202
202
|
//#endregion
|
|
203
203
|
//#region src/auth.ts
|
|
204
204
|
const execFileAsync = promisify(execFile);
|
|
205
|
+
const OAUTH_EXPIRY_SKEW_MS = 300 * 1e3;
|
|
205
206
|
/**
|
|
206
207
|
* Resolves Claude OAuth: explicit token → oauth file → CLAUDE_CODE_OAUTH_TOKEN → keychain.
|
|
207
208
|
*/
|
|
208
209
|
var FileClaudeCodeAuthStore = class {
|
|
209
210
|
oauthFile;
|
|
210
211
|
accessToken;
|
|
212
|
+
refresh;
|
|
211
213
|
constructor(options = {}) {
|
|
212
214
|
this.oauthFile = options.oauthFile ?? null;
|
|
213
215
|
this.accessToken = nonEmptyString(options.accessToken) ?? null;
|
|
216
|
+
this.refresh = options.refresh ?? null;
|
|
214
217
|
}
|
|
215
218
|
async status() {
|
|
216
219
|
try {
|
|
@@ -235,7 +238,13 @@ var FileClaudeCodeAuthStore = class {
|
|
|
235
238
|
source: "static"
|
|
236
239
|
};
|
|
237
240
|
if (this.oauthFile) try {
|
|
238
|
-
|
|
241
|
+
let raw = JSON.parse(await readFile(this.oauthFile, "utf8"));
|
|
242
|
+
if (!isRecord(raw)) throw new ClaudeCodeAuthError("auth_invalid", `Invalid OAuth file: ${this.oauthFile}`);
|
|
243
|
+
const expiresAt = nonEmptyString(raw.expiresAt);
|
|
244
|
+
if (expiresAt !== void 0 && Date.parse(expiresAt) - Date.now() < OAUTH_EXPIRY_SKEW_MS && nonEmptyString(raw.refreshToken) && this.refresh) {
|
|
245
|
+
raw = await this.refresh(raw);
|
|
246
|
+
await writeFile(this.oauthFile, `${JSON.stringify(raw, null, 2)}\n`);
|
|
247
|
+
}
|
|
239
248
|
if (!isRecord(raw)) throw new ClaudeCodeAuthError("auth_invalid", `Invalid OAuth file: ${this.oauthFile}`);
|
|
240
249
|
const accessToken = nonEmptyString(raw.accessToken) ?? nonEmptyString(raw.access_token);
|
|
241
250
|
if (!accessToken) throw new ClaudeCodeAuthError("auth_missing", `No accessToken in ${this.oauthFile}`);
|
|
@@ -306,6 +315,93 @@ var ClaudeCodeAuthError = class extends Error {
|
|
|
306
315
|
}
|
|
307
316
|
};
|
|
308
317
|
//#endregion
|
|
318
|
+
//#region src/login.ts
|
|
319
|
+
const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
320
|
+
const CLAUDE_CONSOLE_BASE = "https://console.anthropic.com";
|
|
321
|
+
const CLAUDE_CODE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
322
|
+
const CLAUDE_LOGIN_SCOPE = "org:create_api_key user:profile user:inference";
|
|
323
|
+
function tokenEndpoint(consoleBase) {
|
|
324
|
+
return `${consoleBase.replace(/\/+$/, "")}/v1/oauth/token`;
|
|
325
|
+
}
|
|
326
|
+
async function requestTokens(fetchImpl, consoleBase, body, signal) {
|
|
327
|
+
const response = await fetchImpl(tokenEndpoint(consoleBase), {
|
|
328
|
+
method: "POST",
|
|
329
|
+
headers: { "content-type": "application/json" },
|
|
330
|
+
body: JSON.stringify(body),
|
|
331
|
+
signal
|
|
332
|
+
});
|
|
333
|
+
if (!response.ok) throw new ClaudeCodeAuthError("auth_invalid", `Claude OAuth token request failed with HTTP ${response.status}`);
|
|
334
|
+
const parsed = await response.json().catch(() => null);
|
|
335
|
+
if (!isRecord(parsed)) throw new ClaudeCodeAuthError("auth_invalid", "Claude OAuth token response is not a JSON object");
|
|
336
|
+
const accessToken = nonEmptyString(parsed.access_token);
|
|
337
|
+
if (!accessToken) throw new ClaudeCodeAuthError("auth_invalid", "Claude OAuth token response is missing access_token");
|
|
338
|
+
const expiresIn = Number(parsed.expires_in);
|
|
339
|
+
const account = isRecord(parsed.account) ? parsed.account : {};
|
|
340
|
+
return {
|
|
341
|
+
accessToken,
|
|
342
|
+
refreshToken: nonEmptyString(parsed.refresh_token) ?? null,
|
|
343
|
+
expiresAt: Number.isFinite(expiresIn) && expiresIn > 0 ? new Date(Date.now() + expiresIn * 1e3).toISOString() : null,
|
|
344
|
+
scopes: typeof parsed.scope === "string" ? parsed.scope.split(" ").filter(Boolean) : null,
|
|
345
|
+
subscriptionType: nonEmptyString(parsed.subscription_type) ?? nonEmptyString(account.subscription_type) ?? null,
|
|
346
|
+
...nonEmptyString(account.email_address) ? { emailAddress: nonEmptyString(account.email_address) } : {}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** Runs the copy-back OAuth flow and returns a refreshable pool secret. */
|
|
350
|
+
async function runClaudeCodeLogin(options) {
|
|
351
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
352
|
+
const consoleBase = options.consoleBase ?? CLAUDE_CONSOLE_BASE;
|
|
353
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
354
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
355
|
+
const state = randomBytes(32).toString("base64url");
|
|
356
|
+
const redirectUri = `${consoleBase.replace(/\/+$/, "")}/oauth/code/callback`;
|
|
357
|
+
const params = new URLSearchParams({
|
|
358
|
+
code: "true",
|
|
359
|
+
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
360
|
+
response_type: "code",
|
|
361
|
+
redirect_uri: redirectUri,
|
|
362
|
+
scope: CLAUDE_LOGIN_SCOPE,
|
|
363
|
+
code_challenge: challenge,
|
|
364
|
+
code_challenge_method: "S256",
|
|
365
|
+
state
|
|
366
|
+
});
|
|
367
|
+
options.onPending?.({
|
|
368
|
+
verificationUrl: `${CLAUDE_AUTHORIZE_URL}?${params.toString()}`,
|
|
369
|
+
requiresCodeInput: true
|
|
370
|
+
});
|
|
371
|
+
const pasted = (await options.promptForCode()).trim();
|
|
372
|
+
if (!pasted) throw new ClaudeCodeAuthError("auth_invalid", "Empty authorization code");
|
|
373
|
+
const [code, returnedState] = pasted.split("#");
|
|
374
|
+
if (!nonEmptyString(code)) throw new ClaudeCodeAuthError("auth_invalid", "Authorization code is missing the code part");
|
|
375
|
+
if (returnedState && returnedState !== state) throw new ClaudeCodeAuthError("auth_invalid", "Authorization code state mismatch — copy the full string from the callback page");
|
|
376
|
+
return requestTokens(fetchImpl, consoleBase, {
|
|
377
|
+
grant_type: "authorization_code",
|
|
378
|
+
code,
|
|
379
|
+
state: returnedState ?? state,
|
|
380
|
+
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
381
|
+
redirect_uri: redirectUri,
|
|
382
|
+
code_verifier: verifier
|
|
383
|
+
}, options.signal);
|
|
384
|
+
}
|
|
385
|
+
/** Refreshes a pool secret in place; returns the renewed secret. */
|
|
386
|
+
async function refreshClaudeCodeSecret(secret, options = {}) {
|
|
387
|
+
const refreshToken = nonEmptyString(secret.refreshToken);
|
|
388
|
+
if (!refreshToken) throw new ClaudeCodeAuthError("auth_missing", "Claude OAuth secret has no refreshToken to renew with");
|
|
389
|
+
const renewed = await requestTokens(options.fetch ?? fetch, options.consoleBase ?? CLAUDE_CONSOLE_BASE, {
|
|
390
|
+
grant_type: "refresh_token",
|
|
391
|
+
refresh_token: refreshToken,
|
|
392
|
+
client_id: CLAUDE_CODE_CLIENT_ID
|
|
393
|
+
}, options.signal);
|
|
394
|
+
return {
|
|
395
|
+
...secret,
|
|
396
|
+
accessToken: renewed.accessToken,
|
|
397
|
+
refreshToken: renewed.refreshToken ?? refreshToken,
|
|
398
|
+
expiresAt: renewed.expiresAt ?? secret.expiresAt ?? null,
|
|
399
|
+
scopes: renewed.scopes ?? secret.scopes ?? null,
|
|
400
|
+
subscriptionType: renewed.subscriptionType ?? secret.subscriptionType ?? null,
|
|
401
|
+
...nonEmptyString(renewed.emailAddress) ? { emailAddress: renewed.emailAddress } : {}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
309
405
|
//#region src/credentials.ts
|
|
310
406
|
var PoolAwareClaudeCodeAuthStore = class {
|
|
311
407
|
pool;
|
|
@@ -321,7 +417,10 @@ var PoolAwareClaudeCodeAuthStore = class {
|
|
|
321
417
|
async currentStore() {
|
|
322
418
|
await this.pool.ensureActivePointer();
|
|
323
419
|
const activeId = await this.pool.getActiveId();
|
|
324
|
-
if (activeId) return new FileClaudeCodeAuthStore({
|
|
420
|
+
if (activeId) return new FileClaudeCodeAuthStore({
|
|
421
|
+
oauthFile: this.pool.secretPath(activeId),
|
|
422
|
+
refresh: (secret) => refreshClaudeCodeSecret(secret)
|
|
423
|
+
});
|
|
325
424
|
return new FileClaudeCodeAuthStore();
|
|
326
425
|
}
|
|
327
426
|
};
|
|
@@ -333,8 +432,6 @@ function openClaudeCodeCredentialPool(options = {}) {
|
|
|
333
432
|
});
|
|
334
433
|
}
|
|
335
434
|
function createClaudeCodeCredentials(pool, authStore, options = {}) {
|
|
336
|
-
const loginCommand = options.loginCommand ?? "claude";
|
|
337
|
-
const loginArgs = options.loginArgs ?? ["auth", "login"];
|
|
338
435
|
const capability = () => ({
|
|
339
436
|
mode: "supported",
|
|
340
437
|
canBeginLogin: true,
|
|
@@ -385,23 +482,58 @@ function createClaudeCodeCredentials(pool, authStore, options = {}) {
|
|
|
385
482
|
updatedAt: meta.updatedAt
|
|
386
483
|
};
|
|
387
484
|
};
|
|
485
|
+
const importSecret = async (secret, source) => {
|
|
486
|
+
const email = nonEmptyString(secret.emailAddress);
|
|
487
|
+
const identityKey = email ? `email:${email}` : `token:${createHash("sha256").update(secret.accessToken).digest("hex").slice(0, 16)}`;
|
|
488
|
+
const label = email ?? nonEmptyString(secret.subscriptionType) ?? `claude-${identityKey.slice(-8)}`;
|
|
489
|
+
const id = (await pool.findByIdentityKey(identityKey))?.id ?? credentialIdFromIdentity(identityKey, label);
|
|
490
|
+
const meta = {
|
|
491
|
+
id,
|
|
492
|
+
label,
|
|
493
|
+
detail: nonEmptyString(secret.subscriptionType) ?? nonEmptyString(secret.rateLimitTier) ?? null,
|
|
494
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
495
|
+
source,
|
|
496
|
+
identityKey
|
|
497
|
+
};
|
|
498
|
+
await pool.writeEntry(meta, `${JSON.stringify(secret, null, 2)}\n`);
|
|
499
|
+
if (!await pool.getActiveId()) await pool.setActiveId(id);
|
|
500
|
+
options.quota?.clearLatest?.();
|
|
501
|
+
options.onActiveChange?.();
|
|
502
|
+
return {
|
|
503
|
+
id: meta.id,
|
|
504
|
+
label: meta.label,
|
|
505
|
+
detail: meta.detail,
|
|
506
|
+
updatedAt: meta.updatedAt
|
|
507
|
+
};
|
|
508
|
+
};
|
|
388
509
|
return {
|
|
389
510
|
capability,
|
|
390
511
|
list: () => pool.list(),
|
|
391
512
|
getActive,
|
|
392
513
|
setActive,
|
|
393
514
|
beginLogin: async (loginOptions) => {
|
|
394
|
-
|
|
395
|
-
if (result.status === "completed") return { status: "completed" };
|
|
396
|
-
if (result.status === "cancelled") return { status: "cancelled" };
|
|
397
|
-
if (result.status === "unavailable") return {
|
|
515
|
+
if (!loginOptions?.promptForCode) return {
|
|
398
516
|
status: "unavailable",
|
|
399
|
-
message:
|
|
400
|
-
};
|
|
401
|
-
return {
|
|
402
|
-
status: "failed",
|
|
403
|
-
message: result.message ?? "Login failed"
|
|
517
|
+
message: "Claude login requires promptForCode to collect the pasted authorization code"
|
|
404
518
|
};
|
|
519
|
+
try {
|
|
520
|
+
const secret = await runClaudeCodeLogin({
|
|
521
|
+
signal: loginOptions.signal,
|
|
522
|
+
onPending: loginOptions.onPending,
|
|
523
|
+
promptForCode: loginOptions.promptForCode,
|
|
524
|
+
fetch: options.loginFetch
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
status: "completed",
|
|
528
|
+
credentialId: (await importSecret(secret, "login:oauth")).id
|
|
529
|
+
};
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (loginOptions.signal?.aborted) return { status: "cancelled" };
|
|
532
|
+
return {
|
|
533
|
+
status: "failed",
|
|
534
|
+
message: errorMessage(error)
|
|
535
|
+
};
|
|
536
|
+
}
|
|
405
537
|
},
|
|
406
538
|
importDefault: async () => {
|
|
407
539
|
const vendor = new FileClaudeCodeAuthStore();
|
|
@@ -1649,4 +1781,4 @@ function itemsDiverged(active, items) {
|
|
|
1649
1781
|
return false;
|
|
1650
1782
|
}
|
|
1651
1783
|
//#endregion
|
|
1652
|
-
export { FileClaudeCodeAuthStore, PoolAwareClaudeCodeAuthStore, StaticClaudeCodeAuthStore, createClaudeCodeCredentials, createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, openClaudeCodeCredentialPool, resolveClaudeCodeOAuthAccess, resolveWireLogDir };
|
|
1784
|
+
export { FileClaudeCodeAuthStore, PoolAwareClaudeCodeAuthStore, StaticClaudeCodeAuthStore, createClaudeCodeCredentials, createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, openClaudeCodeCredentialPool, refreshClaudeCodeSecret, resolveClaudeCodeOAuthAccess, resolveWireLogDir, runClaudeCodeLogin };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demicodes/provider-claude-code",
|
|
3
3
|
"description": "Claude Code provider adapter for Demi.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@demicodes/core": "^0.3.2",
|
|
15
|
-
"@demicodes/provider": "^0.
|
|
15
|
+
"@demicodes/provider": "^0.4.0",
|
|
16
16
|
"@demicodes/utils": "^0.3.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@demicodes/agent": "^0.3.
|
|
19
|
+
"@demicodes/agent": "^0.3.3",
|
|
20
20
|
"@demicodes/shell": "^0.3.2"
|
|
21
21
|
},
|
|
22
22
|
"license": "Apache-2.0",
|