@1sat/cli 0.0.12 → 0.0.13

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.13",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
@@ -1,39 +1,103 @@
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
+
11
+ const { toBase64, toArray } = Utils
13
12
 
14
13
  const MCP_URL = process.env.ONESAT_MCP_URL ?? 'http://127.0.0.1:3322'
15
14
  const KEY_DIR = `${process.env.HOME}/.1sat-wallet`
16
15
  const CLIENT_KEY_PATH = `${KEY_DIR}/mcp-agent.key`
16
+ const AUTH_PROTOCOL_ID: WalletProtocol = [2, 'authrite message signature']
17
17
 
18
18
  function log(msg: string): void {
19
19
  process.stderr.write(`[1sat mcp-proxy] ${msg}\n`)
20
20
  }
21
21
 
22
+ function generateNonce(): string {
23
+ const bytes = new Uint8Array(32)
24
+ crypto.getRandomValues(bytes)
25
+ return toBase64(Array.from(bytes))
26
+ }
27
+
22
28
  function getClientKey(): PrivateKey {
23
29
  mkdirSync(KEY_DIR, { recursive: true })
24
-
25
30
  if (existsSync(CLIENT_KEY_PATH)) {
26
31
  return PrivateKey.fromWif(readFileSync(CLIENT_KEY_PATH, 'utf-8').trim())
27
32
  }
28
-
29
33
  const key = PrivateKey.fromRandom()
30
34
  writeFileSync(CLIENT_KEY_PATH, key.toWif(), { mode: 0o600 })
31
35
  log('Generated new agent identity key')
32
36
  return key
33
37
  }
34
38
 
39
+ interface Session {
40
+ serverIdentityKey: string
41
+ serverNonce: string
42
+ clientKey: PrivateKey
43
+ }
44
+
45
+ async function handshake(key: PrivateKey): Promise<Session> {
46
+ const pubkey = key.toPublicKey().toString()
47
+ const clientNonce = generateNonce()
48
+
49
+ const res = await fetch(`${MCP_URL}/.well-known/auth`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({
53
+ authrite: '0.1',
54
+ messageType: 'initialRequest',
55
+ identityKey: pubkey,
56
+ nonce: clientNonce,
57
+ }),
58
+ })
59
+
60
+ if (!res.ok) throw new Error(`Handshake failed: HTTP ${res.status}`)
61
+
62
+ const data = await res.json()
63
+ if (data.messageType !== 'initialResponse') throw new Error(`Unexpected: ${data.messageType}`)
64
+
65
+ const serverNonce = data.nonce as string
66
+ const deriver = new KeyDeriver(key)
67
+ const serverPub = deriver.derivePublicKey(
68
+ AUTH_PROTOCOL_ID, `${serverNonce} ${clientNonce}`, data.identityKey as string, false,
69
+ )
70
+
71
+ const clientNonceBytes = toArray(clientNonce, 'base64')
72
+ const serverNonceBytes = toArray(serverNonce, 'base64')
73
+ const msgHash = Hash.sha256(Array.from(new Uint8Array([...clientNonceBytes, ...serverNonceBytes])))
74
+
75
+ const sig = Signature.fromDER(data.signature as string, 'hex')
76
+ if (!serverPub.verify(Array.from(msgHash), sig)) throw new Error('Server signature verification failed')
77
+
78
+ log(`Authenticated with ${(data.identityKey as string).slice(0, 12)}...`)
79
+ return { serverIdentityKey: data.identityKey as string, serverNonce, clientKey: key }
80
+ }
81
+
82
+ function signHeaders(session: Session, pathname: string): Record<string, string> {
83
+ const { serverIdentityKey, serverNonce, clientKey: key } = session
84
+ const nonce = generateNonce()
85
+ const deriver = new KeyDeriver(key)
86
+ const derivedKey = deriver.derivePrivateKey(AUTH_PROTOCOL_ID, `${nonce} ${serverNonce}`, serverIdentityKey)
87
+ const payload = new TextEncoder().encode(pathname)
88
+ const msgHash = Hash.sha256(Array.from(payload))
89
+ const sig = derivedKey.sign(Array.from(msgHash))
90
+
91
+ return {
92
+ 'x-bsv-auth-version': '0.1',
93
+ 'x-bsv-auth-identity-key': key.toPublicKey().toString(),
94
+ 'x-bsv-auth-nonce': nonce,
95
+ 'x-bsv-auth-your-nonce': serverNonce,
96
+ 'x-bsv-auth-signature': sig.toDER('hex') as string,
97
+ }
98
+ }
99
+
35
100
  export async function handleMcpProxyCommand(): Promise<void> {
36
- // Health check before doing anything else
37
101
  try {
38
102
  await fetch(MCP_URL, { signal: AbortSignal.timeout(2000) })
39
103
  } catch {
@@ -42,13 +106,9 @@ export async function handleMcpProxyCommand(): Promise<void> {
42
106
  }
43
107
 
44
108
  const key = getClientKey()
45
- const wallet = new ProtoWallet(key)
46
- const authFetch = new AuthFetch(wallet)
47
-
48
- log(`Connecting to ${MCP_URL}/mcp`)
109
+ const session = await handshake(key)
49
110
 
50
111
  let mcpSessionId: string | null = null
51
-
52
112
  const decoder = new TextDecoder()
53
113
  const reader = Bun.stdin.stream().getReader()
54
114
  let buffer = ''
@@ -63,12 +123,12 @@ export async function handleMcpProxyCommand(): Promise<void> {
63
123
  while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
64
124
  const line = buffer.slice(0, newlineIdx).trim()
65
125
  buffer = buffer.slice(newlineIdx + 1)
66
-
67
126
  if (!line) continue
68
127
 
69
128
  const headers: Record<string, string> = {
70
129
  'Content-Type': 'application/json',
71
130
  Accept: 'application/json, text/event-stream',
131
+ ...signHeaders(session, '/mcp'),
72
132
  }
73
133
 
74
134
  if (mcpSessionId) {
@@ -76,11 +136,7 @@ export async function handleMcpProxyCommand(): Promise<void> {
76
136
  }
77
137
 
78
138
  try {
79
- const res = await authFetch.fetch(`${MCP_URL}/mcp`, {
80
- method: 'POST',
81
- body: line,
82
- headers,
83
- })
139
+ const res = await fetch(`${MCP_URL}/mcp`, { method: 'POST', headers, body: line })
84
140
 
85
141
  const sessionHeader = res.headers.get('mcp-session-id')
86
142
  if (sessionHeader) mcpSessionId = sessionHeader
@@ -102,13 +158,11 @@ export async function handleMcpProxyCommand(): Promise<void> {
102
158
  } catch (err) {
103
159
  const msg = err instanceof Error ? err.message : String(err)
104
160
  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
- )
161
+ process.stdout.write(`${JSON.stringify({
162
+ jsonrpc: '2.0',
163
+ error: { code: -32000, message: `MCP proxy error: ${msg}` },
164
+ id: null,
165
+ })}\n`)
112
166
  }
113
167
  }
114
168
  }