@stripe/link-cli 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli.js +359 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,11 +63,12 @@ Link CLI can run as a local MCP server. Add the following to your MCP client con
|
|
|
63
63
|
Use `serve` to expose link-cli as an MCP endpoint over HTTP. This is useful for remote or containerised agents that can't launch a local subprocess.
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
link-cli serve #
|
|
66
|
+
link-cli serve # binds 127.0.0.1:54321 (loopback only)
|
|
67
67
|
link-cli serve --port 8080
|
|
68
|
+
link-cli serve --host 0.0.0.0 # expose beyond localhost (see warning below)
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
The
|
|
71
|
+
The server only handles the `/mcp` endpoint (and `/.well-known/skills/` discovery); any other path returns `404`. It binds to `127.0.0.1` by default so only the local host can reach it. Anyone who can reach the port can use this CLI's authenticated Link session, so only override `--host` on a trusted, isolated network — doing so prints a warning.
|
|
71
72
|
|
|
72
73
|
## Quickstart
|
|
73
74
|
|
package/dist/cli.js
CHANGED
|
@@ -11282,6 +11282,11 @@ var BalancesResource = class extends BaseResource {
|
|
|
11282
11282
|
}
|
|
11283
11283
|
buildUrl(params) {
|
|
11284
11284
|
const url = new URL(this.endpoint);
|
|
11285
|
+
if (params.sources !== void 0) {
|
|
11286
|
+
for (const source of params.sources) {
|
|
11287
|
+
url.searchParams.append("sources[]", source);
|
|
11288
|
+
}
|
|
11289
|
+
}
|
|
11285
11290
|
if (params.limit !== void 0) {
|
|
11286
11291
|
url.searchParams.set("limit", String(params.limit));
|
|
11287
11292
|
}
|
|
@@ -13097,6 +13102,7 @@ var BalancesList = ({
|
|
|
13097
13102
|
// src/commands/balances/schema.ts
|
|
13098
13103
|
import { z as z2 } from "incur";
|
|
13099
13104
|
var listOptions = z2.object({
|
|
13105
|
+
source: z2.array(z2.string()).default([]).describe("Filter by source ID. Repeat to include multiple sources."),
|
|
13100
13106
|
limit: z2.coerce.number().int().positive().max(100).optional().describe("Maximum number of balances to return (1-100)."),
|
|
13101
13107
|
startingAfter: z2.string().optional().describe("Cursor: return balances after this balance ID."),
|
|
13102
13108
|
endingBefore: z2.string().optional().describe("Cursor: return balances before this balance ID.")
|
|
@@ -13117,6 +13123,7 @@ function createBalancesCli(createResource, authStorage2, envAccessToken2) {
|
|
|
13117
13123
|
const opts = c.options;
|
|
13118
13124
|
const resource = createResource();
|
|
13119
13125
|
const params = {};
|
|
13126
|
+
if (opts.source.length > 0) params.sources = opts.source;
|
|
13120
13127
|
if (opts.limit !== void 0) params.limit = opts.limit;
|
|
13121
13128
|
if (opts.startingAfter !== void 0)
|
|
13122
13129
|
params.starting_after = opts.startingAfter;
|
|
@@ -13814,11 +13821,11 @@ function getMethodDetails(request) {
|
|
|
13814
13821
|
}
|
|
13815
13822
|
function resolveStripeChallenge(challenges) {
|
|
13816
13823
|
const stripeChallenge = challenges.find(
|
|
13817
|
-
(challenge) => challenge.method === "stripe" && challenge.intent === "charge"
|
|
13824
|
+
(challenge) => challenge.method === "stripe" && (challenge.intent === "charge" || challenge.intent === "session")
|
|
13818
13825
|
);
|
|
13819
13826
|
if (!stripeChallenge) {
|
|
13820
13827
|
throw new Error(
|
|
13821
|
-
"WWW-Authenticate header does not include a stripe charge challenge"
|
|
13828
|
+
"WWW-Authenticate header does not include a stripe charge or session challenge"
|
|
13822
13829
|
);
|
|
13823
13830
|
}
|
|
13824
13831
|
if (typeof stripeChallenge.request !== "object" || stripeChallenge.request == null || Array.isArray(stripeChallenge.request)) {
|
|
@@ -13853,7 +13860,7 @@ function decodeStripeChallenge(challengeHeader) {
|
|
|
13853
13860
|
id: challenge.id,
|
|
13854
13861
|
realm: challenge.realm,
|
|
13855
13862
|
method: "stripe",
|
|
13856
|
-
intent:
|
|
13863
|
+
intent: challenge.intent,
|
|
13857
13864
|
description: challenge.description,
|
|
13858
13865
|
digest: challenge.digest,
|
|
13859
13866
|
expires: challenge.expires,
|
|
@@ -13898,8 +13905,19 @@ function createStripePaymentClient(spt) {
|
|
|
13898
13905
|
});
|
|
13899
13906
|
}
|
|
13900
13907
|
});
|
|
13908
|
+
const stripeSession = Method.toClient(
|
|
13909
|
+
{ ...StripeMethods.charge, intent: "session" },
|
|
13910
|
+
{
|
|
13911
|
+
async createCredential({ challenge }) {
|
|
13912
|
+
return Credential.serialize({
|
|
13913
|
+
challenge,
|
|
13914
|
+
payload: { action: "open", grantedToken: spt }
|
|
13915
|
+
});
|
|
13916
|
+
}
|
|
13917
|
+
}
|
|
13918
|
+
);
|
|
13901
13919
|
return Mppx.create({
|
|
13902
|
-
methods: [stripeCharge],
|
|
13920
|
+
methods: [stripeCharge, stripeSession],
|
|
13903
13921
|
polyfill: false,
|
|
13904
13922
|
transport: Transport.from({
|
|
13905
13923
|
name: "stripe-http",
|
|
@@ -13917,7 +13935,7 @@ function createStripePaymentClient(spt) {
|
|
|
13917
13935
|
})
|
|
13918
13936
|
});
|
|
13919
13937
|
}
|
|
13920
|
-
async function
|
|
13938
|
+
async function runMppPayWithSpendRequest(url, spendRequestId, method, data, headers, repository) {
|
|
13921
13939
|
const spendRequest = await repository.getSpendRequest(spendRequestId, {
|
|
13922
13940
|
include: ["shared_payment_token"]
|
|
13923
13941
|
});
|
|
@@ -13938,7 +13956,15 @@ async function runMppPay(url, spendRequestId, method, data, headers, repository)
|
|
|
13938
13956
|
if (!spendRequest.shared_payment_token) {
|
|
13939
13957
|
throw new Error("Spend request does not have a shared payment token");
|
|
13940
13958
|
}
|
|
13941
|
-
|
|
13959
|
+
return payWithSpt(
|
|
13960
|
+
url,
|
|
13961
|
+
spendRequest.shared_payment_token.id,
|
|
13962
|
+
method,
|
|
13963
|
+
data,
|
|
13964
|
+
headers
|
|
13965
|
+
);
|
|
13966
|
+
}
|
|
13967
|
+
async function payWithSpt(url, spt, method, data, headers) {
|
|
13942
13968
|
const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
|
|
13943
13969
|
const requestHeaders = buildHeaders(data, headers);
|
|
13944
13970
|
const initialResponse = await fetch(url, {
|
|
@@ -13960,72 +13986,156 @@ async function runMppPay(url, spendRequestId, method, data, headers, repository)
|
|
|
13960
13986
|
});
|
|
13961
13987
|
return readPayResult(retryResponse);
|
|
13962
13988
|
}
|
|
13989
|
+
async function runMppPayFullFlow(opts) {
|
|
13990
|
+
const {
|
|
13991
|
+
url,
|
|
13992
|
+
method,
|
|
13993
|
+
data,
|
|
13994
|
+
headers,
|
|
13995
|
+
context,
|
|
13996
|
+
amountOverride,
|
|
13997
|
+
paymentMethodId,
|
|
13998
|
+
test,
|
|
13999
|
+
repository,
|
|
14000
|
+
paymentMethodsFactory,
|
|
14001
|
+
onStep,
|
|
14002
|
+
onApprovalUrl
|
|
14003
|
+
} = opts;
|
|
14004
|
+
const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
|
|
14005
|
+
const requestHeaders = buildHeaders(data, headers);
|
|
14006
|
+
onStep?.("probing");
|
|
14007
|
+
const probeResponse = await fetch(url, {
|
|
14008
|
+
method: httpMethod,
|
|
14009
|
+
body: data,
|
|
14010
|
+
headers: requestHeaders
|
|
14011
|
+
});
|
|
14012
|
+
if (probeResponse.status !== 402) {
|
|
14013
|
+
return readPayResult(probeResponse);
|
|
14014
|
+
}
|
|
14015
|
+
const wwwAuth = probeResponse.headers.get("www-authenticate");
|
|
14016
|
+
if (!wwwAuth) {
|
|
14017
|
+
throw new Error("URL returned 402 but no WWW-Authenticate header");
|
|
14018
|
+
}
|
|
14019
|
+
const decoded = decodeStripeChallenge(wwwAuth);
|
|
14020
|
+
const networkId = decoded.network_id;
|
|
14021
|
+
const challengeAmount = decoded.request_json.amount ? Number(decoded.request_json.amount) : void 0;
|
|
14022
|
+
const challengeCurrency = decoded.request_json.currency ?? "usd";
|
|
14023
|
+
const amount = amountOverride ?? challengeAmount;
|
|
14024
|
+
if (!amount) {
|
|
14025
|
+
throw new Error(
|
|
14026
|
+
"Could not determine amount from 402 challenge. Pass --amount explicitly."
|
|
14027
|
+
);
|
|
14028
|
+
}
|
|
14029
|
+
let pmId = paymentMethodId;
|
|
14030
|
+
if (!pmId) {
|
|
14031
|
+
onStep?.("creating");
|
|
14032
|
+
const pmResource = paymentMethodsFactory();
|
|
14033
|
+
const methods = await pmResource.list();
|
|
14034
|
+
if (!methods.length) {
|
|
14035
|
+
throw new Error(
|
|
14036
|
+
"No payment methods found. Add one with `link-cli payment-methods add`."
|
|
14037
|
+
);
|
|
14038
|
+
}
|
|
14039
|
+
pmId = methods[0].id;
|
|
14040
|
+
}
|
|
14041
|
+
onStep?.("creating");
|
|
14042
|
+
const spendRequest = await repository.createSpendRequest({
|
|
14043
|
+
payment_details: pmId,
|
|
14044
|
+
credential_type: "shared_payment_token",
|
|
14045
|
+
network_id: networkId,
|
|
14046
|
+
amount,
|
|
14047
|
+
currency: challengeCurrency,
|
|
14048
|
+
context,
|
|
14049
|
+
request_approval: true,
|
|
14050
|
+
test: test || void 0
|
|
14051
|
+
});
|
|
14052
|
+
onStep?.("approving");
|
|
14053
|
+
if (spendRequest.approval_url) {
|
|
14054
|
+
onApprovalUrl?.(spendRequest.approval_url);
|
|
14055
|
+
}
|
|
14056
|
+
const approved = await pollUntilApproved(repository, spendRequest.id);
|
|
14057
|
+
if (approved.status !== "approved") {
|
|
14058
|
+
throw new Error(
|
|
14059
|
+
`Spend request was not approved (status: ${approved.status})`
|
|
14060
|
+
);
|
|
14061
|
+
}
|
|
14062
|
+
onStep?.("signing");
|
|
14063
|
+
let withSpt = await repository.getSpendRequest(spendRequest.id, {
|
|
14064
|
+
include: ["shared_payment_token"]
|
|
14065
|
+
});
|
|
14066
|
+
for (let i = 0; i < 3 && withSpt && !withSpt.shared_payment_token; i++) {
|
|
14067
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
14068
|
+
withSpt = await repository.getSpendRequest(spendRequest.id, {
|
|
14069
|
+
include: ["shared_payment_token"]
|
|
14070
|
+
});
|
|
14071
|
+
}
|
|
14072
|
+
if (!withSpt?.shared_payment_token) {
|
|
14073
|
+
throw new Error("Failed to retrieve shared payment token");
|
|
14074
|
+
}
|
|
14075
|
+
onStep?.("submitting");
|
|
14076
|
+
return payWithSpt(
|
|
14077
|
+
url,
|
|
14078
|
+
withSpt.shared_payment_token.id,
|
|
14079
|
+
method,
|
|
14080
|
+
data,
|
|
14081
|
+
headers
|
|
14082
|
+
);
|
|
14083
|
+
}
|
|
13963
14084
|
function MppPay({
|
|
13964
14085
|
url,
|
|
13965
14086
|
spendRequestId,
|
|
13966
14087
|
method,
|
|
13967
14088
|
data,
|
|
13968
14089
|
headers,
|
|
14090
|
+
context,
|
|
14091
|
+
amountOverride,
|
|
14092
|
+
paymentMethodId,
|
|
14093
|
+
test,
|
|
13969
14094
|
repository,
|
|
14095
|
+
paymentMethodsFactory,
|
|
13970
14096
|
onComplete
|
|
13971
14097
|
}) {
|
|
13972
|
-
const [step, setStep] = useState5(
|
|
14098
|
+
const [step, setStep] = useState5(
|
|
14099
|
+
spendRequestId ? "signing" : "probing"
|
|
14100
|
+
);
|
|
13973
14101
|
const [result, setResult] = useState5(null);
|
|
13974
14102
|
const [error, setError] = useState5(null);
|
|
14103
|
+
const [approvalUrl, setApprovalUrl] = useState5(null);
|
|
13975
14104
|
useEffect5(() => {
|
|
13976
14105
|
(async () => {
|
|
13977
14106
|
try {
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
`Spend request ${spendRequestId} must have credential_type 'shared_payment_token' (current: '${type}')`
|
|
13989
|
-
);
|
|
13990
|
-
}
|
|
13991
|
-
if (spendRequest.status !== "approved") {
|
|
13992
|
-
throw new Error(
|
|
13993
|
-
`Spend request must be approved (current status: ${spendRequest.status})`
|
|
14107
|
+
let payResult;
|
|
14108
|
+
if (spendRequestId) {
|
|
14109
|
+
setStep("signing");
|
|
14110
|
+
payResult = await runMppPayWithSpendRequest(
|
|
14111
|
+
url,
|
|
14112
|
+
spendRequestId,
|
|
14113
|
+
method,
|
|
14114
|
+
data,
|
|
14115
|
+
headers,
|
|
14116
|
+
repository
|
|
13994
14117
|
);
|
|
13995
|
-
}
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
|
|
14001
|
-
const requestHeaders = buildHeaders(data, headers);
|
|
14002
|
-
setStep("probing");
|
|
14003
|
-
const initialResponse = await fetch(url, {
|
|
14004
|
-
method: httpMethod,
|
|
14005
|
-
body: data,
|
|
14006
|
-
headers: requestHeaders
|
|
14007
|
-
});
|
|
14008
|
-
if (initialResponse.status !== 402) {
|
|
14009
|
-
const payResult2 = await readPayResult(initialResponse);
|
|
14010
|
-
setResult(payResult2);
|
|
14011
|
-
setStep("done");
|
|
14012
|
-
onComplete(payResult2);
|
|
14013
|
-
return;
|
|
14014
|
-
}
|
|
14015
|
-
setStep("signing");
|
|
14016
|
-
const authHeader = await createStripePaymentClient(spt).createCredential(
|
|
14017
|
-
initialResponse
|
|
14018
|
-
);
|
|
14019
|
-
setStep("submitting");
|
|
14020
|
-
const retryResponse = await fetch(url, {
|
|
14021
|
-
method: httpMethod,
|
|
14022
|
-
body: data,
|
|
14023
|
-
headers: {
|
|
14024
|
-
...requestHeaders,
|
|
14025
|
-
Authorization: authHeader
|
|
14118
|
+
} else {
|
|
14119
|
+
if (!context) {
|
|
14120
|
+
throw new Error(
|
|
14121
|
+
"--context is required for the full MPP flow (min 100 chars)"
|
|
14122
|
+
);
|
|
14026
14123
|
}
|
|
14027
|
-
|
|
14028
|
-
|
|
14124
|
+
payResult = await runMppPayFullFlow({
|
|
14125
|
+
url,
|
|
14126
|
+
method,
|
|
14127
|
+
data,
|
|
14128
|
+
headers,
|
|
14129
|
+
context,
|
|
14130
|
+
amountOverride,
|
|
14131
|
+
paymentMethodId,
|
|
14132
|
+
test: test ?? false,
|
|
14133
|
+
repository,
|
|
14134
|
+
paymentMethodsFactory,
|
|
14135
|
+
onStep: setStep,
|
|
14136
|
+
onApprovalUrl: (u) => setApprovalUrl(u)
|
|
14137
|
+
});
|
|
14138
|
+
}
|
|
14029
14139
|
setResult(payResult);
|
|
14030
14140
|
setStep("done");
|
|
14031
14141
|
onComplete(payResult);
|
|
@@ -14034,10 +14144,24 @@ function MppPay({
|
|
|
14034
14144
|
onComplete(null);
|
|
14035
14145
|
}
|
|
14036
14146
|
})();
|
|
14037
|
-
}, [
|
|
14147
|
+
}, [
|
|
14148
|
+
url,
|
|
14149
|
+
spendRequestId,
|
|
14150
|
+
method,
|
|
14151
|
+
data,
|
|
14152
|
+
headers,
|
|
14153
|
+
context,
|
|
14154
|
+
amountOverride,
|
|
14155
|
+
paymentMethodId,
|
|
14156
|
+
test,
|
|
14157
|
+
repository,
|
|
14158
|
+
paymentMethodsFactory,
|
|
14159
|
+
onComplete
|
|
14160
|
+
]);
|
|
14038
14161
|
const stepLabels = {
|
|
14039
|
-
|
|
14040
|
-
|
|
14162
|
+
probing: "Probing URL for 402 challenge",
|
|
14163
|
+
creating: "Creating spend request",
|
|
14164
|
+
approving: "Waiting for approval",
|
|
14041
14165
|
signing: "Signing credential",
|
|
14042
14166
|
submitting: "Submitting payment",
|
|
14043
14167
|
done: "Done"
|
|
@@ -14049,12 +14173,19 @@ function MppPay({
|
|
|
14049
14173
|
] });
|
|
14050
14174
|
}
|
|
14051
14175
|
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
14052
|
-
step !== "done" && /* @__PURE__ */
|
|
14053
|
-
/* @__PURE__ */ jsx10(
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14176
|
+
step !== "done" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
14177
|
+
/* @__PURE__ */ jsx10(Box7, { children: /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
|
|
14178
|
+
/* @__PURE__ */ jsx10(Spinner4, { type: "dots" }),
|
|
14179
|
+
" ",
|
|
14180
|
+
stepLabels[step],
|
|
14181
|
+
"..."
|
|
14182
|
+
] }) }),
|
|
14183
|
+
step === "approving" && approvalUrl && /* @__PURE__ */ jsx10(Box7, { marginTop: 1, paddingX: 2, children: /* @__PURE__ */ jsxs7(Text9, { children: [
|
|
14184
|
+
"Approve in Link app:",
|
|
14185
|
+
" ",
|
|
14186
|
+
/* @__PURE__ */ jsx10(Text9, { bold: true, color: "blue", children: approvalUrl })
|
|
14187
|
+
] }) })
|
|
14188
|
+
] }),
|
|
14058
14189
|
result && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
14059
14190
|
/* @__PURE__ */ jsxs7(
|
|
14060
14191
|
Text9,
|
|
@@ -14221,7 +14352,7 @@ var SptFlow = ({
|
|
|
14221
14352
|
setStep("mpp-pay-gate");
|
|
14222
14353
|
await waitForEnter();
|
|
14223
14354
|
setStep("mpp-pay");
|
|
14224
|
-
const payResponse = await
|
|
14355
|
+
const payResponse = await runMppPayWithSpendRequest(
|
|
14225
14356
|
DEMO_CLIMATE_API_URL,
|
|
14226
14357
|
result.id,
|
|
14227
14358
|
"POST",
|
|
@@ -14641,12 +14772,22 @@ function DecodeChallengeView({
|
|
|
14641
14772
|
// src/commands/mpp/schema.ts
|
|
14642
14773
|
import { z as z4 } from "incur";
|
|
14643
14774
|
var payOptions = z4.object({
|
|
14644
|
-
spendRequestId: z4.string().describe(
|
|
14645
|
-
'Approved spend request ID with credential_type "shared_payment_token"'
|
|
14775
|
+
spendRequestId: z4.string().optional().describe(
|
|
14776
|
+
'Approved spend request ID with credential_type "shared_payment_token". If omitted, the command handles the full flow: probe URL, parse challenge, create spend request, get approval, and pay.'
|
|
14646
14777
|
),
|
|
14647
14778
|
method: z4.string().optional().describe("HTTP method (default: GET, or POST if --data is provided)"),
|
|
14648
14779
|
data: z4.string().optional().describe("Request body (implies POST if --method is not set)"),
|
|
14649
|
-
header: z4.array(z4.string()).default([]).describe('Request header in "Name: Value" format (repeatable)')
|
|
14780
|
+
header: z4.array(z4.string()).default([]).describe('Request header in "Name: Value" format (repeatable)'),
|
|
14781
|
+
context: z4.string().min(100).optional().describe(
|
|
14782
|
+
"Min 100 chars \u2014 describe the purchase and rationale; the user reads this when approving. Required when --spend-request-id is not provided."
|
|
14783
|
+
),
|
|
14784
|
+
amount: z4.coerce.number().int().positive().optional().describe(
|
|
14785
|
+
"Amount in cents (derived from 402 challenge if omitted; required if challenge has no amount)"
|
|
14786
|
+
),
|
|
14787
|
+
paymentMethodId: z4.string().optional().describe("Payment method ID (uses default if omitted)"),
|
|
14788
|
+
test: z4.boolean().default(false).describe(
|
|
14789
|
+
"Use test mode (creates testmode credentials from test card data)"
|
|
14790
|
+
)
|
|
14650
14791
|
});
|
|
14651
14792
|
var decodeOptions = z4.object({
|
|
14652
14793
|
challenge: z4.string().describe(
|
|
@@ -14656,12 +14797,12 @@ var decodeOptions = z4.object({
|
|
|
14656
14797
|
|
|
14657
14798
|
// src/commands/mpp/index.tsx
|
|
14658
14799
|
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
14659
|
-
function createMppCli(repository, authStorage2, envAccessToken2) {
|
|
14800
|
+
function createMppCli(repository, paymentMethodsFactory, authStorage2, envAccessToken2) {
|
|
14660
14801
|
const cli2 = Cli4.create("mpp", {
|
|
14661
14802
|
description: "Machine payment protocol (MPP) commands"
|
|
14662
14803
|
});
|
|
14663
14804
|
cli2.command("pay", {
|
|
14664
|
-
description: "
|
|
14805
|
+
description: "Pay a URL via the Machine Payment Protocol. Handles the full 402 flow: probes the URL, parses the challenge, creates a spend request, gets approval, and pays with the SPT. Pass --spend-request-id to skip creation and use a pre-approved spend request.",
|
|
14665
14806
|
args: z5.object({
|
|
14666
14807
|
url: z5.string().describe("URL to pay")
|
|
14667
14808
|
}),
|
|
@@ -14669,7 +14810,7 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
|
|
|
14669
14810
|
alias: { method: "X", data: "d", header: "H" },
|
|
14670
14811
|
outputPolicy: "agent-only",
|
|
14671
14812
|
middleware: [requireAuth(authStorage2, envAccessToken2)],
|
|
14672
|
-
async run(c) {
|
|
14813
|
+
async *run(c) {
|
|
14673
14814
|
const url = c.args.url;
|
|
14674
14815
|
const opts = c.options;
|
|
14675
14816
|
const method = opts.method;
|
|
@@ -14686,7 +14827,12 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
|
|
|
14686
14827
|
method,
|
|
14687
14828
|
data,
|
|
14688
14829
|
headers,
|
|
14830
|
+
context: opts.context,
|
|
14831
|
+
amountOverride: opts.amount,
|
|
14832
|
+
paymentMethodId: opts.paymentMethodId,
|
|
14833
|
+
test: opts.test,
|
|
14689
14834
|
repository,
|
|
14835
|
+
paymentMethodsFactory,
|
|
14690
14836
|
onComplete: (result) => {
|
|
14691
14837
|
capturedResult = result;
|
|
14692
14838
|
}
|
|
@@ -14699,14 +14845,90 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
|
|
|
14699
14845
|
}
|
|
14700
14846
|
);
|
|
14701
14847
|
}
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14848
|
+
if (opts.spendRequestId) {
|
|
14849
|
+
yield await runMppPayWithSpendRequest(
|
|
14850
|
+
url,
|
|
14851
|
+
opts.spendRequestId,
|
|
14852
|
+
method,
|
|
14853
|
+
data,
|
|
14854
|
+
headers,
|
|
14855
|
+
repository
|
|
14856
|
+
);
|
|
14857
|
+
return;
|
|
14858
|
+
}
|
|
14859
|
+
const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
|
|
14860
|
+
const requestHeaders = buildHeaders(data, headers);
|
|
14861
|
+
const probeResponse = await fetch(url, {
|
|
14862
|
+
method: httpMethod,
|
|
14863
|
+
body: data,
|
|
14864
|
+
headers: requestHeaders
|
|
14865
|
+
});
|
|
14866
|
+
if (probeResponse.status !== 402) {
|
|
14867
|
+
yield await readPayResult(probeResponse);
|
|
14868
|
+
return;
|
|
14869
|
+
}
|
|
14870
|
+
const wwwAuth = probeResponse.headers.get("www-authenticate");
|
|
14871
|
+
if (!wwwAuth) {
|
|
14872
|
+
return c.error({
|
|
14873
|
+
code: "INVALID_RESPONSE",
|
|
14874
|
+
message: "URL returned 402 but no WWW-Authenticate header"
|
|
14875
|
+
});
|
|
14876
|
+
}
|
|
14877
|
+
const decoded = decodeStripeChallenge(wwwAuth);
|
|
14878
|
+
const networkId = decoded.network_id;
|
|
14879
|
+
const challengeAmount = decoded.request_json.amount ? Number(decoded.request_json.amount) : void 0;
|
|
14880
|
+
const challengeCurrency = decoded.request_json.currency ?? "usd";
|
|
14881
|
+
const amount = opts.amount ?? challengeAmount;
|
|
14882
|
+
if (!amount) {
|
|
14883
|
+
return c.error({
|
|
14884
|
+
code: "INVALID_INPUT",
|
|
14885
|
+
message: "Could not determine amount from 402 challenge. Pass --amount explicitly."
|
|
14886
|
+
});
|
|
14887
|
+
}
|
|
14888
|
+
if (!opts.context) {
|
|
14889
|
+
return c.error({
|
|
14890
|
+
code: "INVALID_INPUT",
|
|
14891
|
+
message: "--context is required for the full MPP flow (min 100 chars). Describe the purchase and rationale."
|
|
14892
|
+
});
|
|
14893
|
+
}
|
|
14894
|
+
let pmId = opts.paymentMethodId;
|
|
14895
|
+
if (!pmId) {
|
|
14896
|
+
const pmResource = paymentMethodsFactory();
|
|
14897
|
+
const methods = await pmResource.list();
|
|
14898
|
+
if (!methods.length) {
|
|
14899
|
+
return c.error({
|
|
14900
|
+
code: "NO_PAYMENT_METHOD",
|
|
14901
|
+
message: "No payment methods found. Add one with `link-cli payment-methods add`."
|
|
14902
|
+
});
|
|
14903
|
+
}
|
|
14904
|
+
pmId = methods[0].id;
|
|
14905
|
+
}
|
|
14906
|
+
const spendRequest = await repository.createSpendRequest({
|
|
14907
|
+
payment_details: pmId,
|
|
14908
|
+
credential_type: "shared_payment_token",
|
|
14909
|
+
network_id: networkId,
|
|
14910
|
+
amount,
|
|
14911
|
+
currency: challengeCurrency,
|
|
14912
|
+
context: opts.context,
|
|
14913
|
+
request_approval: true,
|
|
14914
|
+
test: opts.test || void 0
|
|
14915
|
+
});
|
|
14916
|
+
const nextFlags = [`--spend-request-id ${spendRequest.id}`];
|
|
14917
|
+
if (method) nextFlags.push(`-X ${method}`);
|
|
14918
|
+
if (data) nextFlags.push(`-d '${data}'`);
|
|
14919
|
+
if (headers) {
|
|
14920
|
+
for (const h of headers) nextFlags.push(`-H '${h}'`);
|
|
14921
|
+
}
|
|
14922
|
+
const nextCommand = `mpp pay ${url} ${nextFlags.join(" ")}`;
|
|
14923
|
+
yield {
|
|
14924
|
+
...spendRequest,
|
|
14925
|
+
instruction: `Present the approval_url to the user and ask them to approve in the Link app. Then call \`spend-request retrieve ${spendRequest.id} --interval 2 --max-attempts 300\` to poll until approved. Once approved, run the _next.command to complete payment. Do not wait for the user to reply \u2014 start polling immediately.`,
|
|
14926
|
+
_next: {
|
|
14927
|
+
poll_command: `spend-request retrieve ${spendRequest.id} --interval 2 --max-attempts 300`,
|
|
14928
|
+
pay_command: nextCommand,
|
|
14929
|
+
until: "status changes from pending_approval, then run pay_command"
|
|
14930
|
+
}
|
|
14931
|
+
};
|
|
14710
14932
|
}
|
|
14711
14933
|
});
|
|
14712
14934
|
cli2.command("decode", {
|
|
@@ -15083,17 +15305,47 @@ async function sendWebResponse(webRes, res) {
|
|
|
15083
15305
|
res.writeHead(webRes.status);
|
|
15084
15306
|
res.end(Buffer.from(buffer));
|
|
15085
15307
|
}
|
|
15308
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
|
|
15309
|
+
function isLoopbackHost(host) {
|
|
15310
|
+
return LOOPBACK_HOSTS.has(host.toLowerCase());
|
|
15311
|
+
}
|
|
15312
|
+
function isAllowedOrigin(origin) {
|
|
15313
|
+
if (!origin) return true;
|
|
15314
|
+
try {
|
|
15315
|
+
return isLoopbackHost(new URL(origin).hostname);
|
|
15316
|
+
} catch {
|
|
15317
|
+
return false;
|
|
15318
|
+
}
|
|
15319
|
+
}
|
|
15320
|
+
function isAllowedRoute(method, pathname) {
|
|
15321
|
+
if (pathname === "/mcp") return true;
|
|
15322
|
+
if (pathname.startsWith("/.well-known/skills/") && method === "GET")
|
|
15323
|
+
return true;
|
|
15324
|
+
return false;
|
|
15325
|
+
}
|
|
15086
15326
|
function createServeCli(rootCli) {
|
|
15087
15327
|
return Cli8.create("serve", {
|
|
15088
15328
|
description: "Start an HTTP server exposing link-cli as an MCP endpoint at /mcp",
|
|
15089
15329
|
options: z7.object({
|
|
15090
|
-
port: z7.coerce.number().default(54321).describe("Port to listen on")
|
|
15330
|
+
port: z7.coerce.number().default(54321).describe("Port to listen on"),
|
|
15331
|
+
host: z7.string().default("127.0.0.1").describe(
|
|
15332
|
+
"Host/interface to bind. Defaults to loopback; set explicitly (e.g. 0.0.0.0) to expose beyond localhost."
|
|
15333
|
+
)
|
|
15091
15334
|
}),
|
|
15092
15335
|
async run(c) {
|
|
15093
|
-
const { port } = c.options;
|
|
15336
|
+
const { port, host } = c.options;
|
|
15094
15337
|
const server = createServer(
|
|
15095
15338
|
async (req, res) => {
|
|
15096
|
-
|
|
15339
|
+
const origin = req.headers.origin;
|
|
15340
|
+
if (!isAllowedOrigin(origin)) {
|
|
15341
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
15342
|
+
res.end(JSON.stringify({ error: "forbidden origin" }));
|
|
15343
|
+
return;
|
|
15344
|
+
}
|
|
15345
|
+
if (origin) {
|
|
15346
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
15347
|
+
res.setHeader("Vary", "Origin");
|
|
15348
|
+
}
|
|
15097
15349
|
res.setHeader(
|
|
15098
15350
|
"Access-Control-Allow-Methods",
|
|
15099
15351
|
"GET, POST, DELETE, OPTIONS"
|
|
@@ -15107,6 +15359,12 @@ function createServeCli(rootCli) {
|
|
|
15107
15359
|
res.end();
|
|
15108
15360
|
return;
|
|
15109
15361
|
}
|
|
15362
|
+
const pathname = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
|
|
15363
|
+
if (!isAllowedRoute(req.method ?? "GET", pathname)) {
|
|
15364
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
15365
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
15366
|
+
return;
|
|
15367
|
+
}
|
|
15110
15368
|
try {
|
|
15111
15369
|
const webReq = await nodeRequestToWebRequest(req, port);
|
|
15112
15370
|
const webRes = await rootCli.fetch(webReq);
|
|
@@ -15120,9 +15378,16 @@ function createServeCli(rootCli) {
|
|
|
15120
15378
|
);
|
|
15121
15379
|
await new Promise((resolve, reject) => {
|
|
15122
15380
|
server.on("error", reject);
|
|
15123
|
-
server.listen(port, () => {
|
|
15381
|
+
server.listen(port, host, () => {
|
|
15382
|
+
if (!isLoopbackHost(host)) {
|
|
15383
|
+
process.stderr.write(
|
|
15384
|
+
`WARNING: link-cli serve is bound to ${host}, which may be reachable beyond localhost.
|
|
15385
|
+
Any caller that can reach this port can use the authenticated Link session of this CLI. Only do this on a trusted, isolated network.
|
|
15386
|
+
`
|
|
15387
|
+
);
|
|
15388
|
+
}
|
|
15124
15389
|
process.stderr.write(
|
|
15125
|
-
`link-cli MCP server listening on http
|
|
15390
|
+
`link-cli MCP server listening on http://${host}:${port}/mcp
|
|
15126
15391
|
`
|
|
15127
15392
|
);
|
|
15128
15393
|
});
|
|
@@ -17850,7 +18115,7 @@ function cacheUpdateInfo(value, ttlMs = UPDATE_CACHE_TTL_MS) {
|
|
|
17850
18115
|
}
|
|
17851
18116
|
|
|
17852
18117
|
// src/cli.tsx
|
|
17853
|
-
var cliVersion = "0.
|
|
18118
|
+
var cliVersion = "0.10.0";
|
|
17854
18119
|
var cliName = "@stripe/link-cli";
|
|
17855
18120
|
var defaultHeaders = {
|
|
17856
18121
|
"User-Agent": `link-cli/${cliVersion}`
|
|
@@ -17941,7 +18206,14 @@ if (!hiddenCli) {
|
|
|
17941
18206
|
envAccessToken
|
|
17942
18207
|
)
|
|
17943
18208
|
);
|
|
17944
|
-
cli.command(
|
|
18209
|
+
cli.command(
|
|
18210
|
+
createMppCli(
|
|
18211
|
+
spendRequestRepo,
|
|
18212
|
+
() => factory.createPaymentMethodsResource(),
|
|
18213
|
+
authStorage,
|
|
18214
|
+
envAccessToken
|
|
18215
|
+
)
|
|
18216
|
+
);
|
|
17945
18217
|
cli.command(
|
|
17946
18218
|
createReportCli(
|
|
17947
18219
|
() => factory.createReportResource(),
|