@askalf/dario 3.3.0 → 3.4.1

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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * CC OAuth Auto-Detection
3
+ *
4
+ * Scans the installed Claude Code binary to extract its OAuth configuration
5
+ * (client_id, authorize URL, token URL, scopes). Eliminates the need to
6
+ * hardcode values that Anthropic rotates between CC releases.
7
+ *
8
+ * CC ships two OAuth client configurations in one binary:
9
+ *
10
+ * 1. LOCAL flow — used when the OAuth client owns the callback
11
+ * (i.e. runs an HTTP server on localhost). This is what dario does.
12
+ * Identified by OAUTH_FILE_SUFFIX:"-local-oauth" next to the CLIENT_ID.
13
+ *
14
+ * 2. PLATFORM flow — used when the callback is hosted at
15
+ * platform.claude.com/oauth/code/callback. Different CLIENT_ID.
16
+ * Not applicable to dario.
17
+ *
18
+ * We scan for the LOCAL block and extract its config.
19
+ *
20
+ * Results are cached per-binary-hash at ~/.dario/cc-oauth-cache.json so
21
+ * startup only re-scans when the user upgrades Claude Code.
22
+ */
23
+ import { readFile, writeFile, mkdir, stat, open as openFile } from 'node:fs/promises';
24
+ import { existsSync } from 'node:fs';
25
+ import { homedir, platform } from 'node:os';
26
+ import { join, dirname } from 'node:path';
27
+ import { createHash } from 'node:crypto';
28
+ // Last-resort fallback if CC binary can't be found or scanned.
29
+ // These values are the known-good v2.1.104 local-oauth flow.
30
+ const FALLBACK = {
31
+ clientId: '22422756-60c9-4084-8eb7-27705fd5cf9a',
32
+ authorizeUrl: 'https://claude.com/cai/oauth/authorize',
33
+ tokenUrl: 'https://platform.claude.com/v1/oauth/token',
34
+ scopes: 'user:profile user:inference user:sessions:claude_code user:mcp_servers',
35
+ source: 'fallback',
36
+ };
37
+ const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache.json');
38
+ function candidatePaths() {
39
+ const home = homedir();
40
+ if (platform() === 'win32') {
41
+ return [
42
+ join(home, '.local', 'bin', 'claude.exe'),
43
+ join(home, 'AppData', 'Roaming', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
44
+ join(home, 'AppData', 'Roaming', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.mjs'),
45
+ join(home, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
46
+ join(home, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.mjs'),
47
+ ];
48
+ }
49
+ return [
50
+ join(home, '.local', 'bin', 'claude'),
51
+ '/usr/local/bin/claude',
52
+ '/opt/homebrew/bin/claude',
53
+ '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
54
+ '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.mjs',
55
+ '/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js',
56
+ join(home, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
57
+ join(home, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.mjs'),
58
+ ];
59
+ }
60
+ function findCCBinary() {
61
+ const override = process.env['DARIO_CC_PATH'];
62
+ if (override && existsSync(override))
63
+ return override;
64
+ for (const p of candidatePaths()) {
65
+ if (existsSync(p))
66
+ return p;
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * Fast fingerprint of a binary for caching. We hash the first 64KB plus
72
+ * size+mtime — this discriminates CC versions without reading GBs off disk.
73
+ */
74
+ async function fingerprintBinary(path) {
75
+ const st = await stat(path);
76
+ const fh = await openFile(path, 'r');
77
+ try {
78
+ const buf = Buffer.alloc(Math.min(65536, st.size));
79
+ await fh.read(buf, 0, buf.length, 0);
80
+ const h = createHash('sha256');
81
+ h.update(buf);
82
+ h.update(String(st.size));
83
+ h.update(String(st.mtimeMs));
84
+ return h.digest('hex').slice(0, 16);
85
+ }
86
+ finally {
87
+ await fh.close();
88
+ }
89
+ }
90
+ /**
91
+ * Scan binary bytes for the LOCAL-oauth OAuth block.
92
+ * Uses Buffer.indexOf to locate anchor strings, then slices a small
93
+ * window of context to run regexes on. This avoids converting the
94
+ * whole binary to a JS string.
95
+ */
96
+ export function scanBinaryForOAuthConfig(buf) {
97
+ // Anchor: `OAUTH_FILE_SUFFIX:"-local-oauth"` — this is the config-block
98
+ // occurrence, not the switch-case string literal. The switch-case produces
99
+ // just `-local-oauth` bytes, but the config object serializes as
100
+ // `OAUTH_FILE_SUFFIX:"-local-oauth"` with the key+quote prefix, which is
101
+ // stable across minified CC builds.
102
+ const anchor = Buffer.from('OAUTH_FILE_SUFFIX:"-local-oauth"');
103
+ let anchorIdx = buf.indexOf(anchor);
104
+ // Fallback anchor — some builds may tokenize differently.
105
+ if (anchorIdx === -1) {
106
+ const looseAnchor = Buffer.from('"-local-oauth"');
107
+ anchorIdx = buf.indexOf(looseAnchor);
108
+ }
109
+ if (anchorIdx === -1)
110
+ return null;
111
+ // The CLIENT_ID sits within a few hundred bytes BEFORE the anchor
112
+ // (in the same config object). Extract a window around it.
113
+ const windowStart = Math.max(0, anchorIdx - 1024);
114
+ const windowEnd = Math.min(buf.length, anchorIdx + 64);
115
+ const localBlock = buf.slice(windowStart, windowEnd).toString('latin1');
116
+ // Pick the CLIENT_ID that's CLOSEST to the anchor (last occurrence in window).
117
+ const cidRegex = /CLIENT_ID\s*:\s*"([0-9a-f-]{36})"/gi;
118
+ let lastCid = null;
119
+ let m;
120
+ while ((m = cidRegex.exec(localBlock)) !== null) {
121
+ if (m[1])
122
+ lastCid = m[1];
123
+ }
124
+ if (!lastCid)
125
+ return null;
126
+ const clientId = lastCid;
127
+ // Authorize URL: CLAUDE_AI_AUTHORIZE_URL appears once in the binary.
128
+ const authAnchor = Buffer.from('CLAUDE_AI_AUTHORIZE_URL');
129
+ const authIdx = buf.indexOf(authAnchor);
130
+ let authorizeUrl = FALLBACK.authorizeUrl;
131
+ if (authIdx !== -1) {
132
+ const w = buf.slice(authIdx, Math.min(buf.length, authIdx + 256)).toString('latin1');
133
+ const m = /CLAUDE_AI_AUTHORIZE_URL\s*:\s*"([^"]+)"/.exec(w);
134
+ if (m && m[1])
135
+ authorizeUrl = m[1];
136
+ }
137
+ // Token URL: TOKEN_URL — look for the one under platform.claude.com/.../oauth/token
138
+ const tokenAnchor = Buffer.from('TOKEN_URL');
139
+ let searchFrom = 0;
140
+ let tokenUrl = FALLBACK.tokenUrl;
141
+ while (searchFrom < buf.length) {
142
+ const idx = buf.indexOf(tokenAnchor, searchFrom);
143
+ if (idx === -1)
144
+ break;
145
+ const w = buf.slice(idx, Math.min(buf.length, idx + 128)).toString('latin1');
146
+ const m = /TOKEN_URL\s*:\s*"(https:\/\/[^"]*\/oauth\/token[^"]*)"/.exec(w);
147
+ if (m && m[1]) {
148
+ tokenUrl = m[1];
149
+ break;
150
+ }
151
+ searchFrom = idx + tokenAnchor.length;
152
+ }
153
+ // Scopes: contiguous quoted string of "user:X user:Y user:Z ..."
154
+ // Search for an anchor like "user:profile " which is the first scope.
155
+ const scopeAnchor = Buffer.from('"user:profile ');
156
+ let scopes = FALLBACK.scopes;
157
+ const scopeIdx = buf.indexOf(scopeAnchor);
158
+ if (scopeIdx !== -1) {
159
+ const w = buf.slice(scopeIdx, Math.min(buf.length, scopeIdx + 512)).toString('latin1');
160
+ const m = /"(user:profile(?:\s+user:[a-z_:]+)+)"/.exec(w);
161
+ if (m && m[1])
162
+ scopes = m[1];
163
+ }
164
+ return { clientId, authorizeUrl, tokenUrl, scopes };
165
+ }
166
+ async function loadCache() {
167
+ try {
168
+ const raw = await readFile(CACHE_PATH, 'utf-8');
169
+ const parsed = JSON.parse(raw);
170
+ if (parsed?.hash && parsed?.config?.clientId) {
171
+ return { hash: parsed.hash, config: parsed.config };
172
+ }
173
+ }
174
+ catch { /* no cache */ }
175
+ return null;
176
+ }
177
+ async function saveCache(hash, config) {
178
+ try {
179
+ await mkdir(dirname(CACHE_PATH), { recursive: true });
180
+ await writeFile(CACHE_PATH, JSON.stringify({ hash, config, savedAt: Date.now() }, null, 2));
181
+ }
182
+ catch { /* ignore cache write errors */ }
183
+ }
184
+ let memoized = null;
185
+ /**
186
+ * Get the OAuth config for dario to use. Scans the installed CC binary
187
+ * on first call, caches to disk, and memoizes in-process for subsequent
188
+ * calls. If no binary is found or scanning fails, falls back to the
189
+ * known-good v2.1.104 values.
190
+ */
191
+ export async function detectCCOAuthConfig() {
192
+ if (memoized)
193
+ return memoized;
194
+ try {
195
+ const ccPath = findCCBinary();
196
+ if (!ccPath) {
197
+ memoized = FALLBACK;
198
+ return memoized;
199
+ }
200
+ const hash = await fingerprintBinary(ccPath);
201
+ // Check cache
202
+ const cached = await loadCache();
203
+ if (cached && cached.hash === hash) {
204
+ memoized = { ...cached.config, source: 'cached', ccPath, ccHash: hash };
205
+ return memoized;
206
+ }
207
+ // Read binary and scan
208
+ const buf = await readFile(ccPath);
209
+ const scanned = scanBinaryForOAuthConfig(buf);
210
+ if (!scanned) {
211
+ memoized = { ...FALLBACK, ccPath, ccHash: hash };
212
+ return memoized;
213
+ }
214
+ const detected = {
215
+ ...scanned,
216
+ source: 'detected',
217
+ ccPath,
218
+ ccHash: hash,
219
+ };
220
+ await saveCache(hash, detected);
221
+ memoized = detected;
222
+ return memoized;
223
+ }
224
+ catch {
225
+ memoized = FALLBACK;
226
+ return memoized;
227
+ }
228
+ }
229
+ /** Test-only: reset in-process memoization. */
230
+ export function _resetDetectorCache() {
231
+ memoized = null;
232
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Claude Code request template — auto-extracted from CC v2.1.104 MITM capture.
2
+ * Claude Code request template.
3
3
  *
4
4
  * Tool definitions, system prompt, and request structure are loaded from
5
- * cc-template-data.json (extracted via MITM proxy from real CC session).
6
- * This ensures byte-level fidelity with real CC requests.
5
+ * cc-template-data.json and sent verbatim this gives byte-level fidelity
6
+ * with the shape of a real Claude Code request.
7
7
  */
8
- /** CC's exact tool definitions — loaded from MITM capture. */
8
+ /** CC's exact tool definitions — loaded from the template JSON. */
9
9
  export declare const CC_TOOL_DEFINITIONS: {
10
10
  name: string;
11
11
  description: string;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Claude Code request template — auto-extracted from CC v2.1.104 MITM capture.
2
+ * Claude Code request template.
3
3
  *
4
4
  * Tool definitions, system prompt, and request structure are loaded from
5
- * cc-template-data.json (extracted via MITM proxy from real CC session).
6
- * This ensures byte-level fidelity with real CC requests.
5
+ * cc-template-data.json and sent verbatim this gives byte-level fidelity
6
+ * with the shape of a real Claude Code request.
7
7
  */
8
8
  import { readFileSync } from 'node:fs';
9
9
  import { join, dirname } from 'node:path';
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  // Load template data at module init — fail fast if missing
13
13
  const TEMPLATE = JSON.parse(readFileSync(join(__dirname, 'cc-template-data.json'), 'utf-8'));
14
- /** CC's exact tool definitions — loaded from MITM capture. */
14
+ /** CC's exact tool definitions — loaded from the template JSON. */
15
15
  export const CC_TOOL_DEFINITIONS = TEMPLATE.tools;
16
16
  /** CC's static system prompt (~25KB). */
17
17
  export const CC_SYSTEM_PROMPT = TEMPLATE.system_prompt;
@@ -182,7 +182,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
182
182
  systemText = systemText.replace(pattern, '');
183
183
  }
184
184
  // ── Build the CC request from template ──
185
- // Key order matches CC v2.1.104 MITM capture exactly:
185
+ // Key order matches CC v2.1.104 exactly:
186
186
  // model, messages, system, tools, metadata, max_tokens, thinking, context_management, output_config, stream
187
187
  //
188
188
  // System prompt structure (3 blocks, matching real CC):
package/dist/cli.js CHANGED
@@ -126,12 +126,11 @@ async function proxy() {
126
126
  process.exit(1);
127
127
  }
128
128
  const verbose = args.includes('--verbose') || args.includes('-v');
129
- const cliBackend = args.includes('--cli');
130
129
  const passthrough = args.includes('--passthrough') || args.includes('--thin');
131
130
  const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
132
131
  const modelArg = args.find(a => a.startsWith('--model='));
133
132
  const model = modelArg ? modelArg.split('=')[1] : undefined;
134
- await startProxy({ port, verbose, model, cliBackend, passthrough, preserveTools });
133
+ await startProxy({ port, verbose, model, passthrough, preserveTools });
135
134
  }
136
135
  async function help() {
137
136
  console.log(`
@@ -149,15 +148,14 @@ async function help() {
149
148
  Shortcuts: opus, sonnet, haiku
150
149
  Full IDs: claude-opus-4-6, claude-sonnet-4-6
151
150
  Default: passthrough (client decides)
152
- --cli Use Claude CLI as backend (bypasses rate limits)
153
- --passthrough Thin proxy — OAuth swap only, no injection
151
+ --passthrough, --thin Thin proxy OAuth swap only, no injection
154
152
  --preserve-tools Keep client tool schemas (for agents with custom tools)
155
153
  --port=PORT Port to listen on (default: 3456)
156
154
  --verbose, -v Log all requests
157
155
 
158
156
  Quick start:
159
157
  dario login # auto-detects Claude Code credentials
160
- dario proxy # or: dario proxy --cli --model=opus
158
+ dario proxy --model=opus # or: dario proxy --passthrough
161
159
 
162
160
  Then point any Anthropic SDK at http://localhost:3456:
163
161
  export ANTHROPIC_BASE_URL=http://localhost:3456
package/dist/oauth.js CHANGED
@@ -8,13 +8,17 @@ import { randomBytes, createHash } from 'node:crypto';
8
8
  import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
9
9
  import { dirname, join } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
- // Claude Code's public OAuth client (PKCE, no secret needed) — extracted from CC v2.1.104 binary
12
- const OAUTH_CLIENT_ID = '22422756-60c9-4084-8eb7-27705fd5cf9a';
13
- // Max Plan OAuth (for Claude Pro/Max subscriptions) claude.com/cai/oauth/authorize
14
- const OAUTH_AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
15
- const OAUTH_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
16
- // Max plan scopes (excludes org:create_api_key which requires Console plan)
17
- const OAUTH_SCOPES = 'user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
11
+ import { detectCCOAuthConfig } from './cc-oauth-detect.js';
12
+ // OAuth config is auto-detected at runtime from the installed Claude Code
13
+ // binary. This eliminates the "Anthropic rotated the client_id again" class
14
+ // of bugs — dario stays in sync with whatever CC version the user has
15
+ // installed, forever. See cc-oauth-detect.ts for the scanner.
16
+ //
17
+ // Hardcoded fallbacks live in cc-oauth-detect.ts and are the known-good
18
+ // CC v2.1.104 local-oauth flow values.
19
+ async function getOAuthConfig() {
20
+ return detectCCOAuthConfig();
21
+ }
18
22
  // Refresh 30 min before expiry
19
23
  const REFRESH_BUFFER_MS = 30 * 60 * 1000;
20
24
  // After a failed refresh, don't retry for 60s to avoid spam
@@ -116,17 +120,18 @@ export async function startAutoOAuthFlow() {
116
120
  server.listen(0, 'localhost', async () => {
117
121
  const addr = server.address();
118
122
  port = typeof addr === 'object' && addr ? addr.port : 0;
123
+ const cfg = await getOAuthConfig();
119
124
  const params = new URLSearchParams({
120
125
  code: 'true',
121
- client_id: OAUTH_CLIENT_ID,
126
+ client_id: cfg.clientId,
122
127
  response_type: 'code',
123
128
  redirect_uri: `http://localhost:${port}/callback`,
124
- scope: OAUTH_SCOPES,
129
+ scope: cfg.scopes,
125
130
  code_challenge: codeChallenge,
126
131
  code_challenge_method: 'S256',
127
132
  state,
128
133
  });
129
- const authUrl = `${OAUTH_AUTHORIZE_URL}?${params.toString()}`;
134
+ const authUrl = `${cfg.authorizeUrl}?${params.toString()}`;
130
135
  // Open browser
131
136
  console.log(' Opening browser to sign in...');
132
137
  console.log(` If the browser didn't open, visit: ${authUrl}`);
@@ -152,12 +157,13 @@ export async function startAutoOAuthFlow() {
152
157
  * Exchange code using the localhost redirect URI.
153
158
  */
154
159
  async function exchangeCodeWithRedirect(code, codeVerifier, state, port) {
155
- const res = await fetch(OAUTH_TOKEN_URL, {
160
+ const cfg = await getOAuthConfig();
161
+ const res = await fetch(cfg.tokenUrl, {
156
162
  method: 'POST',
157
163
  headers: { 'Content-Type': 'application/json' },
158
164
  body: JSON.stringify({
159
165
  grant_type: 'authorization_code',
160
- client_id: OAUTH_CLIENT_ID,
166
+ client_id: cfg.clientId,
161
167
  code,
162
168
  redirect_uri: `http://localhost:${port}/callback`,
163
169
  code_verifier: codeVerifier,
@@ -201,16 +207,17 @@ async function doRefreshTokens() {
201
207
  throw new Error('No refresh token available. Run `dario login` first.');
202
208
  }
203
209
  const oauth = creds.claudeAiOauth;
210
+ const cfg = await getOAuthConfig();
204
211
  for (let attempt = 0; attempt < 3; attempt++) {
205
212
  if (attempt > 0)
206
213
  await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
207
- const res = await fetch(OAUTH_TOKEN_URL, {
214
+ const res = await fetch(cfg.tokenUrl, {
208
215
  method: 'POST',
209
216
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
210
217
  body: new URLSearchParams({
211
218
  grant_type: 'refresh_token',
212
219
  refresh_token: oauth.refreshToken,
213
- client_id: OAUTH_CLIENT_ID,
220
+ client_id: cfg.clientId,
214
221
  }),
215
222
  signal: AbortSignal.timeout(15000),
216
223
  });
package/dist/proxy.d.ts CHANGED
@@ -2,7 +2,6 @@ interface ProxyOptions {
2
2
  port?: number;
3
3
  verbose?: boolean;
4
4
  model?: string;
5
- cliBackend?: boolean;
6
5
  passthrough?: boolean;
7
6
  preserveTools?: boolean;
8
7
  }