@visa/cli 4.1.0-rc.153 → 4.1.0-rc.154
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/README.md +1 -0
- package/dist/checkout-engine/cli-engine.js +1 -3
- package/dist/checkout-engine/confirmed-merchants.d.ts +31 -0
- package/dist/checkout-engine/confirmed-merchants.js +136 -0
- package/dist/checkout-engine/index.d.ts +3 -0
- package/dist/checkout-engine/index.js +3 -0
- package/dist/checkout-engine/known-merchants.d.ts +10 -0
- package/dist/checkout-engine/known-merchants.js +38 -0
- package/dist/checkout-engine/receipt-dir.d.ts +6 -0
- package/dist/checkout-engine/receipt-dir.js +8 -0
- package/dist/cli.js +268 -267
- package/dist/mcp-server/index.js +174 -174
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -180,6 +180,7 @@ is required so the CLI never guesses which agent can spend.
|
|
|
180
180
|
| `wallet_directory_pay` | Directory find + pay in one call, same policy path |
|
|
181
181
|
| `wallet_history` | Journaled payment receipts |
|
|
182
182
|
| `wallet_fund` | Funding instructions for the wallet address |
|
|
183
|
+
| `checkout_merchants` | Read-only: merchants where your card has completed real checkouts, from this device's receipts |
|
|
183
184
|
| `get_status` | Account and wallet state summary |
|
|
184
185
|
| `feedback` | Submit feedback on a tool result |
|
|
185
186
|
| `reset` | Clear local auth state and credentials |
|
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Every browser/network primitive is injectable (CliEngineDeps) so the session/
|
|
12
12
|
// timer/store lifecycle is unit-testable without launching Chromium.
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
13
|
import { readFile } from 'node:fs/promises';
|
|
16
14
|
import { launchCheckoutBrowser } from './browser-launch.js';
|
|
15
|
+
import { RECEIPT_DIR } from './receipt-dir.js';
|
|
17
16
|
import { prepareCheckout as realPrepareCheckout, submitApprovedCheckout as realSubmitApprovedCheckout, InMemoryPreparedCheckoutStore, } from './executor.js';
|
|
18
17
|
import { claimMandatePickup as realClaimMandatePickup, runHostedApproval as realRunHostedApproval, } from './hosted-approval.js';
|
|
19
18
|
import { VgsLiveInstrument, decimalToMinor, minorToDecimal, } from './vgs-live-instrument.js';
|
|
@@ -147,7 +146,6 @@ function failedPay(detail) {
|
|
|
147
146
|
credentialDisclosed: false,
|
|
148
147
|
};
|
|
149
148
|
}
|
|
150
|
-
const RECEIPT_DIR = join(homedir(), '.visa-mcp', 'checkout-receipts');
|
|
151
149
|
// Must match the prepared-checkout store TTL so a session and its store entry
|
|
152
150
|
// expire together — an abandoned review can't leak the browser + state.
|
|
153
151
|
const PREPARED_TTL_MS = 5 * 60 * 1000;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** One confirmed charge, in the receipt's own terms. */
|
|
2
|
+
export type ConfirmedCharge = {
|
|
3
|
+
recordedAt: string;
|
|
4
|
+
amount: string;
|
|
5
|
+
currency: string;
|
|
6
|
+
/** Receipt file basename, so an operator can open the evidence log. */
|
|
7
|
+
receiptFile: string;
|
|
8
|
+
};
|
|
9
|
+
export type ConfirmedMerchant = {
|
|
10
|
+
/** The checkout page the card went through. */
|
|
11
|
+
url: string;
|
|
12
|
+
host: string;
|
|
13
|
+
/**
|
|
14
|
+
* Curated human identity (known-merchants.ts), present only when someone has
|
|
15
|
+
* identified who is behind this checkout URL. Hosted payment links carry an
|
|
16
|
+
* opaque path on the PSP's host, so without this a row names nobody.
|
|
17
|
+
*/
|
|
18
|
+
name?: string;
|
|
19
|
+
category?: string;
|
|
20
|
+
website?: string;
|
|
21
|
+
confirmedCount: number;
|
|
22
|
+
lastConfirmedAt: string;
|
|
23
|
+
charges: ConfirmedCharge[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Merchants this device has completed a real card checkout at, newest first.
|
|
27
|
+
*
|
|
28
|
+
* Never throws: a missing directory (nothing has ever been checked out here)
|
|
29
|
+
* and an unreadable one both read as an empty registry.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readConfirmedMerchants(receiptDir?: string): Promise<ConfirmedMerchant[]>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// "Where can I use my card?", the merchant registry derived from this
|
|
2
|
+
// device's checkout receipts.
|
|
3
|
+
//
|
|
4
|
+
// Receipts (receipt.ts) are the only local record that a real merchant
|
|
5
|
+
// checkout completed on this box. This module reads them back and answers one
|
|
6
|
+
// question: which checkout pages has this card actually gone through?
|
|
7
|
+
//
|
|
8
|
+
// A merchant only counts when BOTH halves agree. The engine's own outcome must
|
|
9
|
+
// be 'confirmed' (the merchant showed a definitive success) AND the VIC
|
|
10
|
+
// confirmation must have posted APPROVED (the network side of the same
|
|
11
|
+
// purchase). Either half alone is a claim, not a completion: a 'confirmed' with
|
|
12
|
+
// no posted confirmation is exactly the reconciliation case receipt.ts flags,
|
|
13
|
+
// and a posted DECLINED is a completed report of a failure.
|
|
14
|
+
//
|
|
15
|
+
// Reading is best-effort by construction. The receipts directory is an operator
|
|
16
|
+
// artifact that anything on the box can touch, so an unreadable or malformed
|
|
17
|
+
// file is skipped rather than failing the whole read. One bad file must not
|
|
18
|
+
// hide every merchant behind it.
|
|
19
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { KNOWN_MERCHANT_IDENTITIES } from './known-merchants.js';
|
|
22
|
+
import { RECEIPT_DIR } from './receipt-dir.js';
|
|
23
|
+
/**
|
|
24
|
+
* The URL a receipt's charge happened at.
|
|
25
|
+
*
|
|
26
|
+
* The receipt's top-level merchant block records the HOST only, on purpose: a
|
|
27
|
+
* full checkout URL can carry cart/session identifiers, so it stays out of the
|
|
28
|
+
* shareable summary. The evidence log still holds it (the executor's first
|
|
29
|
+
* 'navigation' step is the page the run opened), so recover it from there and
|
|
30
|
+
* fall back to the host when the evidence is absent or shaped differently.
|
|
31
|
+
*/
|
|
32
|
+
function checkoutUrlOf(receipt) {
|
|
33
|
+
const navigation = receipt.evidence.steps.find((step) => step?.type === 'navigation');
|
|
34
|
+
const data = navigation?.data;
|
|
35
|
+
const url = typeof data === 'object' && data !== null ? data.url : undefined;
|
|
36
|
+
return typeof url === 'string' && url.length > 0 ? url : `https://${receipt.merchant.host}/`;
|
|
37
|
+
}
|
|
38
|
+
function isConfirmedCompletion(receipt) {
|
|
39
|
+
return (receipt.outcome === 'confirmed' &&
|
|
40
|
+
receipt.vicConfirmation?.posted === true &&
|
|
41
|
+
receipt.vicConfirmation.transactionStatus === 'APPROVED');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse one receipt file, or null when it is not a v1 receipt this module can
|
|
45
|
+
* read. Deliberately permissive about everything the grouping does not touch:
|
|
46
|
+
* the file was written by an older or newer engine and only has to carry the
|
|
47
|
+
* fields read below.
|
|
48
|
+
*/
|
|
49
|
+
function parseReceipt(json) {
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(json);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
58
|
+
return null;
|
|
59
|
+
const receipt = parsed;
|
|
60
|
+
if (receipt.schema !== 'checkout-agent-receipt/v1')
|
|
61
|
+
return null;
|
|
62
|
+
if (typeof receipt.recordedAt !== 'string')
|
|
63
|
+
return null;
|
|
64
|
+
if (typeof receipt.merchant?.host !== 'string')
|
|
65
|
+
return null;
|
|
66
|
+
if (typeof receipt.transaction?.amount !== 'string')
|
|
67
|
+
return null;
|
|
68
|
+
if (typeof receipt.transaction?.currency !== 'string')
|
|
69
|
+
return null;
|
|
70
|
+
if (!Array.isArray(receipt.evidence?.steps))
|
|
71
|
+
return null;
|
|
72
|
+
return receipt;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Merchants this device has completed a real card checkout at, newest first.
|
|
76
|
+
*
|
|
77
|
+
* Never throws: a missing directory (nothing has ever been checked out here)
|
|
78
|
+
* and an unreadable one both read as an empty registry.
|
|
79
|
+
*/
|
|
80
|
+
export async function readConfirmedMerchants(receiptDir = RECEIPT_DIR) {
|
|
81
|
+
let names;
|
|
82
|
+
try {
|
|
83
|
+
names = await readdir(receiptDir);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const byUrl = new Map();
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
if (!name.endsWith('.json'))
|
|
91
|
+
continue;
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = await readFile(join(receiptDir, name), 'utf8');
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const receipt = parseReceipt(raw);
|
|
100
|
+
if (!receipt || !isConfirmedCompletion(receipt))
|
|
101
|
+
continue;
|
|
102
|
+
const url = checkoutUrlOf(receipt);
|
|
103
|
+
const charge = {
|
|
104
|
+
recordedAt: receipt.recordedAt,
|
|
105
|
+
amount: receipt.transaction.amount,
|
|
106
|
+
currency: receipt.transaction.currency,
|
|
107
|
+
receiptFile: name,
|
|
108
|
+
};
|
|
109
|
+
const existing = byUrl.get(url);
|
|
110
|
+
if (existing) {
|
|
111
|
+
existing.confirmedCount += 1;
|
|
112
|
+
existing.charges.push(charge);
|
|
113
|
+
if (charge.recordedAt > existing.lastConfirmedAt) {
|
|
114
|
+
existing.lastConfirmedAt = charge.recordedAt;
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const identity = KNOWN_MERCHANT_IDENTITIES[url];
|
|
119
|
+
byUrl.set(url, {
|
|
120
|
+
url,
|
|
121
|
+
host: receipt.merchant.host,
|
|
122
|
+
// Conditional spread on purpose: an unidentified merchant carries no
|
|
123
|
+
// identity keys at all, rather than keys holding undefined.
|
|
124
|
+
...(identity ?? {}),
|
|
125
|
+
confirmedCount: 1,
|
|
126
|
+
lastConfirmedAt: charge.recordedAt,
|
|
127
|
+
charges: [charge],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const merchants = [...byUrl.values()];
|
|
131
|
+
for (const merchant of merchants) {
|
|
132
|
+
merchant.charges.sort((left, right) => (left.recordedAt < right.recordedAt ? 1 : -1));
|
|
133
|
+
}
|
|
134
|
+
merchants.sort((left, right) => (left.lastConfirmedAt < right.lastConfirmedAt ? 1 : -1));
|
|
135
|
+
return merchants;
|
|
136
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { createCliCheckoutEngine, type CliReviewInput, type CliReviewFacts, type CliPayInput, type CliReceiptFacts, type CliStartMandateInput, type CliMandateFacts, type CliEngineDeps, } from './cli-engine.js';
|
|
2
2
|
export { prepareCheckout, submitApprovedCheckout, runCheckout, InMemoryPreparedCheckoutStore, } from './executor.js';
|
|
3
3
|
export type { CheckoutResult, CheckoutReview, CheckoutOutcome } from './executor.js';
|
|
4
|
+
export { readConfirmedMerchants, type ConfirmedMerchant, type ConfirmedCharge, } from './confirmed-merchants.js';
|
|
5
|
+
export { KNOWN_MERCHANT_IDENTITIES, type MerchantIdentity } from './known-merchants.js';
|
|
6
|
+
export { RECEIPT_DIR } from './receipt-dir.js';
|
|
4
7
|
export { HostedApprovalDeclinedError, sanitizeApprovalIntent, APPROVAL_INTENT_MAX_CHARS, } from './hosted-approval.js';
|
|
5
8
|
export { createCardMandate, drawFromMandate, MandateDrawDeclinedError, DEFAULT_MANDATE_MAX_DRAWS, type CreateCardMandateInput, type CreateCardMandateDeps, type CardMandateFacts, type DrawFromMandateInput, type DrawFromMandateDeps, type DrawResult, type CardMandateMerchant, } from './mandate/card-mandate.js';
|
|
6
9
|
export { MandateLedger, remainingMinor, defaultLedgerPath, CARD_MANDATE_LEDGER_VERSION, type CardMandateRecord, type CardMandateLedgerFile, type CardMandateDraw, type CardMandateReservation, } from './mandate/mandate-ledger.js';
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// primitives are re-exported for direct/embedded use.
|
|
4
4
|
export { createCliCheckoutEngine, } from './cli-engine.js';
|
|
5
5
|
export { prepareCheckout, submitApprovedCheckout, runCheckout, InMemoryPreparedCheckoutStore, } from './executor.js';
|
|
6
|
+
export { readConfirmedMerchants, } from './confirmed-merchants.js';
|
|
7
|
+
export { KNOWN_MERCHANT_IDENTITIES } from './known-merchants.js';
|
|
8
|
+
export { RECEIPT_DIR } from './receipt-dir.js';
|
|
6
9
|
export { HostedApprovalDeclinedError, sanitizeApprovalIntent, APPROVAL_INTENT_MAX_CHARS, } from './hosted-approval.js';
|
|
7
10
|
export { createCardMandate, drawFromMandate, MandateDrawDeclinedError, DEFAULT_MANDATE_MAX_DRAWS, } from './mandate/card-mandate.js';
|
|
8
11
|
export { MandateLedger, remainingMinor, defaultLedgerPath, CARD_MANDATE_LEDGER_VERSION, } from './mandate/mandate-ledger.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type MerchantIdentity = {
|
|
2
|
+
/** Human-recognizable merchant name. */
|
|
3
|
+
name: string;
|
|
4
|
+
/** What the merchant is, e.g. "charity (animal rescue)". */
|
|
5
|
+
category: string;
|
|
6
|
+
/** The merchant's own site, when known — not the checkout URL. */
|
|
7
|
+
website?: string;
|
|
8
|
+
};
|
|
9
|
+
/** Checkout URL → who is actually behind it. Confirmed live 2026-07/2026-08. */
|
|
10
|
+
export declare const KNOWN_MERCHANT_IDENTITIES: Readonly<Record<string, MerchantIdentity>>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Curated identities for checkout URLs the registry cannot name on its own.
|
|
2
|
+
//
|
|
3
|
+
// Hosted payment links are anonymous by construction: the page host is the
|
|
4
|
+
// PSP's (donate.stripe.com, buy.stripe.com), the path is an opaque token, and
|
|
5
|
+
// the receipt records nothing else. So a registry row for a completed payment
|
|
6
|
+
// link answers "where can I use my card?" with a string no human recognizes.
|
|
7
|
+
// This map carries the once-per-link human identification, keyed by the exact
|
|
8
|
+
// checkout URL the receipts group on.
|
|
9
|
+
//
|
|
10
|
+
// Curation rule: an entry is added only after a checkout at that URL completed
|
|
11
|
+
// for real (engine outcome 'confirmed' + VIC APPROVED — the same admission rule
|
|
12
|
+
// confirmed-merchants.ts enforces) and the merchant behind the link was
|
|
13
|
+
// identified by a person. The map never creates registry rows; it only names
|
|
14
|
+
// rows the device's own receipts already earned. A URL missing here still
|
|
15
|
+
// lists — it just shows as its host.
|
|
16
|
+
/** Checkout URL → who is actually behind it. Confirmed live 2026-07/2026-08. */
|
|
17
|
+
export const KNOWN_MERCHANT_IDENTITIES = {
|
|
18
|
+
'https://donate.stripe.com/3cscPcaqY20H8ta4gh': {
|
|
19
|
+
name: 'NC Seaside Animal Rescue',
|
|
20
|
+
category: 'charity (animal rescue, NC nonprofit)',
|
|
21
|
+
website: 'https://www.ncseasideanimalrescue.org/make-a-donation',
|
|
22
|
+
},
|
|
23
|
+
'https://donate.stripe.com/14k3ei9TYgwFclq145': {
|
|
24
|
+
name: 'FreeCAD',
|
|
25
|
+
category: 'open-source project donation (CAD software)',
|
|
26
|
+
website: 'https://www.freecad.org',
|
|
27
|
+
},
|
|
28
|
+
'https://buy.stripe.com/dR6eWGaK41MX2YgaEF': {
|
|
29
|
+
name: 'scribepod',
|
|
30
|
+
category: 'indie creator tip jar (AI podcast project)',
|
|
31
|
+
website: 'https://github.com/yacineMTB/scribepod',
|
|
32
|
+
},
|
|
33
|
+
'https://donate.stripe.com/7sIcNj9Gs4WO6d29AA': {
|
|
34
|
+
name: 'n0bleKing GRAFFITI',
|
|
35
|
+
category: 'indie creator tip jar (pixel art and fonts)',
|
|
36
|
+
website: 'https://nobleking-graffiti.itch.io',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Where this device's checkout receipts live. Owned by its own module so the
|
|
5
|
+
* writer (cli-engine.ts) and the reader (confirmed-merchants.ts) can never
|
|
6
|
+
* drift onto two paths.
|
|
7
|
+
*/
|
|
8
|
+
export const RECEIPT_DIR = join(homedir(), '.visa-mcp', 'checkout-receipts');
|