@cortexkit/anthropic-auth-core 1.0.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/LICENSE +21 -0
- package/README.md +8 -0
- package/dist/accounts.d.ts +121 -0
- package/dist/accounts.js +606 -0
- package/dist/auth.d.ts +16 -0
- package/dist/auth.js +93 -0
- package/dist/cache1h.d.ts +33 -0
- package/dist/cache1h.js +97 -0
- package/dist/cch.d.ts +36 -0
- package/dist/cch.js +77 -0
- package/dist/constants.d.ts +39 -0
- package/dist/constants.js +56 -0
- package/dist/dump.d.ts +33 -0
- package/dist/dump.js +218 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +66 -0
- package/dist/pkce.d.ts +5 -0
- package/dist/pkce.js +17 -0
- package/dist/quotas.d.ts +23 -0
- package/dist/quotas.js +100 -0
- package/dist/relay.d.ts +27 -0
- package/dist/relay.js +867 -0
- package/package.json +32 -0
package/dist/auth.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { AUTHORIZE_URLS, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, TOKEN_URL, } from "./constants.js";
|
|
2
|
+
import { generatePKCE } from "./pkce.js";
|
|
3
|
+
function generateState() {
|
|
4
|
+
return crypto.randomUUID().replace(/-/g, '');
|
|
5
|
+
}
|
|
6
|
+
function parseCallbackInput(input) {
|
|
7
|
+
const trimmed = input.trim();
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(trimmed);
|
|
10
|
+
const code = url.searchParams.get('code');
|
|
11
|
+
const state = url.searchParams.get('state');
|
|
12
|
+
if (code && state) {
|
|
13
|
+
return { code, state };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Fall through to legacy/manual formats.
|
|
18
|
+
}
|
|
19
|
+
const hashSplits = trimmed.split('#');
|
|
20
|
+
if (hashSplits.length === 2 && hashSplits[0] && hashSplits[1]) {
|
|
21
|
+
return { code: hashSplits[0], state: hashSplits[1] };
|
|
22
|
+
}
|
|
23
|
+
const params = new URLSearchParams(trimmed);
|
|
24
|
+
const code = params.get('code');
|
|
25
|
+
const state = params.get('state');
|
|
26
|
+
if (code && state) {
|
|
27
|
+
return { code, state };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
async function exchangeCode(callback, verifier, redirectUri) {
|
|
32
|
+
const result = await fetch(TOKEN_URL, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
Accept: 'application/json, text/plain, */*',
|
|
37
|
+
'User-Agent': 'axios/1.13.6',
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
code: callback.code,
|
|
41
|
+
state: callback.state,
|
|
42
|
+
grant_type: 'authorization_code',
|
|
43
|
+
client_id: CLIENT_ID,
|
|
44
|
+
redirect_uri: redirectUri,
|
|
45
|
+
code_verifier: verifier,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
if (!result.ok) {
|
|
49
|
+
return {
|
|
50
|
+
type: 'failed',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const json = (await result.json());
|
|
54
|
+
return {
|
|
55
|
+
type: 'success',
|
|
56
|
+
refresh: json.refresh_token,
|
|
57
|
+
access: json.access_token,
|
|
58
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function authorize(mode) {
|
|
62
|
+
const pkce = await generatePKCE();
|
|
63
|
+
const state = generateState();
|
|
64
|
+
const url = new URL(AUTHORIZE_URLS[mode], import.meta.url);
|
|
65
|
+
url.searchParams.set('code', 'true');
|
|
66
|
+
url.searchParams.set('client_id', CLIENT_ID);
|
|
67
|
+
url.searchParams.set('response_type', 'code');
|
|
68
|
+
url.searchParams.set('redirect_uri', CODE_CALLBACK_URL);
|
|
69
|
+
url.searchParams.set('scope', OAUTH_SCOPES.join(' '));
|
|
70
|
+
url.searchParams.set('code_challenge', pkce.challenge);
|
|
71
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
72
|
+
url.searchParams.set('state', state);
|
|
73
|
+
return {
|
|
74
|
+
url: url.toString(),
|
|
75
|
+
redirectUri: CODE_CALLBACK_URL,
|
|
76
|
+
state,
|
|
77
|
+
verifier: pkce.verifier,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function exchange(input, verifier, redirectUri, expectedState) {
|
|
81
|
+
const callback = parseCallbackInput(input);
|
|
82
|
+
if (!callback) {
|
|
83
|
+
return {
|
|
84
|
+
type: 'failed',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (expectedState && callback.state !== expectedState) {
|
|
88
|
+
return {
|
|
89
|
+
type: 'failed',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return exchangeCode(callback, verifier, redirectUri);
|
|
93
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Cache1hMode } from './constants.ts';
|
|
2
|
+
export declare const CACHE_1H_COMMAND_NAME = "claude-cache";
|
|
3
|
+
export type Cache1hCommandAction = {
|
|
4
|
+
type: 'status';
|
|
5
|
+
} | {
|
|
6
|
+
type: 'enable';
|
|
7
|
+
} | {
|
|
8
|
+
type: 'disable';
|
|
9
|
+
} | {
|
|
10
|
+
type: 'mode';
|
|
11
|
+
mode: Cache1hMode;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'usage';
|
|
14
|
+
};
|
|
15
|
+
export declare function isCache1hEnabled(): boolean;
|
|
16
|
+
export declare function getCache1hMode(): "explicit" | "automatic" | "hybrid";
|
|
17
|
+
export declare function setCache1hEnabled(enabled: boolean): void;
|
|
18
|
+
export declare function setCache1hMode(mode: Cache1hMode): void;
|
|
19
|
+
export declare function setCache1hState(input: {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
mode: Cache1hMode;
|
|
22
|
+
}): void;
|
|
23
|
+
export declare function resetCache1hState(): void;
|
|
24
|
+
export declare function parseCache1hCommandAction(argumentsText: string): Cache1hCommandAction;
|
|
25
|
+
export declare function buildCache1hStatusSummary(input?: {
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
mode?: Cache1hMode;
|
|
28
|
+
}): string;
|
|
29
|
+
export declare function executeCache1hCommand(input: {
|
|
30
|
+
argumentsText: string;
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
mode?: Cache1hMode;
|
|
33
|
+
}): string;
|
package/dist/cache1h.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { DEFAULT_CACHE_1H_MODE } from "./constants.js";
|
|
2
|
+
export const CACHE_1H_COMMAND_NAME = 'claude-cache';
|
|
3
|
+
const CACHE_1H_STATUS_TITLE = '## Claude Cache Status';
|
|
4
|
+
const CACHE_1H_ENABLED_TITLE = '## Claude Cache Enabled';
|
|
5
|
+
const CACHE_1H_DISABLED_TITLE = '## Claude Cache Disabled';
|
|
6
|
+
const CACHE_1H_USAGE_TITLE = '## Claude Cache Usage';
|
|
7
|
+
const CACHE_1H_USAGE = 'Usage: `/claude-cache`, `/claude-cache on`, `/claude-cache off`, or `/claude-cache mode explicit|automatic|hybrid`.';
|
|
8
|
+
let cache1hEnabled = false;
|
|
9
|
+
let cache1hMode = DEFAULT_CACHE_1H_MODE;
|
|
10
|
+
export function isCache1hEnabled() {
|
|
11
|
+
return cache1hEnabled;
|
|
12
|
+
}
|
|
13
|
+
export function getCache1hMode() {
|
|
14
|
+
return cache1hMode;
|
|
15
|
+
}
|
|
16
|
+
export function setCache1hEnabled(enabled) {
|
|
17
|
+
cache1hEnabled = enabled;
|
|
18
|
+
}
|
|
19
|
+
export function setCache1hMode(mode) {
|
|
20
|
+
cache1hMode = mode;
|
|
21
|
+
}
|
|
22
|
+
export function setCache1hState(input) {
|
|
23
|
+
cache1hEnabled = input.enabled;
|
|
24
|
+
cache1hMode = input.mode;
|
|
25
|
+
}
|
|
26
|
+
export function resetCache1hState() {
|
|
27
|
+
cache1hEnabled = false;
|
|
28
|
+
cache1hMode = DEFAULT_CACHE_1H_MODE;
|
|
29
|
+
}
|
|
30
|
+
export function parseCache1hCommandAction(argumentsText) {
|
|
31
|
+
const normalized = argumentsText.trim().split(/\s+/).filter(Boolean);
|
|
32
|
+
if (normalized.length === 0)
|
|
33
|
+
return { type: 'status' };
|
|
34
|
+
if (normalized.length === 1 && normalized[0] === 'on')
|
|
35
|
+
return { type: 'enable' };
|
|
36
|
+
if (normalized.length === 1 && normalized[0] === 'off')
|
|
37
|
+
return { type: 'disable' };
|
|
38
|
+
if (normalized.length === 2 &&
|
|
39
|
+
normalized[0] === 'mode' &&
|
|
40
|
+
(normalized[1] === 'explicit' ||
|
|
41
|
+
normalized[1] === 'automatic' ||
|
|
42
|
+
normalized[1] === 'hybrid')) {
|
|
43
|
+
return { type: 'mode', mode: normalized[1] };
|
|
44
|
+
}
|
|
45
|
+
return { type: 'usage' };
|
|
46
|
+
}
|
|
47
|
+
export function buildCache1hStatusSummary(input) {
|
|
48
|
+
const enabled = input?.enabled ?? cache1hEnabled;
|
|
49
|
+
const mode = input?.mode ?? cache1hMode;
|
|
50
|
+
return [
|
|
51
|
+
CACHE_1H_STATUS_TITLE,
|
|
52
|
+
'',
|
|
53
|
+
`- Enabled: ${enabled ? 'enabled' : 'disabled'}`,
|
|
54
|
+
`- Mode: ${mode}`,
|
|
55
|
+
'- Persisted: ~/.config/opencode/anthropic-auth.json',
|
|
56
|
+
'- Scope: main sessions only; subagent sessions keep default ephemeral cache behavior',
|
|
57
|
+
'- Modes: explicit = existing OpenCode breakpoints; automatic = top-level cache_control only; hybrid = system + messages[0] + top-level cache_control',
|
|
58
|
+
'- TTL: 1h when enabled for main sessions; default ephemeral cache behavior otherwise',
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|
|
61
|
+
export function executeCache1hCommand(input) {
|
|
62
|
+
const action = parseCache1hCommandAction(input.argumentsText);
|
|
63
|
+
const enabled = input.enabled ?? cache1hEnabled;
|
|
64
|
+
const mode = input.mode ?? cache1hMode;
|
|
65
|
+
if (action.type === 'status')
|
|
66
|
+
return buildCache1hStatusSummary({ enabled, mode });
|
|
67
|
+
if (action.type === 'enable') {
|
|
68
|
+
return [
|
|
69
|
+
CACHE_1H_ENABLED_TITLE,
|
|
70
|
+
'',
|
|
71
|
+
buildCache1hStatusSummary({ enabled: true, mode }),
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
if (action.type === 'disable') {
|
|
75
|
+
return [
|
|
76
|
+
CACHE_1H_DISABLED_TITLE,
|
|
77
|
+
'',
|
|
78
|
+
buildCache1hStatusSummary({ enabled: false, mode }),
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
if (action.type === 'mode') {
|
|
82
|
+
return [
|
|
83
|
+
CACHE_1H_STATUS_TITLE,
|
|
84
|
+
'',
|
|
85
|
+
`Mode updated to \`${action.mode}\`.`,
|
|
86
|
+
'',
|
|
87
|
+
buildCache1hStatusSummary({ enabled, mode: action.mode }),
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
return [
|
|
91
|
+
CACHE_1H_USAGE_TITLE,
|
|
92
|
+
'',
|
|
93
|
+
CACHE_1H_USAGE,
|
|
94
|
+
'',
|
|
95
|
+
buildCache1hStatusSummary({ enabled, mode }),
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
package/dist/cch.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type Message = {
|
|
2
|
+
role?: string;
|
|
3
|
+
content?: string | Array<{
|
|
4
|
+
type?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
}>;
|
|
7
|
+
};
|
|
8
|
+
export declare const CCH_PATTERN: RegExp;
|
|
9
|
+
/**
|
|
10
|
+
* Extract text from the first user message's first text block.
|
|
11
|
+
* Kept for diagnostics/backward-compatible tests; CCH signing no longer uses it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractFirstUserMessageText(messages: Message[]): string;
|
|
14
|
+
/**
|
|
15
|
+
* Compute Claude Code's cch token over the final serialized request body.
|
|
16
|
+
*
|
|
17
|
+
* Real Claude Code signs the full body bytes with xxHash64 using a fixed seed,
|
|
18
|
+
* masks to 20 bits, and writes that value into the billing-header placeholder.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeCCH(bodyBytes: Uint8Array): Promise<string>;
|
|
21
|
+
export declare function signRequestBody(bodyString: string): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Compute a stable 3-character suffix for cc_version.
|
|
24
|
+
*
|
|
25
|
+
* The previous implementation sampled the first user message, which could make
|
|
26
|
+
* the system billing header change with conversation content. Keep this suffix
|
|
27
|
+
* stable for a day so it resembles a rotating Claude Code fingerprint without
|
|
28
|
+
* busting prompt-cache prefixes on every turn.
|
|
29
|
+
*/
|
|
30
|
+
export declare function computeVersionSuffix(version?: string, date?: Date): string;
|
|
31
|
+
/**
|
|
32
|
+
* Build the billing header with a cch placeholder.
|
|
33
|
+
* signRequestBody() must run after final request serialization to replace it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildBillingHeaderValue(_messages: Message[], version: string | undefined, entrypoint: string, date?: Date): string;
|
|
36
|
+
export {};
|
package/dist/cch.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import xxhashInit from 'xxhash-wasm';
|
|
3
|
+
import { CLAUDE_CODE_VERSION } from "./constants.js";
|
|
4
|
+
const CCH_SEED = 0x6e52736ac806831en;
|
|
5
|
+
export const CCH_PATTERN = /\bcch=([0-9a-f]{5});/;
|
|
6
|
+
let xxhashPromise = null;
|
|
7
|
+
let xxhash64Raw = null;
|
|
8
|
+
async function ensureXxhash() {
|
|
9
|
+
if (xxhash64Raw)
|
|
10
|
+
return;
|
|
11
|
+
xxhashPromise ??= (async () => {
|
|
12
|
+
const hasher = await xxhashInit();
|
|
13
|
+
xxhash64Raw = hasher.h64Raw;
|
|
14
|
+
})();
|
|
15
|
+
await xxhashPromise;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract text from the first user message's first text block.
|
|
19
|
+
* Kept for diagnostics/backward-compatible tests; CCH signing no longer uses it.
|
|
20
|
+
*/
|
|
21
|
+
export function extractFirstUserMessageText(messages) {
|
|
22
|
+
const userMsg = messages.find((message) => message.role === 'user');
|
|
23
|
+
if (!userMsg)
|
|
24
|
+
return '';
|
|
25
|
+
const { content } = userMsg;
|
|
26
|
+
if (typeof content === 'string')
|
|
27
|
+
return content;
|
|
28
|
+
if (Array.isArray(content)) {
|
|
29
|
+
const textBlock = content.find((block) => block.type === 'text');
|
|
30
|
+
if (textBlock?.text)
|
|
31
|
+
return textBlock.text;
|
|
32
|
+
}
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Compute Claude Code's cch token over the final serialized request body.
|
|
37
|
+
*
|
|
38
|
+
* Real Claude Code signs the full body bytes with xxHash64 using a fixed seed,
|
|
39
|
+
* masks to 20 bits, and writes that value into the billing-header placeholder.
|
|
40
|
+
*/
|
|
41
|
+
export async function computeCCH(bodyBytes) {
|
|
42
|
+
await ensureXxhash();
|
|
43
|
+
const hash = xxhash64Raw?.(bodyBytes, CCH_SEED) ?? 0n;
|
|
44
|
+
return (hash & 0xfffffn).toString(16).padStart(5, '0');
|
|
45
|
+
}
|
|
46
|
+
export async function signRequestBody(bodyString) {
|
|
47
|
+
if (!CCH_PATTERN.test(bodyString))
|
|
48
|
+
return bodyString;
|
|
49
|
+
const token = await computeCCH(new TextEncoder().encode(bodyString));
|
|
50
|
+
return bodyString.replace(CCH_PATTERN, `cch=${token};`);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compute a stable 3-character suffix for cc_version.
|
|
54
|
+
*
|
|
55
|
+
* The previous implementation sampled the first user message, which could make
|
|
56
|
+
* the system billing header change with conversation content. Keep this suffix
|
|
57
|
+
* stable for a day so it resembles a rotating Claude Code fingerprint without
|
|
58
|
+
* busting prompt-cache prefixes on every turn.
|
|
59
|
+
*/
|
|
60
|
+
export function computeVersionSuffix(version = CLAUDE_CODE_VERSION, date = new Date()) {
|
|
61
|
+
const dayStamp = date.toISOString().slice(0, 10);
|
|
62
|
+
return createHash('sha256')
|
|
63
|
+
.update(`${dayStamp}${version}`)
|
|
64
|
+
.digest('hex')
|
|
65
|
+
.slice(0, 3);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the billing header with a cch placeholder.
|
|
69
|
+
* signRequestBody() must run after final request serialization to replace it.
|
|
70
|
+
*/
|
|
71
|
+
export function buildBillingHeaderValue(_messages, version = CLAUDE_CODE_VERSION, entrypoint, date = new Date()) {
|
|
72
|
+
const suffix = computeVersionSuffix(version, date);
|
|
73
|
+
return ('x-anthropic-billing-header: ' +
|
|
74
|
+
`cc_version=${version}.${suffix}; ` +
|
|
75
|
+
`cc_entrypoint=${entrypoint}; ` +
|
|
76
|
+
'cch=00000;');
|
|
77
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
2
|
+
export declare const AUTHORIZE_URLS: {
|
|
3
|
+
readonly console: "https://platform.claude.com/oauth/authorize";
|
|
4
|
+
readonly max: "https://claude.ai/oauth/authorize";
|
|
5
|
+
};
|
|
6
|
+
export declare const CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
|
|
7
|
+
export declare const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
8
|
+
export declare const OAUTH_SCOPES: string[];
|
|
9
|
+
export declare const TOOL_PREFIX = "mcp_";
|
|
10
|
+
export declare const REQUIRED_BETAS: string[];
|
|
11
|
+
export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
|
|
12
|
+
export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
13
|
+
export declare const CCH_SALT = "59cf53e54c78";
|
|
14
|
+
export declare const CCH_POSITIONS: number[];
|
|
15
|
+
export declare const CLAUDE_CODE_VERSION = "2.1.87";
|
|
16
|
+
export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
17
|
+
export declare const USER_AGENT = "claude-cli/2.1.87 (external, cli)";
|
|
18
|
+
export declare const CACHE_1H_MODES: readonly ["explicit", "automatic", "hybrid"];
|
|
19
|
+
export type Cache1hMode = (typeof CACHE_1H_MODES)[number];
|
|
20
|
+
export declare const DEFAULT_CACHE_1H_MODE: Cache1hMode;
|
|
21
|
+
/**
|
|
22
|
+
* Anchors that identify paragraphs to remove from the system prompt.
|
|
23
|
+
* Any paragraph (text between blank lines) containing one of these
|
|
24
|
+
* strings is removed entirely.
|
|
25
|
+
*
|
|
26
|
+
* This is resilient to upstream rewording — as long as the anchor
|
|
27
|
+
* string (typically a URL) still appears somewhere in the paragraph,
|
|
28
|
+
* the removal works regardless of how the surrounding text changes.
|
|
29
|
+
*/
|
|
30
|
+
export declare const PARAGRAPH_REMOVAL_ANCHORS: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Inline text replacements applied after paragraph removal.
|
|
33
|
+
* These handle cases where "OpenCode" appears inside a paragraph
|
|
34
|
+
* we want to keep (so we can't remove the whole paragraph).
|
|
35
|
+
*/
|
|
36
|
+
export declare const TEXT_REPLACEMENTS: {
|
|
37
|
+
match: string;
|
|
38
|
+
replacement: string;
|
|
39
|
+
}[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
2
|
+
export const AUTHORIZE_URLS = {
|
|
3
|
+
console: 'https://platform.claude.com/oauth/authorize',
|
|
4
|
+
max: 'https://claude.ai/oauth/authorize',
|
|
5
|
+
};
|
|
6
|
+
export const CODE_CALLBACK_URL = 'https://platform.claude.com/oauth/code/callback';
|
|
7
|
+
export const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
8
|
+
export const OAUTH_SCOPES = [
|
|
9
|
+
'org:create_api_key',
|
|
10
|
+
'user:profile',
|
|
11
|
+
'user:inference',
|
|
12
|
+
'user:sessions:claude_code',
|
|
13
|
+
'user:mcp_servers',
|
|
14
|
+
'user:file_upload',
|
|
15
|
+
];
|
|
16
|
+
export const TOOL_PREFIX = 'mcp_';
|
|
17
|
+
export const REQUIRED_BETAS = [
|
|
18
|
+
'oauth-2025-04-20',
|
|
19
|
+
'interleaved-thinking-2025-05-14',
|
|
20
|
+
];
|
|
21
|
+
export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
|
|
22
|
+
export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
23
|
+
export const CCH_SALT = '59cf53e54c78';
|
|
24
|
+
export const CCH_POSITIONS = [4, 7, 20];
|
|
25
|
+
export const CLAUDE_CODE_VERSION = '2.1.87';
|
|
26
|
+
export const CLAUDE_CODE_ENTRYPOINT = 'sdk-cli';
|
|
27
|
+
export const USER_AGENT = 'claude-cli/2.1.87 (external, cli)';
|
|
28
|
+
export const CACHE_1H_MODES = ['explicit', 'automatic', 'hybrid'];
|
|
29
|
+
export const DEFAULT_CACHE_1H_MODE = 'explicit';
|
|
30
|
+
/**
|
|
31
|
+
* Anchors that identify paragraphs to remove from the system prompt.
|
|
32
|
+
* Any paragraph (text between blank lines) containing one of these
|
|
33
|
+
* strings is removed entirely.
|
|
34
|
+
*
|
|
35
|
+
* This is resilient to upstream rewording — as long as the anchor
|
|
36
|
+
* string (typically a URL) still appears somewhere in the paragraph,
|
|
37
|
+
* the removal works regardless of how the surrounding text changes.
|
|
38
|
+
*/
|
|
39
|
+
export const PARAGRAPH_REMOVAL_ANCHORS = [
|
|
40
|
+
// Help/feedback block — references the OpenCode GitHub repo
|
|
41
|
+
'github.com/anomalyco/opencode',
|
|
42
|
+
// OpenCode docs guidance — references the OpenCode docs URL
|
|
43
|
+
'opencode.ai/docs',
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Inline text replacements applied after paragraph removal.
|
|
47
|
+
* These handle cases where "OpenCode" appears inside a paragraph
|
|
48
|
+
* we want to keep (so we can't remove the whole paragraph).
|
|
49
|
+
*/
|
|
50
|
+
export const TEXT_REPLACEMENTS = [
|
|
51
|
+
{ match: 'if OpenCode honestly', replacement: 'if the assistant honestly' },
|
|
52
|
+
{
|
|
53
|
+
match: 'Here is some useful information about the environment you are running in:',
|
|
54
|
+
replacement: 'Environment context you are running in:',
|
|
55
|
+
},
|
|
56
|
+
];
|
package/dist/dump.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const CLAUDE_DUMP_COMMAND_NAME = "claude-dump";
|
|
2
|
+
export type DumpCommandAction = {
|
|
3
|
+
type: 'status';
|
|
4
|
+
} | {
|
|
5
|
+
type: 'enable';
|
|
6
|
+
} | {
|
|
7
|
+
type: 'disable';
|
|
8
|
+
} | {
|
|
9
|
+
type: 'usage';
|
|
10
|
+
};
|
|
11
|
+
export declare function isDumpEnabled(): boolean;
|
|
12
|
+
export declare function setDumpEnabled(enabled: boolean): void;
|
|
13
|
+
export declare function resetDumpState(): void;
|
|
14
|
+
export declare function getDumpDirectory(): string;
|
|
15
|
+
export declare function parseDumpCommandAction(argumentsText: string): DumpCommandAction;
|
|
16
|
+
export declare function buildDumpStatusSummary(input?: {
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
}): string;
|
|
19
|
+
export declare function executeDumpCommand(input: {
|
|
20
|
+
argumentsText: string;
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
}): string;
|
|
23
|
+
export declare function dumpRelayRequest(input: {
|
|
24
|
+
affinity: string;
|
|
25
|
+
transport: 'http' | 'websocket';
|
|
26
|
+
protocol: 1 | 2;
|
|
27
|
+
mode: 'full_sync' | 'patch';
|
|
28
|
+
status?: number;
|
|
29
|
+
bodyText: string;
|
|
30
|
+
previousBodyText?: string;
|
|
31
|
+
payload: unknown;
|
|
32
|
+
relayBytes: number;
|
|
33
|
+
}): Promise<void>;
|
package/dist/dump.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { relayLog } from "./logger.js";
|
|
6
|
+
export const CLAUDE_DUMP_COMMAND_NAME = 'claude-dump';
|
|
7
|
+
const DUMP_STATUS_TITLE = '## Claude Dump Status';
|
|
8
|
+
const DUMP_ENABLED_TITLE = '## Claude Dump Enabled';
|
|
9
|
+
const DUMP_DISABLED_TITLE = '## Claude Dump Disabled';
|
|
10
|
+
const DUMP_USAGE_TITLE = '## Claude Dump Usage';
|
|
11
|
+
const DUMP_USAGE = 'Usage: `/claude-dump`, `/claude-dump on`, or `/claude-dump off`.';
|
|
12
|
+
const DUMP_DIR = join(tmpdir(), 'opencode-anthropic-auth-dumps');
|
|
13
|
+
let dumpEnabled = false;
|
|
14
|
+
let nextDumpId = 0;
|
|
15
|
+
export function isDumpEnabled() {
|
|
16
|
+
return dumpEnabled;
|
|
17
|
+
}
|
|
18
|
+
export function setDumpEnabled(enabled) {
|
|
19
|
+
dumpEnabled = enabled;
|
|
20
|
+
}
|
|
21
|
+
export function resetDumpState() {
|
|
22
|
+
dumpEnabled = false;
|
|
23
|
+
}
|
|
24
|
+
export function getDumpDirectory() {
|
|
25
|
+
return DUMP_DIR;
|
|
26
|
+
}
|
|
27
|
+
export function parseDumpCommandAction(argumentsText) {
|
|
28
|
+
const normalized = argumentsText.trim().split(/\s+/).filter(Boolean);
|
|
29
|
+
if (normalized.length === 0)
|
|
30
|
+
return { type: 'status' };
|
|
31
|
+
if (normalized.length === 1 && normalized[0] === 'on')
|
|
32
|
+
return { type: 'enable' };
|
|
33
|
+
if (normalized.length === 1 && normalized[0] === 'off')
|
|
34
|
+
return { type: 'disable' };
|
|
35
|
+
return { type: 'usage' };
|
|
36
|
+
}
|
|
37
|
+
export function buildDumpStatusSummary(input) {
|
|
38
|
+
const enabled = input?.enabled ?? dumpEnabled;
|
|
39
|
+
return [
|
|
40
|
+
DUMP_STATUS_TITLE,
|
|
41
|
+
'',
|
|
42
|
+
`- Enabled: ${enabled ? 'enabled' : 'disabled'}`,
|
|
43
|
+
`- Directory: ${DUMP_DIR}`,
|
|
44
|
+
'- Persisted: ~/.config/opencode/anthropic-auth.json',
|
|
45
|
+
'- Captures: final rewritten Anthropic body plus redacted relay payload metadata',
|
|
46
|
+
'- Warning: body dumps may contain prompt/session content; turn this off after debugging',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
export function executeDumpCommand(input) {
|
|
50
|
+
const action = parseDumpCommandAction(input.argumentsText);
|
|
51
|
+
const enabled = input.enabled ?? dumpEnabled;
|
|
52
|
+
if (action.type === 'status')
|
|
53
|
+
return buildDumpStatusSummary({ enabled });
|
|
54
|
+
if (action.type === 'enable') {
|
|
55
|
+
return [
|
|
56
|
+
DUMP_ENABLED_TITLE,
|
|
57
|
+
'',
|
|
58
|
+
buildDumpStatusSummary({ enabled: true }),
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|
|
61
|
+
if (action.type === 'disable') {
|
|
62
|
+
return [
|
|
63
|
+
DUMP_DISABLED_TITLE,
|
|
64
|
+
'',
|
|
65
|
+
buildDumpStatusSummary({ enabled: false }),
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
return [
|
|
69
|
+
DUMP_USAGE_TITLE,
|
|
70
|
+
'',
|
|
71
|
+
DUMP_USAGE,
|
|
72
|
+
'',
|
|
73
|
+
buildDumpStatusSummary({ enabled }),
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
function shortAffinity(affinity) {
|
|
77
|
+
return affinity.length <= 16 ? affinity : `${affinity.slice(0, 12)}…`;
|
|
78
|
+
}
|
|
79
|
+
function hashText(value) {
|
|
80
|
+
return createHash('sha256').update(value).digest('hex');
|
|
81
|
+
}
|
|
82
|
+
function cchToken(bodyText) {
|
|
83
|
+
return bodyText.match(/\bcch=([0-9a-f]{5});/)?.[1] ?? null;
|
|
84
|
+
}
|
|
85
|
+
function diffSummary(previousBodyText, bodyText) {
|
|
86
|
+
if (previousBodyText == null)
|
|
87
|
+
return null;
|
|
88
|
+
if (previousBodyText === bodyText) {
|
|
89
|
+
return {
|
|
90
|
+
changed: false,
|
|
91
|
+
firstByte: -1,
|
|
92
|
+
lastPreviousByte: -1,
|
|
93
|
+
lastCurrentByte: -1,
|
|
94
|
+
previousBytes: previousBodyText.length,
|
|
95
|
+
currentBytes: bodyText.length,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
let firstByte = 0;
|
|
99
|
+
while (firstByte < previousBodyText.length &&
|
|
100
|
+
firstByte < bodyText.length &&
|
|
101
|
+
previousBodyText[firstByte] === bodyText[firstByte]) {
|
|
102
|
+
firstByte += 1;
|
|
103
|
+
}
|
|
104
|
+
let previousTail = previousBodyText.length - 1;
|
|
105
|
+
let currentTail = bodyText.length - 1;
|
|
106
|
+
while (previousTail >= firstByte &&
|
|
107
|
+
currentTail >= firstByte &&
|
|
108
|
+
previousBodyText[previousTail] === bodyText[currentTail]) {
|
|
109
|
+
previousTail -= 1;
|
|
110
|
+
currentTail -= 1;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
changed: true,
|
|
114
|
+
firstByte,
|
|
115
|
+
lastPreviousByte: previousTail,
|
|
116
|
+
lastCurrentByte: currentTail,
|
|
117
|
+
changedPreviousBytes: previousTail - firstByte + 1,
|
|
118
|
+
changedCurrentBytes: currentTail - firstByte + 1,
|
|
119
|
+
previousBytes: previousBodyText.length,
|
|
120
|
+
currentBytes: bodyText.length,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function parseBody(bodyText) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(bodyText);
|
|
126
|
+
return parsed != null &&
|
|
127
|
+
typeof parsed === 'object' &&
|
|
128
|
+
!Array.isArray(parsed)
|
|
129
|
+
? parsed
|
|
130
|
+
: null;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
relayLog(`dump body parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function hashJson(value) {
|
|
138
|
+
return hashText(JSON.stringify(value));
|
|
139
|
+
}
|
|
140
|
+
function bodyStructureSummary(bodyText) {
|
|
141
|
+
const parsed = parseBody(bodyText);
|
|
142
|
+
if (!parsed)
|
|
143
|
+
return { parseable: false };
|
|
144
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
145
|
+
const system = Array.isArray(parsed.system) ? parsed.system : [];
|
|
146
|
+
const message0 = messages[0];
|
|
147
|
+
const messagesAfter0 = messages.slice(1);
|
|
148
|
+
return {
|
|
149
|
+
parseable: true,
|
|
150
|
+
model: typeof parsed.model === 'string' ? parsed.model : undefined,
|
|
151
|
+
stream: parsed.stream,
|
|
152
|
+
systemCount: system.length,
|
|
153
|
+
messagesCount: messages.length,
|
|
154
|
+
systemHash: hashJson(system),
|
|
155
|
+
message0Hash: message0 === undefined ? null : hashJson(message0),
|
|
156
|
+
message0Bytes: message0 === undefined ? 0 : JSON.stringify(message0).length,
|
|
157
|
+
messagesAfter0Hash: hashJson(messagesAfter0),
|
|
158
|
+
messagesAfter0Bytes: JSON.stringify(messagesAfter0).length,
|
|
159
|
+
cch: cchToken(bodyText),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function redactForDump(value) {
|
|
163
|
+
if (Array.isArray(value))
|
|
164
|
+
return value.map(redactForDump);
|
|
165
|
+
if (value == null || typeof value !== 'object')
|
|
166
|
+
return value;
|
|
167
|
+
const redacted = {};
|
|
168
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
169
|
+
const lower = key.toLowerCase();
|
|
170
|
+
if (lower === 'authorization' ||
|
|
171
|
+
lower === 'x-api-key' ||
|
|
172
|
+
lower === 'cookie' ||
|
|
173
|
+
lower === 'set-cookie') {
|
|
174
|
+
redacted[key] = '[redacted]';
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
redacted[key] = redactForDump(entry);
|
|
178
|
+
}
|
|
179
|
+
return redacted;
|
|
180
|
+
}
|
|
181
|
+
export async function dumpRelayRequest(input) {
|
|
182
|
+
if (!dumpEnabled)
|
|
183
|
+
return;
|
|
184
|
+
nextDumpId += 1;
|
|
185
|
+
const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${String(nextDumpId).padStart(5, '0')}-${input.transport}-p${input.protocol}-${input.mode}`;
|
|
186
|
+
const prefix = join(DUMP_DIR, id);
|
|
187
|
+
try {
|
|
188
|
+
await mkdir(DUMP_DIR, { recursive: true });
|
|
189
|
+
const metadata = {
|
|
190
|
+
id,
|
|
191
|
+
createdAt: new Date().toISOString(),
|
|
192
|
+
session: shortAffinity(input.affinity),
|
|
193
|
+
transport: input.transport,
|
|
194
|
+
protocol: input.protocol,
|
|
195
|
+
mode: input.mode,
|
|
196
|
+
status: input.status,
|
|
197
|
+
bodyBytes: input.bodyText.length,
|
|
198
|
+
relayBytes: input.relayBytes,
|
|
199
|
+
bodyHash: hashText(input.bodyText),
|
|
200
|
+
diff: diffSummary(input.previousBodyText, input.bodyText),
|
|
201
|
+
body: bodyStructureSummary(input.bodyText),
|
|
202
|
+
files: {
|
|
203
|
+
body: `${prefix}.body.json`,
|
|
204
|
+
relay: `${prefix}.relay.json`,
|
|
205
|
+
metadata: `${prefix}.meta.json`,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
await Promise.all([
|
|
209
|
+
writeFile(`${prefix}.body.json`, input.bodyText, 'utf8'),
|
|
210
|
+
writeFile(`${prefix}.relay.json`, `${JSON.stringify(redactForDump(input.payload), null, 2)}\n`, 'utf8'),
|
|
211
|
+
writeFile(`${prefix}.meta.json`, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8'),
|
|
212
|
+
]);
|
|
213
|
+
relayLog(`dumped request id=${id} session=${shortAffinity(input.affinity)} body=${prefix}.body.json meta=${prefix}.meta.json`);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
relayLog(`dump failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|