@agentwonderland/mcp 0.1.49 → 0.1.51
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/dist/core/__tests__/payments.test.js +26 -5
- package/dist/core/config.d.ts +22 -0
- package/dist/core/config.js +20 -0
- package/dist/core/link-cli.js +97 -25
- package/dist/core/payments.js +20 -6
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/tools/__tests__/wallet.test.js +3 -0
- package/dist/tools/wallet.js +7 -1
- package/package.json +1 -1
- package/src/core/__tests__/payments.test.ts +39 -5
- package/src/core/config.ts +46 -0
- package/src/core/link-cli.ts +114 -28
- package/src/core/payments.ts +21 -6
- package/src/core/version.ts +1 -1
- package/src/tools/__tests__/wallet.test.ts +9 -0
- package/src/tools/wallet.ts +7 -0
|
@@ -21,6 +21,7 @@ vi.mock("../config.js", () => ({
|
|
|
21
21
|
getApiUrl: () => "http://api.test",
|
|
22
22
|
getCardConfig: () => currentCard,
|
|
23
23
|
getLinkConfig: () => currentLink,
|
|
24
|
+
getLinkCooldown: () => null,
|
|
24
25
|
getConfig: () => ({ defaultPaymentMethod: currentDefaultPaymentMethod }),
|
|
25
26
|
getDefaultWallet: () => currentDefaultWallet,
|
|
26
27
|
getWallets: () => currentWallets,
|
|
@@ -124,16 +125,36 @@ describe("payment method initialization", () => {
|
|
|
124
125
|
networkId: "profile_test",
|
|
125
126
|
});
|
|
126
127
|
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
127
|
-
amount: "
|
|
128
|
+
amount: "25",
|
|
128
129
|
currency: "usd",
|
|
129
130
|
networkId: "profile_test",
|
|
130
131
|
paymentMethodId: "csmrpd_link_123",
|
|
131
132
|
}));
|
|
132
133
|
const linkTokenRequest = mockCreateLinkSharedPaymentToken.mock.calls[0]?.[0];
|
|
133
|
-
expect(linkTokenRequest?.context).toContain("up to USD
|
|
134
|
+
expect(linkTokenRequest?.context).toContain("up to USD 0.25");
|
|
134
135
|
expect(linkTokenRequest?.context).toContain("quoted at USD 0.25");
|
|
135
136
|
});
|
|
136
|
-
it("
|
|
137
|
+
it("uses an explicit Link approval override when configured", async () => {
|
|
138
|
+
process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "2000";
|
|
139
|
+
currentLink = {
|
|
140
|
+
paymentMethodId: "csmrpd_link_123",
|
|
141
|
+
label: "Visa ****4242",
|
|
142
|
+
};
|
|
143
|
+
currentDefaultPaymentMethod = "link";
|
|
144
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
145
|
+
await getPaymentFetch("link");
|
|
146
|
+
const stripeConfig = mockStripe.mock.calls[0]?.[0];
|
|
147
|
+
await stripeConfig.createToken({
|
|
148
|
+
amount: "25",
|
|
149
|
+
currency: "usd",
|
|
150
|
+
expiresAt: 1778290000,
|
|
151
|
+
networkId: "profile_test",
|
|
152
|
+
});
|
|
153
|
+
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
154
|
+
amount: "2000",
|
|
155
|
+
}));
|
|
156
|
+
});
|
|
157
|
+
it("allows a low env override while never approving below the quoted amount", async () => {
|
|
137
158
|
process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "10";
|
|
138
159
|
currentLink = {
|
|
139
160
|
paymentMethodId: "csmrpd_link_123",
|
|
@@ -143,13 +164,13 @@ describe("payment method initialization", () => {
|
|
|
143
164
|
await getPaymentFetch("link");
|
|
144
165
|
const stripeConfig = mockStripe.mock.calls[0]?.[0];
|
|
145
166
|
await stripeConfig.createToken({
|
|
146
|
-
amount: "
|
|
167
|
+
amount: "25",
|
|
147
168
|
currency: "usd",
|
|
148
169
|
expiresAt: 1778290000,
|
|
149
170
|
networkId: "profile_test",
|
|
150
171
|
});
|
|
151
172
|
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
152
|
-
amount: "
|
|
173
|
+
amount: "25",
|
|
153
174
|
}));
|
|
154
175
|
});
|
|
155
176
|
it("advertises Link as Stripe SPT compatibility", async () => {
|
package/dist/core/config.d.ts
CHANGED
|
@@ -22,6 +22,22 @@ export interface PendingLinkSetup {
|
|
|
22
22
|
phrase: string;
|
|
23
23
|
createdAt: string;
|
|
24
24
|
}
|
|
25
|
+
export interface PendingLinkSpendRequest {
|
|
26
|
+
id: string;
|
|
27
|
+
approvalUrl?: string;
|
|
28
|
+
amount: string;
|
|
29
|
+
currency: string;
|
|
30
|
+
context: string;
|
|
31
|
+
expiresAt: number;
|
|
32
|
+
networkId: string;
|
|
33
|
+
paymentMethodId: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
}
|
|
36
|
+
export interface LinkCooldown {
|
|
37
|
+
reason: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
blockedUntil: string;
|
|
40
|
+
}
|
|
25
41
|
export interface SpendPolicy {
|
|
26
42
|
maxPerTxUsd?: number;
|
|
27
43
|
maxPerDayUsd?: number;
|
|
@@ -43,6 +59,8 @@ export interface Config {
|
|
|
43
59
|
card: CardConfig | null;
|
|
44
60
|
link: LinkConfig | null;
|
|
45
61
|
pendingLinkSetup?: PendingLinkSetup | null;
|
|
62
|
+
pendingLinkSpendRequest?: PendingLinkSpendRequest | null;
|
|
63
|
+
linkCooldown?: LinkCooldown | null;
|
|
46
64
|
pendingCardSetupToken?: string | null;
|
|
47
65
|
favorites: string[];
|
|
48
66
|
/** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
|
|
@@ -103,6 +121,10 @@ export declare function removeWallet(id: string): void;
|
|
|
103
121
|
export declare function getCardConfig(): CardConfig | null;
|
|
104
122
|
export declare function getLinkConfig(): LinkConfig | null;
|
|
105
123
|
export declare function getPendingLinkSetup(): PendingLinkSetup | null;
|
|
124
|
+
export declare function getPendingLinkSpendRequest(): PendingLinkSpendRequest | null;
|
|
125
|
+
export declare function setPendingLinkSpendRequest(pendingLinkSpendRequest: PendingLinkSpendRequest | null): void;
|
|
126
|
+
export declare function getLinkCooldown(): LinkCooldown | null;
|
|
127
|
+
export declare function setLinkCooldown(linkCooldown: LinkCooldown | null): void;
|
|
106
128
|
/**
|
|
107
129
|
* Save card configuration after setup.
|
|
108
130
|
*/
|
package/dist/core/config.js
CHANGED
|
@@ -38,6 +38,8 @@ function migrateIfNeeded(raw) {
|
|
|
38
38
|
card: raw.card ?? null,
|
|
39
39
|
link: raw.link ?? null,
|
|
40
40
|
pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
|
|
41
|
+
pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
|
|
42
|
+
linkCooldown: raw.linkCooldown ?? null,
|
|
41
43
|
pendingCardSetupToken: r.pendingCardSetupToken ?? null,
|
|
42
44
|
favorites: r.favorites ?? [],
|
|
43
45
|
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
@@ -113,6 +115,8 @@ function migrateIfNeeded(raw) {
|
|
|
113
115
|
card,
|
|
114
116
|
link: raw.link ?? null,
|
|
115
117
|
pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
|
|
118
|
+
pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
|
|
119
|
+
linkCooldown: raw.linkCooldown ?? null,
|
|
116
120
|
pendingCardSetupToken: null,
|
|
117
121
|
favorites: [],
|
|
118
122
|
confirmBeforeSpend: true,
|
|
@@ -138,6 +142,8 @@ export function getConfig() {
|
|
|
138
142
|
card: null,
|
|
139
143
|
link: null,
|
|
140
144
|
pendingLinkSetup: null,
|
|
145
|
+
pendingLinkSpendRequest: null,
|
|
146
|
+
linkCooldown: null,
|
|
141
147
|
pendingCardSetupToken: null,
|
|
142
148
|
favorites: [],
|
|
143
149
|
confirmBeforeSpend: true,
|
|
@@ -301,6 +307,18 @@ export function getLinkConfig() {
|
|
|
301
307
|
export function getPendingLinkSetup() {
|
|
302
308
|
return getConfig().pendingLinkSetup ?? null;
|
|
303
309
|
}
|
|
310
|
+
export function getPendingLinkSpendRequest() {
|
|
311
|
+
return getConfig().pendingLinkSpendRequest ?? null;
|
|
312
|
+
}
|
|
313
|
+
export function setPendingLinkSpendRequest(pendingLinkSpendRequest) {
|
|
314
|
+
saveConfig({ pendingLinkSpendRequest });
|
|
315
|
+
}
|
|
316
|
+
export function getLinkCooldown() {
|
|
317
|
+
return getConfig().linkCooldown ?? null;
|
|
318
|
+
}
|
|
319
|
+
export function setLinkCooldown(linkCooldown) {
|
|
320
|
+
saveConfig({ linkCooldown });
|
|
321
|
+
}
|
|
304
322
|
/**
|
|
305
323
|
* Save card configuration after setup.
|
|
306
324
|
*/
|
|
@@ -330,12 +348,14 @@ export function setLinkConfig(link) {
|
|
|
330
348
|
link,
|
|
331
349
|
defaultPaymentMethod: "link",
|
|
332
350
|
pendingLinkSetup: null,
|
|
351
|
+
pendingLinkSpendRequest: null,
|
|
333
352
|
});
|
|
334
353
|
}
|
|
335
354
|
else {
|
|
336
355
|
saveConfig({
|
|
337
356
|
link,
|
|
338
357
|
pendingLinkSetup: null,
|
|
358
|
+
pendingLinkSpendRequest: null,
|
|
339
359
|
defaultPaymentMethod: current.defaultPaymentMethod === "link"
|
|
340
360
|
? undefined
|
|
341
361
|
: current.defaultPaymentMethod,
|
package/dist/core/link-cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { getPendingLinkSpendRequest, setLinkCooldown, setPendingLinkSpendRequest, } from "./config.js";
|
|
3
4
|
const execFileAsync = promisify(execFile);
|
|
4
5
|
const LINK_CLI_PACKAGE = "@stripe/link-cli";
|
|
5
6
|
const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -92,6 +93,34 @@ function extractSpendRequestApproval(output) {
|
|
|
92
93
|
}
|
|
93
94
|
return null;
|
|
94
95
|
}
|
|
96
|
+
function isMatchingPendingSpendRequest(pending, params) {
|
|
97
|
+
if (!pending)
|
|
98
|
+
return false;
|
|
99
|
+
if (pending.amount !== params.amount)
|
|
100
|
+
return false;
|
|
101
|
+
if (pending.currency.toLowerCase() !== params.currency.toLowerCase())
|
|
102
|
+
return false;
|
|
103
|
+
if (pending.context !== params.context)
|
|
104
|
+
return false;
|
|
105
|
+
if (pending.networkId !== params.networkId)
|
|
106
|
+
return false;
|
|
107
|
+
if (pending.paymentMethodId !== params.paymentMethodId)
|
|
108
|
+
return false;
|
|
109
|
+
if (pending.expiresAt <= Math.floor(Date.now() / 1000))
|
|
110
|
+
return false;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function isProjectedSpendCapError(message) {
|
|
114
|
+
return /projected daily spend|projected spend|exceeds limit/i.test(message);
|
|
115
|
+
}
|
|
116
|
+
function recordLinkCooldown(reason) {
|
|
117
|
+
const now = new Date();
|
|
118
|
+
setLinkCooldown({
|
|
119
|
+
reason,
|
|
120
|
+
createdAt: now.toISOString(),
|
|
121
|
+
blockedUntil: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
95
124
|
function normalizePaymentMethods(output) {
|
|
96
125
|
const values = Array.isArray(output)
|
|
97
126
|
? output
|
|
@@ -181,6 +210,22 @@ export async function listLinkPaymentMethods() {
|
|
|
181
210
|
return normalizePaymentMethods(output);
|
|
182
211
|
}
|
|
183
212
|
export async function createLinkSharedPaymentToken(params) {
|
|
213
|
+
const existing = getPendingLinkSpendRequest();
|
|
214
|
+
if (isMatchingPendingSpendRequest(existing, params)) {
|
|
215
|
+
try {
|
|
216
|
+
console.error(`Resuming pending Link approval: ${existing.id}`);
|
|
217
|
+
const spt = await retrieveSharedPaymentToken(existing.id, existing.approvalUrl);
|
|
218
|
+
setPendingLinkSpendRequest(null);
|
|
219
|
+
return spt;
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
223
|
+
if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
setPendingLinkSpendRequest(null);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
184
229
|
const args = [
|
|
185
230
|
"spend-request",
|
|
186
231
|
"create",
|
|
@@ -207,6 +252,16 @@ export async function createLinkSharedPaymentToken(params) {
|
|
|
207
252
|
}
|
|
208
253
|
catch (err) {
|
|
209
254
|
const message = err instanceof Error ? err.message : String(err);
|
|
255
|
+
if (isProjectedSpendCapError(message)) {
|
|
256
|
+
recordLinkCooldown(message);
|
|
257
|
+
throw new Error([
|
|
258
|
+
"Link is temporarily blocked by Stripe's projected-spend cap.",
|
|
259
|
+
"Reauthing Link or switching cards in the same Link account will not fix this.",
|
|
260
|
+
"Use USDC for now, wait for the rolling Link window to clear, or ask Stripe to raise/clear the merchant projected-spend cap.",
|
|
261
|
+
"",
|
|
262
|
+
message,
|
|
263
|
+
].join("\n"));
|
|
264
|
+
}
|
|
210
265
|
if (/invalid network_id|could not retrieve merchant information/i.test(message)) {
|
|
211
266
|
throw new Error([
|
|
212
267
|
message,
|
|
@@ -219,41 +274,58 @@ export async function createLinkSharedPaymentToken(params) {
|
|
|
219
274
|
}
|
|
220
275
|
const spt = extractSharedPaymentToken(output);
|
|
221
276
|
if (spt) {
|
|
277
|
+
setPendingLinkSpendRequest(null);
|
|
222
278
|
return spt;
|
|
223
279
|
}
|
|
224
280
|
const approval = extractSpendRequestApproval(output);
|
|
225
281
|
if (approval?.id && approval.status === "pending_approval") {
|
|
282
|
+
setPendingLinkSpendRequest({
|
|
283
|
+
id: approval.id,
|
|
284
|
+
approvalUrl: approval.approvalUrl,
|
|
285
|
+
amount: params.amount,
|
|
286
|
+
currency: params.currency,
|
|
287
|
+
context: params.context,
|
|
288
|
+
expiresAt: params.expiresAt,
|
|
289
|
+
networkId: params.networkId,
|
|
290
|
+
paymentMethodId: params.paymentMethodId,
|
|
291
|
+
createdAt: new Date().toISOString(),
|
|
292
|
+
});
|
|
226
293
|
if (approval.approvalUrl) {
|
|
227
294
|
console.error(`Link approval required: ${approval.approvalUrl}`);
|
|
228
295
|
}
|
|
229
|
-
|
|
296
|
+
const retrievedSpt = await retrieveSharedPaymentToken(approval.id, approval.approvalUrl);
|
|
297
|
+
setPendingLinkSpendRequest(null);
|
|
298
|
+
return retrievedSpt;
|
|
299
|
+
}
|
|
300
|
+
{
|
|
301
|
+
throw new Error("Link spend request completed without a shared payment token in the CLI response.");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function retrieveSharedPaymentToken(spendRequestId, approvalUrl) {
|
|
305
|
+
let retrieved = await runLinkCli([
|
|
306
|
+
"spend-request",
|
|
307
|
+
"retrieve",
|
|
308
|
+
spendRequestId,
|
|
309
|
+
"--interval",
|
|
310
|
+
"2",
|
|
311
|
+
"--max-attempts",
|
|
312
|
+
"150",
|
|
313
|
+
]);
|
|
314
|
+
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
315
|
+
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
316
|
+
await sleep(2_000);
|
|
317
|
+
retrieved = await runLinkCli([
|
|
230
318
|
"spend-request",
|
|
231
319
|
"retrieve",
|
|
232
|
-
|
|
233
|
-
"--interval",
|
|
234
|
-
"2",
|
|
235
|
-
"--max-attempts",
|
|
236
|
-
"150",
|
|
320
|
+
spendRequestId,
|
|
237
321
|
]);
|
|
238
|
-
|
|
239
|
-
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
240
|
-
await sleep(2_000);
|
|
241
|
-
retrieved = await runLinkCli([
|
|
242
|
-
"spend-request",
|
|
243
|
-
"retrieve",
|
|
244
|
-
approval.id,
|
|
245
|
-
]);
|
|
246
|
-
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
247
|
-
}
|
|
248
|
-
if (retrievedSpt) {
|
|
249
|
-
return retrievedSpt;
|
|
250
|
-
}
|
|
251
|
-
throw new Error([
|
|
252
|
-
"Link spend request finished without a shared payment token.",
|
|
253
|
-
approval.approvalUrl ? `Approval URL: ${approval.approvalUrl}` : undefined,
|
|
254
|
-
].filter(Boolean).join("\n"));
|
|
322
|
+
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
255
323
|
}
|
|
256
|
-
{
|
|
257
|
-
|
|
324
|
+
if (retrievedSpt) {
|
|
325
|
+
return retrievedSpt;
|
|
258
326
|
}
|
|
327
|
+
throw new Error([
|
|
328
|
+
"Link spend request finished without a shared payment token.",
|
|
329
|
+
approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
|
|
330
|
+
].filter(Boolean).join("\n"));
|
|
259
331
|
}
|
package/dist/core/payments.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Users can configure multiple wallets with different chains and select
|
|
10
10
|
* which to use per-request via `--pay-with <wallet-id|chain|card>`.
|
|
11
11
|
*/
|
|
12
|
-
import { getConfig, getWallets, getDefaultWallet, getCardConfig, getLinkConfig, resolveWalletAndChain, getApiUrl, } from "./config.js";
|
|
12
|
+
import { getConfig, getWallets, getDefaultWallet, getCardConfig, getLinkConfig, getLinkCooldown, resolveWalletAndChain, getApiUrl, } from "./config.js";
|
|
13
13
|
// Feature flag: disable card payment for launch. Flip to true once Stripe
|
|
14
14
|
// approves the live SPT issuer. Config/card state is still persisted so this
|
|
15
15
|
// can be re-enabled without reconfiguring wallets.
|
|
@@ -26,7 +26,6 @@ const REGISTRY_METHOD_MAP = {
|
|
|
26
26
|
card: "stripe_card",
|
|
27
27
|
link: "stripe_card",
|
|
28
28
|
};
|
|
29
|
-
const DEFAULT_LINK_APPROVAL_LIMIT_CENTS = 10_000;
|
|
30
29
|
const ACCEPTED_PAYMENT_ALIASES = {
|
|
31
30
|
tempo: ["tempo_usdc", "tempo"],
|
|
32
31
|
base: ["base_usdc", "base"],
|
|
@@ -164,10 +163,24 @@ function formatMinorCurrencyAmount(currency, amount) {
|
|
|
164
163
|
function getLinkApprovalLimitAmount(actualAmount) {
|
|
165
164
|
const actualAmountCents = Number(actualAmount);
|
|
166
165
|
const configuredLimit = Number(process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS);
|
|
167
|
-
const
|
|
168
|
-
? Math.max(Math.floor(configuredLimit)
|
|
169
|
-
:
|
|
170
|
-
return String(
|
|
166
|
+
const approvalLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
|
|
167
|
+
? Math.max(actualAmountCents, Math.floor(configuredLimit))
|
|
168
|
+
: actualAmountCents;
|
|
169
|
+
return String(approvalLimit);
|
|
170
|
+
}
|
|
171
|
+
function assertLinkNotCoolingDown() {
|
|
172
|
+
const cooldown = getLinkCooldown();
|
|
173
|
+
if (!cooldown)
|
|
174
|
+
return;
|
|
175
|
+
const blockedUntil = Date.parse(cooldown.blockedUntil);
|
|
176
|
+
if (!Number.isFinite(blockedUntil) || blockedUntil <= Date.now())
|
|
177
|
+
return;
|
|
178
|
+
throw new Error([
|
|
179
|
+
"Link is temporarily blocked by Stripe's projected-spend cap.",
|
|
180
|
+
"Reauthing Link or switching cards in the same Link account will not fix this.",
|
|
181
|
+
`Try again after ${cooldown.blockedUntil}, use USDC, or ask Stripe to raise/clear the merchant projected-spend cap.`,
|
|
182
|
+
`Last Link error: ${cooldown.reason}`,
|
|
183
|
+
].join("\n"));
|
|
171
184
|
}
|
|
172
185
|
function buildLinkApprovalContext(params) {
|
|
173
186
|
const amountText = formatMinorCurrencyAmount(params.currency, params.amount);
|
|
@@ -188,6 +201,7 @@ async function initLink() {
|
|
|
188
201
|
methods: [stripe({
|
|
189
202
|
paymentMethod: linkConfig.paymentMethodId,
|
|
190
203
|
createToken: async (params) => {
|
|
204
|
+
assertLinkNotCoolingDown();
|
|
191
205
|
const approvalAmount = getLinkApprovalLimitAmount(params.amount);
|
|
192
206
|
console.error(`Requesting Link approval up to ${formatMinorCurrencyAmount(params.currency, approvalAmount)} ` +
|
|
193
207
|
`for this ${formatMinorCurrencyAmount(params.currency, params.amount)} Agent Wonderland run.`);
|
package/dist/core/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export declare const MCP_PACKAGE_VERSION = "0.1.51";
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export const MCP_PACKAGE_VERSION = "0.1.51";
|
|
@@ -5,6 +5,7 @@ const state = vi.hoisted(() => ({
|
|
|
5
5
|
card: null,
|
|
6
6
|
link: null,
|
|
7
7
|
pendingLinkSetup: null,
|
|
8
|
+
linkCooldown: null,
|
|
8
9
|
pendingCardSetupToken: null,
|
|
9
10
|
spendPolicies: {},
|
|
10
11
|
defaultPaymentMethod: undefined,
|
|
@@ -53,6 +54,7 @@ vi.mock("../../core/config.js", () => ({
|
|
|
53
54
|
},
|
|
54
55
|
getCardConfig: () => state.card,
|
|
55
56
|
getLinkConfig: () => state.link,
|
|
57
|
+
getLinkCooldown: () => state.linkCooldown,
|
|
56
58
|
getPendingLinkSetup: () => state.pendingLinkSetup,
|
|
57
59
|
getPendingCardSetupToken: () => state.pendingCardSetupToken,
|
|
58
60
|
getSpendPolicy: (walletId) => state.spendPolicies[walletId] ?? null,
|
|
@@ -130,6 +132,7 @@ function resetState() {
|
|
|
130
132
|
state.card = null;
|
|
131
133
|
state.link = null;
|
|
132
134
|
state.pendingLinkSetup = null;
|
|
135
|
+
state.linkCooldown = null;
|
|
133
136
|
state.pendingCardSetupToken = null;
|
|
134
137
|
state.spendPolicies = {};
|
|
135
138
|
state.defaultPaymentMethod = undefined;
|
package/dist/tools/wallet.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getWallets, getCardConfig, getLinkConfig, getPendingLinkSetup, setPendingLinkSetup, setLinkConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setDefaultPaymentMethod, setSpendPolicy, } from "../core/config.js";
|
|
2
|
+
import { getWallets, getCardConfig, getLinkConfig, getLinkCooldown, getPendingLinkSetup, setPendingLinkSetup, setLinkConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setDefaultPaymentMethod, setSpendPolicy, } from "../core/config.js";
|
|
3
3
|
import { getWalletAddress, isCardPaymentEnabled } from "../core/payments.js";
|
|
4
4
|
import { fetchUsdcBalance } from "../core/balances.js";
|
|
5
5
|
import { getSettings } from "../core/settings.js";
|
|
@@ -110,6 +110,12 @@ export function registerWalletTools(server) {
|
|
|
110
110
|
lines.push(auth.authenticated
|
|
111
111
|
? " Link CLI: authenticated"
|
|
112
112
|
: " Link CLI: not authenticated — run npx @stripe/link-cli auth login");
|
|
113
|
+
const cooldown = getLinkCooldown();
|
|
114
|
+
const blockedUntil = cooldown ? Date.parse(cooldown.blockedUntil) : NaN;
|
|
115
|
+
if (cooldown && Number.isFinite(blockedUntil) && blockedUntil > Date.now()) {
|
|
116
|
+
lines.push(` Link status: temporarily blocked by Stripe projected-spend cap until ${cooldown.blockedUntil}`);
|
|
117
|
+
lines.push(" Link note: reauthing or switching cards in the same Link account will not clear this cap.");
|
|
118
|
+
}
|
|
113
119
|
}
|
|
114
120
|
if (pendingCardSetupToken) {
|
|
115
121
|
lines.push(" Card setup: pending confirmation");
|
package/package.json
CHANGED
|
@@ -26,6 +26,7 @@ vi.mock("../config.js", () => ({
|
|
|
26
26
|
getApiUrl: () => "http://api.test",
|
|
27
27
|
getCardConfig: () => currentCard,
|
|
28
28
|
getLinkConfig: () => currentLink,
|
|
29
|
+
getLinkCooldown: () => null,
|
|
29
30
|
getConfig: () => ({ defaultPaymentMethod: currentDefaultPaymentMethod }),
|
|
30
31
|
getDefaultWallet: () => currentDefaultWallet,
|
|
31
32
|
getWallets: () => currentWallets,
|
|
@@ -166,18 +167,51 @@ describe("payment method initialization", () => {
|
|
|
166
167
|
|
|
167
168
|
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(
|
|
168
169
|
expect.objectContaining({
|
|
169
|
-
amount: "
|
|
170
|
+
amount: "25",
|
|
170
171
|
currency: "usd",
|
|
171
172
|
networkId: "profile_test",
|
|
172
173
|
paymentMethodId: "csmrpd_link_123",
|
|
173
174
|
}),
|
|
174
175
|
);
|
|
175
176
|
const linkTokenRequest = mockCreateLinkSharedPaymentToken.mock.calls[0]?.[0] as { context: string } | undefined;
|
|
176
|
-
expect(linkTokenRequest?.context).toContain("up to USD
|
|
177
|
+
expect(linkTokenRequest?.context).toContain("up to USD 0.25");
|
|
177
178
|
expect(linkTokenRequest?.context).toContain("quoted at USD 0.25");
|
|
178
179
|
});
|
|
179
180
|
|
|
180
|
-
it("
|
|
181
|
+
it("uses an explicit Link approval override when configured", async () => {
|
|
182
|
+
process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "2000";
|
|
183
|
+
currentLink = {
|
|
184
|
+
paymentMethodId: "csmrpd_link_123",
|
|
185
|
+
label: "Visa ****4242",
|
|
186
|
+
};
|
|
187
|
+
currentDefaultPaymentMethod = "link";
|
|
188
|
+
|
|
189
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
190
|
+
await getPaymentFetch("link");
|
|
191
|
+
|
|
192
|
+
const stripeConfig = mockStripe.mock.calls[0]?.[0] as {
|
|
193
|
+
createToken: (params: {
|
|
194
|
+
amount: string;
|
|
195
|
+
currency: string;
|
|
196
|
+
expiresAt: number;
|
|
197
|
+
networkId: string;
|
|
198
|
+
}) => Promise<string>;
|
|
199
|
+
};
|
|
200
|
+
await stripeConfig.createToken({
|
|
201
|
+
amount: "25",
|
|
202
|
+
currency: "usd",
|
|
203
|
+
expiresAt: 1778290000,
|
|
204
|
+
networkId: "profile_test",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
amount: "2000",
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("allows a low env override while never approving below the quoted amount", async () => {
|
|
181
215
|
process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "10";
|
|
182
216
|
currentLink = {
|
|
183
217
|
paymentMethodId: "csmrpd_link_123",
|
|
@@ -196,7 +230,7 @@ describe("payment method initialization", () => {
|
|
|
196
230
|
}) => Promise<string>;
|
|
197
231
|
};
|
|
198
232
|
await stripeConfig.createToken({
|
|
199
|
-
amount: "
|
|
233
|
+
amount: "25",
|
|
200
234
|
currency: "usd",
|
|
201
235
|
expiresAt: 1778290000,
|
|
202
236
|
networkId: "profile_test",
|
|
@@ -204,7 +238,7 @@ describe("payment method initialization", () => {
|
|
|
204
238
|
|
|
205
239
|
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(
|
|
206
240
|
expect.objectContaining({
|
|
207
|
-
amount: "
|
|
241
|
+
amount: "25",
|
|
208
242
|
}),
|
|
209
243
|
);
|
|
210
244
|
});
|
package/src/core/config.ts
CHANGED
|
@@ -32,6 +32,24 @@ export interface PendingLinkSetup {
|
|
|
32
32
|
createdAt: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface PendingLinkSpendRequest {
|
|
36
|
+
id: string;
|
|
37
|
+
approvalUrl?: string;
|
|
38
|
+
amount: string;
|
|
39
|
+
currency: string;
|
|
40
|
+
context: string;
|
|
41
|
+
expiresAt: number;
|
|
42
|
+
networkId: string;
|
|
43
|
+
paymentMethodId: string;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LinkCooldown {
|
|
48
|
+
reason: string;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
blockedUntil: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
export interface SpendPolicy {
|
|
36
54
|
maxPerTxUsd?: number;
|
|
37
55
|
maxPerDayUsd?: number;
|
|
@@ -55,6 +73,8 @@ export interface Config {
|
|
|
55
73
|
card: CardConfig | null;
|
|
56
74
|
link: LinkConfig | null;
|
|
57
75
|
pendingLinkSetup?: PendingLinkSetup | null;
|
|
76
|
+
pendingLinkSpendRequest?: PendingLinkSpendRequest | null;
|
|
77
|
+
linkCooldown?: LinkCooldown | null;
|
|
58
78
|
pendingCardSetupToken?: string | null;
|
|
59
79
|
favorites: string[];
|
|
60
80
|
/** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
|
|
@@ -113,6 +133,8 @@ interface LegacyConfig {
|
|
|
113
133
|
card?: CardConfig | null;
|
|
114
134
|
link?: LinkConfig | null;
|
|
115
135
|
pendingLinkSetup?: PendingLinkSetup | null;
|
|
136
|
+
pendingLinkSpendRequest?: PendingLinkSpendRequest | null;
|
|
137
|
+
linkCooldown?: LinkCooldown | null;
|
|
116
138
|
pendingCardSetupToken?: string | null;
|
|
117
139
|
spendPolicies?: Record<string, SpendPolicy>;
|
|
118
140
|
spendLedger?: SpendLedgerEntry[];
|
|
@@ -136,6 +158,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
136
158
|
card: raw.card ?? null,
|
|
137
159
|
link: raw.link ?? null,
|
|
138
160
|
pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
|
|
161
|
+
pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
|
|
162
|
+
linkCooldown: raw.linkCooldown ?? null,
|
|
139
163
|
pendingCardSetupToken: (r.pendingCardSetupToken as string | null | undefined) ?? null,
|
|
140
164
|
favorites: r.favorites as string[] ?? [],
|
|
141
165
|
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
@@ -214,6 +238,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
214
238
|
card,
|
|
215
239
|
link: raw.link ?? null,
|
|
216
240
|
pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
|
|
241
|
+
pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
|
|
242
|
+
linkCooldown: raw.linkCooldown ?? null,
|
|
217
243
|
pendingCardSetupToken: null,
|
|
218
244
|
favorites: [],
|
|
219
245
|
confirmBeforeSpend: true,
|
|
@@ -243,6 +269,8 @@ export function getConfig(): Config {
|
|
|
243
269
|
card: null,
|
|
244
270
|
link: null,
|
|
245
271
|
pendingLinkSetup: null,
|
|
272
|
+
pendingLinkSpendRequest: null,
|
|
273
|
+
linkCooldown: null,
|
|
246
274
|
pendingCardSetupToken: null,
|
|
247
275
|
favorites: [],
|
|
248
276
|
confirmBeforeSpend: true,
|
|
@@ -432,6 +460,22 @@ export function getPendingLinkSetup(): PendingLinkSetup | null {
|
|
|
432
460
|
return getConfig().pendingLinkSetup ?? null;
|
|
433
461
|
}
|
|
434
462
|
|
|
463
|
+
export function getPendingLinkSpendRequest(): PendingLinkSpendRequest | null {
|
|
464
|
+
return getConfig().pendingLinkSpendRequest ?? null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function setPendingLinkSpendRequest(pendingLinkSpendRequest: PendingLinkSpendRequest | null): void {
|
|
468
|
+
saveConfig({ pendingLinkSpendRequest });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function getLinkCooldown(): LinkCooldown | null {
|
|
472
|
+
return getConfig().linkCooldown ?? null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function setLinkCooldown(linkCooldown: LinkCooldown | null): void {
|
|
476
|
+
saveConfig({ linkCooldown });
|
|
477
|
+
}
|
|
478
|
+
|
|
435
479
|
/**
|
|
436
480
|
* Save card configuration after setup.
|
|
437
481
|
*/
|
|
@@ -461,11 +505,13 @@ export function setLinkConfig(link: LinkConfig | null): void {
|
|
|
461
505
|
link,
|
|
462
506
|
defaultPaymentMethod: "link",
|
|
463
507
|
pendingLinkSetup: null,
|
|
508
|
+
pendingLinkSpendRequest: null,
|
|
464
509
|
});
|
|
465
510
|
} else {
|
|
466
511
|
saveConfig({
|
|
467
512
|
link,
|
|
468
513
|
pendingLinkSetup: null,
|
|
514
|
+
pendingLinkSpendRequest: null,
|
|
469
515
|
defaultPaymentMethod: current.defaultPaymentMethod === "link"
|
|
470
516
|
? undefined
|
|
471
517
|
: current.defaultPaymentMethod,
|
package/src/core/link-cli.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
getPendingLinkSpendRequest,
|
|
5
|
+
setLinkCooldown,
|
|
6
|
+
setPendingLinkSpendRequest,
|
|
7
|
+
type PendingLinkSpendRequest,
|
|
8
|
+
} from "./config.js";
|
|
3
9
|
|
|
4
10
|
const execFileAsync = promisify(execFile);
|
|
5
11
|
const LINK_CLI_PACKAGE = "@stripe/link-cli";
|
|
@@ -116,6 +122,40 @@ function extractSpendRequestApproval(output: unknown): { id: string; approvalUrl
|
|
|
116
122
|
return null;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
function isMatchingPendingSpendRequest(
|
|
126
|
+
pending: PendingLinkSpendRequest | null,
|
|
127
|
+
params: {
|
|
128
|
+
amount: string;
|
|
129
|
+
currency: string;
|
|
130
|
+
context: string;
|
|
131
|
+
expiresAt: number;
|
|
132
|
+
networkId: string;
|
|
133
|
+
paymentMethodId: string;
|
|
134
|
+
},
|
|
135
|
+
): pending is PendingLinkSpendRequest {
|
|
136
|
+
if (!pending) return false;
|
|
137
|
+
if (pending.amount !== params.amount) return false;
|
|
138
|
+
if (pending.currency.toLowerCase() !== params.currency.toLowerCase()) return false;
|
|
139
|
+
if (pending.context !== params.context) return false;
|
|
140
|
+
if (pending.networkId !== params.networkId) return false;
|
|
141
|
+
if (pending.paymentMethodId !== params.paymentMethodId) return false;
|
|
142
|
+
if (pending.expiresAt <= Math.floor(Date.now() / 1000)) return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isProjectedSpendCapError(message: string): boolean {
|
|
147
|
+
return /projected daily spend|projected spend|exceeds limit/i.test(message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function recordLinkCooldown(reason: string): void {
|
|
151
|
+
const now = new Date();
|
|
152
|
+
setLinkCooldown({
|
|
153
|
+
reason,
|
|
154
|
+
createdAt: now.toISOString(),
|
|
155
|
+
blockedUntil: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
119
159
|
function normalizePaymentMethods(output: unknown): LinkCliPaymentMethod[] {
|
|
120
160
|
const values = Array.isArray(output)
|
|
121
161
|
? output
|
|
@@ -215,6 +255,22 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
215
255
|
networkId: string;
|
|
216
256
|
paymentMethodId: string;
|
|
217
257
|
}): Promise<string> {
|
|
258
|
+
const existing = getPendingLinkSpendRequest();
|
|
259
|
+
if (isMatchingPendingSpendRequest(existing, params)) {
|
|
260
|
+
try {
|
|
261
|
+
console.error(`Resuming pending Link approval: ${existing.id}`);
|
|
262
|
+
const spt = await retrieveSharedPaymentToken(existing.id, existing.approvalUrl);
|
|
263
|
+
setPendingLinkSpendRequest(null);
|
|
264
|
+
return spt;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
267
|
+
if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
setPendingLinkSpendRequest(null);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
218
274
|
const args = [
|
|
219
275
|
"spend-request",
|
|
220
276
|
"create",
|
|
@@ -242,6 +298,18 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
242
298
|
output = await runLinkCli(args);
|
|
243
299
|
} catch (err) {
|
|
244
300
|
const message = err instanceof Error ? err.message : String(err);
|
|
301
|
+
if (isProjectedSpendCapError(message)) {
|
|
302
|
+
recordLinkCooldown(message);
|
|
303
|
+
throw new Error(
|
|
304
|
+
[
|
|
305
|
+
"Link is temporarily blocked by Stripe's projected-spend cap.",
|
|
306
|
+
"Reauthing Link or switching cards in the same Link account will not fix this.",
|
|
307
|
+
"Use USDC for now, wait for the rolling Link window to clear, or ask Stripe to raise/clear the merchant projected-spend cap.",
|
|
308
|
+
"",
|
|
309
|
+
message,
|
|
310
|
+
].join("\n"),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
245
313
|
if (/invalid network_id|could not retrieve merchant information/i.test(message)) {
|
|
246
314
|
throw new Error(
|
|
247
315
|
[
|
|
@@ -256,45 +324,63 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
256
324
|
}
|
|
257
325
|
const spt = extractSharedPaymentToken(output);
|
|
258
326
|
if (spt) {
|
|
327
|
+
setPendingLinkSpendRequest(null);
|
|
259
328
|
return spt;
|
|
260
329
|
}
|
|
261
330
|
|
|
262
331
|
const approval = extractSpendRequestApproval(output);
|
|
263
332
|
if (approval?.id && approval.status === "pending_approval") {
|
|
333
|
+
setPendingLinkSpendRequest({
|
|
334
|
+
id: approval.id,
|
|
335
|
+
approvalUrl: approval.approvalUrl,
|
|
336
|
+
amount: params.amount,
|
|
337
|
+
currency: params.currency,
|
|
338
|
+
context: params.context,
|
|
339
|
+
expiresAt: params.expiresAt,
|
|
340
|
+
networkId: params.networkId,
|
|
341
|
+
paymentMethodId: params.paymentMethodId,
|
|
342
|
+
createdAt: new Date().toISOString(),
|
|
343
|
+
});
|
|
264
344
|
if (approval.approvalUrl) {
|
|
265
345
|
console.error(`Link approval required: ${approval.approvalUrl}`);
|
|
266
346
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
approval.id,
|
|
271
|
-
"--interval",
|
|
272
|
-
"2",
|
|
273
|
-
"--max-attempts",
|
|
274
|
-
"150",
|
|
275
|
-
]);
|
|
276
|
-
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
277
|
-
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
278
|
-
await sleep(2_000);
|
|
279
|
-
retrieved = await runLinkCli([
|
|
280
|
-
"spend-request",
|
|
281
|
-
"retrieve",
|
|
282
|
-
approval.id,
|
|
283
|
-
]);
|
|
284
|
-
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
285
|
-
}
|
|
286
|
-
if (retrievedSpt) {
|
|
287
|
-
return retrievedSpt;
|
|
288
|
-
}
|
|
289
|
-
throw new Error(
|
|
290
|
-
[
|
|
291
|
-
"Link spend request finished without a shared payment token.",
|
|
292
|
-
approval.approvalUrl ? `Approval URL: ${approval.approvalUrl}` : undefined,
|
|
293
|
-
].filter(Boolean).join("\n"),
|
|
294
|
-
);
|
|
347
|
+
const retrievedSpt = await retrieveSharedPaymentToken(approval.id, approval.approvalUrl);
|
|
348
|
+
setPendingLinkSpendRequest(null);
|
|
349
|
+
return retrievedSpt;
|
|
295
350
|
}
|
|
296
351
|
|
|
297
352
|
{
|
|
298
353
|
throw new Error("Link spend request completed without a shared payment token in the CLI response.");
|
|
299
354
|
}
|
|
300
355
|
}
|
|
356
|
+
|
|
357
|
+
async function retrieveSharedPaymentToken(spendRequestId: string, approvalUrl?: string): Promise<string> {
|
|
358
|
+
let retrieved = await runLinkCli([
|
|
359
|
+
"spend-request",
|
|
360
|
+
"retrieve",
|
|
361
|
+
spendRequestId,
|
|
362
|
+
"--interval",
|
|
363
|
+
"2",
|
|
364
|
+
"--max-attempts",
|
|
365
|
+
"150",
|
|
366
|
+
]);
|
|
367
|
+
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
368
|
+
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
369
|
+
await sleep(2_000);
|
|
370
|
+
retrieved = await runLinkCli([
|
|
371
|
+
"spend-request",
|
|
372
|
+
"retrieve",
|
|
373
|
+
spendRequestId,
|
|
374
|
+
]);
|
|
375
|
+
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
376
|
+
}
|
|
377
|
+
if (retrievedSpt) {
|
|
378
|
+
return retrievedSpt;
|
|
379
|
+
}
|
|
380
|
+
throw new Error(
|
|
381
|
+
[
|
|
382
|
+
"Link spend request finished without a shared payment token.",
|
|
383
|
+
approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
|
|
384
|
+
].filter(Boolean).join("\n"),
|
|
385
|
+
);
|
|
386
|
+
}
|
package/src/core/payments.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getDefaultWallet,
|
|
17
17
|
getCardConfig,
|
|
18
18
|
getLinkConfig,
|
|
19
|
+
getLinkCooldown,
|
|
19
20
|
resolveWalletAndChain,
|
|
20
21
|
getApiUrl,
|
|
21
22
|
type WalletEntry,
|
|
@@ -41,8 +42,6 @@ const REGISTRY_METHOD_MAP: Record<string, string> = {
|
|
|
41
42
|
link: "stripe_card",
|
|
42
43
|
};
|
|
43
44
|
|
|
44
|
-
const DEFAULT_LINK_APPROVAL_LIMIT_CENTS = 10_000;
|
|
45
|
-
|
|
46
45
|
const ACCEPTED_PAYMENT_ALIASES: Record<string, string[]> = {
|
|
47
46
|
tempo: ["tempo_usdc", "tempo"],
|
|
48
47
|
base: ["base_usdc", "base"],
|
|
@@ -200,10 +199,25 @@ function formatMinorCurrencyAmount(currency: string, amount: string): string {
|
|
|
200
199
|
function getLinkApprovalLimitAmount(actualAmount: string): string {
|
|
201
200
|
const actualAmountCents = Number(actualAmount);
|
|
202
201
|
const configuredLimit = Number(process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS);
|
|
203
|
-
const
|
|
204
|
-
? Math.max(Math.floor(configuredLimit)
|
|
205
|
-
:
|
|
206
|
-
return String(
|
|
202
|
+
const approvalLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
|
|
203
|
+
? Math.max(actualAmountCents, Math.floor(configuredLimit))
|
|
204
|
+
: actualAmountCents;
|
|
205
|
+
return String(approvalLimit);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertLinkNotCoolingDown(): void {
|
|
209
|
+
const cooldown = getLinkCooldown();
|
|
210
|
+
if (!cooldown) return;
|
|
211
|
+
const blockedUntil = Date.parse(cooldown.blockedUntil);
|
|
212
|
+
if (!Number.isFinite(blockedUntil) || blockedUntil <= Date.now()) return;
|
|
213
|
+
throw new Error(
|
|
214
|
+
[
|
|
215
|
+
"Link is temporarily blocked by Stripe's projected-spend cap.",
|
|
216
|
+
"Reauthing Link or switching cards in the same Link account will not fix this.",
|
|
217
|
+
`Try again after ${cooldown.blockedUntil}, use USDC, or ask Stripe to raise/clear the merchant projected-spend cap.`,
|
|
218
|
+
`Last Link error: ${cooldown.reason}`,
|
|
219
|
+
].join("\n"),
|
|
220
|
+
);
|
|
207
221
|
}
|
|
208
222
|
|
|
209
223
|
function buildLinkApprovalContext(params: {
|
|
@@ -239,6 +253,7 @@ async function initLink(): Promise<typeof fetch | null> {
|
|
|
239
253
|
expiresAt: number;
|
|
240
254
|
metadata?: Record<string, string>;
|
|
241
255
|
}) => {
|
|
256
|
+
assertLinkNotCoolingDown();
|
|
242
257
|
const approvalAmount = getLinkApprovalLimitAmount(params.amount);
|
|
243
258
|
console.error(
|
|
244
259
|
`Requesting Link approval up to ${formatMinorCurrencyAmount(params.currency, approvalAmount)} ` +
|
package/src/core/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export const MCP_PACKAGE_VERSION = "0.1.51";
|
|
@@ -29,6 +29,12 @@ type PendingLinkSetup = {
|
|
|
29
29
|
createdAt: string;
|
|
30
30
|
} | null;
|
|
31
31
|
|
|
32
|
+
type LinkCooldown = {
|
|
33
|
+
reason: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
blockedUntil: string;
|
|
36
|
+
} | null;
|
|
37
|
+
|
|
32
38
|
type WalletToolResult = {
|
|
33
39
|
content: Array<{ type: "text"; text: string }>;
|
|
34
40
|
};
|
|
@@ -39,6 +45,7 @@ const state = vi.hoisted(() => ({
|
|
|
39
45
|
card: null as CardConfig,
|
|
40
46
|
link: null as LinkConfig,
|
|
41
47
|
pendingLinkSetup: null as PendingLinkSetup,
|
|
48
|
+
linkCooldown: null as LinkCooldown,
|
|
42
49
|
pendingCardSetupToken: null as string | null,
|
|
43
50
|
spendPolicies: {} as Record<string, unknown>,
|
|
44
51
|
defaultPaymentMethod: undefined as string | undefined,
|
|
@@ -93,6 +100,7 @@ vi.mock("../../core/config.js", () => ({
|
|
|
93
100
|
},
|
|
94
101
|
getCardConfig: () => state.card,
|
|
95
102
|
getLinkConfig: () => state.link,
|
|
103
|
+
getLinkCooldown: () => state.linkCooldown,
|
|
96
104
|
getPendingLinkSetup: () => state.pendingLinkSetup,
|
|
97
105
|
getPendingCardSetupToken: () => state.pendingCardSetupToken,
|
|
98
106
|
getSpendPolicy: (walletId: string) => state.spendPolicies[walletId] ?? null,
|
|
@@ -176,6 +184,7 @@ function resetState(): void {
|
|
|
176
184
|
state.card = null;
|
|
177
185
|
state.link = null;
|
|
178
186
|
state.pendingLinkSetup = null;
|
|
187
|
+
state.linkCooldown = null;
|
|
179
188
|
state.pendingCardSetupToken = null;
|
|
180
189
|
state.spendPolicies = {};
|
|
181
190
|
state.defaultPaymentMethod = undefined;
|
package/src/tools/wallet.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getWallets,
|
|
5
5
|
getCardConfig,
|
|
6
6
|
getLinkConfig,
|
|
7
|
+
getLinkCooldown,
|
|
7
8
|
getPendingLinkSetup,
|
|
8
9
|
setPendingLinkSetup,
|
|
9
10
|
setLinkConfig,
|
|
@@ -157,6 +158,12 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
157
158
|
lines.push(auth.authenticated
|
|
158
159
|
? " Link CLI: authenticated"
|
|
159
160
|
: " Link CLI: not authenticated — run npx @stripe/link-cli auth login");
|
|
161
|
+
const cooldown = getLinkCooldown();
|
|
162
|
+
const blockedUntil = cooldown ? Date.parse(cooldown.blockedUntil) : NaN;
|
|
163
|
+
if (cooldown && Number.isFinite(blockedUntil) && blockedUntil > Date.now()) {
|
|
164
|
+
lines.push(` Link status: temporarily blocked by Stripe projected-spend cap until ${cooldown.blockedUntil}`);
|
|
165
|
+
lines.push(" Link note: reauthing or switching cards in the same Link account will not clear this cap.");
|
|
166
|
+
}
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
if (pendingCardSetupToken) {
|