@1sat/cli 0.0.11 → 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 +7 -8
- package/src/cli.ts +5 -0
- package/src/commands/config.ts +2 -2
- package/src/commands/init.ts +35 -2
- package/src/commands/mcp-proxy.ts +169 -0
- package/src/config.ts +2 -2
- package/src/context.ts +2 -2
- package/src/help.ts +2 -2
- package/src/keys.ts +52 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1sat/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/cli.ts",
|
|
@@ -16,16 +16,15 @@
|
|
|
16
16
|
"keywords": ["1sat", "bsv", "ordinals", "cli"],
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@1sat/actions": "0.0.
|
|
20
|
-
"@1sat/client": "0.0.
|
|
21
|
-
"@1sat/types": "0.0.
|
|
19
|
+
"@1sat/actions": "0.0.62",
|
|
20
|
+
"@1sat/client": "0.0.17",
|
|
21
|
+
"@1sat/types": "0.0.14",
|
|
22
22
|
"@1sat/wallet-node": ">=0.0.13",
|
|
23
|
-
"@bsv/sdk": "^2.0.
|
|
23
|
+
"@bsv/sdk": "^2.0.6",
|
|
24
24
|
"chalk": "^5.0.0",
|
|
25
25
|
"@clack/prompts": "^0.8.0",
|
|
26
|
-
"bitcoin-backup": "^0.0.
|
|
27
|
-
"dotenv": "^17.0.0"
|
|
28
|
-
"sigma-protocol": "^0.1.9"
|
|
26
|
+
"bitcoin-backup": "^0.0.11",
|
|
27
|
+
"dotenv": "^17.0.0"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
30
|
"@types/bun": "^1.3.9",
|
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { parseGlobalFlags } from './args'
|
|
10
10
|
import { handleActionCommand } from './commands/action'
|
|
11
|
+
import { handleMcpProxyCommand } from './commands/mcp-proxy'
|
|
11
12
|
import { handleConfigCommand } from './commands/config'
|
|
12
13
|
import { handleIdentityCommand } from './commands/identity'
|
|
13
14
|
import { handleInitCommand } from './commands/init'
|
|
@@ -88,6 +89,10 @@ async function main(): Promise<void> {
|
|
|
88
89
|
await handleTxCommand(rest, flags)
|
|
89
90
|
break
|
|
90
91
|
|
|
92
|
+
case 'mcp-proxy':
|
|
93
|
+
await handleMcpProxyCommand()
|
|
94
|
+
break
|
|
95
|
+
|
|
91
96
|
case 'help':
|
|
92
97
|
printHelp()
|
|
93
98
|
break
|
package/src/commands/config.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
const SETTABLE_KEYS: Array<keyof OneSatCliConfig> = [
|
|
29
29
|
'chain',
|
|
30
30
|
'dataDir',
|
|
31
|
-
'
|
|
31
|
+
'activeRemote',
|
|
32
32
|
'storageIdentityKey',
|
|
33
33
|
]
|
|
34
34
|
|
|
@@ -69,7 +69,7 @@ function configShow(opts: GlobalFlags): void {
|
|
|
69
69
|
printKeyValue({
|
|
70
70
|
chain: config.chain,
|
|
71
71
|
dataDir: config.dataDir,
|
|
72
|
-
|
|
72
|
+
activeRemote: config.activeRemote ?? '(not set)',
|
|
73
73
|
storageIdentityKey: config.storageIdentityKey ?? '(not set)',
|
|
74
74
|
})
|
|
75
75
|
console.log()
|
package/src/commands/init.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from '@clack/prompts'
|
|
22
22
|
import type { GlobalFlags } from '../args'
|
|
23
23
|
import { ensureConfigDir, loadConfig, saveConfig } from '../config'
|
|
24
|
-
import { hasKey, saveKey } from '../keys'
|
|
24
|
+
import { cacheKeyPassword, hasKey, saveKey } from '../keys'
|
|
25
25
|
import { fatal, formatSuccess, formatValue, formatWarning } from '../output'
|
|
26
26
|
|
|
27
27
|
export async function handleInitCommand(
|
|
@@ -143,6 +143,25 @@ export async function handleInitCommand(
|
|
|
143
143
|
fatal('Passwords do not match.')
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// 3.5. Touch ID protection (macOS arm64)
|
|
147
|
+
let useTouchID = false
|
|
148
|
+
try {
|
|
149
|
+
const { isTouchIDAvailable } = await import('bitcoin-backup')
|
|
150
|
+
if (isTouchIDAvailable()) {
|
|
151
|
+
const enableTouchID = await confirm({
|
|
152
|
+
message:
|
|
153
|
+
'Enable Touch ID? (unlock your wallet without typing a password)',
|
|
154
|
+
})
|
|
155
|
+
if (isCancel(enableTouchID)) {
|
|
156
|
+
cancel('Setup cancelled.')
|
|
157
|
+
process.exit(0)
|
|
158
|
+
}
|
|
159
|
+
useTouchID = enableTouchID as boolean
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// bitcoin-backup Touch ID not available — skip silently
|
|
163
|
+
}
|
|
164
|
+
|
|
146
165
|
// 4. Optional: storage identity key
|
|
147
166
|
const storageId = await text({
|
|
148
167
|
message: 'Storage identity key (for wallet persistence):',
|
|
@@ -159,6 +178,19 @@ export async function handleInitCommand(
|
|
|
159
178
|
|
|
160
179
|
await saveKey(wif, pw as string)
|
|
161
180
|
|
|
181
|
+
// Cache password with Touch ID if user opted in
|
|
182
|
+
if (useTouchID) {
|
|
183
|
+
try {
|
|
184
|
+
await cacheKeyPassword(pw as string)
|
|
185
|
+
} catch {
|
|
186
|
+
console.log(
|
|
187
|
+
formatWarning(
|
|
188
|
+
' Touch ID caching failed. You can enable it later with "1sat touchid enable".',
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
162
194
|
saveConfig({
|
|
163
195
|
...loadConfig(),
|
|
164
196
|
chain: chain as 'main' | 'test',
|
|
@@ -168,5 +200,6 @@ export async function handleInitCommand(
|
|
|
168
200
|
const pk = PrivateKey.fromWif(wif)
|
|
169
201
|
const address = pk.toPublicKey().toAddress()
|
|
170
202
|
|
|
171
|
-
|
|
203
|
+
const touchIdNote = useTouchID ? ' (Touch ID enabled)' : ''
|
|
204
|
+
outro(formatSuccess(`Wallet configured${touchIdNote}! Address: ${address}`))
|
|
172
205
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1sat mcp-proxy — stdio-to-HTTP bridge for the wallet-desktop MCP server.
|
|
3
|
+
*
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { Hash, KeyDeriver, PrivateKey, Signature, Utils } from '@bsv/sdk'
|
|
9
|
+
import type { WalletProtocol } from '@bsv/sdk'
|
|
10
|
+
|
|
11
|
+
const { toBase64, toArray } = Utils
|
|
12
|
+
|
|
13
|
+
const MCP_URL = process.env.ONESAT_MCP_URL ?? 'http://127.0.0.1:3322'
|
|
14
|
+
const KEY_DIR = `${process.env.HOME}/.1sat-wallet`
|
|
15
|
+
const CLIENT_KEY_PATH = `${KEY_DIR}/mcp-agent.key`
|
|
16
|
+
const AUTH_PROTOCOL_ID: WalletProtocol = [2, 'authrite message signature']
|
|
17
|
+
|
|
18
|
+
function log(msg: string): void {
|
|
19
|
+
process.stderr.write(`[1sat mcp-proxy] ${msg}\n`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateNonce(): string {
|
|
23
|
+
const bytes = new Uint8Array(32)
|
|
24
|
+
crypto.getRandomValues(bytes)
|
|
25
|
+
return toBase64(Array.from(bytes))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getClientKey(): PrivateKey {
|
|
29
|
+
mkdirSync(KEY_DIR, { recursive: true })
|
|
30
|
+
if (existsSync(CLIENT_KEY_PATH)) {
|
|
31
|
+
return PrivateKey.fromWif(readFileSync(CLIENT_KEY_PATH, 'utf-8').trim())
|
|
32
|
+
}
|
|
33
|
+
const key = PrivateKey.fromRandom()
|
|
34
|
+
writeFileSync(CLIENT_KEY_PATH, key.toWif(), { mode: 0o600 })
|
|
35
|
+
log('Generated new agent identity key')
|
|
36
|
+
return key
|
|
37
|
+
}
|
|
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
|
+
|
|
100
|
+
export async function handleMcpProxyCommand(): Promise<void> {
|
|
101
|
+
try {
|
|
102
|
+
await fetch(MCP_URL, { signal: AbortSignal.timeout(2000) })
|
|
103
|
+
} catch {
|
|
104
|
+
log(`Server not reachable at ${MCP_URL} — is 1Sat wallet running?`)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const key = getClientKey()
|
|
109
|
+
const session = await handshake(key)
|
|
110
|
+
|
|
111
|
+
let mcpSessionId: string | null = null
|
|
112
|
+
const decoder = new TextDecoder()
|
|
113
|
+
const reader = Bun.stdin.stream().getReader()
|
|
114
|
+
let buffer = ''
|
|
115
|
+
|
|
116
|
+
while (true) {
|
|
117
|
+
const { done, value } = await reader.read()
|
|
118
|
+
if (done) break
|
|
119
|
+
|
|
120
|
+
buffer += decoder.decode(value, { stream: true })
|
|
121
|
+
|
|
122
|
+
let newlineIdx: number
|
|
123
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
124
|
+
const line = buffer.slice(0, newlineIdx).trim()
|
|
125
|
+
buffer = buffer.slice(newlineIdx + 1)
|
|
126
|
+
if (!line) continue
|
|
127
|
+
|
|
128
|
+
const headers: Record<string, string> = {
|
|
129
|
+
'Content-Type': 'application/json',
|
|
130
|
+
Accept: 'application/json, text/event-stream',
|
|
131
|
+
...signHeaders(session, '/mcp'),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (mcpSessionId) {
|
|
135
|
+
headers['mcp-session-id'] = mcpSessionId
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`${MCP_URL}/mcp`, { method: 'POST', headers, body: line })
|
|
140
|
+
|
|
141
|
+
const sessionHeader = res.headers.get('mcp-session-id')
|
|
142
|
+
if (sessionHeader) mcpSessionId = sessionHeader
|
|
143
|
+
|
|
144
|
+
const contentType = res.headers.get('content-type') ?? ''
|
|
145
|
+
|
|
146
|
+
if (contentType.includes('text/event-stream')) {
|
|
147
|
+
const text = await res.text()
|
|
148
|
+
for (const eventLine of text.split('\n')) {
|
|
149
|
+
if (eventLine.startsWith('data: ')) {
|
|
150
|
+
const data = eventLine.slice(6).trim()
|
|
151
|
+
if (data) process.stdout.write(`${data}\n`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const body = await res.text()
|
|
156
|
+
if (body.trim()) process.stdout.write(`${body.trim()}\n`)
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
160
|
+
log(`Request failed: ${msg}`)
|
|
161
|
+
process.stdout.write(`${JSON.stringify({
|
|
162
|
+
jsonrpc: '2.0',
|
|
163
|
+
error: { code: -32000, message: `MCP proxy error: ${msg}` },
|
|
164
|
+
id: null,
|
|
165
|
+
})}\n`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -16,8 +16,8 @@ export interface OneSatCliConfig {
|
|
|
16
16
|
chain: 'main' | 'test'
|
|
17
17
|
/** Data directory for wallet databases */
|
|
18
18
|
dataDir: string
|
|
19
|
-
/** Remote storage URL for wallet
|
|
20
|
-
|
|
19
|
+
/** Remote storage URL for wallet sync */
|
|
20
|
+
activeRemote?: string
|
|
21
21
|
/** Storage identity key for wallet persistence */
|
|
22
22
|
storageIdentityKey?: string
|
|
23
23
|
}
|
package/src/context.ts
CHANGED
|
@@ -44,10 +44,10 @@ export async function loadContext(
|
|
|
44
44
|
},
|
|
45
45
|
useNullAsDefault: true,
|
|
46
46
|
},
|
|
47
|
-
|
|
47
|
+
activeRemote: config.activeRemote,
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
walletResult.monitor
|
|
50
|
+
walletResult.monitor?.startTasks()
|
|
51
51
|
|
|
52
52
|
const ctx = createContext(walletResult.wallet, {
|
|
53
53
|
services: walletResult.services,
|
package/src/help.ts
CHANGED
|
@@ -10,9 +10,9 @@ const VERSION = (() => {
|
|
|
10
10
|
const pkg = JSON.parse(
|
|
11
11
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
12
12
|
)
|
|
13
|
-
return pkg.version || '0.0.
|
|
13
|
+
return pkg.version || '0.0.11'
|
|
14
14
|
} catch {
|
|
15
|
-
return '0.0.
|
|
15
|
+
return '0.0.11'
|
|
16
16
|
}
|
|
17
17
|
})()
|
|
18
18
|
|
package/src/keys.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Encrypted key management for the 1sat CLI.
|
|
3
3
|
*
|
|
4
4
|
* Key resolution priority:
|
|
5
|
-
* 1. PRIVATE_KEY_WIF env var
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
5
|
+
* 1. PRIVATE_KEY_WIF env var (headless/CI)
|
|
6
|
+
* 2. Touch ID cached password → decrypt keys.bep (macOS arm64)
|
|
7
|
+
* 3. Explicit password → decrypt keys.bep
|
|
8
|
+
* 4. Fail with guidance
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { arch, platform } from 'node:os'
|
|
11
13
|
import { join } from 'node:path'
|
|
12
14
|
import { PrivateKey } from '@bsv/sdk'
|
|
13
15
|
import { type WifBackup, decryptBackup, encryptBackup } from 'bitcoin-backup'
|
|
@@ -28,9 +30,19 @@ export function hasKey(): boolean {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
33
|
+
* Check if Touch ID is available for password caching.
|
|
34
|
+
*/
|
|
35
|
+
export function isTouchIDAvailable(): boolean {
|
|
36
|
+
return platform() === 'darwin' && arch() === 'arm64'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load the private key from env, Touch ID cache, or password.
|
|
32
41
|
*
|
|
33
|
-
*
|
|
42
|
+
* Resolution order:
|
|
43
|
+
* 1. PRIVATE_KEY_WIF env var
|
|
44
|
+
* 2. Touch ID cached password (if available)
|
|
45
|
+
* 3. Explicit password parameter
|
|
34
46
|
*/
|
|
35
47
|
export async function loadKey(password?: string): Promise<PrivateKey> {
|
|
36
48
|
// Priority 1: Environment variable
|
|
@@ -47,14 +59,29 @@ export async function loadKey(password?: string): Promise<PrivateKey> {
|
|
|
47
59
|
)
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
const encrypted = readFileSync(keysPath, 'utf8')
|
|
63
|
+
|
|
64
|
+
// Priority 2a: Try Touch ID cached password
|
|
65
|
+
let resolvedPassword = password
|
|
66
|
+
if (!resolvedPassword) {
|
|
67
|
+
try {
|
|
68
|
+
const { getCachedPassword } = await import('bitcoin-backup')
|
|
69
|
+
const cached = await getCachedPassword(keysPath)
|
|
70
|
+
if (cached) {
|
|
71
|
+
resolvedPassword = cached
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Touch ID not available or no cached password — fall through
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!resolvedPassword) {
|
|
51
79
|
throw new Error(
|
|
52
|
-
'Password required to decrypt key file. Pass --password or
|
|
80
|
+
'Password required to decrypt key file. Pass --password, set ONESAT_PASSWORD, or run "1sat init --touchid" to enable Touch ID.',
|
|
53
81
|
)
|
|
54
82
|
}
|
|
55
83
|
|
|
56
|
-
const
|
|
57
|
-
const backup = await decryptBackup(encrypted, password)
|
|
84
|
+
const backup = await decryptBackup(encrypted, resolvedPassword)
|
|
58
85
|
if (!('wif' in backup) || typeof backup.wif !== 'string') {
|
|
59
86
|
throw new Error('Key file does not contain a WIF key.')
|
|
60
87
|
}
|
|
@@ -80,6 +107,22 @@ export async function saveKey(wif: string, password: string): Promise<void> {
|
|
|
80
107
|
writeFileSync(keysPath, encrypted, { mode: 0o600 })
|
|
81
108
|
}
|
|
82
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Cache the password for keys.bep using Touch ID.
|
|
112
|
+
*/
|
|
113
|
+
export async function cacheKeyPassword(password: string): Promise<void> {
|
|
114
|
+
const { cachePassword } = await import('bitcoin-backup')
|
|
115
|
+
await cachePassword(getKeysPath(), password)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Remove the cached password for keys.bep.
|
|
120
|
+
*/
|
|
121
|
+
export async function forgetKeyPassword(): Promise<void> {
|
|
122
|
+
const { forgetPassword } = await import('bitcoin-backup')
|
|
123
|
+
await forgetPassword(getKeysPath())
|
|
124
|
+
}
|
|
125
|
+
|
|
83
126
|
/**
|
|
84
127
|
* Resolve a password from flag or environment variable.
|
|
85
128
|
*/
|