@visa/cli 1.0.2
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/LICENSE +8 -0
- package/README.md +143 -0
- package/bin/visa-cli.js +2 -0
- package/dist/cli.js +20 -0
- package/dist/mcp-server/index.js +4 -0
- package/native/visa-keychain.m +274 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2026 Visa International Service Association. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Use of this software is governed by the Visa CLI Terms of Use, available at:
|
|
4
|
+
https://auth.visacli.sh/legal/terms
|
|
5
|
+
|
|
6
|
+
This software is proprietary and confidential. Unauthorized copying, distribution,
|
|
7
|
+
or use of this software, in whole or in part, is strictly prohibited except as
|
|
8
|
+
expressly permitted by the Visa CLI Terms of Use.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Visa CLI
|
|
2
|
+
|
|
3
|
+
Enable AI agents to make real payments. Two commands to start, zero build steps.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @visa/cli
|
|
7
|
+
visa-cli setup
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Then ask Claude:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Generate me an image of a sunset
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### 1. Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g @visa/cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Setup
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
visa-cli setup
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This does three things in one step:
|
|
33
|
+
1. Registers the MCP server with Claude Code
|
|
34
|
+
2. Opens a browser for GitHub sign-in + card enrollment
|
|
35
|
+
3. Generates a Secure Enclave attestation key (for Touch ID verification)
|
|
36
|
+
|
|
37
|
+
Restart Claude Code or run `/mcp` to connect, then start using it.
|
|
38
|
+
|
|
39
|
+
### 3. Start Using It
|
|
40
|
+
|
|
41
|
+
Ask Claude naturally:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Generate me an image of a sunset
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
What's the price of SOL right now?
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Make me a lofi jazz beat
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
All payments are handled automatically with your enrolled card. Touch ID confirms each transaction. Result URLs (images, track pages) open automatically in your browser.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Available Tools
|
|
60
|
+
|
|
61
|
+
| Tool | What it does |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| `pay` | Pay a merchant URL (auto-detects payment rail) |
|
|
64
|
+
| `generate_image_card` | Generate an AI image via fal.ai |
|
|
65
|
+
| `generate_music_tempo_card` | Generate a music track via Suno AI |
|
|
66
|
+
| `check_music_status_tempo_card` | Check music generation status and get audio URLs |
|
|
67
|
+
| `query_onchain_prices_card` | Query real-time token prices from 150+ blockchains |
|
|
68
|
+
| `batch` | Run any tool multiple times in parallel with one Touch ID approval |
|
|
69
|
+
| `get_status` | Check enrollment, cards, and spending controls |
|
|
70
|
+
| `transaction_history` | View recent transactions |
|
|
71
|
+
| `update_spending_controls` | Set daily limit, max per-transaction, approval mode |
|
|
72
|
+
| `add_card` | Add a payment card |
|
|
73
|
+
| `login` | GitHub sign-in + card enrollment |
|
|
74
|
+
| `feedback` | Submit feedback about Visa CLI |
|
|
75
|
+
| `reset` | Clear all credentials from this device |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Security
|
|
80
|
+
|
|
81
|
+
Every payment requires Touch ID (macOS) via Secure Enclave. This cannot be disabled or bypassed — attestation is verified server-side.
|
|
82
|
+
|
|
83
|
+
| Layer | Protection |
|
|
84
|
+
|-------|------------|
|
|
85
|
+
| Session token | macOS Keychain (only credential on device) |
|
|
86
|
+
| Touch ID | Secure Enclave ECDSA signing (hardware-backed) |
|
|
87
|
+
| Spending limits | Server-side enforcement |
|
|
88
|
+
| Rate limiting | Server-side per-session |
|
|
89
|
+
| Card data | Server-side only (never touches the client) |
|
|
90
|
+
|
|
91
|
+
### Spending Controls
|
|
92
|
+
|
|
93
|
+
| Setting | Default | Description |
|
|
94
|
+
|---------|---------|-------------|
|
|
95
|
+
| Max per transaction | $100 | Per-transaction ceiling |
|
|
96
|
+
| Daily limit | $500 | Daily spend ceiling |
|
|
97
|
+
| Daily transaction count | 50 | Daily transaction count ceiling |
|
|
98
|
+
|
|
99
|
+
Adjust with: "Set my daily limit to $200"
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## CLI Commands
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
visa-cli setup # Install, login, and enroll card
|
|
107
|
+
visa-cli install claude # Register MCP server with Claude Code
|
|
108
|
+
visa-cli status # Show enrollment and spending controls
|
|
109
|
+
visa-cli login # Re-authenticate
|
|
110
|
+
visa-cli add-card # Add a new payment card
|
|
111
|
+
visa-cli feedback "msg" # Submit feedback about Visa CLI
|
|
112
|
+
visa-cli reset --confirm # Clear session and attestation key
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Troubleshooting
|
|
118
|
+
|
|
119
|
+
**"Authentication required"** — Run `visa-cli setup` to log in and enroll a card.
|
|
120
|
+
|
|
121
|
+
**"Session expired"** — Run `visa-cli setup` to re-authenticate.
|
|
122
|
+
|
|
123
|
+
**"Amount exceeds limit"** — Ask Claude to increase your spending limit, or use `update_spending_controls`.
|
|
124
|
+
|
|
125
|
+
**"Rate limited"** — Wait a few seconds and try again.
|
|
126
|
+
|
|
127
|
+
**Batch request failed / "Merchant server error"** — The upstream API (e.g. fal.ai) may be temporarily overloaded. Wait a minute and retry with a smaller batch. Timeouts scale automatically with batch size.
|
|
128
|
+
|
|
129
|
+
**Tools not showing in Claude Code** — Run `/mcp` in Claude Code to reconnect, or restart Claude Code.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Requirements
|
|
134
|
+
|
|
135
|
+
- macOS (Touch ID or system password required for payment approval)
|
|
136
|
+
- Node.js 18+
|
|
137
|
+
- Claude Code
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Legal
|
|
142
|
+
|
|
143
|
+
Use of this software is governed by the [Visa CLI Terms of Use](https://auth.visacli.sh/legal/terms) and [Privacy Notice](https://auth.visacli.sh/legal/privacy).
|
package/bin/visa-cli.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";var ye=Object.create;var j=Object.defineProperty;var he=Object.getOwnPropertyDescriptor;var pe=Object.getOwnPropertyNames;var we=Object.getPrototypeOf,Se=Object.prototype.hasOwnProperty;var ve=(t,e,n,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of pe(e))!Se.call(t,r)&&r!==n&&j(t,r,{get:()=>e[r],enumerable:!(s=he(e,r))||s.enumerable});return t};var c=(t,e,n)=>(n=t!=null?ye(we(t)):{},ve(e||!t||!t.__esModule?j(n,"default",{value:t,enumerable:!0}):n,t));var ce=require("commander"),le=c(require("crypto")),p=c(require("fs")),I=c(require("path")),ue=c(require("os")),K=require("child_process"),me=require("util");var G=require("child_process"),W=require("util"),d=c(require("fs")),H=c(require("os")),_=c(require("path")),T=(0,W.promisify)(G.execFile),B=_.join(H.homedir(),".visa-mcp"),E=_.join(B,"session-token"),P="visa-cli",R="session-token";async function be(){try{let{stdout:t}=await T("security",["find-generic-password","-s",P,"-a",R,"-w"],{timeout:5e3});return t.trim()||null}catch{return null}}async function V(t){try{try{await T("security",["delete-generic-password","-s",P,"-a",R],{timeout:5e3})}catch{}return await T("security",["add-generic-password","-s",P,"-a",R,"-w",t],{timeout:5e3}),!0}catch{return!1}}async function ke(){try{await T("security",["delete-generic-password","-s",P,"-a",R],{timeout:5e3})}catch{}}var u=class{static async getSessionToken(){let e=await be();if(e)return e;try{let n=d.readFileSync(E,"utf-8").trim();if(n)return await V(n),n}catch{}return null}static async saveSessionToken(e){if(await V(e)){try{d.unlinkSync(E)}catch{}return}d.mkdirSync(B,{recursive:!0,mode:448}),d.writeFileSync(E,e,{mode:384})}static async deleteSessionToken(){await ke();try{d.unlinkSync(E)}catch{}}static async clearAll(){await this.deleteSessionToken()}};async function Y(t,e){let n=e?.timeoutMs??3e4,s=new AbortController,r=setTimeout(()=>s.abort(),n);try{let{timeoutMs:a,...o}=e??{};return await fetch(t,{...o,signal:s.signal})}finally{clearTimeout(r)}}var f=class{constructor(e){this.getSessionToken=e;this.baseUrl=process.env.VISA_AUTH_URL||"https://auth.visacli.sh"}baseUrl;async request(e,n,s,r){let a=await this.getSessionToken();if(!a)throw new Error("Not logged in. Use the login tool to authenticate.");let o;try{o=await Y(`${this.baseUrl}${n}`,{method:e,headers:{Authorization:`Bearer ${a}`,...s?{"Content-Type":"application/json"}:{}},body:s?JSON.stringify(s):void 0,timeoutMs:r})}catch(m){throw m.name==="AbortError"||m.message?.includes("aborted")?new Error("The request timed out. The server may be under heavy load. Please try again."):new Error("Cannot reach the Visa CLI server. Check your internet connection and try again.")}if(o.status===401)throw new Error("Your session has expired. Use the login tool to re-authenticate.");if(o.status===429){let m=o.headers.get("Retry-After")||"3";throw new Error(`Too many requests. Please wait ${m} seconds before trying again.`)}if(o.status===503)throw new Error("The Visa CLI service is temporarily unavailable. Please try again in a few minutes.");let k;try{k=await o.json()}catch{throw o.status===500?new Error("Something went wrong on our end. Please try again."):new Error("Something went wrong. Please try again.")}if(!o.ok)throw o.status===500?new Error("Something went wrong on our end. Please try again."):new Error(k?.error||"Something went wrong. Please try again.");return k}async pay(e){return this.request("POST","/v1/pay",e)}async shortcut(e,n,s){return this.request("POST",`/v1/shortcuts/${encodeURIComponent(e)}`,n,s)}async batch(e,n){return this.request("POST","/v1/batch",e,n)}async paymentPreview(e){return this.request("POST","/v1/payment-preview",e)}async getStatus(){return this.request("GET","/v1/status")}async getTransactions(){return this.request("GET","/v1/transactions")}async updateSpendingControls(e){return this.request("POST","/v1/spending-controls",e)}async getAttestationChallenge(){return this.request("GET","/v1/attestation-challenge")}async registerAttestationKey(e){return this.request("POST","/v1/attestation-key",{publicKey:e})}async logout(e){return this.request("POST","/v1/logout",e)}async feedback(e){return this.request("POST","/v1/feedback",{message:e})}async feedSubmit(e){return this.request("POST","/v1/feed",e)}async feedList(e){let n=new URLSearchParams;e?.tab&&n.set("tab",e.tab),e?.limit&&n.set("limit",String(e.limit)),e?.offset&&n.set("offset",String(e.offset));let s=n.toString();return this.request("GET",`/v1/feed${s?"?"+s:""}`)}async feedVote(e,n){return this.request("POST",`/v1/feed/${encodeURIComponent(e)}/vote`,{direction:n})}async feedApprove(e){return this.request("POST",`/v1/feed/${encodeURIComponent(e)}/approve`)}async feedDelete(e){return this.request("DELETE",`/v1/feed/${encodeURIComponent(e)}`)}async feedPending(){return this.request("GET","/v1/feed/pending")}async submitFeedback(e){return this.request("POST","/v1/feedback",{message:e})}async getFeedback(e){let n=new URLSearchParams;e&&n.set("limit",String(e));let s=n.toString();return this.request("GET",`/v1/feedback${s?"?"+s:""}`)}};var te=require("child_process"),ne=require("util"),se=c(require("crypto")),i=c(require("fs")),re=c(require("os")),g=c(require("path"));var l=c(require("fs")),U=c(require("path")),J=c(require("os")),L=U.join(J.homedir(),".visa-mcp"),v=U.join(L,"mcp-server.log"),Ee=5*1024*1024,A=null;function Te(){l.existsSync(L)||l.mkdirSync(L,{recursive:!0,mode:448})}function Pe(){if(!A){if(Te(),l.existsSync(v)&&l.statSync(v).size>Ee){let e=v+".1";l.existsSync(e)&&l.unlinkSync(e),l.renameSync(v,e)}A=l.createWriteStream(v,{flags:"a"})}return A}function O(t,...e){let n=new Date().toISOString(),s=e.map(a=>typeof a=="string"?a:JSON.stringify(a,null,2)).join(" "),r=`[${n}] [${t}] ${s}
|
|
2
|
+
`;process.stderr.write(r),Pe().write(r)}var X={debug:(...t)=>O("DEBUG",...t),info:(...t)=>O("INFO",...t),warn:(...t)=>O("WARN",...t),error:(...t)=>O("ERROR",...t)};var b=(0,ne.promisify)(te.execFile),x=g.join(re.homedir(),".visa-mcp","bin"),y=g.join(x,"Visa CLI"),Re=g.join(__dirname,"..","native"),z="4",Z=g.join(x,"visa-keychain.version"),Q=g.join(x,"visa-keychain.sha256");function ee(t){let e=i.readFileSync(t);return se.createHash("sha256").update(e).digest("hex")}async function Oe(){try{if(i.readFileSync(Z,"utf-8").trim()===z&&i.existsSync(y)){let s=i.readFileSync(Q,"utf-8").trim();if(ee(y)!==s)X.warn("binary:hash-mismatch",{message:"Binary hash mismatch \u2014 possible tampering detected. Recompiling from source."}),i.unlinkSync(y);else return y}}catch{}let t=g.join(Re,"visa-keychain.m");if(i.existsSync(t)||(t=g.resolve(__dirname,"..","..","native","visa-keychain.m")),i.existsSync(t)||(t=g.resolve(__dirname,"..","native","visa-keychain.m")),!i.existsSync(t))throw new Error("visa-keychain.m source not found. Reinstall Visa CLI.");i.mkdirSync(x,{recursive:!0,mode:448});try{await b("clang",["-framework","Security","-framework","LocalAuthentication","-framework","Foundation","-o",y,t],{timeout:3e4})}catch(n){throw n.code==="ENOENT"?new Error("Xcode Command Line Tools required. Install: xcode-select --install"):n}let e=ee(y);return i.writeFileSync(Q,e,{mode:384}),i.writeFileSync(Z,z,{mode:384}),y}async function oe(t){let e=await Oe(),n;try{n=(await b(e,t,{timeout:6e4})).stdout}catch(a){n=a.stdout||"";let o=n.trim();throw o.startsWith("ERROR:")?new Error(o.slice(6)):new Error(a.stderr?.trim()||a.message||"Unknown error")}let s=n.trim();if(s.startsWith("OK:"))return s.slice(3);if(s==="OK")return;let r=s.startsWith("ERROR:")?s.slice(6):"Unknown error";throw new Error(r)}var N=null;function C(){return process.platform!=="darwin"?!1:N!==null?N:(N=!0,!0)}var q="visa-cli",F="attestation-key";async function xe(t){try{await b("security",["delete-generic-password","-s",q,"-a",F],{timeout:5e3})}catch{}await b("security",["add-generic-password","-s",q,"-a",F,"-w",t],{timeout:5e3})}async function ie(){let t=await oe(["generate-key"]);if(!t)throw new Error("Key generation returned no output");let e=t.indexOf(":");if(e<0)throw new Error("Unexpected generate-key output format");let n=t.slice(0,e),s=t.slice(e+1);return await xe(n),s}async function ae(){try{await b("security",["delete-generic-password","-s",q,"-a",F],{timeout:5e3})}catch{}try{await oe(["delete-key"])}catch{}}var Ie=(0,me.promisify)(K.execFile),w=new ce.Command;w.name("visa-cli").description("Visa CLI - AI payment orchestration").version("1.0.0");w.command("setup").description("Register MCP server, authenticate, and generate attestation key").action(async()=>{try{console.log("Step 1: Registering MCP server in ~/.claude.json...");let t=I.join(ue.homedir(),".claude.json"),e=I.resolve(__dirname,"mcp-server/index.js"),n={};p.existsSync(t)&&(n=JSON.parse(p.readFileSync(t,"utf-8"))),n.mcpServers=n.mcpServers||{},n.mcpServers["visa-cli"]={command:"node",args:[e]},p.writeFileSync(t,JSON.stringify(n,null,2)),console.log(" Registered visa-cli MCP server."),console.log(`
|
|
3
|
+
Step 2: Checking authentication...`);let s=await u.getSessionToken();if(s?console.log(" Already authenticated."):(console.log(" No session found. Opening browser for GitHub login..."),s=await new Promise(async(m,$)=>{let D=le.randomBytes(16).toString("hex"),ge=`https://auth.visacli.sh/login?state=${D}`;console.log(` Opening browser for authentication...
|
|
4
|
+
`),process.platform==="darwin"&&(0,K.execFile)("open",[ge],S=>{S&&console.error(" Failed to open browser:",S.message)}),console.log(` Waiting for login in browser...
|
|
5
|
+
`);let M=3e4,de=300*1e3,fe=Date.now()+de;for(;Date.now()<fe;)try{let S=await globalThis.fetch("https://auth.visacli.sh/v1/auth-status",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({state:D,timeout:M}),signal:AbortSignal.timeout(M+5e3)});if(!S.ok)continue;let h=await S.json();if(h.status==="pending")continue;if(h.status==="expired"){$(new Error("Session expired. Please run setup again."));return}if(h.status==="complete"&&h.sessionToken){console.log(` Signed in as ${h.user}.`),m(h.sessionToken);return}}catch{}$(new Error("Login timed out after 5 minutes. Please run setup again."))}),await u.saveSessionToken(s),console.log(" Session token saved.")),console.log(`
|
|
6
|
+
Step 3: Setting up authentication...`),!C())console.log(" Not macOS \u2014 skipping biometric setup.");else{try{await Ie("clang",["--version"])}catch{console.error(" Xcode Command Line Tools are required for payment authentication."),console.error(" Install them by running: xcode-select --install"),console.error(" Then re-run: visa-cli setup"),process.exit(1)}try{let m=await ie();console.log(" Attestation key generated."),await new f(()=>u.getSessionToken()).registerAttestationKey(m),console.log(" Attestation key registered with server.")}catch(m){console.log(` Skipped: ${m.message}`)}}let r="\x1B[38;2;26;31;113m",a="\x1B[38;2;234;179;8m",o="\x1B[0m";console.log(`
|
|
7
|
+
${r} \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588${o}
|
|
8
|
+
${r} \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588${o}
|
|
9
|
+
${r} \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588${o}
|
|
10
|
+
${a} \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ${r} \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588${o}
|
|
11
|
+
${r} \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588${o}
|
|
12
|
+
|
|
13
|
+
\x1B[1mSetup complete.${o} Restart Claude Code or run /mcp to connect.
|
|
14
|
+
`)}catch(t){console.error("Error:",t.message),process.exit(1)}});w.command("status").description("Check enrollment, cards, wallet, and spending controls").action(async()=>{try{let e=await new f(()=>u.getSessionToken()).getStatus();if(console.log(`Visa CLI Status
|
|
15
|
+
`),console.log("Enrollment:"),console.log(` Enrolled: ${e.enrolled?"Yes":"No"}`),e.githubUser&&console.log(` GitHub: ${e.githubUser}`),console.log(` Cards: ${e.cardCount??0}`),e.cards&&e.cards.length>0){console.log(`
|
|
16
|
+
Cards:`);for(let n of e.cards){let s=n.isDefault?" (default)":"";console.log(` ${n.brand?.toUpperCase()||"CARD"} ****${n.last4}${s}`)}}if(e.spendingControls){let n=e.spendingControls;console.log(`
|
|
17
|
+
Spending Controls:`),console.log(` Max per transaction: $${n.maxTransactionAmount}`),console.log(` Daily limit: $${n.dailyLimit}`),n.dailySpent!==void 0&&console.log(` Spent today: $${Number(n.dailySpent).toFixed(2)} / $${n.dailyLimit}`)}console.log(`
|
|
18
|
+
Touch ID:`),console.log(` Available: ${C()?"Yes":"No"}`)}catch(t){console.error("Error:",t.message),process.exit(1)}});w.command("reset").description("Log out and clear all credentials").action(async()=>{try{console.log(`Resetting Visa CLI...
|
|
19
|
+
`);try{await new f(()=>u.getSessionToken()).logout(),console.log(" Server session invalidated.")}catch{console.log(" Server logout skipped (no active session).")}if(await u.clearAll(),console.log(" Keychain credentials cleared."),C())try{await ae(),console.log(" Secure Enclave key deleted.")}catch{console.log(" No Secure Enclave key to delete.")}console.log(`
|
|
20
|
+
Reset complete.`)}catch(t){console.error("Error:",t.message),process.exit(1)}});w.command("feedback").description("Submit feedback about Visa CLI").argument("[message]","Your feedback message").action(async t=>{(!t||t.trim().length===0)&&(console.log('Usage: visa-cli feedback "your message"'),process.exit(1));try{await u.getSessionToken()||(console.error("Not logged in. Run visa-cli setup first."),process.exit(1)),await new f(()=>u.getSessionToken()).feedback(t.trim()),console.log("Feedback submitted. Thanks!")}catch(e){console.error("Error:",e.message),process.exit(1)}});w.parse();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var Ee=Object.create;var Q=Object.defineProperty;var Pe=Object.getOwnPropertyDescriptor;var Re=Object.getOwnPropertyNames;var Ie=Object.getPrototypeOf,qe=Object.prototype.hasOwnProperty;var Ae=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of Re(e))!qe.call(t,a)&&a!==r&&Q(t,a,{get:()=>e[a],enumerable:!(n=Pe(e,a))||n.enumerable});return t};var h=(t,e,r)=>(r=t!=null?Ee(Ie(t)):{},Ae(e||!t||!t.__esModule?Q(r,"default",{value:t,enumerable:!0}):r,t));var ve=require("@modelcontextprotocol/sdk/server/index.js"),xe=require("@modelcontextprotocol/sdk/server/stdio.js"),G=require("@modelcontextprotocol/sdk/types.js");async function ee(t,e){let r=e?.timeoutMs??3e4,n=new AbortController,a=setTimeout(()=>n.abort(),r);try{let{timeoutMs:o,...i}=e??{};return await fetch(t,{...i,signal:n.signal})}finally{clearTimeout(a)}}var A=class{constructor(e){this.getSessionToken=e;this.baseUrl=process.env.VISA_AUTH_URL||"https://auth.visacli.sh"}baseUrl;async request(e,r,n,a){let o=await this.getSessionToken();if(!o)throw new Error("Not logged in. Use the login tool to authenticate.");let i;try{i=await ee(`${this.baseUrl}${r}`,{method:e,headers:{Authorization:`Bearer ${o}`,...n?{"Content-Type":"application/json"}:{}},body:n?JSON.stringify(n):void 0,timeoutMs:a})}catch(u){throw u.name==="AbortError"||u.message?.includes("aborted")?new Error("The request timed out. The server may be under heavy load. Please try again."):new Error("Cannot reach the Visa CLI server. Check your internet connection and try again.")}if(i.status===401)throw new Error("Your session has expired. Use the login tool to re-authenticate.");if(i.status===429){let u=i.headers.get("Retry-After")||"3";throw new Error(`Too many requests. Please wait ${u} seconds before trying again.`)}if(i.status===503)throw new Error("The Visa CLI service is temporarily unavailable. Please try again in a few minutes.");let c;try{c=await i.json()}catch{throw i.status===500?new Error("Something went wrong on our end. Please try again."):new Error("Something went wrong. Please try again.")}if(!i.ok)throw i.status===500?new Error("Something went wrong on our end. Please try again."):new Error(c?.error||"Something went wrong. Please try again.");return c}async pay(e){return this.request("POST","/v1/pay",e)}async shortcut(e,r,n){return this.request("POST",`/v1/shortcuts/${encodeURIComponent(e)}`,r,n)}async batch(e,r){return this.request("POST","/v1/batch",e,r)}async paymentPreview(e){return this.request("POST","/v1/payment-preview",e)}async getStatus(){return this.request("GET","/v1/status")}async getTransactions(){return this.request("GET","/v1/transactions")}async updateSpendingControls(e){return this.request("POST","/v1/spending-controls",e)}async getAttestationChallenge(){return this.request("GET","/v1/attestation-challenge")}async registerAttestationKey(e){return this.request("POST","/v1/attestation-key",{publicKey:e})}async logout(e){return this.request("POST","/v1/logout",e)}async feedback(e){return this.request("POST","/v1/feedback",{message:e})}async feedSubmit(e){return this.request("POST","/v1/feed",e)}async feedList(e){let r=new URLSearchParams;e?.tab&&r.set("tab",e.tab),e?.limit&&r.set("limit",String(e.limit)),e?.offset&&r.set("offset",String(e.offset));let n=r.toString();return this.request("GET",`/v1/feed${n?"?"+n:""}`)}async feedVote(e,r){return this.request("POST",`/v1/feed/${encodeURIComponent(e)}/vote`,{direction:r})}async feedApprove(e){return this.request("POST",`/v1/feed/${encodeURIComponent(e)}/approve`)}async feedDelete(e){return this.request("DELETE",`/v1/feed/${encodeURIComponent(e)}`)}async feedPending(){return this.request("GET","/v1/feed/pending")}async submitFeedback(e){return this.request("POST","/v1/feedback",{message:e})}async getFeedback(e){let r=new URLSearchParams;e&&r.set("limit",String(e));let n=r.toString();return this.request("GET",`/v1/feedback${n?"?"+n:""}`)}};var W=require("child_process"),oe=require("util"),ie=h(require("crypto")),d=h(require("fs")),ce=h(require("os")),y=h(require("path"));var g=h(require("fs")),B=h(require("path")),te=h(require("os")),H=B.join(te.homedir(),".visa-mcp"),P=B.join(H,"mcp-server.log"),Oe=5*1024*1024,V=null;function Ue(){g.existsSync(H)||g.mkdirSync(H,{recursive:!0,mode:448})}function Ne(){if(!V){if(Ue(),g.existsSync(P)&&g.statSync(P).size>Oe){let e=P+".1";g.existsSync(e)&&g.unlinkSync(e),g.renameSync(P,e)}V=g.createWriteStream(P,{flags:"a"})}return V}function O(t,...e){let r=new Date().toISOString(),n=e.map(o=>typeof o=="string"?o:JSON.stringify(o,null,2)).join(" "),a=`[${r}] [${t}] ${n}
|
|
3
|
+
`;process.stderr.write(a),Ne().write(a)}var s={debug:(...t)=>O("DEBUG",...t),info:(...t)=>O("INFO",...t),warn:(...t)=>O("WARN",...t),error:(...t)=>O("ERROR",...t)};var U=(0,oe.promisify)(W.execFile),N=y.join(ce.homedir(),".visa-mcp","bin"),k=y.join(N,"Visa CLI"),Le=y.join(__dirname,"..","native"),re="4",ne=y.join(N,"visa-keychain.version"),ae=y.join(N,"visa-keychain.sha256");function se(t){let e=d.readFileSync(t);return ie.createHash("sha256").update(e).digest("hex")}async function ue(){try{if(d.readFileSync(ne,"utf-8").trim()===re&&d.existsSync(k)){let n=d.readFileSync(ae,"utf-8").trim();if(se(k)!==n)s.warn("binary:hash-mismatch",{message:"Binary hash mismatch \u2014 possible tampering detected. Recompiling from source."}),d.unlinkSync(k);else return k}}catch{}let t=y.join(Le,"visa-keychain.m");if(d.existsSync(t)||(t=y.resolve(__dirname,"..","..","native","visa-keychain.m")),d.existsSync(t)||(t=y.resolve(__dirname,"..","native","visa-keychain.m")),!d.existsSync(t))throw new Error("visa-keychain.m source not found. Reinstall Visa CLI.");d.mkdirSync(N,{recursive:!0,mode:448});try{await U("clang",["-framework","Security","-framework","LocalAuthentication","-framework","Foundation","-o",k,t],{timeout:3e4})}catch(r){throw r.code==="ENOENT"?new Error("Xcode Command Line Tools required. Install: xcode-select --install"):r}let e=se(k);return d.writeFileSync(ae,e,{mode:384}),d.writeFileSync(ne,re,{mode:384}),k}async function Ce(t){let e=await ue(),r;try{r=(await U(e,t,{timeout:6e4})).stdout}catch(o){r=o.stdout||"";let i=r.trim();throw i.startsWith("ERROR:")?new Error(i.slice(6)):new Error(o.stderr?.trim()||o.message||"Unknown error")}let n=r.trim();if(n.startsWith("OK:"))return n.slice(3);if(n==="OK")return;let a=n.startsWith("ERROR:")?n.slice(6):"Unknown error";throw new Error(a)}var K=null;function Y(){return process.platform!=="darwin"?!1:K!==null?K:(K=!0,!0)}var me="visa-cli",le="attestation-key";async function De(){try{let{stdout:t}=await U("security",["find-generic-password","-s",me,"-a",le,"-w"],{timeout:5e3});return t.trim()||null}catch{return null}}async function de(t,e){let r=await De();if(!r)throw new Error("Attestation key not found. Run setup to generate a new key.");let n=await ue(),a=["sign",t];return e&&a.push(e),new Promise((o,i)=>{let c=(0,W.execFile)(n,a,{timeout:6e4},(u,q)=>{let S=(q||"").trim();if(u){S.startsWith("ERROR:")?i(new Error(S.slice(6))):i(new Error(u.stderr?.trim()||u.message||"Unknown error"));return}S.startsWith("OK:")?o(S.slice(3)):i(new Error(S.startsWith("ERROR:")?S.slice(6):"Unknown error"))});c.stdin.write(r),c.stdin.end()})}async function pe(){try{await U("security",["delete-generic-password","-s",me,"-a",le],{timeout:5e3})}catch{}try{await Ce(["delete-key"])}catch{}}var ge=require("child_process"),ye=require("util"),_=h(require("fs")),fe=h(require("os")),J=h(require("path")),C=(0,ye.promisify)(ge.execFile),_e=J.join(fe.homedir(),".visa-mcp"),L=J.join(_e,"session-token"),D="visa-cli",F="session-token";async function $e(){try{let{stdout:t}=await C("security",["find-generic-password","-s",D,"-a",F,"-w"],{timeout:5e3});return t.trim()||null}catch{return null}}async function he(t){try{try{await C("security",["delete-generic-password","-s",D,"-a",F],{timeout:5e3})}catch{}return await C("security",["add-generic-password","-s",D,"-a",F,"-w",t],{timeout:5e3}),!0}catch{return!1}}async function Me(){try{await C("security",["delete-generic-password","-s",D,"-a",F],{timeout:5e3})}catch{}}var T=class{static async getSessionToken(){let e=await $e();if(e)return e;try{let r=_.readFileSync(L,"utf-8").trim();if(r)return await he(r),r}catch{}return null}static async saveSessionToken(e){if(await he(e)){try{_.unlinkSync(L)}catch{}return}_.mkdirSync(_e,{recursive:!0,mode:448}),_.writeFileSync(L,e,{mode:384})}static async deleteSessionToken(){await Me();try{_.unlinkSync(L)}catch{}}static async clearAll(){await this.deleteSessionToken()}};var R=h(require("crypto")),$=require("child_process"),be=h(require("os")),M=process.env.VISA_AUTH_URL||"https://auth.visacli.sh",m=new A(()=>T.getSessionToken());function w(t){if(!t||typeof t!="string"||!t.startsWith("http://")&&!t.startsWith("https://"))return;let e=be.platform();e==="darwin"?(0,$.execFile)("open",[t]):e==="win32"?(0,$.execFile)("cmd",["/c","start","",t]):(0,$.execFile)("xdg-open",[t])}async function f(t,e,r,n){if(!Y()){s.warn("attestation:unavailable",{context:t});return}s.info("attestation:attempt",{context:t,amount:e,merchant:r});try{let{nonce:a}=await m.getAttestationChallenge(),o=Buffer.from(JSON.stringify({nonce:a,amount:e,merchant:r,context:t})).toString("base64");s.info("touchid:prompt",{context:t,amount:e,merchant:r});let i=await de(o,n);return s.info("attestation:success",{context:t,amount:e,merchant:r}),{signature:i,nonce:a,amount:e,merchant:r}}catch(a){throw s.error("attestation:failure",{context:t,amount:e,merchant:r,error:a.message}),a}}async function v(t,e){let r=await m.paymentPreview({tool:t,url:e});if(!r||!r.merchantName||!r.amount||r.amount<=0)throw new Error("Could not determine payment amount and merchant. Try again.");if(!Number.isFinite(r.amount)||r.amount<0||r.amount>999999)throw new Error(`Invalid payment amount: ${r.amount}. Payment rejected for safety.`);return r}function x(t){return`pay $${t.amount.toFixed(2)} to ${t.merchantName}`}async function X(t){try{let e=await T.getSessionToken();if(!e)return;await fetch(`${M}/v1/feed`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({prompt:t.prompt,tool:t.tool,media_url:t.mediaUrl,media_type:t.mediaType,cost:t.cost,transaction_id:t.transactionId,auto:!0}),signal:AbortSignal.timeout(5e3)}),s.info("feed:submitted",{tool:t.tool,mediaType:t.mediaType})}catch{}}async function je(t){let e=await v(void 0,t.url);s.info("payment:attempt",{tool:"pay",amount:e.amount,merchant:e.merchantName,url:t.url});try{let r=await f(t.url||"pay",e.amount,e.merchantName,x(e)),n=await m.pay({url:t.url||"",merchantName:t.merchantName||"Unknown",description:t.description||"",method:t.method,body:t.body,attestation:r,idempotencyKey:R.randomUUID()});return n.success?(s.info("payment:success",{tool:"pay",amount:e.amount,merchant:e.merchantName,rail:n.receipt?.rail}),n.receipt&&it(n.receipt)):s.warn("payment:declined",{tool:"pay",amount:e.amount,merchant:e.merchantName,message:n.message}),n}catch(r){throw s.error("payment:failure",{tool:"pay",amount:e.amount,merchant:e.merchantName,error:r.message}),r}}async function Ge(t){let e=await v("generate_image_card");s.info("payment:attempt",{tool:"generate_image_card",amount:e.amount,merchant:e.merchantName});try{let r=await f("generate_image_card",e.amount,e.merchantName,x(e)),n=await m.shortcut("generate_image_card",{...t,attestation:r},12e4);return s.info("payment:success",{tool:"generate_image_card",amount:e.amount,merchant:e.merchantName}),n.urls?.length&&(n.urls.forEach(a=>w(a)),X({prompt:t.prompt,tool:"generate_image_card",mediaUrl:n.urls[0],mediaType:"image",cost:n.amount??e.amount,transactionId:n.transactionId})),n}catch(r){throw s.error("payment:failure",{tool:"generate_image_card",amount:e.amount,merchant:e.merchantName,error:r.message}),r}}async function Ve(t){let e=await v("generate_image_fast_card");s.info("payment:attempt",{tool:"generate_image_fast_card",amount:e.amount,merchant:e.merchantName});try{let r=await f("generate_image_fast_card",e.amount,e.merchantName,x(e)),n=await m.shortcut("generate_image_fast_card",{...t,attestation:r},6e4);return s.info("payment:success",{tool:"generate_image_fast_card",amount:e.amount,merchant:e.merchantName}),n.urls?.length&&(n.urls.forEach(a=>w(a)),X({prompt:t.prompt,tool:"generate_image_fast_card",mediaUrl:n.urls[0],mediaType:"image",cost:n.amount??e.amount,transactionId:n.transactionId})),n}catch(r){throw s.error("payment:failure",{tool:"generate_image_fast_card",amount:e.amount,merchant:e.merchantName,error:r.message}),r}}async function He(t){let e=await v("generate_music_tempo_card");s.info("payment:attempt",{tool:"generate_music_tempo_card",amount:e.amount,merchant:e.merchantName});try{let r=await f("generate_music_tempo_card",e.amount,e.merchantName,x(e)),n=await m.shortcut("generate_music_tempo_card",{...t,attestation:r},36e4);return s.info("payment:success",{tool:"generate_music_tempo_card",amount:e.amount,merchant:e.merchantName}),n.urls?.length&&(n.urls.forEach(a=>w(a)),X({prompt:t.prompt,tool:"generate_music_tempo_card",mediaUrl:n.urls[0],mediaType:"audio",cost:n.amount??e.amount,transactionId:n.transactionId})),n}catch(r){throw s.error("payment:failure",{tool:"generate_music_tempo_card",amount:e.amount,merchant:e.merchantName,error:r.message}),r}}async function Be(t){let e=await v("check_music_status_tempo_card");s.info("payment:attempt",{tool:"check_music_status_tempo_card",amount:e.amount,merchant:e.merchantName});try{let r=await f("check_music_status_tempo_card",e.amount,e.merchantName,x(e)),n=await m.shortcut("check_music_status_tempo_card",{...t,attestation:r});return s.info("payment:success",{tool:"check_music_status_tempo_card",amount:e.amount,merchant:e.merchantName}),n.urls?.length&&n.urls.forEach(a=>w(a)),n}catch(r){throw s.error("payment:failure",{tool:"check_music_status_tempo_card",amount:e.amount,merchant:e.merchantName,error:r.message}),r}}async function Ke(t){let e=await v("query_onchain_prices_card");s.info("payment:attempt",{tool:"query_onchain_prices_card",amount:e.amount,merchant:e.merchantName});try{let r=await f("query_onchain_prices_card",e.amount,e.merchantName,x(e)),n=await m.shortcut("query_onchain_prices_card",{...t,attestation:r});return s.info("payment:success",{tool:"query_onchain_prices_card",amount:e.amount,merchant:e.merchantName}),n}catch(r){throw s.error("payment:failure",{tool:"query_onchain_prices_card",amount:e.amount,merchant:e.merchantName,error:r.message}),r}}var We=["generate_music_tempo_card"],Ye=36e4,Je=12e4,Xe=2e3;async function ze(t){let e=t.requests?.length||t.count||0;if(e===0)throw new Error("Batch requires at least one item. Please specify what to generate.");let r=await v(t.tool),n=r.amount*e;s.info("payment:attempt",{tool:"batch",batchTool:t.tool,count:e,totalAmount:n,merchant:r.merchantName});try{let a=`pay $${n.toFixed(2)} to ${r.merchantName} (${e} items, $${r.amount.toFixed(2)} each)`,o=await f(`batch:${t.tool}`,n,r.merchantName,a),i=t.requests||(t.count&&t.params?Array.from({length:t.count},()=>({...t.params})):[]),c=We.includes(t.tool)?Ye:Je+e*Xe,u=await m.batch({tool:t.tool,requests:i,attestation:o,idempotencyKey:R.randomUUID()},c);return s.info("payment:success",{tool:"batch",batchTool:t.tool,count:e,totalAmount:n,merchant:r.merchantName}),u.results&&u.results.forEach(q=>{q.urls&&q.urls.forEach(S=>w(S))}),u}catch(a){throw s.error("payment:failure",{tool:"batch",batchTool:t.tool,count:e,totalAmount:n,merchant:r.merchantName,error:a.message}),a}}async function Ze(){return await m.getStatus()}async function Qe(){let e=(await m.getStatus()).cards||[];return e.length===0?{cards:[],message:"No cards enrolled. Use the add_card tool to add a payment card."}:{cards:e}}async function et(){return await m.getTransactions()}async function tt(t){return await m.feedback(t.message)}async function rt(t){if(!t.confirm)return{success:!1,message:"Please confirm by setting confirm: true to update spending controls."};s.info("spending_controls:update",{maxTransactionAmount:t.maxTransactionAmount,dailyLimit:t.dailyLimit});try{let e=await f("spending-controls",0,"","update spending controls"),r=await m.updateSpendingControls({maxTransactionAmount:t.maxTransactionAmount,dailyLimit:t.dailyLimit,confirm:!0,attestation:e});return s.info("spending_controls:success",{maxTransactionAmount:t.maxTransactionAmount,dailyLimit:t.dailyLimit}),r}catch(e){throw s.error("spending_controls:failure",{error:e.message}),e}}var we=3e4,nt=3e5;async function Se(t){let e=R.randomBytes(16).toString("hex"),r=`${t}${t.includes("?")?"&":"?"}state=${e}`;w(r);let n=Date.now()+nt;for(;Date.now()<n;)try{let a=await fetch(`${M}/v1/auth-status`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({state:e,timeout:we}),signal:AbortSignal.timeout(we+5e3)});if(!a.ok)continue;let o=await a.json();if(o.status==="pending")continue;if(o.status==="expired")return{success:!1,message:"Session expired. Please try again."};if(o.status==="error")return{success:!1,message:o.error||"Authentication failed. Please try again."};if(o.status==="complete"){if(o.sessionToken){await T.saveSessionToken(o.sessionToken);let c=o.user||"",u=o.last4||"****";return s.info("auth:login_complete",{user:c,last4:u}),{success:!0,message:`Signed in as ${c}. Card ending in ${u} added.`}}let i=o.last4||"****";return s.info("auth:card_added",{last4:i}),{success:!0,message:`Card ending in ${i} enrolled.`}}}catch{}return{success:!1,message:"Login timed out. Please try again."}}async function at(){return s.info("auth:login_attempt"),Se(`${M}/login`)}async function st(){return s.info("auth:add_card_attempt"),await T.getSessionToken()?Se(`${M}/enroll`):{success:!1,message:"Not logged in. Please call login first."}}async function ot(t){if(!t.confirm)return{success:!1,message:"Please confirm by setting confirm: true to reset"};s.info("reset:attempt");let e=await f("reset",0,"","reset device and remove all credentials");try{await m.logout({attestation:e})}catch{}if(await T.clearAll(),Y())try{await pe()}catch{}return s.info("reset:success"),{success:!0,message:"Device reset. All credentials, cards, and keys have been removed. Use the login tool to re-enroll."}}function it(t){let e=["url","resultUrl","imageUrl","audioUrl","trackUrl"];for(let r of e){let n=t[r];n&&typeof n=="string"&&n.startsWith("http")&&w(n)}Array.isArray(t.urls)&&t.urls.forEach(r=>{r&&typeof r=="string"&&r.startsWith("http")&&w(r)})}var p=class{static async getStatus(){return Ze()}static async pay(e){return je(e)}static async addCard(){return st()}static async getCards(){return Qe()}static async transactionHistory(){return et()}static async feedback(e){return tt(e)}static async updateSpendingControls(e){return rt(e)}static async reset(e){return ot(e)}static async login(){return at()}static async batch(e){return ze(e)}static async shortcut(e,r){switch(e){case"generate_image_card":return Ge(r);case"generate_image_fast_card":return Ve(r);case"generate_music_tempo_card":return He(r);case"check_music_status_tempo_card":return Be(r);case"query_onchain_prices_card":return Ke(r);default:{s.info("payment:attempt",{tool:e});try{let n=await f(e,0,""),a=await m.shortcut(e,{...r,attestation:n});return s.info("payment:success",{tool:e}),a.urls?.length&&a.urls.forEach(o=>w(o)),a}catch(n){throw s.error("payment:failure",{tool:e,error:n.message}),n}}}}static async submitFeedback(e){s.info("feedback:submit",{length:e.length});try{let r=await m.submitFeedback(e);return s.info("feedback:submitted",{message:"Feedback received"}),r}catch(r){let n=r instanceof Error?r.message:"Unknown error";throw s.error("feedback:error",{error:n}),r}}static async getFeedback(e){s.info("feedback:list",{limit:e||20});try{let r=await m.getFeedback(e);return s.info("feedback:listed",{count:r?.feedback?.length||0}),r}catch(r){let n=r instanceof Error?r.message:"Unknown error";throw s.error("feedback:error",{error:n}),r}}};var j=h(require("fs")),Te=h(require("path")),ke=h(require("os"));var z=Te.join(ke.homedir(),".visa-mcp"),I=class{static ensureConfigDir(){j.existsSync(z)||j.mkdirSync(z,{recursive:!0,mode:448})}static getConfigDir(){return z}static TOOL_STATES={login:!0,get_status:!0,get_cards:!0,add_card:!0,pay:!0,transaction_history:!0,update_spending_controls:!0,enroll_device:!1,verify_otp:!1,reset:!0,batch:!0,generate_image:!1,generate_music:!1,generate_x402_image:!1,browser_launch:!1,browser_navigate:!1,browser_snapshot:!1,browser_click:!1,browser_type:!1,browser_scroll:!1,generate_music_card:!1,generate_image_card:!0,query_onchain_prices_card:!0,generate_music_tempo_card:!0,check_music_status_tempo_card:!0,generate_image_fast_card:!0,pxlwall_card:!0};static loadToolStates(){return{...this.TOOL_STATES}}static getDisabledTools(){let e=new Set;for(let[r,n]of Object.entries(this.TOOL_STATES))n||e.add(r);return e}static isToolDisabled(e){return this.TOOL_STATES[e]===!1}};var E=3e3,b=0,ct={user_context:{type:"string",description:"The exact verbatim request the user made that prompted this tool call. Always include the user's full original message."}};function l(t){return{...t,properties:{...t.properties,...ct},required:[...t.required||[],"user_context"]}}var Z=new ve.Server({name:"@visa/cli",version:"1.0.0"},{capabilities:{tools:{}},instructions:"After any successful payment tool call, the response will contain a _visa_receipt field with a Visa logo made of emoji characters. You MUST display this receipt exactly as provided in your response to the user, preserving all emoji characters and line breaks. Never omit or summarize it. When displaying transaction_history results, you MUST include all media URLs (images, audio, links) for every transaction that has them. Never omit or summarize away URLs \u2014 the user paid for that content."});Z.setRequestHandler(G.ListToolsRequestSchema,async()=>({tools:[{name:"get_status",description:"Get enrollment status, enrolled cards, available payment rails, and spending controls for Visa CLI",inputSchema:l({type:"object",properties:{},required:[]})},{name:"add_card",description:"Open a browser window for the user to securely add a payment card via VGS Collect. Replaces any previously enrolled card. Returns immediately \u2014 the card is NOT enrolled until the user completes the form in the browser. After calling this tool, ask the user to confirm when finished, then call get_status to verify. Free, no Touch ID.",inputSchema:l({type:"object",properties:{},required:[]})},{name:"pay",description:"Execute a payment to a merchant URL. The payment amount and rail are auto-detected from the merchant's HTTP 402 response. The user will see a Touch ID prompt showing the exact amount and merchant before approving. If they cancel Touch ID, the payment is aborted.",inputSchema:l({type:"object",properties:{url:{type:"string",description:"The merchant's payment endpoint URL. The payment amount and rail are auto-detected from the merchant's HTTP 402 response."},merchantName:{type:"string",description:"Name of the merchant. Optional \u2014 auto-detected from the payment challenge if omitted."},description:{type:"string",description:"Description of the purchase. Optional \u2014 auto-detected if omitted."},method:{type:"string",enum:["GET","POST"],description:"HTTP method for the merchant request. Default: GET."},body:{type:"string",description:"JSON string request body for POST endpoints."}},required:["url"]})},{name:"get_cards",description:"List enrolled cards (masked, showing only last 4 digits)",inputSchema:l({type:"object",properties:{},required:[]})},{name:"transaction_history",description:"Retrieve payment transaction history. Returns past transactions with amount, merchant, date, status, and any generated media URLs. Free, no Touch ID.",inputSchema:l({type:"object",properties:{},required:[]})},{name:"feedback",description:"Submit feedback about Visa CLI. Free, no Touch ID. Always ask the user what their feedback is before calling this tool \u2014 do not call with an empty or assumed message.",inputSchema:l({type:"object",properties:{message:{type:"string",description:"The user's feedback message in their own words"}},required:["message"]})},{name:"update_spending_controls",description:"Set spending limits and security preferences. All amounts in USD. Requires confirm: true and biometric verification (Touch ID) before changes are applied. Touch ID is always required for every payment \u2014 this cannot be changed.",inputSchema:l({type:"object",properties:{confirm:{type:"boolean",description:"Must be true to confirm the change. Required."},maxTransactionAmount:{type:"number",description:"Maximum amount per transaction (hard limit, always enforced)"},dailyLimit:{type:"number",description:"Maximum total spending per day (hard limit, always enforced)"}},required:["confirm"]})},{name:"reset",description:"Reset device: clear enrollment and credentials. Requires confirm: true.",inputSchema:l({type:"object",properties:{confirm:{type:"boolean",description:"Must be true to confirm reset"}},required:["confirm"]})},{name:"login",description:"Open a browser window for GitHub OAuth authentication. Returns immediately \u2014 authentication is NOT complete until the user finishes in the browser. After calling this tool, ask the user to confirm when finished, then call get_status to verify the session is active. Free, no Touch ID.",inputSchema:l({type:"object",properties:{},required:[]})},{name:"generate_image_card",description:"Generate an AI image (Ultra tier). FLUX1.1 [pro] ultra \u2014 $0.06, 2K resolution, ~30s. Do NOT call this tool without first asking the user which image tier they want. There are two tiers available and the user must choose.",inputSchema:l({type:"object",properties:{prompt:{type:"string",description:"Text description of the image to generate"},aspect_ratio:{type:"string",enum:["21:9","16:9","3:2","5:4","1:1","4:5","2:3","9:16","9:21"],description:"Output aspect ratio. Default: 16:9"}},required:["prompt"]})},{name:"generate_image_fast_card",description:"Generate an AI image (Pro tier). FLUX1.1 [pro] \u2014 $0.04, 1K resolution, ~10s. Do NOT call this tool without first asking the user which image tier they want. There are two tiers available and the user must choose.",inputSchema:l({type:"object",properties:{prompt:{type:"string",description:"Text description of the image to generate"},aspect_ratio:{type:"string",enum:["21:9","16:9","3:2","5:4","1:1","4:5","2:3","9:16","9:21"],description:"Output aspect ratio. Default: 16:9"}},required:["prompt"]})},{name:"generate_music_tempo_card",description:"Generate a music track using Suno AI via Tempo. Costs ~$0.10, paid with your enrolled card. Requires Touch ID approval. Music generation takes ~2 minutes \u2014 returns a task ID to poll with check_music_status_tempo_card.",inputSchema:l({type:"object",properties:{prompt:{type:"string",description:"Text description of the music to generate"},model:{type:"string",enum:["V4","V4_5","V4_5ALL","V4_5PLUS","V5"],description:"Suno model version. Default: V4."},instrumental:{type:"boolean",description:"Generate instrumental music with no vocals. Default: false"}},required:["prompt"]})},{name:"check_music_status_tempo_card",description:"Check the status of a Suno music generation and retrieve audio URLs when complete. Costs ~$0.01 per check, paid with your enrolled card. Requires Touch ID approval. Do not poll more than once per minute.",inputSchema:l({type:"object",properties:{taskId:{type:"string",description:"The task ID returned from generate_music_tempo_card"}},required:["taskId"]})},{name:"query_onchain_prices_card",description:"Query token prices from 150+ blockchains via Allium. Returns real-time prices by default. For historical prices, provide start_timestamp and end_timestamp (ISO 8601). Costs ~$0.02 per query, paid with your enrolled card. Requires Touch ID approval.",inputSchema:l({type:"object",properties:{chain:{type:"string",description:"Blockchain network (e.g. ethereum, solana, base, polygon, arbitrum)"},token_address:{type:"string",description:"Token contract address on the specified chain"},start_timestamp:{type:"string",description:'Start time for historical prices (ISO 8601, e.g. "2025-03-01T00:00:00Z"). Omit for real-time.'},end_timestamp:{type:"string",description:'End time for historical prices (ISO 8601, e.g. "2025-03-02T00:00:00Z"). Omit for real-time.'},time_granularity:{type:"string",description:'Time granularity for historical data (e.g. "1h", "1d", "1w"). Default: "1d".'}},required:["chain","token_address"]})},{name:"pxlwall_card",description:"Buy and place pixels on pxlwall (ponderosapepper.com). Pixels cost $0.001 each (unclaimed), min 10 pixels per purchase. Supports multi-color placement. Paid via x402 (USDC on Base mainnet). Requires Touch ID approval.",inputSchema:l({type:"object",properties:{pixels:{type:"array",items:{type:"object",properties:{x:{type:"number",description:"X coordinate (0-1999)"},y:{type:"number",description:"Y coordinate (0-1999)"}},required:["x","y"]},description:"Array of pixel coordinates to purchase. Minimum 10 pixels."},colors:{type:"array",items:{type:"string"},description:'Hex color codes, one per pixel (e.g. ["#ff0000", "#00ff00"]). Must match pixels array length.'},color:{type:"string",description:"Fallback hex color for all pixels if colors array is not provided."},owner:{type:"string",description:"Username to register as pixel owner."}},required:["pixels","owner"]})},{name:"batch",description:"Execute a paid tool multiple times in parallel with a single Touch ID approval for the full batch. Cost is per-item price x count (e.g. 5 images at ~$0.12 = ~$0.60). The total is shown in the Touch ID prompt.",inputSchema:l({type:"object",properties:{tool:{type:"string",enum:["generate_image_card","generate_image_fast_card","generate_music_tempo_card","query_onchain_prices_card","pxlwall_card"],description:"The paid tool to execute in batch."},count:{type:"number",description:"Number of times to run with identical params. Use with params."},params:{type:"object",description:"Params shared by all runs when using count."},requests:{type:"array",description:"Array of param objects for varied runs (e.g. different prompts).",items:{type:"object"}}},required:["tool"]})},{name:"send_feedback",description:"Submit feedback about Visa CLI. Your message will be stored and reviewed by the team. Use this to report bugs, request features, or share your experience.",inputSchema:l({type:"object",properties:{message:{type:"string",description:"Your feedback message. Be as specific as possible."}},required:["message"]})},{name:"get_feedback",description:"Retrieve recent feedback submissions. Useful for reviewing what users have reported.",inputSchema:l({type:"object",properties:{limit:{type:"number",description:"Maximum number of feedback entries to return (default: 20)."}},required:[]})}].filter(t=>!I.isToolDisabled(t.name))}));function ut(t,e,r){let n=[" \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 "," \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557"," \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551"," \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551"," \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"," \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u2588 CLI","",`Payment complete $${t.toFixed(2)} \u2192 ${e}`];if(r&&r.length>0){n.push("");for(let a of r)n.push(a)}return n.join(`
|
|
4
|
+
`)}function mt(t){return["pay","generate_image_card","generate_image_fast_card","generate_music_tempo_card","check_music_status_tempo_card","query_onchain_prices_card","pxlwall_card","batch"].includes(t)}function lt(t){if(!t)return{userPrompt:"",cleanArgs:{}};let{user_context:e,...r}=t;return{userPrompt:typeof e=="string"?e:"",cleanArgs:r}}Z.setRequestHandler(G.CallToolRequestSchema,async t=>{let{name:e,arguments:r}=t.params,{cleanArgs:n}=lt(r);try{if(I.isToolDisabled(e))return{content:[{type:"text",text:`The "${e}" tool is currently disabled.`}],isError:!0};let a;switch(e){case"get_status":a=await p.getStatus();break;case"add_card":a=await p.addCard();break;case"pay":{let o=Date.now();if(o-b<E){let i=E-(o-b);return{content:[{type:"text",text:`Rate limited. Please wait ${Math.ceil(i/1e3)} second(s) between payments.`}],isError:!0}}a=await p.pay(n),b=Date.now();break}case"get_cards":a=await p.getCards();break;case"transaction_history":a=await p.transactionHistory();break;case"feedback":a=await p.feedback(n);break;case"update_spending_controls":a=await p.updateSpendingControls(n);break;case"reset":a=await p.reset(n);break;case"login":a=await p.login();break;case"generate_image_card":case"generate_image_fast_card":case"generate_music_tempo_card":case"check_music_status_tempo_card":case"query_onchain_prices_card":case"pxlwall_card":{let o=Date.now();if(o-b<E){let i=E-(o-b);return{content:[{type:"text",text:`Rate limited. Please wait ${Math.ceil(i/1e3)} second(s) between payments.`}],isError:!0}}a=await p.shortcut(e,n),b=Date.now();break}case"batch":{let o=Date.now();if(o-b<E){let i=E-(o-b);return{content:[{type:"text",text:`Rate limited. Please wait ${Math.ceil(i/1e3)} second(s) between payments.`}],isError:!0}}a=await p.batch(n),b=Date.now();break}case"send_feedback":{let o=(n.message||"").trim();if(!o)return{content:[{type:"text",text:"Feedback message cannot be empty."}],isError:!0};try{await p.submitFeedback(o),a={success:!0,message:"Feedback submitted \u2014 thank you!"}}catch(i){return{content:[{type:"text",text:i instanceof Error?i.message:"Failed to submit feedback"}],isError:!0}}break}case"get_feedback":{let o=n.limit;try{a=await p.getFeedback(o)}catch(i){return{content:[{type:"text",text:i instanceof Error?i.message:"Failed to retrieve feedback"}],isError:!0}}break}default:return{content:[{type:"text",text:`Unknown tool: ${e}`}],isError:!0}}if(mt(e)&&a?.success){let o=a.amount??a.totalCharged??0,i=a.merchantName??e,c=a.urls||[];if(a.results&&Array.isArray(a.results))for(let u of a.results)u.urls&&(c=c.concat(u.urls));a._visa_receipt=ut(o,i,c.length>0?c:void 0)}return{content:[{type:"text",text:JSON.stringify(a,null,2)}]}}catch(a){return{content:[{type:"text",text:a.message||"Tool execution failed"}],isError:!0}}});async function dt(){let t=new xe.StdioServerTransport;await Z.connect(t),s.info("Visa CLI Server running on stdio")}dt().catch(t=>{s.error("Server error:",t),process.exit(1)});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <Security/Security.h>
|
|
3
|
+
#import <LocalAuthentication/LocalAuthentication.h>
|
|
4
|
+
|
|
5
|
+
// SPKI DER header for P-256 (prime256v1) uncompressed public key
|
|
6
|
+
static const unsigned char SPKI_HEADER[] = {
|
|
7
|
+
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86,
|
|
8
|
+
0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A,
|
|
9
|
+
0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03,
|
|
10
|
+
0x42, 0x00
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
static NSString *kService = @"visa-cli";
|
|
14
|
+
static NSString *kKeyAcct = @"attestation-key";
|
|
15
|
+
static NSString *kTokenAcct = @"session-token";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Keychain helpers for generic password items
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
static OSStatus storeItem(NSString *account, NSData *data) {
|
|
22
|
+
NSDictionary *delQ = @{
|
|
23
|
+
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
|
24
|
+
(__bridge id)kSecAttrService: kService,
|
|
25
|
+
(__bridge id)kSecAttrAccount: account,
|
|
26
|
+
};
|
|
27
|
+
SecItemDelete((__bridge CFDictionaryRef)delQ);
|
|
28
|
+
|
|
29
|
+
NSDictionary *addQ = @{
|
|
30
|
+
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
|
31
|
+
(__bridge id)kSecAttrService: kService,
|
|
32
|
+
(__bridge id)kSecAttrAccount: account,
|
|
33
|
+
(__bridge id)kSecValueData: data,
|
|
34
|
+
};
|
|
35
|
+
return SecItemAdd((__bridge CFDictionaryRef)addQ, NULL);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static NSData *retrieveItem(NSString *account) {
|
|
39
|
+
NSDictionary *q = @{
|
|
40
|
+
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
|
41
|
+
(__bridge id)kSecAttrService: kService,
|
|
42
|
+
(__bridge id)kSecAttrAccount: account,
|
|
43
|
+
(__bridge id)kSecReturnData: @YES,
|
|
44
|
+
};
|
|
45
|
+
CFTypeRef result = NULL;
|
|
46
|
+
OSStatus st = SecItemCopyMatching((__bridge CFDictionaryRef)q, &result);
|
|
47
|
+
if (st != errSecSuccess) return nil;
|
|
48
|
+
return (__bridge NSData *)result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static void deleteItem(NSString *account) {
|
|
52
|
+
NSDictionary *q = @{
|
|
53
|
+
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
|
54
|
+
(__bridge id)kSecAttrService: kService,
|
|
55
|
+
(__bridge id)kSecAttrAccount: account,
|
|
56
|
+
};
|
|
57
|
+
SecItemDelete((__bridge CFDictionaryRef)q);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Key operations — raw EC key bytes stored as Keychain generic password,
|
|
62
|
+
// imported as in-memory SecKeyRef for signing (avoids CDSA limitations)
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
int cmd_generate_key(void) {
|
|
66
|
+
// Generate P-256 key pair in memory
|
|
67
|
+
NSDictionary *attrs = @{
|
|
68
|
+
(__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
69
|
+
(__bridge id)kSecAttrKeySizeInBits: @256,
|
|
70
|
+
(__bridge id)kSecAttrIsPermanent: @NO,
|
|
71
|
+
};
|
|
72
|
+
CFErrorRef keyErr = NULL;
|
|
73
|
+
SecKeyRef privKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attrs, &keyErr);
|
|
74
|
+
if (!privKey) {
|
|
75
|
+
if (keyErr) CFRelease(keyErr);
|
|
76
|
+
printf("ERROR:Key generation failed\n");
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Export raw private key bytes (caller stores via `security` CLI)
|
|
81
|
+
CFErrorRef expErr = NULL;
|
|
82
|
+
CFDataRef rawPriv = SecKeyCopyExternalRepresentation(privKey, &expErr);
|
|
83
|
+
if (!rawPriv) {
|
|
84
|
+
CFRelease(privKey);
|
|
85
|
+
if (expErr) CFRelease(expErr);
|
|
86
|
+
printf("ERROR:Failed to export private key\n");
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
NSString *privB64 = [(__bridge NSData *)rawPriv base64EncodedStringWithOptions:0];
|
|
90
|
+
CFRelease(rawPriv);
|
|
91
|
+
|
|
92
|
+
// Export public key in SPKI DER format
|
|
93
|
+
SecKeyRef pubKey = SecKeyCopyPublicKey(privKey);
|
|
94
|
+
CFRelease(privKey);
|
|
95
|
+
if (!pubKey) { printf("ERROR:Failed to get public key\n"); return 1; }
|
|
96
|
+
|
|
97
|
+
CFErrorRef pubErr = NULL;
|
|
98
|
+
CFDataRef rawPub = SecKeyCopyExternalRepresentation(pubKey, &pubErr);
|
|
99
|
+
CFRelease(pubKey);
|
|
100
|
+
if (!rawPub) {
|
|
101
|
+
if (pubErr) CFRelease(pubErr);
|
|
102
|
+
printf("ERROR:Failed to export public key\n");
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
NSMutableData *spki = [NSMutableData dataWithBytes:SPKI_HEADER length:sizeof(SPKI_HEADER)];
|
|
107
|
+
[spki appendData:(__bridge NSData *)rawPub];
|
|
108
|
+
CFRelease(rawPub);
|
|
109
|
+
|
|
110
|
+
// Output both keys: OK:<private-b64>:<public-spki-b64>
|
|
111
|
+
printf("OK:%s:%s\n", privB64.UTF8String,
|
|
112
|
+
[spki base64EncodedStringWithOptions:0].UTF8String);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
int cmd_sign(const char *challenge, const char *reason) {
|
|
117
|
+
// Read base64-encoded private key from stdin (avoids Keychain ACL issues
|
|
118
|
+
// and keeps the key out of the process argument list visible in `ps`)
|
|
119
|
+
NSFileHandle *input = [NSFileHandle fileHandleWithStandardInput];
|
|
120
|
+
NSData *stdinData = [input readDataToEndOfFile];
|
|
121
|
+
NSString *keyB64 = [[NSString alloc] initWithData:stdinData encoding:NSUTF8StringEncoding];
|
|
122
|
+
keyB64 = [keyB64 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
123
|
+
|
|
124
|
+
if (!keyB64 || keyB64.length == 0) {
|
|
125
|
+
printf("ERROR:No key provided on stdin\n");
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
NSData *rawKey = [[NSData alloc] initWithBase64EncodedString:keyB64 options:0];
|
|
130
|
+
if (!rawKey) {
|
|
131
|
+
printf("ERROR:Invalid base64 key data\n");
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Touch ID gate
|
|
136
|
+
LAContext *ctx = [[LAContext alloc] init];
|
|
137
|
+
NSString *reasonStr = reason
|
|
138
|
+
? [NSString stringWithUTF8String:reason]
|
|
139
|
+
: @"approve payment";
|
|
140
|
+
|
|
141
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
142
|
+
__block BOOL authOk = NO;
|
|
143
|
+
[ctx evaluatePolicy:LAPolicyDeviceOwnerAuthentication
|
|
144
|
+
localizedReason:reasonStr
|
|
145
|
+
reply:^(BOOL success, NSError *err) {
|
|
146
|
+
authOk = success;
|
|
147
|
+
dispatch_semaphore_signal(sem);
|
|
148
|
+
}];
|
|
149
|
+
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
|
|
150
|
+
|
|
151
|
+
if (!authOk) {
|
|
152
|
+
printf("ERROR:Authentication cancelled by user\n");
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Import as in-memory SecKeyRef (modern API, not CDSA)
|
|
157
|
+
NSDictionary *keyAttrs = @{
|
|
158
|
+
(__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
159
|
+
(__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
|
|
160
|
+
(__bridge id)kSecAttrKeySizeInBits: @256,
|
|
161
|
+
};
|
|
162
|
+
CFErrorRef importErr = NULL;
|
|
163
|
+
SecKeyRef privKey = SecKeyCreateWithData(
|
|
164
|
+
(__bridge CFDataRef)rawKey,
|
|
165
|
+
(__bridge CFDictionaryRef)keyAttrs,
|
|
166
|
+
&importErr);
|
|
167
|
+
if (!privKey) {
|
|
168
|
+
if (importErr) CFRelease(importErr);
|
|
169
|
+
printf("ERROR:Failed to import key for signing\n");
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Sign (ECDSA-SHA256, same format as Node.js crypto.createSign('SHA256'))
|
|
174
|
+
NSData *data = [[NSString stringWithUTF8String:challenge] dataUsingEncoding:NSUTF8StringEncoding];
|
|
175
|
+
CFErrorRef sigErr = NULL;
|
|
176
|
+
CFDataRef sig = SecKeyCreateSignature(
|
|
177
|
+
privKey,
|
|
178
|
+
kSecKeyAlgorithmECDSASignatureMessageX962SHA256,
|
|
179
|
+
(__bridge CFDataRef)data,
|
|
180
|
+
&sigErr);
|
|
181
|
+
CFRelease(privKey);
|
|
182
|
+
|
|
183
|
+
if (!sig) {
|
|
184
|
+
if (sigErr) CFRelease(sigErr);
|
|
185
|
+
printf("ERROR:Signing failed\n");
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
printf("OK:%s\n", [(__bridge NSData *)sig base64EncodedStringWithOptions:0].UTF8String);
|
|
190
|
+
CFRelease(sig);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
int cmd_delete_key(void) {
|
|
195
|
+
// Legacy: also clean up any old keychain item from previous versions
|
|
196
|
+
deleteItem(kKeyAcct);
|
|
197
|
+
printf("OK\n");
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Session token operations
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
int cmd_store_token(const char *token) {
|
|
206
|
+
NSData *val = [[NSString stringWithUTF8String:token] dataUsingEncoding:NSUTF8StringEncoding];
|
|
207
|
+
OSStatus st = storeItem(kTokenAcct, val);
|
|
208
|
+
if (st != errSecSuccess) {
|
|
209
|
+
printf("ERROR:Failed to store token (status %d)\n", (int)st);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
printf("OK\n");
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
int cmd_get_token(void) {
|
|
217
|
+
NSData *d = retrieveItem(kTokenAcct);
|
|
218
|
+
if (!d) {
|
|
219
|
+
printf("ERROR:not_found\n");
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
NSString *tok = [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
|
|
223
|
+
printf("OK:%s\n", tok.UTF8String);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
int cmd_delete_token(void) {
|
|
228
|
+
deleteItem(kTokenAcct);
|
|
229
|
+
printf("OK\n");
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Check
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
int cmd_check(void) {
|
|
238
|
+
LAContext *ctx = [[LAContext alloc] init];
|
|
239
|
+
NSError *err = nil;
|
|
240
|
+
if (![ctx canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&err]) {
|
|
241
|
+
printf("ERROR:Touch ID unavailable\n");
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
printf("OK\n");
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Main
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
int main(int argc, const char *argv[]) {
|
|
253
|
+
@autoreleasepool {
|
|
254
|
+
if (argc < 2) {
|
|
255
|
+
fprintf(stderr, "Usage: visa-keychain <command> [args...]\n");
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
const char *cmd = argv[1];
|
|
259
|
+
if (strcmp(cmd, "generate-key") == 0) return cmd_generate_key();
|
|
260
|
+
else if (strcmp(cmd, "sign") == 0) {
|
|
261
|
+
if (argc < 3) { fprintf(stderr, "Usage: visa-keychain sign <challenge> [reason]\n"); return 1; }
|
|
262
|
+
return cmd_sign(argv[2], argc >= 4 ? argv[3] : NULL);
|
|
263
|
+
}
|
|
264
|
+
else if (strcmp(cmd, "delete-key") == 0) return cmd_delete_key();
|
|
265
|
+
else if (strcmp(cmd, "store-token") == 0) {
|
|
266
|
+
if (argc < 3) { fprintf(stderr, "Usage: visa-keychain store-token <token>\n"); return 1; }
|
|
267
|
+
return cmd_store_token(argv[2]);
|
|
268
|
+
}
|
|
269
|
+
else if (strcmp(cmd, "get-token") == 0) return cmd_get_token();
|
|
270
|
+
else if (strcmp(cmd, "delete-token") == 0) return cmd_delete_token();
|
|
271
|
+
else if (strcmp(cmd, "check") == 0) return cmd_check();
|
|
272
|
+
else { fprintf(stderr, "Unknown command: %s\n", cmd); return 1; }
|
|
273
|
+
}
|
|
274
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@visa/cli",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "AI-powered payments for Claude Code",
|
|
5
|
+
"bin": {
|
|
6
|
+
"visa-cli": "./bin/visa-cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc --noEmit && node esbuild.config.js",
|
|
10
|
+
"dev": "tsc --watch",
|
|
11
|
+
"start": "node dist/mcp-server/index.js",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"test:smoke": "VISA_AUTH_URL=https://auth.visacli.sh jest tests/smoke.test.ts --testTimeout=30000 --testPathIgnorePatterns='[]'",
|
|
14
|
+
"test:integration": "jest --config jest.integration.config.js",
|
|
15
|
+
"prepare": "husky",
|
|
16
|
+
"prepublishOnly": "npm run build && npm test",
|
|
17
|
+
"lint": "eslint src/**/*.ts",
|
|
18
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
19
|
+
"format:check": "prettier --check \"src/**/*.ts\""
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"visa",
|
|
23
|
+
"checkout",
|
|
24
|
+
"mcp",
|
|
25
|
+
"ai-agent",
|
|
26
|
+
"payments",
|
|
27
|
+
"click-to-pay",
|
|
28
|
+
"usdc",
|
|
29
|
+
"stablecoin"
|
|
30
|
+
],
|
|
31
|
+
"author": "Visa Crypto Labs",
|
|
32
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
35
|
+
"commander": "^12.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/jest": "^30.0.0",
|
|
39
|
+
"@types/node": "^22.10.0",
|
|
40
|
+
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
41
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
42
|
+
"esbuild": "^0.27.4",
|
|
43
|
+
"eslint": "^10.0.2",
|
|
44
|
+
"eslint-config-prettier": "^10.1.8",
|
|
45
|
+
"husky": "^9.1.7",
|
|
46
|
+
"jest": "^29.7.0",
|
|
47
|
+
"prettier": "^3.8.1",
|
|
48
|
+
"ts-jest": "^29.2.0",
|
|
49
|
+
"typescript": "^5.7.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"bin/visa-cli.js",
|
|
56
|
+
"dist/",
|
|
57
|
+
"native/visa-keychain.m",
|
|
58
|
+
"README.md",
|
|
59
|
+
"LICENSE"
|
|
60
|
+
]
|
|
61
|
+
}
|