@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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/cli.js +359 -87
  3. 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 # listens on port 54321 by default
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 MCP endpoint is available at `/mcp` on the chosen port.
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: "charge",
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 runMppPay(url, spendRequestId, method, data, headers, repository) {
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
- const spt = spendRequest.shared_payment_token.id;
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("retrieving");
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
- setStep("retrieving");
13979
- const spendRequest = await repository.getSpendRequest(spendRequestId, {
13980
- include: ["shared_payment_token"]
13981
- });
13982
- if (!spendRequest) {
13983
- throw new Error(`Spend request ${spendRequestId} not found`);
13984
- }
13985
- if (spendRequest.credential_type !== "shared_payment_token") {
13986
- const type = spendRequest.credential_type ?? "card";
13987
- throw new Error(
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
- if (!spendRequest.shared_payment_token) {
13997
- throw new Error("Spend request does not have a shared payment token");
13998
- }
13999
- const spt = spendRequest.shared_payment_token.id;
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
- const payResult = await readPayResult(retryResponse);
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
- }, [url, spendRequestId, method, data, headers, repository, onComplete]);
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
- retrieving: "Retrieving spend request",
14040
- probing: "Probing URL",
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__ */ jsx10(Box7, { children: /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
14053
- /* @__PURE__ */ jsx10(Spinner4, { type: "dots" }),
14054
- " ",
14055
- stepLabels[step],
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 runMppPay(
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: "Complete a machine payment protocol (MPP) payment using an approved spend request",
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
- return runMppPay(
14703
- url,
14704
- opts.spendRequestId,
14705
- method,
14706
- data,
14707
- headers,
14708
- repository
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
- res.setHeader("Access-Control-Allow-Origin", "*");
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://localhost:${port}/mcp
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.9.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(createMppCli(spendRequestRepo, authStorage, envAccessToken));
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(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stripe/link-cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-cli": "./dist/cli.js"