@askalf/dario 3.4.0 → 3.4.3

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.
@@ -5,20 +5,32 @@
5
5
  * (client_id, authorize URL, token URL, scopes). Eliminates the need to
6
6
  * hardcode values that Anthropic rotates between CC releases.
7
7
  *
8
- * CC ships two OAuth client configurations in one binary:
8
+ * CC ships three OAuth config factories in one binary (dev/staging/prod),
9
+ * selected at runtime by an environment switch that is hardcoded to "prod"
10
+ * in shipped builds. Only the PROD block is live; "local" and "staging"
11
+ * are dead code paths.
9
12
  *
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
+ * PROD block (the one we want):
14
+ * BASE_API_URL: "https://api.anthropic.com"
15
+ * CLAUDE_AI_AUTHORIZE_URL: "https://claude.com/cai/oauth/authorize"
16
+ * TOKEN_URL: "https://platform.claude.com/v1/oauth/token"
17
+ * CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
18
+ * OAUTH_FILE_SUFFIX: ""
13
19
  *
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.
20
+ * LOCAL block (dead code in shipped builds CC pointing at localhost:8000
21
+ * etc. as its own dev stack, NOT about "client uses a localhost callback"):
22
+ * BASE_API_URL: "http://localhost:8000"
23
+ * CLIENT_ID: "22422756-60c9-4084-8eb7-27705fd5cf9a"
24
+ * OAUTH_FILE_SUFFIX: "-local-oauth"
17
25
  *
18
- * We scan for the LOCAL block and extract its config.
26
+ * Dario uses CC's own automatic OAuth flow the prod client is registered
27
+ * with `http://localhost:${port}/callback` exactly as dario sends. (The
28
+ * "MANUAL_REDIRECT_URL" on platform.claude.com is only used when dario's
29
+ * local HTTP server can't bind a port; dario never hits that path.)
19
30
  *
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.
31
+ * Results are cached per-binary-hash at ~/.dario/cc-oauth-cache-v2.json so
32
+ * startup only re-scans when the user upgrades Claude Code. The -v2 suffix
33
+ * invalidates the v3.4.0-v3.4.2 caches that held the wrong (dev) client_id.
22
34
  */
23
35
  import { readFile, writeFile, mkdir, stat, open as openFile } from 'node:fs/promises';
24
36
  import { existsSync } from 'node:fs';
@@ -26,15 +38,18 @@ import { homedir, platform } from 'node:os';
26
38
  import { join, dirname } from 'node:path';
27
39
  import { createHash } from 'node:crypto';
28
40
  // 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.
41
+ // These values are the CC v2.1.104 PROD OAuth config, extracted from
42
+ // the `nh$` object in the shipped binary.
30
43
  const FALLBACK = {
31
- clientId: '22422756-60c9-4084-8eb7-27705fd5cf9a',
44
+ clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
32
45
  authorizeUrl: 'https://claude.com/cai/oauth/authorize',
33
46
  tokenUrl: 'https://platform.claude.com/v1/oauth/token',
34
- scopes: 'user:profile user:inference user:sessions:claude_code user:mcp_servers',
47
+ scopes: 'user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',
35
48
  source: 'fallback',
36
49
  };
37
- const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache.json');
50
+ // -v2 suffix invalidates the v3.4.0-v3.4.2 cache that pinned the wrong
51
+ // (dev) client_id extracted from the dead-code -local-oauth block.
52
+ const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache-v2.json');
38
53
  function candidatePaths() {
39
54
  const home = homedir();
40
55
  if (platform() === 'win32') {
@@ -88,72 +103,47 @@ async function fingerprintBinary(path) {
88
103
  }
89
104
  }
90
105
  /**
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.
106
+ * Scan binary bytes for the PROD OAuth config block.
107
+ *
108
+ * Anchors on `BASE_API_URL:"https://api.anthropic.com"` this literal
109
+ * only appears inside the prod config object (`nh$`). The LOCAL-dev block
110
+ * uses `http://localhost:8000` for the same key, and there's no staging
111
+ * block present in shipped builds. Once we find the anchor, the CLIENT_ID,
112
+ * CLAUDE_AI_AUTHORIZE_URL, TOKEN_URL, and scopes all live within a ~1.5KB
113
+ * window after it.
95
114
  */
96
115
  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
- }
116
+ const anchor = Buffer.from('BASE_API_URL:"https://api.anthropic.com"');
117
+ const anchorIdx = buf.indexOf(anchor);
109
118
  if (anchorIdx === -1)
110
119
  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)
120
+ // The prod config object is laid out roughly as one line of minified JS.
121
+ // Take a generous window to be safe across minifier differences.
122
+ const windowStart = anchorIdx;
123
+ const windowEnd = Math.min(buf.length, anchorIdx + 2048);
124
+ const prodBlock = buf.slice(windowStart, windowEnd).toString('latin1');
125
+ const cidMatch = /CLIENT_ID\s*:\s*"([0-9a-f-]{36})"/i.exec(prodBlock);
126
+ if (!cidMatch || !cidMatch[1])
127
+ return null;
128
+ const clientId = cidMatch[1];
129
+ // Defensive: if we somehow matched the dev client_id, reject — the
130
+ // anchor should have put us in the prod block, but this guards against
131
+ // the block being laid out in an unexpected order across builds.
132
+ if (clientId === '22422756-60c9-4084-8eb7-27705fd5cf9a')
125
133
  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
134
  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;
135
+ const authMatch = /CLAUDE_AI_AUTHORIZE_URL\s*:\s*"([^"]+)"/.exec(prodBlock);
136
+ if (authMatch && authMatch[1])
137
+ authorizeUrl = authMatch[1];
140
138
  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 ');
139
+ const tokenMatch = /TOKEN_URL\s*:\s*"(https:\/\/[^"]*\/oauth\/token[^"]*)"/.exec(prodBlock);
140
+ if (tokenMatch && tokenMatch[1])
141
+ tokenUrl = tokenMatch[1];
142
+ // Scopes live in a separate array-of-strings block elsewhere in the
143
+ // binary, not inside the prod config object itself. Search globally
144
+ // for the first quoted `user:profile ...` run.
156
145
  let scopes = FALLBACK.scopes;
146
+ const scopeAnchor = Buffer.from('"user:profile ');
157
147
  const scopeIdx = buf.indexOf(scopeAnchor);
158
148
  if (scopeIdx !== -1) {
159
149
  const w = buf.slice(scopeIdx, Math.min(buf.length, scopeIdx + 512)).toString('latin1');
@@ -886,4 +886,4 @@
886
886
  "WebSearch",
887
887
  "Write"
888
888
  ]
889
- }
889
+ }
@@ -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;
@@ -46,6 +46,16 @@ const TOOL_MAP = {
46
46
  browse: { ccTool: 'WebFetch', translateArgs: (a) => ({ url: a.url || '' }) },
47
47
  notebook: { ccTool: 'NotebookEdit' },
48
48
  notebook_edit: { ccTool: 'NotebookEdit' },
49
+ // Additional client tool mappings
50
+ browser: { ccTool: 'WebFetch', translateArgs: (a) => ({ url: String(a.url || '') }) },
51
+ message: { ccTool: 'AskUserQuestion', translateArgs: (a) => ({ question: String(a.message || a.content || '') }) },
52
+ todo_read: { ccTool: 'TodoWrite', translateArgs: () => ({ todos: [] }) },
53
+ todo_write: { ccTool: 'TodoWrite', translateArgs: (a) => ({ todos: a.todos || [] }) },
54
+ notebook_read: { ccTool: 'NotebookEdit', translateArgs: (a) => ({ notebook_path: String(a.notebook_path || a.path || '') }) },
55
+ enter_plan_mode: { ccTool: 'EnterPlanMode' },
56
+ exit_plan_mode: { ccTool: 'ExitPlanMode' },
57
+ enter_worktree: { ccTool: 'EnterWorktree', translateArgs: (a) => ({ path: a.path }) },
58
+ exit_worktree: { ccTool: 'ExitWorktree' },
49
59
  };
50
60
  /**
51
61
  * Build a CC-template request from a client request.
@@ -79,34 +89,47 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
79
89
  const activeToolMap = new Map();
80
90
  const unmappedTools = [];
81
91
  if (clientTools && !opts.preserveTools) {
92
+ // Two passes so the unmapped-tool distributor can avoid colliding with
93
+ // CC tools the client already uses directly. Without this, a client
94
+ // sending both `WebSearch` and some unmapped tool like `memory_get`
95
+ // could have both forward-map to `WebSearch`, and the reverse map would
96
+ // then rewrite real `WebSearch` responses to the collided client name.
97
+ const claimedCC = new Set();
82
98
  for (const tool of clientTools) {
83
99
  const name = (tool.name || '').toLowerCase();
84
100
  const mapping = TOOL_MAP[name];
85
101
  if (mapping) {
86
102
  activeToolMap.set(tool.name, mapping);
103
+ claimedCC.add(mapping.ccTool);
87
104
  }
88
- else {
89
- unmappedTools.push(tool.name);
90
- // Distribute unmapped tools across CC tool names to avoid suspicious
91
- // patterns where every unknown tool maps to Bash
92
- const CC_FALLBACK_TOOLS = ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
93
- const fallbackTool = CC_FALLBACK_TOOLS[unmappedTools.length % CC_FALLBACK_TOOLS.length];
94
- activeToolMap.set(tool.name, {
95
- ccTool: fallbackTool,
96
- translateArgs: (a) => {
97
- // Translate args to match the CC tool's expected schema
98
- switch (fallbackTool) {
99
- case 'Bash': return { command: `echo "${JSON.stringify(a).slice(0, 200)}"` };
100
- case 'Read': return { file_path: String(a.path || a.file || a.url || '/tmp/output') };
101
- case 'Grep': return { pattern: String(a.query || a.pattern || a.search || '.'), path: '.' };
102
- case 'Glob': return { pattern: String(a.pattern || a.glob || '*') };
103
- case 'WebSearch': return { query: String(a.query || a.q || a.search || '') };
104
- case 'WebFetch': return { url: String(a.url || a.uri || '') };
105
- default: return a;
106
- }
107
- },
108
- });
109
- }
105
+ }
106
+ const CC_FALLBACK_TOOLS = ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
107
+ for (const tool of clientTools) {
108
+ const name = (tool.name || '').toLowerCase();
109
+ if (TOOL_MAP[name])
110
+ continue;
111
+ unmappedTools.push(tool.name);
112
+ // Exclude CC tools the client already uses so we never create a
113
+ // two-client-names-to-one-CC-tool collision. If every fallback is
114
+ // claimed (rare: client already uses 6+ CC tools), fall back to the
115
+ // full pool and accept the ambiguity.
116
+ const pool = CC_FALLBACK_TOOLS.filter(t => !claimedCC.has(t));
117
+ const fallbackPool = pool.length > 0 ? pool : CC_FALLBACK_TOOLS;
118
+ const fallbackTool = fallbackPool[(unmappedTools.length - 1) % fallbackPool.length];
119
+ activeToolMap.set(tool.name, {
120
+ ccTool: fallbackTool,
121
+ translateArgs: (a) => {
122
+ switch (fallbackTool) {
123
+ case 'Bash': return { command: `echo "${JSON.stringify(a).slice(0, 200)}"` };
124
+ case 'Read': return { file_path: String(a.path || a.file || a.url || '/tmp/output') };
125
+ case 'Grep': return { pattern: String(a.query || a.pattern || a.search || '.'), path: '.' };
126
+ case 'Glob': return { pattern: String(a.pattern || a.glob || '*') };
127
+ case 'WebSearch': return { query: String(a.query || a.q || a.search || '') };
128
+ case 'WebFetch': return { url: String(a.url || a.uri || '') };
129
+ default: return a;
130
+ }
131
+ },
132
+ });
110
133
  }
111
134
  }
112
135
  // ── Remap tool_use and tool_result references in message history ──
@@ -182,7 +205,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
182
205
  systemText = systemText.replace(pattern, '');
183
206
  }
184
207
  // ── Build the CC request from template ──
185
- // Key order matches CC v2.1.104 MITM capture exactly:
208
+ // Key order matches CC v2.1.104 exactly:
186
209
  // model, messages, system, tools, metadata, max_tokens, thinking, context_management, output_config, stream
187
210
  //
188
211
  // System prompt structure (3 blocks, matching real CC):
@@ -232,14 +255,26 @@ export function reverseMapResponse(responseBody, toolMap) {
232
255
  if (toolMap.size === 0)
233
256
  return responseBody;
234
257
  let result = responseBody;
235
- // Build reverse map: CC tool name → original client tool name
258
+ // Build reverse map: CC tool name → original client tool name.
259
+ // Two passes so identity mappings (client sent a tool with the real CC
260
+ // name) claim their CC slot first and can never be overwritten by a
261
+ // non-identity entry. Without this, a collision between a direct
262
+ // `WebSearch` and an unmapped-tool fallback landing on `WebSearch` could
263
+ // rewrite the real search response to the wrong client name.
236
264
  const reverseMap = new Map();
265
+ const identityClaimed = new Set();
237
266
  for (const [clientName, mapping] of toolMap) {
238
- // Only add if not a direct CC tool name
239
- if (clientName.toLowerCase() !== mapping.ccTool.toLowerCase()) {
240
- reverseMap.set(mapping.ccTool, clientName);
267
+ if (clientName.toLowerCase() === mapping.ccTool.toLowerCase()) {
268
+ identityClaimed.add(mapping.ccTool);
241
269
  }
242
270
  }
271
+ for (const [clientName, mapping] of toolMap) {
272
+ if (clientName.toLowerCase() === mapping.ccTool.toLowerCase())
273
+ continue;
274
+ if (identityClaimed.has(mapping.ccTool))
275
+ continue;
276
+ reverseMap.set(mapping.ccTool, clientName);
277
+ }
243
278
  for (const [ccName, clientName] of reverseMap) {
244
279
  result = result.replace(new RegExp(`"name"\\s*:\\s*"${ccName}"`, 'g'), `"name":"${clientName}"`);
245
280
  }
package/dist/cli.js CHANGED
@@ -125,13 +125,22 @@ async function proxy() {
125
125
  console.error('[dario] Invalid port. Must be 1-65535.');
126
126
  process.exit(1);
127
127
  }
128
+ // Bind address — accepts --host=<addr>; falls through to DARIO_HOST env
129
+ // var or the default of 127.0.0.1 inside startProxy. The sanity check
130
+ // here only rejects obviously bad shapes; real address validation
131
+ // happens when the OS tries to bind.
132
+ const hostArg = args.find(a => a.startsWith('--host='));
133
+ const host = hostArg ? hostArg.split('=')[1] : undefined;
134
+ if (host !== undefined && !/^[a-zA-Z0-9._:-]+$/.test(host)) {
135
+ console.error('[dario] Invalid --host. Must be an IP address or hostname.');
136
+ process.exit(1);
137
+ }
128
138
  const verbose = args.includes('--verbose') || args.includes('-v');
129
- const cliBackend = args.includes('--cli');
130
139
  const passthrough = args.includes('--passthrough') || args.includes('--thin');
131
140
  const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
132
141
  const modelArg = args.find(a => a.startsWith('--model='));
133
142
  const model = modelArg ? modelArg.split('=')[1] : undefined;
134
- await startProxy({ port, verbose, model, cliBackend, passthrough, preserveTools });
143
+ await startProxy({ port, host, verbose, model, passthrough, preserveTools });
135
144
  }
136
145
  async function help() {
137
146
  console.log(`
@@ -149,15 +158,20 @@ async function help() {
149
158
  Shortcuts: opus, sonnet, haiku
150
159
  Full IDs: claude-opus-4-6, claude-sonnet-4-6
151
160
  Default: passthrough (client decides)
152
- --cli Use Claude CLI as backend (bypasses rate limits)
153
- --passthrough Thin proxy — OAuth swap only, no injection
161
+ --passthrough, --thin Thin proxy OAuth swap only, no injection
154
162
  --preserve-tools Keep client tool schemas (for agents with custom tools)
155
163
  --port=PORT Port to listen on (default: 3456)
164
+ --host=ADDRESS Address to bind to (default: 127.0.0.1)
165
+ Use 0.0.0.0 to accept connections from other machines.
166
+ Alternatively set DARIO_HOST env var.
167
+ When binding non-loopback, also set DARIO_API_KEY
168
+ so unauthenticated LAN hosts can't proxy through
169
+ your OAuth subscription.
156
170
  --verbose, -v Log all requests
157
171
 
158
172
  Quick start:
159
173
  dario login # auto-detects Claude Code credentials
160
- dario proxy # or: dario proxy --cli --model=opus
174
+ dario proxy --model=opus # or: dario proxy --passthrough
161
175
 
162
176
  Then point any Anthropic SDK at http://localhost:3456:
163
177
  export ANTHROPIC_BASE_URL=http://localhost:3456
package/dist/proxy.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  interface ProxyOptions {
2
2
  port?: number;
3
+ host?: string;
3
4
  verbose?: boolean;
4
5
  model?: string;
5
- cliBackend?: boolean;
6
6
  passthrough?: boolean;
7
7
  preserveTools?: boolean;
8
8
  }