@askalf/dario 3.4.1 → 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.
- package/dist/cc-oauth-detect.d.ts +30 -14
- package/dist/cc-oauth-detect.js +62 -72
- package/dist/cc-template-data.json +1 -1
- package/dist/cc-template.js +61 -26
- package/dist/cli.js +17 -1
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +36 -5
- package/package.json +4 -2
|
@@ -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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
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
|
export interface DetectedOAuthConfig {
|
|
24
36
|
clientId: string;
|
|
@@ -30,10 +42,14 @@ export interface DetectedOAuthConfig {
|
|
|
30
42
|
ccHash?: string;
|
|
31
43
|
}
|
|
32
44
|
/**
|
|
33
|
-
* Scan binary bytes for the
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
45
|
+
* Scan binary bytes for the PROD OAuth config block.
|
|
46
|
+
*
|
|
47
|
+
* Anchors on `BASE_API_URL:"https://api.anthropic.com"` — this literal
|
|
48
|
+
* only appears inside the prod config object (`nh$`). The LOCAL-dev block
|
|
49
|
+
* uses `http://localhost:8000` for the same key, and there's no staging
|
|
50
|
+
* block present in shipped builds. Once we find the anchor, the CLIENT_ID,
|
|
51
|
+
* CLAUDE_AI_AUTHORIZE_URL, TOKEN_URL, and scopes all live within a ~1.5KB
|
|
52
|
+
* window after it.
|
|
37
53
|
*/
|
|
38
54
|
export declare function scanBinaryForOAuthConfig(buf: Buffer): Omit<DetectedOAuthConfig, 'source' | 'ccPath' | 'ccHash'> | null;
|
|
39
55
|
/**
|
package/dist/cc-oauth-detect.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
112
|
-
//
|
|
113
|
-
const windowStart =
|
|
114
|
-
const windowEnd = Math.min(buf.length, anchorIdx +
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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');
|
package/dist/cc-template.js
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 ──
|
|
@@ -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
|
-
|
|
239
|
-
|
|
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,12 +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
139
|
const passthrough = args.includes('--passthrough') || args.includes('--thin');
|
|
130
140
|
const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
|
|
131
141
|
const modelArg = args.find(a => a.startsWith('--model='));
|
|
132
142
|
const model = modelArg ? modelArg.split('=')[1] : undefined;
|
|
133
|
-
await startProxy({ port, verbose, model, passthrough, preserveTools });
|
|
143
|
+
await startProxy({ port, host, verbose, model, passthrough, preserveTools });
|
|
134
144
|
}
|
|
135
145
|
async function help() {
|
|
136
146
|
console.log(`
|
|
@@ -151,6 +161,12 @@ async function help() {
|
|
|
151
161
|
--passthrough, --thin Thin proxy — OAuth swap only, no injection
|
|
152
162
|
--preserve-tools Keep client tool schemas (for agents with custom tools)
|
|
153
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.
|
|
154
170
|
--verbose, -v Log all requests
|
|
155
171
|
|
|
156
172
|
Quick start:
|
package/dist/proxy.d.ts
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -13,7 +13,16 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts
|
|
|
13
13
|
const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
|
|
14
14
|
const BODY_READ_TIMEOUT_MS = 30_000; // 30s — prevents slow-loris on body reads
|
|
15
15
|
const MAX_CONCURRENT = 10; // Max concurrent upstream requests
|
|
16
|
-
const
|
|
16
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
17
|
+
// A host is "loopback" if it's one of the well-known localhost literals.
|
|
18
|
+
// Used to decide whether to warn at startup about binding to a reachable
|
|
19
|
+
// interface — binding anywhere else means other machines can reach the
|
|
20
|
+
// proxy and should only be done with DARIO_API_KEY set.
|
|
21
|
+
function isLoopbackHost(host) {
|
|
22
|
+
if (host === '127.0.0.1' || host === '::1' || host === 'localhost')
|
|
23
|
+
return true;
|
|
24
|
+
return host.startsWith('127.');
|
|
25
|
+
}
|
|
17
26
|
// Simple semaphore for concurrency control
|
|
18
27
|
class Semaphore {
|
|
19
28
|
max;
|
|
@@ -308,6 +317,7 @@ function enrich429(body, headers) {
|
|
|
308
317
|
}
|
|
309
318
|
export async function startProxy(opts = {}) {
|
|
310
319
|
const port = opts.port ?? DEFAULT_PORT;
|
|
320
|
+
const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
|
|
311
321
|
const verbose = opts.verbose ?? false;
|
|
312
322
|
const passthrough = opts.passthrough ?? false;
|
|
313
323
|
// Verify auth before starting
|
|
@@ -354,7 +364,11 @@ export async function startProxy(opts = {}) {
|
|
|
354
364
|
// Optional proxy authentication — pre-encode key buffer for performance
|
|
355
365
|
const apiKey = process.env.DARIO_API_KEY;
|
|
356
366
|
const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
|
|
357
|
-
|
|
367
|
+
// CORS origin defaults to the localhost URL the proxy is served at. Users
|
|
368
|
+
// binding to a non-loopback address (e.g. a Tailscale interface) can
|
|
369
|
+
// override via DARIO_CORS_ORIGIN — otherwise browser-based clients hitting
|
|
370
|
+
// dario over the mesh will be blocked by their browser's CORS check.
|
|
371
|
+
const corsOrigin = process.env.DARIO_CORS_ORIGIN || `http://localhost:${port}`;
|
|
358
372
|
// Security headers for all responses
|
|
359
373
|
const SECURITY_HEADERS = {
|
|
360
374
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -754,22 +768,39 @@ export async function startProxy(opts = {}) {
|
|
|
754
768
|
}
|
|
755
769
|
process.exit(1);
|
|
756
770
|
});
|
|
757
|
-
server.listen(port,
|
|
771
|
+
server.listen(port, host, () => {
|
|
758
772
|
const modeLine = passthrough
|
|
759
773
|
? 'Mode: passthrough (OAuth swap only, no injection)'
|
|
760
774
|
: `OAuth: ${status.status} (expires in ${status.expiresIn})`;
|
|
761
775
|
const modelLine = modelOverride ? `Model: ${modelOverride} (all requests)` : 'Model: passthrough (client decides)';
|
|
776
|
+
// Display URL uses `localhost` for loopback binds and the literal host
|
|
777
|
+
// for exposed binds, so the printed URL is the one a client would
|
|
778
|
+
// actually use to reach the proxy.
|
|
779
|
+
const displayHost = isLoopbackHost(host) ? 'localhost' : host;
|
|
762
780
|
console.log('');
|
|
763
|
-
console.log(` dario — http
|
|
781
|
+
console.log(` dario — http://${displayHost}:${port}`);
|
|
764
782
|
console.log('');
|
|
765
783
|
console.log(' Your Claude subscription is now an API.');
|
|
766
784
|
console.log('');
|
|
767
785
|
console.log(' Usage:');
|
|
768
|
-
console.log(` ANTHROPIC_BASE_URL=http
|
|
786
|
+
console.log(` ANTHROPIC_BASE_URL=http://${displayHost}:${port}`);
|
|
769
787
|
console.log(' ANTHROPIC_API_KEY=dario');
|
|
770
788
|
console.log('');
|
|
771
789
|
console.log(` ${modeLine}`);
|
|
772
790
|
console.log(` ${modelLine}`);
|
|
791
|
+
if (!isLoopbackHost(host)) {
|
|
792
|
+
console.log('');
|
|
793
|
+
console.log(` ⚠ Bound to ${host} — reachable from other machines on the network.`);
|
|
794
|
+
if (!apiKey) {
|
|
795
|
+
console.log(' DARIO_API_KEY is not set. Any host that can reach this port can');
|
|
796
|
+
console.log(' proxy requests through your OAuth subscription. Set DARIO_API_KEY');
|
|
797
|
+
console.log(' before exposing dario beyond loopback.');
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
console.log(' DARIO_API_KEY is set — clients must send x-api-key or Authorization');
|
|
801
|
+
console.log(' to be accepted.');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
773
804
|
console.log('');
|
|
774
805
|
});
|
|
775
806
|
// Session presence heartbeat — keeps the OAuth session marked active
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.3",
|
|
4
4
|
"description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
"start": "node dist/cli.js",
|
|
27
27
|
"dev": "tsx src/cli.ts",
|
|
28
28
|
"e2e": "node test/e2e.mjs",
|
|
29
|
-
"compat": "node test/compat.mjs"
|
|
29
|
+
"compat": "node test/compat.mjs",
|
|
30
|
+
"lint:pkg": "node scripts/check-package-json.mjs",
|
|
31
|
+
"fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\""
|
|
30
32
|
},
|
|
31
33
|
"keywords": [
|
|
32
34
|
"claude",
|