@1sat/cli 0.0.12 → 0.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
@@ -24,7 +24,8 @@
24
24
  "chalk": "^5.0.0",
25
25
  "@clack/prompts": "^0.8.0",
26
26
  "bitcoin-backup": "^0.0.11",
27
- "dotenv": "^17.0.0"
27
+ "dotenv": "^17.0.0",
28
+ "evlog": "^2.10.0"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@types/bun": "^1.3.9",
@@ -1,54 +1,126 @@
1
1
  /**
2
2
  * 1sat mcp-proxy — stdio-to-HTTP bridge for the wallet-desktop MCP server.
3
3
  *
4
- * Authenticates with BRC-103/104 via AuthFetch + ProtoWallet and proxies
5
- * JSON-RPC newline-delimited messages from stdin to the local MCP server
6
- * at :3322, writing responses to stdout.
7
- *
8
- * Used by Claude Code's .mcp.json to connect agents to the running
9
- * 1Sat desktop wallet.
4
+ * Performs BRC-31 handshake, then proxies JSON-RPC messages from stdin
5
+ * to the MCP server at :3322 with signed auth headers on each request.
10
6
  */
11
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
12
- import { AuthFetch, PrivateKey, ProtoWallet } from '@bsv/sdk'
8
+ import { Hash, KeyDeriver, PrivateKey, Signature, Utils } from '@bsv/sdk'
9
+ import type { WalletProtocol } from '@bsv/sdk'
10
+ import { createLogger, initLogger } from 'evlog'
11
+
12
+ const { toBase64, toArray } = Utils
13
+
14
+ initLogger({ env: { service: '1sat-mcp-proxy' } })
13
15
 
14
16
  const MCP_URL = process.env.ONESAT_MCP_URL ?? 'http://127.0.0.1:3322'
15
17
  const KEY_DIR = `${process.env.HOME}/.1sat-wallet`
16
18
  const CLIENT_KEY_PATH = `${KEY_DIR}/mcp-agent.key`
19
+ const AUTH_PROTOCOL_ID: WalletProtocol = [2, 'authrite message signature']
17
20
 
18
- function log(msg: string): void {
19
- process.stderr.write(`[1sat mcp-proxy] ${msg}\n`)
21
+ function generateNonce(): string {
22
+ const bytes = new Uint8Array(32)
23
+ crypto.getRandomValues(bytes)
24
+ return toBase64(Array.from(bytes))
20
25
  }
21
26
 
22
27
  function getClientKey(): PrivateKey {
23
28
  mkdirSync(KEY_DIR, { recursive: true })
24
-
25
29
  if (existsSync(CLIENT_KEY_PATH)) {
26
30
  return PrivateKey.fromWif(readFileSync(CLIENT_KEY_PATH, 'utf-8').trim())
27
31
  }
28
-
29
32
  const key = PrivateKey.fromRandom()
30
33
  writeFileSync(CLIENT_KEY_PATH, key.toWif(), { mode: 0o600 })
31
- log('Generated new agent identity key')
34
+ const log = createLogger({ context: 'startup' })
35
+ log.set({ event: 'key_generated', path: CLIENT_KEY_PATH })
36
+ log.emit()
32
37
  return key
33
38
  }
34
39
 
40
+ interface Session {
41
+ serverIdentityKey: string
42
+ serverNonce: string
43
+ clientKey: PrivateKey
44
+ }
45
+
46
+ async function handshake(key: PrivateKey): Promise<Session> {
47
+ const pubkey = key.toPublicKey().toString()
48
+ const clientNonce = generateNonce()
49
+
50
+ const res = await fetch(`${MCP_URL}/.well-known/auth`, {
51
+ method: 'POST',
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify({
54
+ authrite: '0.1',
55
+ messageType: 'initialRequest',
56
+ identityKey: pubkey,
57
+ nonce: clientNonce,
58
+ }),
59
+ })
60
+
61
+ if (!res.ok) throw new Error(`Handshake failed: HTTP ${res.status}`)
62
+
63
+ const data = await res.json()
64
+ if (data.messageType !== 'initialResponse') throw new Error(`Unexpected: ${data.messageType}`)
65
+
66
+ const serverNonce = data.nonce as string
67
+ const deriver = new KeyDeriver(key)
68
+ const serverPub = deriver.derivePublicKey(
69
+ AUTH_PROTOCOL_ID, `${serverNonce} ${clientNonce}`, data.identityKey as string, false,
70
+ )
71
+
72
+ const clientNonceBytes = toArray(clientNonce, 'base64')
73
+ const serverNonceBytes = toArray(serverNonce, 'base64')
74
+ const msgHash = Hash.sha256(Array.from(new Uint8Array([...clientNonceBytes, ...serverNonceBytes])))
75
+
76
+ const sig = Signature.fromDER(data.signature as string, 'hex')
77
+ if (!serverPub.verify(Array.from(msgHash), sig)) throw new Error('Server signature verification failed')
78
+
79
+ const log = createLogger({ context: 'auth' })
80
+ log.set({ event: 'handshake_complete', serverIdentityKey: (data.identityKey as string).slice(0, 16) })
81
+ log.emit()
82
+
83
+ return { serverIdentityKey: data.identityKey as string, serverNonce, clientKey: key }
84
+ }
85
+
86
+ function signHeaders(session: Session, pathname: string): Record<string, string> {
87
+ const { serverIdentityKey, serverNonce, clientKey: key } = session
88
+ const nonce = generateNonce()
89
+ const deriver = new KeyDeriver(key)
90
+ const derivedKey = deriver.derivePrivateKey(AUTH_PROTOCOL_ID, `${nonce} ${serverNonce}`, serverIdentityKey)
91
+ const payload = new TextEncoder().encode(pathname)
92
+ const msgHash = Hash.sha256(Array.from(payload))
93
+ const sig = derivedKey.sign(Array.from(msgHash))
94
+
95
+ return {
96
+ 'x-bsv-auth-version': '0.1',
97
+ 'x-bsv-auth-identity-key': key.toPublicKey().toString(),
98
+ 'x-bsv-auth-nonce': nonce,
99
+ 'x-bsv-auth-your-nonce': serverNonce,
100
+ 'x-bsv-auth-signature': sig.toDER('hex') as string,
101
+ }
102
+ }
103
+
35
104
  export async function handleMcpProxyCommand(): Promise<void> {
36
- // Health check before doing anything else
105
+ const startLog = createLogger({ context: 'startup' })
106
+
37
107
  try {
38
108
  await fetch(MCP_URL, { signal: AbortSignal.timeout(2000) })
39
109
  } catch {
40
- log(`Server not reachable at ${MCP_URL} — is 1Sat wallet running?`)
110
+ startLog.set({ event: 'server_unreachable', url: MCP_URL })
111
+ startLog.emit()
112
+ process.stderr.write(`[1sat mcp-proxy] Server not reachable at ${MCP_URL} — is 1Sat wallet running?\n`)
41
113
  process.exit(1)
42
114
  }
43
115
 
44
- const key = getClientKey()
45
- const wallet = new ProtoWallet(key)
46
- const authFetch = new AuthFetch(wallet)
116
+ startLog.set({ event: 'started', url: MCP_URL })
117
+ startLog.emit()
47
118
 
48
- log(`Connecting to ${MCP_URL}/mcp`)
119
+ const key = getClientKey()
120
+ const session = await handshake(key)
49
121
 
50
122
  let mcpSessionId: string | null = null
51
-
123
+ let requestCount = 0
52
124
  const decoder = new TextDecoder()
53
125
  const reader = Bun.stdin.stream().getReader()
54
126
  let buffer = ''
@@ -63,12 +135,15 @@ export async function handleMcpProxyCommand(): Promise<void> {
63
135
  while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
64
136
  const line = buffer.slice(0, newlineIdx).trim()
65
137
  buffer = buffer.slice(newlineIdx + 1)
66
-
67
138
  if (!line) continue
68
139
 
140
+ requestCount++
141
+ const reqLog = createLogger({ context: 'proxy' })
142
+
69
143
  const headers: Record<string, string> = {
70
144
  'Content-Type': 'application/json',
71
145
  Accept: 'application/json, text/event-stream',
146
+ ...signHeaders(session, '/mcp'),
72
147
  }
73
148
 
74
149
  if (mcpSessionId) {
@@ -76,17 +151,28 @@ export async function handleMcpProxyCommand(): Promise<void> {
76
151
  }
77
152
 
78
153
  try {
79
- const res = await authFetch.fetch(`${MCP_URL}/mcp`, {
80
- method: 'POST',
81
- body: line,
82
- headers,
83
- })
154
+ let method: string | undefined
155
+ try {
156
+ method = JSON.parse(line).method
157
+ } catch {}
158
+
159
+ const res = await fetch(`${MCP_URL}/mcp`, { method: 'POST', headers, body: line })
84
160
 
85
161
  const sessionHeader = res.headers.get('mcp-session-id')
86
162
  if (sessionHeader) mcpSessionId = sessionHeader
87
163
 
88
164
  const contentType = res.headers.get('content-type') ?? ''
89
165
 
166
+ reqLog.set({
167
+ event: 'request',
168
+ requestNum: requestCount,
169
+ method,
170
+ status: res.status,
171
+ contentType: contentType.split(';')[0],
172
+ sessionId: mcpSessionId?.slice(0, 8),
173
+ })
174
+ reqLog.emit()
175
+
90
176
  if (contentType.includes('text/event-stream')) {
91
177
  const text = await res.text()
92
178
  for (const eventLine of text.split('\n')) {
@@ -101,14 +187,13 @@ export async function handleMcpProxyCommand(): Promise<void> {
101
187
  }
102
188
  } catch (err) {
103
189
  const msg = err instanceof Error ? err.message : String(err)
104
- log(`Request failed: ${msg}`)
105
- process.stdout.write(
106
- `${JSON.stringify({
107
- jsonrpc: '2.0',
108
- error: { code: -32000, message: `MCP proxy error: ${msg}` },
109
- id: null,
110
- })}\n`,
111
- )
190
+ reqLog.set({ event: 'request_failed', requestNum: requestCount, error: msg })
191
+ reqLog.emit()
192
+ process.stdout.write(`${JSON.stringify({
193
+ jsonrpc: '2.0',
194
+ error: { code: -32000, message: `MCP proxy error: ${msg}` },
195
+ id: null,
196
+ })}\n`)
112
197
  }
113
198
  }
114
199
  }