@financedistrict/fdx 0.1.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/.eslintrc.cjs +26 -0
- package/.prettierrc +6 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/bin/commands/call.js +62 -0
- package/bin/commands/logout.js +12 -0
- package/bin/commands/setup.js +148 -0
- package/bin/commands/status.js +58 -0
- package/bin/fdx.js +66 -0
- package/docs/ARCHITECTURE.md +148 -0
- package/docs/DEVELOPMENT.md +147 -0
- package/docs/UNINSTALL.md +57 -0
- package/package.json +57 -0
- package/src/credential-store.js +226 -0
- package/src/factory.js +29 -0
- package/src/index.js +11 -0
- package/src/mcp-auth.js +480 -0
- package/src/mcp-client.js +151 -0
- package/src/storage.js +38 -0
- package/src/utils/args.js +44 -0
- package/src/utils/logger.js +85 -0
- package/src/utils/pkce.js +27 -0
- package/src/wallet-client.js +275 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
FDX is a three-layer system that gives AI agents secure access to blockchain wallets without managing private keys.
|
|
4
|
+
|
|
5
|
+
## System Overview
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ AI Agent │
|
|
10
|
+
│ (Asks: "What's my wallet balance?") │
|
|
11
|
+
└────────────────────────────────┬────────────────────────────────────┘
|
|
12
|
+
│
|
|
13
|
+
│ invokes
|
|
14
|
+
▼
|
|
15
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
16
|
+
│ FDX npm Package │
|
|
17
|
+
│ (@financedistrict/fdx) │
|
|
18
|
+
│ │
|
|
19
|
+
│ ┌─────────────────┐ │
|
|
20
|
+
│ │ CLI Commands │ │
|
|
21
|
+
│ │ │ │
|
|
22
|
+
│ │ • fdx setup │ │
|
|
23
|
+
│ │ • fdx status │ │
|
|
24
|
+
│ │ • fdx call │ │
|
|
25
|
+
│ │ <method> │ │
|
|
26
|
+
│ └─────────────────┘ │
|
|
27
|
+
│ │
|
|
28
|
+
│ ┌──────────────────────────────────────────────────────────────┐ │
|
|
29
|
+
│ │ WalletClient (SDK) │ │
|
|
30
|
+
│ │ • OAuth 2.1 + DCR + PKCE authentication │ │
|
|
31
|
+
│ │ • JSON-RPC 2.0 MCP protocol client │ │
|
|
32
|
+
│ │ • High-level methods for wallet/DeFi operations │ │
|
|
33
|
+
│ └──────────────────────────────────────────────────────────────┘ │
|
|
34
|
+
└────────────────────────────────┬────────────────────────────────────┘
|
|
35
|
+
│
|
|
36
|
+
│ HTTPS + OAuth 2.1
|
|
37
|
+
▼
|
|
38
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
39
|
+
│ Finance District MCP Server │
|
|
40
|
+
│ (https://mcp.fd.xyz) │
|
|
41
|
+
│ │
|
|
42
|
+
│ • User authentication via OAuth 2.1 │
|
|
43
|
+
│ • Smart Account management (EVM + Solana) │
|
|
44
|
+
│ • Multi-chain token transfers │
|
|
45
|
+
│ • DEX aggregation for swaps │
|
|
46
|
+
│ • DeFi yield strategy integration (Aave, Compound, Yearn) │
|
|
47
|
+
│ • X-402 payment protocol support │
|
|
48
|
+
│ │
|
|
49
|
+
│ Supported Chains: │
|
|
50
|
+
│ • Ethereum (1) │
|
|
51
|
+
│ • BNB Smart Chain (56) │
|
|
52
|
+
│ • Arbitrum One (42161) │
|
|
53
|
+
│ • Base (8453) │
|
|
54
|
+
│ • Solana (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp) │
|
|
55
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Key Components
|
|
59
|
+
|
|
60
|
+
### 1. npm Package (`@financedistrict/fdx`)
|
|
61
|
+
|
|
62
|
+
The npm package includes:
|
|
63
|
+
|
|
64
|
+
- **CLI Tool** (`bin/fdx.js`): Command-line interface for setup, status checks, and method invocation
|
|
65
|
+
- **SDK** (`src/wallet-client.js`): High-level JavaScript API with typed methods for each MCP tool
|
|
66
|
+
- **OAuth Client** (`src/mcp-auth.js`): RFC 7591 Dynamic Client Registration with PKCE
|
|
67
|
+
- **MCP Client** (`src/mcp-client.js`): JSON-RPC 2.0 protocol handler with SSE response format
|
|
68
|
+
|
|
69
|
+
### 2. Finance District MCP Server
|
|
70
|
+
|
|
71
|
+
The remote server (hosted at fd.xyz) provides:
|
|
72
|
+
|
|
73
|
+
- **Authentication**: OAuth 2.1 with Microsoft Entra ID (no local keys)
|
|
74
|
+
- **Smart Accounts**: EVM account abstraction via ERC-4337, deterministic Solana addresses
|
|
75
|
+
- **Multi-Chain Support**: Single interface for ETH, BSC, ARB, BASE, SOL
|
|
76
|
+
- **DeFi Integration**: Swap tokens via DEX aggregators, earn yield via Aave/Compound/Yearn
|
|
77
|
+
- **Payment Protocol**: X-402 payment authorization for premium API access
|
|
78
|
+
|
|
79
|
+
## Data Flow
|
|
80
|
+
|
|
81
|
+
### Example: Agent Checks Wallet Balance
|
|
82
|
+
|
|
83
|
+
1. **User asks agent**: _"What's my ETH balance?"_
|
|
84
|
+
2. **Agent invokes CLI**: `fdx call getWalletOverview --chainKey ethereum`
|
|
85
|
+
3. **CLI loads SDK**: `bin/commands/call.js` → `src/wallet-client.js` → `getWalletOverview()`
|
|
86
|
+
4. **SDK authenticates**: Reads OAuth tokens from `~/.fdx/auth.json`
|
|
87
|
+
5. **SDK calls MCP**: HTTPS request to `mcp.fd.xyz` with JSON-RPC 2.0 payload
|
|
88
|
+
6. **Server queries chain**: Fetches balances from Ethereum RPC nodes
|
|
89
|
+
7. **Response flows back**: JSON → SDK → CLI → stdout → Agent reads JSON
|
|
90
|
+
8. **Agent formats answer**: _"You have 0.42 ETH in your Ethereum wallet (0xABC...)"_
|
|
91
|
+
|
|
92
|
+
## Authentication Flow
|
|
93
|
+
|
|
94
|
+
### First-Time Setup (`fdx setup`)
|
|
95
|
+
|
|
96
|
+
1. **Generate PKCE challenge**: `S256(random_verifier)` → code_challenge
|
|
97
|
+
2. **Register OAuth client**: POST to `/oauth/register` (RFC 7591 DCR)
|
|
98
|
+
3. **Open browser**: User grants consent via Microsoft Entra ID
|
|
99
|
+
4. **Exchange code**: Authorization code + verifier → access_token + refresh_token
|
|
100
|
+
5. **Store tokens**: Save to `~/.fdx/auth.json`
|
|
101
|
+
|
|
102
|
+
### Subsequent Requests
|
|
103
|
+
|
|
104
|
+
1. **Load tokens**: Read from `~/.fdx/auth.json`
|
|
105
|
+
2. **Check expiry**: If `access_token` expired, use `refresh_token` to get new token
|
|
106
|
+
3. **Attach header**: `Authorization: Bearer <access_token>`
|
|
107
|
+
4. **Make request**: HTTPS + JSON-RPC to MCP server
|
|
108
|
+
|
|
109
|
+
No private keys, no seed phrases. All wallet operations are server-side with user consent via OAuth.
|
|
110
|
+
|
|
111
|
+
## Security Model
|
|
112
|
+
|
|
113
|
+
- **No Local Keys**: Agent never touches private keys. Wallets are managed server-side.
|
|
114
|
+
- **OAuth 2.1**: Industry-standard authentication with PKCE protection.
|
|
115
|
+
- **Consent-Based**: User authorizes agent via web browser on first setup.
|
|
116
|
+
- **Token Refresh**: Long-lived refresh tokens minimize re-authentication.
|
|
117
|
+
- **Smart Accounts**: ERC-4337 account abstraction allows multi-signature, recovery, upgradability.
|
|
118
|
+
- **Audit Trail**: All transactions are recorded on-chain with transparent history.
|
|
119
|
+
|
|
120
|
+
## Multi-Chain Support
|
|
121
|
+
|
|
122
|
+
FDX abstracts chain differences behind a single API:
|
|
123
|
+
|
|
124
|
+
| Chain | Chain ID | Network Key | Address Format |
|
|
125
|
+
| -------- | -------- | ----------- | -------------- |
|
|
126
|
+
| Ethereum | 1 | ethereum | 0x... |
|
|
127
|
+
| BSC | 56 | bsc | 0x... |
|
|
128
|
+
| Arbitrum | 42161 | arbitrum | 0x... |
|
|
129
|
+
| Base | 8453 | base | 0x... |
|
|
130
|
+
| Solana | (CAIP-2) | solana | base58 |
|
|
131
|
+
|
|
132
|
+
## DeFi Integration
|
|
133
|
+
|
|
134
|
+
The MCP server integrates with DeFi protocols:
|
|
135
|
+
|
|
136
|
+
- **DEX Aggregation**: 1inch, 0x, Jupiter (Solana) for best swap routes
|
|
137
|
+
- **Yield Strategies**: Aave (lending), Compound (lending), Yearn (vaults)
|
|
138
|
+
- **Smart Routing**: Policy-driven swap execution (BestExecution, LowGas, MevProtected)
|
|
139
|
+
|
|
140
|
+
Agents can discover strategies, deposit tokens, and withdraw yield — all through `fdx call`.
|
|
141
|
+
|
|
142
|
+
## Development & Testing
|
|
143
|
+
|
|
144
|
+
See [DEVELOPMENT.md](DEVELOPMENT.md) for running from source.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
Step-by-step instructions to set up FDX for local development on any machine.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js >= 18
|
|
10
|
+
- Git
|
|
11
|
+
- A browser for the OAuth consent flow
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Clone the repo
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/financedistrict-platform/fd-agent-wallet-cli.git
|
|
19
|
+
cd fd-agent-wallet-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 2. Install dependencies and link the CLI
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install
|
|
26
|
+
npm link
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Verify the `fdx` command is available:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
fdx
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Should print usage info with `setup`, `status`, `call` commands.
|
|
36
|
+
|
|
37
|
+
## 3. Run setup
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
fdx setup
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This will:
|
|
44
|
+
|
|
45
|
+
- Discover the OAuth server
|
|
46
|
+
- Register a client dynamically (first time only)
|
|
47
|
+
- Print an authorization URL — open it in your browser
|
|
48
|
+
- Wait for the OAuth callback on `localhost:6260`
|
|
49
|
+
- Exchange the code for tokens
|
|
50
|
+
- Write tokens to `~/.fdx/auth.json`
|
|
51
|
+
|
|
52
|
+
If the machine is headless and you can't open a browser, run `fdx setup` on your local machine instead, then copy the token file:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# From local machine
|
|
56
|
+
scp ~/.fdx/auth.json user@server:~/.fdx/auth.json
|
|
57
|
+
ssh user@server "chmod 600 ~/.fdx/auth.json"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 4. Verify the CLI works
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
fdx status
|
|
64
|
+
fdx call getMyInfo
|
|
65
|
+
fdx call getAppVersion
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
All three should return data. If `fdx call` fails with auth errors, run `fdx setup` again.
|
|
69
|
+
|
|
70
|
+
## 5. Environment Variables
|
|
71
|
+
|
|
72
|
+
| Variable | Description | Default |
|
|
73
|
+
| ------------------ | ------------------ | -------------------------------------- |
|
|
74
|
+
| `FDX_MCP_SERVER` | MCP server URL | `https://mcp.fd.xyz` |
|
|
75
|
+
| `FDX_REDIRECT_URI` | OAuth callback URI | `http://localhost:6260/oauth/callback` |
|
|
76
|
+
| `FDX_STORE_PATH` | Token store path | `~/.fdx/auth.json` |
|
|
77
|
+
| `FDX_LOG_PATH` | Log file path | `~/.fdx/fdx.log` |
|
|
78
|
+
| `FDX_LOG_LEVEL` | Log verbosity (`debug`\|`info`\|`warn`\|`error`\|`off`) | `info` |
|
|
79
|
+
|
|
80
|
+
You can set these inline before a command, as persistent shell exports, or via a `.env` file in the working directory (see `.env.example`). The `.env` file is gitignored so values never end up in the repository.
|
|
81
|
+
|
|
82
|
+
## 6. Testing against a non-production environment
|
|
83
|
+
|
|
84
|
+
The CLI defaults to the production server. To point it at a different environment, override `FDX_MCP_SERVER` — the test server address is shared privately with the test team and must never be committed to the repository.
|
|
85
|
+
|
|
86
|
+
**Option A — inline (one command):**
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
FDX_MCP_SERVER=https://... fdx setup --device
|
|
90
|
+
FDX_MCP_SERVER=https://... fdx call getMyInfo
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Option B — shell export (current session):**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export FDX_MCP_SERVER=https://...
|
|
97
|
+
fdx setup --device
|
|
98
|
+
fdx call getMyInfo
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Option C — `.env` file (persistent, local only):**
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cp .env.example .env
|
|
105
|
+
# edit .env and uncomment FDX_MCP_SERVER=https://...
|
|
106
|
+
fdx setup --device
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The `.env` file is loaded automatically when the `fdx` binary starts. It is gitignored — do not commit it.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Updating after code changes
|
|
114
|
+
|
|
115
|
+
When changes are pushed to the repo:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cd fd-agent-wallet-cli
|
|
119
|
+
git pull
|
|
120
|
+
npm install
|
|
121
|
+
npm link
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Running Tests
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm test
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Linting
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm run lint
|
|
136
|
+
npm run lint:fix # auto-fix
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Troubleshooting
|
|
142
|
+
|
|
143
|
+
| Problem | Fix |
|
|
144
|
+
| ------------------------- | ----------------------------------------------------------------------------- |
|
|
145
|
+
| `fdx: command not found` | Run `npm link` in the repo directory |
|
|
146
|
+
| Auth errors on `fdx call` | Run `fdx setup` to re-authenticate |
|
|
147
|
+
| Token expired | Auto-refreshes via refresh token. If that also expired, run `fdx setup` again |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Uninstall
|
|
2
|
+
|
|
3
|
+
Remove FDX and all associated data.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
|
|
7
|
+
### For npm install users
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Remove auth tokens and config
|
|
11
|
+
rm -rf ~/.fdx
|
|
12
|
+
|
|
13
|
+
# 2. Uninstall the package globally
|
|
14
|
+
npm uninstall -g @financedistrict/fdx
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### For development users (git clone)
|
|
18
|
+
|
|
19
|
+
If you installed from source:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. Remove auth tokens and config
|
|
23
|
+
rm -rf ~/.fdx
|
|
24
|
+
|
|
25
|
+
# 2. Unlink the CLI
|
|
26
|
+
npm unlink -g @financedistrict/fdx
|
|
27
|
+
|
|
28
|
+
# 3. Remove the repo
|
|
29
|
+
rm -rf fd-agent-wallet-cli # or wherever you cloned it
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### OS Credential Store Cleanup
|
|
33
|
+
|
|
34
|
+
If you used FDX with an OS credential store, secrets may persist in the
|
|
35
|
+
system keychain after removing `~/.fdx`. Clean them up manually:
|
|
36
|
+
|
|
37
|
+
**macOS (Keychain):**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
security delete-generic-password -s fdx-wallet
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Linux (libsecret):**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
secret-tool clear service fdx-wallet
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Windows:** Credentials are stored via DPAPI in `~/.fdx/.cred_*` files and
|
|
50
|
+
are removed when you delete the `~/.fdx` directory. No additional cleanup
|
|
51
|
+
is needed.
|
|
52
|
+
|
|
53
|
+
## Verify
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
which fdx # should return nothing
|
|
57
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@financedistrict/fdx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent wallet CLI — a command-line interface to the Finance District MCP wallet server",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fdx": "./bin/fdx.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/**/*.test.js",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"lint:fix": "eslint . --fix",
|
|
13
|
+
"format": "prettier --write ."
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/financedistrict-platform/fd-agent-wallet-cli.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"fdx",
|
|
21
|
+
"finance-district",
|
|
22
|
+
"wallet",
|
|
23
|
+
"mcp",
|
|
24
|
+
"crypto",
|
|
25
|
+
"defi",
|
|
26
|
+
"agent",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"author": "Finance District",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/financedistrict-platform/fd-agent-wallet-cli/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/financedistrict-platform/fd-agent-wallet-cli#readme",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
37
|
+
"axios": "^1.13.5",
|
|
38
|
+
"commander": "^14.0.3",
|
|
39
|
+
"dotenv": "^16.6.1",
|
|
40
|
+
"nanospinner": "^1.2.2",
|
|
41
|
+
"picocolors": "^1.1.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"eslint": "^8.57.1",
|
|
45
|
+
"eslint-config-prettier": "^10.1.8",
|
|
46
|
+
"eslint-plugin-import": "^2.32.0",
|
|
47
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
48
|
+
"prettier": "^3.8.1"
|
|
49
|
+
},
|
|
50
|
+
"type": "commonjs",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const SERVICE = 'fdx-wallet';
|
|
8
|
+
const TIMEOUT = 10000;
|
|
9
|
+
|
|
10
|
+
let _supported;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if the OS credential store is available on this platform.
|
|
14
|
+
* - macOS: Keychain via `security` CLI
|
|
15
|
+
* - Linux: libsecret via `secret-tool` CLI
|
|
16
|
+
* - Windows: DPAPI via PowerShell
|
|
17
|
+
*/
|
|
18
|
+
function isSupported() {
|
|
19
|
+
if (_supported !== undefined) return _supported;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
switch (process.platform) {
|
|
23
|
+
case 'darwin':
|
|
24
|
+
execFileSync('security', ['help'], { stdio: 'pipe', timeout: TIMEOUT });
|
|
25
|
+
_supported = true;
|
|
26
|
+
break;
|
|
27
|
+
case 'linux':
|
|
28
|
+
execFileSync('secret-tool', ['--help'], { stdio: 'pipe', timeout: TIMEOUT });
|
|
29
|
+
_supported = true;
|
|
30
|
+
break;
|
|
31
|
+
case 'win32':
|
|
32
|
+
execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', '$null'], {
|
|
33
|
+
stdio: 'pipe',
|
|
34
|
+
timeout: TIMEOUT,
|
|
35
|
+
});
|
|
36
|
+
_supported = true;
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
_supported = false;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
_supported = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return _supported;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Retrieve a secret from the OS credential store.
|
|
50
|
+
* @param {string} account - Unique account identifier (e.g. MCP server host).
|
|
51
|
+
* @returns {string|null} The secret string, or null if not found / not supported.
|
|
52
|
+
*/
|
|
53
|
+
function getSecret(account) {
|
|
54
|
+
if (!isSupported()) return null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
switch (process.platform) {
|
|
58
|
+
case 'darwin':
|
|
59
|
+
return execFileSync(
|
|
60
|
+
'security',
|
|
61
|
+
['find-generic-password', '-a', account, '-s', SERVICE, '-w'],
|
|
62
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: TIMEOUT },
|
|
63
|
+
).trim();
|
|
64
|
+
|
|
65
|
+
case 'linux':
|
|
66
|
+
return execFileSync('secret-tool', ['lookup', 'service', SERVICE, 'account', account], {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
|
+
timeout: TIMEOUT,
|
|
70
|
+
}).trim();
|
|
71
|
+
|
|
72
|
+
case 'win32':
|
|
73
|
+
return winDecrypt(account);
|
|
74
|
+
|
|
75
|
+
default:
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Store a secret in the OS credential store.
|
|
85
|
+
* @param {string} account - Unique account identifier.
|
|
86
|
+
* @param {string} secret - The secret value to store.
|
|
87
|
+
* @returns {boolean} True if stored successfully, false otherwise.
|
|
88
|
+
*/
|
|
89
|
+
function setSecret(account, secret) {
|
|
90
|
+
if (!isSupported()) return false;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
switch (process.platform) {
|
|
94
|
+
case 'darwin':
|
|
95
|
+
// Delete existing entry first (add-generic-password -U can be unreliable)
|
|
96
|
+
try {
|
|
97
|
+
execFileSync('security', ['delete-generic-password', '-a', account, '-s', SERVICE], {
|
|
98
|
+
stdio: 'pipe',
|
|
99
|
+
timeout: TIMEOUT,
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
// Entry may not exist — ignore
|
|
103
|
+
}
|
|
104
|
+
// Use -w with stdin to avoid leaking the secret in process arguments
|
|
105
|
+
execFileSync('security', ['add-generic-password', '-a', account, '-s', SERVICE, '-w'], {
|
|
106
|
+
input: secret,
|
|
107
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
|
+
timeout: TIMEOUT,
|
|
109
|
+
});
|
|
110
|
+
return true;
|
|
111
|
+
|
|
112
|
+
case 'linux':
|
|
113
|
+
execFileSync(
|
|
114
|
+
'secret-tool',
|
|
115
|
+
['store', '--label', `FDX (${account})`, 'service', SERVICE, 'account', account],
|
|
116
|
+
{ input: secret, stdio: ['pipe', 'pipe', 'pipe'], timeout: TIMEOUT },
|
|
117
|
+
);
|
|
118
|
+
return true;
|
|
119
|
+
|
|
120
|
+
case 'win32':
|
|
121
|
+
winEncrypt(account, secret);
|
|
122
|
+
return true;
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete a secret from the OS credential store.
|
|
134
|
+
* @param {string} account - Unique account identifier.
|
|
135
|
+
*/
|
|
136
|
+
function deleteSecret(account) {
|
|
137
|
+
if (!isSupported()) return;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
switch (process.platform) {
|
|
141
|
+
case 'darwin':
|
|
142
|
+
execFileSync('security', ['delete-generic-password', '-a', account, '-s', SERVICE], {
|
|
143
|
+
stdio: 'pipe',
|
|
144
|
+
timeout: TIMEOUT,
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case 'linux':
|
|
149
|
+
execFileSync('secret-tool', ['clear', 'service', SERVICE, 'account', account], {
|
|
150
|
+
stdio: 'pipe',
|
|
151
|
+
timeout: TIMEOUT,
|
|
152
|
+
});
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'win32':
|
|
156
|
+
winDeleteFile(account);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Credential may not exist — ignore
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Windows DPAPI helpers ---
|
|
165
|
+
// Tokens are encrypted with the current user's Windows login credentials
|
|
166
|
+
// and stored in a file under ~/.fdx/
|
|
167
|
+
|
|
168
|
+
function dpapiPath(account) {
|
|
169
|
+
const crypto = require('crypto');
|
|
170
|
+
const os = require('os');
|
|
171
|
+
const hash = crypto.createHash('sha256').update(account).digest('hex').slice(0, 16);
|
|
172
|
+
return path.join(os.homedir(), '.fdx', `.cred_${hash}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function winEncrypt(account, plaintext) {
|
|
176
|
+
const script = [
|
|
177
|
+
'Add-Type -AssemblyName System.Security',
|
|
178
|
+
'$bytes = [Text.Encoding]::UTF8.GetBytes([Console]::In.ReadToEnd())',
|
|
179
|
+
'$enc = [Security.Cryptography.ProtectedData]::Protect(' +
|
|
180
|
+
'$bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)',
|
|
181
|
+
'[Convert]::ToBase64String($enc)',
|
|
182
|
+
].join('; ');
|
|
183
|
+
|
|
184
|
+
const encrypted = execFileSync(
|
|
185
|
+
'powershell',
|
|
186
|
+
['-NoProfile', '-NonInteractive', '-Command', script],
|
|
187
|
+
{ input: plaintext, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: TIMEOUT },
|
|
188
|
+
).trim();
|
|
189
|
+
|
|
190
|
+
const filePath = dpapiPath(account);
|
|
191
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
192
|
+
fs.writeFileSync(filePath, encrypted, { mode: 0o600 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function winDecrypt(account) {
|
|
196
|
+
const filePath = dpapiPath(account);
|
|
197
|
+
if (!fs.existsSync(filePath)) return null;
|
|
198
|
+
|
|
199
|
+
const encrypted = fs.readFileSync(filePath, 'utf8').trim();
|
|
200
|
+
if (!encrypted) return null;
|
|
201
|
+
|
|
202
|
+
const script = [
|
|
203
|
+
'Add-Type -AssemblyName System.Security',
|
|
204
|
+
'$bytes = [Convert]::FromBase64String([Console]::In.ReadToEnd())',
|
|
205
|
+
'$dec = [Security.Cryptography.ProtectedData]::Unprotect(' +
|
|
206
|
+
'$bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)',
|
|
207
|
+
'[Text.Encoding]::UTF8.GetString($dec)',
|
|
208
|
+
].join('; ');
|
|
209
|
+
|
|
210
|
+
return execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
211
|
+
input: encrypted,
|
|
212
|
+
encoding: 'utf8',
|
|
213
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
214
|
+
timeout: TIMEOUT,
|
|
215
|
+
}).trim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function winDeleteFile(account) {
|
|
219
|
+
try {
|
|
220
|
+
fs.unlinkSync(dpapiPath(account));
|
|
221
|
+
} catch {
|
|
222
|
+
// File may not exist — ignore
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { isSupported, getSecret, setSecret, deleteSecret };
|
package/src/factory.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { WalletClient } = require('./wallet-client');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MCP_SERVER = 'https://mcp.fd.xyz';
|
|
7
|
+
|
|
8
|
+
function createClientFromEnv() {
|
|
9
|
+
const mcpServerUrl = process.env.FDX_MCP_SERVER || DEFAULT_MCP_SERVER;
|
|
10
|
+
const redirectUri = process.env.FDX_REDIRECT_URI;
|
|
11
|
+
const storePath = process.env.FDX_STORE_PATH;
|
|
12
|
+
|
|
13
|
+
const parsed = new URL(mcpServerUrl);
|
|
14
|
+
if (
|
|
15
|
+
parsed.protocol !== 'https:' &&
|
|
16
|
+
parsed.hostname !== 'localhost' &&
|
|
17
|
+
parsed.hostname !== '127.0.0.1'
|
|
18
|
+
) {
|
|
19
|
+
throw new Error('FDX_MCP_SERVER must use HTTPS (HTTP is only allowed for localhost)');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return new WalletClient({
|
|
23
|
+
mcpServerUrl,
|
|
24
|
+
redirectUri: redirectUri || `http://localhost:6260/oauth/callback`,
|
|
25
|
+
storePath: storePath || path.join(os.homedir(), '.fdx', 'auth.json'),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { createClientFromEnv };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { createClientFromEnv } = require('./factory');
|
|
2
|
+
const { MCPAuthClient } = require('./mcp-auth');
|
|
3
|
+
const { MCPClient } = require('./mcp-client');
|
|
4
|
+
const { WalletClient } = require('./wallet-client');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
MCPAuthClient,
|
|
8
|
+
MCPClient,
|
|
9
|
+
WalletClient,
|
|
10
|
+
createClientFromEnv,
|
|
11
|
+
};
|