op-anthropic-auth-v2 0.1.2 → 0.1.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/README.md +18 -4
- package/index.js +109 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,8 @@ an opaque `429` (`{"message":"Error"}`). This plugin rewrites every request to
|
|
|
29
29
|
|
|
30
30
|
- registers a `Claude Pro/Max` browser login using OAuth and PKCE
|
|
31
31
|
- lets OpenCode 2 store credentials and refresh expired tokens
|
|
32
|
-
- sets the `claude-cli` user-agent
|
|
32
|
+
- sets the `claude-cli` user-agent, matching your installed Claude Code version,
|
|
33
|
+
and the required `anthropic-beta` headers
|
|
33
34
|
- adds the `x-anthropic-billing-header` system block
|
|
34
35
|
- prepends the Claude Code identity to the system prompt and relocates remaining
|
|
35
36
|
system text into the first user message
|
|
@@ -59,9 +60,22 @@ This adaptation only targets OpenCode 2. OpenCode 1 users should use the origina
|
|
|
59
60
|
|
|
60
61
|
## Compatibility
|
|
61
62
|
|
|
62
|
-
Built against `opencode2` `0.0.0-beta-18743`.
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
Built against `opencode2` `0.0.0-beta-18743`. The v2 plugin API is beta and may
|
|
64
|
+
change; pin accordingly.
|
|
65
|
+
|
|
66
|
+
The plugin reports a Claude Code version to meet Anthropic's model compatibility
|
|
67
|
+
check. It reads that version from your own install rather than pinning one, in
|
|
68
|
+
this order:
|
|
69
|
+
|
|
70
|
+
1. the `CLAUDE_CODE_VERSION` environment variable, if it looks like a version
|
|
71
|
+
2. a native install — it follows the `claude` symlink on your `PATH` and takes the
|
|
72
|
+
version from the target path
|
|
73
|
+
3. an npm install — it reads the version from `@anthropic-ai/claude-code`'s
|
|
74
|
+
`package.json`
|
|
75
|
+
4. `2.1.265`, if no install is found
|
|
76
|
+
|
|
77
|
+
The lookup is cached for 60 seconds, so a Claude Code update takes effect without
|
|
78
|
+
restarting OpenCode. Set `CLAUDE_CODE_VERSION` to report a specific version.
|
|
65
79
|
|
|
66
80
|
The runtime uses `@openauthjs/openauth` for PKCE generation. Development uses the
|
|
67
81
|
matching `@opencode-ai/plugin` beta types, TypeScript, and Node.js types to check the
|
package/index.js
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
// Port of op-anthropic-auth@0.1.4 to the v2 plugin API.
|
|
3
3
|
// v1 loads op-anthropic-auth itself; this file targets opencode2 only.
|
|
4
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
6
13
|
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
14
|
+
import { basename, delimiter, dirname, join } from "node:path";
|
|
8
15
|
import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
9
16
|
|
|
10
17
|
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
@@ -26,18 +33,109 @@ const PARAGRAPH_REMOVAL_ANCHORS = [
|
|
|
26
33
|
const TEXT_REPLACEMENTS = [
|
|
27
34
|
{ match: "if OpenCode honestly", replacement: "if the assistant honestly" },
|
|
28
35
|
];
|
|
29
|
-
|
|
36
|
+
// Used only when the local Claude Code install cannot be found.
|
|
37
|
+
const FALLBACK_CLAUDE_CODE_VERSION = "2.1.265";
|
|
30
38
|
const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
31
39
|
const BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
32
40
|
const CCH_SALT = "59cf53e54c78";
|
|
33
41
|
const CCH_POSITIONS = [4, 7, 20];
|
|
34
|
-
const
|
|
42
|
+
const USER_AGENT_PREFIX = "claude-cli/";
|
|
35
43
|
const TOKEN_USER_AGENT = "axios/1.13.6";
|
|
44
|
+
const SEMVER = /^\d+\.\d+\.\d+/;
|
|
45
|
+
const VERSION_CACHE_MS = 60_000;
|
|
46
|
+
const NPM_PACKAGE_NAME = "@anthropic-ai/claude-code";
|
|
36
47
|
|
|
37
48
|
function isRecord(value) {
|
|
38
49
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
// ---- Claude Code version discovery ----
|
|
53
|
+
|
|
54
|
+
// Resolve the `claude` launcher through symlinks. The native installer points
|
|
55
|
+
// ~/.local/bin/claude at ~/.local/share/claude/versions/<version>, so the
|
|
56
|
+
// basename is the active version. Never pick the highest entry in versions/ —
|
|
57
|
+
// a failed download leaves a zero-byte file of a version that cannot run.
|
|
58
|
+
function resolveClaudeBinary() {
|
|
59
|
+
const candidates = [];
|
|
60
|
+
const searchPath = process.env.PATH;
|
|
61
|
+
if (searchPath) {
|
|
62
|
+
for (const dir of searchPath.split(delimiter)) {
|
|
63
|
+
if (dir) candidates.push(join(dir, "claude"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
candidates.push(join(homedir(), ".local", "bin", "claude"));
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
try {
|
|
69
|
+
if (existsSync(candidate)) return realpathSync(candidate);
|
|
70
|
+
} catch {
|
|
71
|
+
// Unreadable entry — keep looking.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// npm installs symlink `claude` to <root>/@anthropic-ai/claude-code/cli.js, so
|
|
78
|
+
// walk up from the resolved file to the package manifest.
|
|
79
|
+
function versionFromNpmPackage(binaryPath) {
|
|
80
|
+
let dir = dirname(binaryPath);
|
|
81
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
82
|
+
try {
|
|
83
|
+
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
84
|
+
if (isRecord(manifest) && manifest.name === NPM_PACKAGE_NAME) {
|
|
85
|
+
const version = manifest.version;
|
|
86
|
+
return typeof version === "string" && SEMVER.test(version) ? version : null;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// No manifest here — keep walking up.
|
|
90
|
+
}
|
|
91
|
+
const parent = dirname(dir);
|
|
92
|
+
if (parent === dir) break;
|
|
93
|
+
dir = parent;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function detectClaudeCodeVersion() {
|
|
99
|
+
const override = process.env.CLAUDE_CODE_VERSION;
|
|
100
|
+
if (override && SEMVER.test(override)) return override;
|
|
101
|
+
const binary = resolveClaudeBinary();
|
|
102
|
+
if (!binary) return null;
|
|
103
|
+
const name = basename(binary);
|
|
104
|
+
if (SEMVER.test(name)) return name;
|
|
105
|
+
return versionFromNpmPackage(binary);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let versionCache = { value: "", checkedAt: 0 };
|
|
109
|
+
|
|
110
|
+
// Cached with a TTL rather than resolved once at import: Claude Code updates
|
|
111
|
+
// itself while the OpenCode server stays up for days, and the request hook runs
|
|
112
|
+
// on every call.
|
|
113
|
+
function claudeCodeVersion() {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
if (versionCache.value && now - versionCache.checkedAt < VERSION_CACHE_MS) {
|
|
116
|
+
return versionCache.value;
|
|
117
|
+
}
|
|
118
|
+
let detected = null;
|
|
119
|
+
try {
|
|
120
|
+
detected = detectClaudeCodeVersion();
|
|
121
|
+
} catch {
|
|
122
|
+
detected = null;
|
|
123
|
+
}
|
|
124
|
+
const value = detected ?? FALLBACK_CLAUDE_CODE_VERSION;
|
|
125
|
+
versionCache = { value, checkedAt: now };
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function requestUserAgent() {
|
|
130
|
+
return `${USER_AGENT_PREFIX}${claudeCodeVersion()} (external, cli)`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Match the prefix, not the whole string. The version can change between a
|
|
134
|
+
// request and its response, and an exact match would skip the rewrite.
|
|
135
|
+
function isOwnUserAgent(value) {
|
|
136
|
+
return typeof value === "string" && value.startsWith(USER_AGENT_PREFIX);
|
|
137
|
+
}
|
|
138
|
+
|
|
41
139
|
function isPluginOAuthCredential(value) {
|
|
42
140
|
return (
|
|
43
141
|
isRecord(value) &&
|
|
@@ -282,21 +380,22 @@ function computeCCH(messageText) {
|
|
|
282
380
|
return createHash("sha256").update(messageText).digest("hex").slice(0, 5);
|
|
283
381
|
}
|
|
284
382
|
|
|
285
|
-
function computeVersionSuffix(messageText) {
|
|
383
|
+
function computeVersionSuffix(messageText, version) {
|
|
286
384
|
const chars = CCH_POSITIONS.map((index) => messageText[index] || "0").join("");
|
|
287
385
|
return createHash("sha256")
|
|
288
|
-
.update(`${CCH_SALT}${chars}${
|
|
386
|
+
.update(`${CCH_SALT}${chars}${version}`)
|
|
289
387
|
.digest("hex")
|
|
290
388
|
.slice(0, 3);
|
|
291
389
|
}
|
|
292
390
|
|
|
293
391
|
function buildBillingHeaderValue(messages) {
|
|
294
392
|
const text = extractFirstUserMessageText(messages);
|
|
295
|
-
const
|
|
393
|
+
const version = claudeCodeVersion();
|
|
394
|
+
const suffix = computeVersionSuffix(text, version);
|
|
296
395
|
const cch = computeCCH(text);
|
|
297
396
|
return (
|
|
298
397
|
`${BILLING_HEADER_PREFIX} ` +
|
|
299
|
-
`cc_version=${
|
|
398
|
+
`cc_version=${version}.${suffix}; ` +
|
|
300
399
|
`cc_entrypoint=${CLAUDE_CODE_ENTRYPOINT}; ` +
|
|
301
400
|
`cch=${cch};`
|
|
302
401
|
);
|
|
@@ -501,7 +600,7 @@ const plugin = {
|
|
|
501
600
|
const headers = new Headers(event.request.headers);
|
|
502
601
|
headers.set("authorization", `Bearer ${credential.access}`);
|
|
503
602
|
headers.set("anthropic-beta", mergeBetaHeaders(headers));
|
|
504
|
-
headers.set("user-agent",
|
|
603
|
+
headers.set("user-agent", requestUserAgent());
|
|
505
604
|
headers.delete("x-api-key");
|
|
506
605
|
|
|
507
606
|
if (url.pathname === "/v1/messages" && !url.searchParams.has("beta")) {
|
|
@@ -527,7 +626,7 @@ const plugin = {
|
|
|
527
626
|
await ctx.session.hook(
|
|
528
627
|
"http.response",
|
|
529
628
|
async (event) => {
|
|
530
|
-
if (event.request.headers.get("user-agent")
|
|
629
|
+
if (!isOwnUserAgent(event.request.headers.get("user-agent"))) return;
|
|
531
630
|
// Rewrite unless the response URL is present and provably non-Anthropic.
|
|
532
631
|
try {
|
|
533
632
|
const url = new URL(event.response.url);
|