From 7b0f0335527b8849102092074b8e6c0a1524a0bf Mon Sep 17 00:00:00 2001 From: BlockVectra <5194435+gjwang@users.noreply.github.com> Date: Fri, 6 Nov 2026 01:03:39 +0800 Subject: [PATCH] fix: keep quickstart credentials out of commands and logs --- README.md | 21 +++++++++++++---- README.zh.md | 21 +++++++++++++---- src/index.ts | 6 ----- tests/onboarding-key-reuse.test.ts | 36 +++++++++++++++++++++++++----- 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e192f83..7139e1b 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,6 @@ Illustrative output (addresses, keys, block heights and dataset counts vary; thi Signing verbatim challenge message with EIP-191... Submitting SIWE login... Account Created: true - Session Token: rgs_4f5b... [3/5] Provisioning API key... Key ID: k_xxxxxxxxxxxx @@ -325,12 +324,26 @@ claude mcp add --transport http blockvectra-docs https://docs.blockvectra.com/mc ``` #### Keyed MCP Tools -To enable tools that execute on-chain queries (`rpc_call`, `data_api_get`), pass the API key in the `-H` option: +RPC availability without a key follows the selected chain's public methods and limits. Calls outside that policy, indexed Data API and account tools require your own API key; keyed calls still have method and usage limits. -```bash -claude mcp add --transport http -H "x-api-key: $BLOCKVECTRA_API_KEY" blockvectra-docs https://docs.blockvectra.com/mcp +Merge this entry into your project's `.mcp.json`, preserving any other servers. If the earlier add command created a local entry with the same name, remove that override or update its configuration instead; local entries take precedence over project configuration: + +```json +{ + "mcpServers": { + "blockvectra-docs": { + "type": "http", + "url": "https://docs.blockvectra.com/mcp", + "headers": { + "x-api-key": "${BLOCKVECTRA_API_KEY}" + } + } + } +} ``` +Set `BLOCKVECTRA_API_KEY` privately before starting Claude Code. Keep the literal variable reference in the file; Claude Code [expands it in the header](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcpjson). Do not put the key in CLI arguments or chat. + ### 2. Cursor Configuration Add the server to `.cursor/mcp.json` or through Cursor Settings: diff --git a/README.zh.md b/README.zh.md index 70c5516..89bf5ed 100644 --- a/README.zh.md +++ b/README.zh.md @@ -273,7 +273,6 @@ const result = await collectBalancePages(async (cursor) => { Signing verbatim challenge message with EIP-191... Submitting SIWE login... Account Created: true - Session Token: rgs_4f5b... [3/5] Provisioning API key... Key ID: k_xxxxxxxxxxxx @@ -314,12 +313,26 @@ claude mcp add --transport http blockvectra-docs https://docs.blockvectra.com/mc ``` #### 带 Key 调用工具 -若需使用 `rpc_call` 和 `data_api_get` 两个链上数据工具,通过 `-H` 参数传递 API Key: +免 Key RPC 的可用方法和限额以所选链的公开策略为准。策略之外的调用、索引 Data API 和账户工具需要你自己的 API Key;带 Key 调用仍受方法和用量限制。 -```bash -claude mcp add --transport http -H "x-api-key: $BLOCKVECTRA_API_KEY" blockvectra-docs https://docs.blockvectra.com/mcp +将此条目合并到项目的 `.mcp.json`,保留其他已有服务。如果之前的添加命令已创建同名 local 条目,请移除该覆盖项或直接更新其配置;local 条目优先于项目配置: + +```json +{ + "mcpServers": { + "blockvectra-docs": { + "type": "http", + "url": "https://docs.blockvectra.com/mcp", + "headers": { + "x-api-key": "${BLOCKVECTRA_API_KEY}" + } + } + } +} ``` +启动 Claude Code 前私密设置 `BLOCKVECTRA_API_KEY`。文件中只保留字面的变量引用,由 Claude Code [在请求头中展开](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcpjson);不要将 Key 放入 CLI 参数或聊天。 + ### 2. Cursor 配置 在项目根目录 `.cursor/mcp.json` 或 Cursor 设置中添加配置: diff --git a/src/index.ts b/src/index.ts index 8fbf329..fd36496 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,10 +10,6 @@ const API_BASE = "https://api.blockvectra.com/v1"; const WALLET_FILE = path.resolve(".agent-wallet.json"); const KEY_FILE = path.resolve(".agent-key.json"); -function mask(value: string): string { - return value.length > 8 ? `${value.slice(0, 8)}...` : value; -} - const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function readRpcQuantity(chain: string, apiKey: string, method: "eth_chainId" | "eth_blockNumber") { @@ -51,7 +47,6 @@ async function main() { if (apiKey) { console.log("[1-3/5] Found BLOCKVECTRA_API_KEY in environment, skipping wallet & key provisioning.\n"); - console.log(` API Key: ${mask(apiKey)}`); } else { const { account, source } = loadOrCreateWallet({ walletFile: WALLET_FILE, @@ -104,7 +99,6 @@ async function main() { }; const sessionToken = loginData.session.token; console.log(` Account Created: ${loginData.account_created}`); - console.log(` Session Token: ${mask(sessionToken)}`); // 3. Create an API key console.log("\n[3/5] Provisioning API key..."); diff --git a/tests/onboarding-key-reuse.test.ts b/tests/onboarding-key-reuse.test.ts index 064ea7c..d72c342 100644 --- a/tests/onboarding-key-reuse.test.ts +++ b/tests/onboarding-key-reuse.test.ts @@ -10,7 +10,9 @@ const API_KEY = "rgw_fixture_not_a_real_credential_0123456789"; const SESSION = "rgs_fixture_not_a_real_session_0123456789"; const entry = new URL("../src/index.ts", import.meta.url).href; const loader = new URL("../node_modules/tsx/dist/loader.mjs", import.meta.url).pathname; -function run(directory: string, options: { apiKey?: string; denySave?: boolean; revokeFails?: boolean; expectedStatus?: number } = {}) { +function run(directory: string, options: { apiKey?: string; createdApiKey?: string; session?: string; denySave?: boolean; revokeFails?: boolean; expectedStatus?: number } = {}) { + const apiKey = options.apiKey ?? options.createdApiKey ?? API_KEY; + const session = options.session ?? SESSION; const callsFile = path.join(directory, "calls.json"); fs.writeFileSync(callsFile, "[]"); const env = { ...process.env }; @@ -33,13 +35,13 @@ function run(directory: string, options: { apiKey?: string; denySave?: boolean; fs.writeFileSync(${JSON.stringify(callsFile)}, JSON.stringify(calls)); let body; if (url.endsWith('/auth/siwe/challenge')) body = {message: 'offline fixture challenge'}; - else if (url.endsWith('/auth/siwe/login')) body = {session: {token: ${JSON.stringify(SESSION)}}, account_created: false}; + else if (url.endsWith('/auth/siwe/login')) body = {session: {token: ${JSON.stringify(session)}}, account_created: false}; else if (url.endsWith('/revoke')) return new Response('{}', {status: ${JSON.stringify(options.revokeFails ? 503 : 200)}}); - else if (url.endsWith('/keys')) body = {key: {key_id: 'k_fixture', status: 'active'}, api_key: ${JSON.stringify(API_KEY)}}; + else if (url.endsWith('/keys')) body = {key: {key_id: 'k_fixture', status: 'active'}, api_key: ${JSON.stringify(apiKey)}}; else if (url.endsWith('/chains')) body = {chains: [{chain: 'robinhood_mainnet', chain_id: 4663}]}; else if (url.endsWith('/status/freshness')) body = {meta: {chain: 'robinhood_mainnet'}, data: []}; else if (url.endsWith('/robinhood_mainnet')) { - if (new Headers(init.headers).get('x-api-key') !== ${JSON.stringify(API_KEY)}) throw new Error('wrong fixture credential'); + if (new Headers(init.headers).get('x-api-key') !== ${JSON.stringify(apiKey)}) throw new Error('wrong fixture credential'); const request = JSON.parse(init.body); body = {jsonrpc: '2.0', id: 1, result: request.method === 'eth_chainId' ? '0x1237' : '0x2a'}; } else throw new Error('unplanned fixture request'); @@ -48,8 +50,10 @@ function run(directory: string, options: { apiKey?: string; denySave?: boolean; await import(${JSON.stringify(entry)}); `], {cwd: directory, env, encoding: 'utf8', timeout: 5000}); assert.equal(child.status, options.expectedStatus ?? 0, child.stderr); - assert.ok(!(child.stdout + child.stderr).includes(API_KEY)); - assert.ok(!(child.stdout + child.stderr).includes(SESSION)); + for (const secret of [apiKey, session]) { + assert.ok(!(child.stdout + child.stderr).includes(secret), 'must not print a credential'); + assert.ok(!(child.stdout + child.stderr).includes(secret.slice(0, 8)), 'must not print a credential prefix'); + } const calls = JSON.parse(fs.readFileSync(callsFile, 'utf8')) as Array<{url: string; method: string}>; return {calls, stdout: child.stdout, stderr: child.stderr}; } @@ -69,6 +73,26 @@ test("two complete offline CLI runs create one key and reuse its exact saved byt assert.ok(fs.readFileSync(keyFile).equals(bytes)); }); +for (const [length, apiKey, session] of [ + ['short', 'qA7!sE', 'tK9!rZ'], + ['long', 'qA7!sE_fixture_key_0123456789', 'tK9!rZ_fixture_session_0123456789'], +]) { + for (const fromEnvironment of [false, true]) { + test(`${length} credentials stay out of CLI output (environment key=${fromEnvironment})`, (t) => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'quickstart-no-secret-')); + t.after(() => fs.rmSync(directory, {recursive: true, force: true})); + const {calls, stdout} = run(directory, { + ...(fromEnvironment ? {apiKey} : {createdApiKey: apiKey}), + session, + }); + assert.equal(calls.length, fromEnvironment ? 4 : 7); + assert.equal(calls.filter(c => c.url.includes('/auth/')).length, fromEnvironment ? 0 : 2); + assert.equal(calls.filter(c => c.url.endsWith('/keys')).length, fromEnvironment ? 0 : 1); + assert.match(stdout, /Quickstart Completed Successfully/); + }); + } +} + for (const revokeFails of [false, true]) { test(`a local save failure attempts exactly one revoke (accepted=${!revokeFails}) and stops before RPC`, (t) => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'quickstart-key-save-'));