@stripe/link-cli 0.9.0 → 0.10.1

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 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
 
@@ -230,7 +231,7 @@ A spend request moves through: **create** → **request approval** → **approve
230
231
  **Required fields for create:** `merchant_name`, `merchant_url`, `context`, `amount`. `payment_method_id` is optional — if omitted, your default payment method will be used, or the first eligible one if no default is set.
231
232
 
232
233
  **Constraints:** `context` must be at least 100 characters; `amount` must not exceed 500000 (cents); `currency` must be a 3-letter ISO code. The user has 10 minutes from when approval is requested to approve. Approved credentials (card or SPT) are valid for 12 hours from spend request creation.
233
- **Test mode:** Pass `--test` to create testmode credentials (uses test card `4242424242424242`), useful for development and integration testing without real payment methods.
234
+ **Test mode:** Pass `--test` to create a testmode SpendRequest. A testmode SpendRequest will return test payment credentials (e.g test card `4000009990001984`) rather than a real payment credential. Testmode SpendRequests will not charge the underlying payment method of the SpendRequest. This is useful for development and integration testing without real payment methods.
234
235
 
235
236
  ```bash
236
237
  # Update before approval
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,
@@ -13887,7 +13894,11 @@ function buildHeaders(data, headers) {
13887
13894
  async function readPayResult(response) {
13888
13895
  const responseHeaders = Object.fromEntries(response.headers.entries());
13889
13896
  const body = await response.text();
13890
- return { status: response.status, headers: responseHeaders, body };
13897
+ return sanitizeDeep({
13898
+ status: response.status,
13899
+ headers: responseHeaders,
13900
+ body
13901
+ });
13891
13902
  }
13892
13903
  function createStripePaymentClient(spt) {
13893
13904
  const stripeCharge = Method.toClient(StripeMethods.charge, {
@@ -13898,8 +13909,19 @@ function createStripePaymentClient(spt) {
13898
13909
  });
13899
13910
  }
13900
13911
  });
13912
+ const stripeSession = Method.toClient(
13913
+ { ...StripeMethods.charge, intent: "session" },
13914
+ {
13915
+ async createCredential({ challenge }) {
13916
+ return Credential.serialize({
13917
+ challenge,
13918
+ payload: { action: "open", grantedToken: spt }
13919
+ });
13920
+ }
13921
+ }
13922
+ );
13901
13923
  return Mppx.create({
13902
- methods: [stripeCharge],
13924
+ methods: [stripeCharge, stripeSession],
13903
13925
  polyfill: false,
13904
13926
  transport: Transport.from({
13905
13927
  name: "stripe-http",
@@ -13917,7 +13939,7 @@ function createStripePaymentClient(spt) {
13917
13939
  })
13918
13940
  });
13919
13941
  }
13920
- async function runMppPay(url, spendRequestId, method, data, headers, repository) {
13942
+ async function runMppPayWithSpendRequest(url, spendRequestId, method, data, headers, repository) {
13921
13943
  const spendRequest = await repository.getSpendRequest(spendRequestId, {
13922
13944
  include: ["shared_payment_token"]
13923
13945
  });
@@ -13938,7 +13960,15 @@ async function runMppPay(url, spendRequestId, method, data, headers, repository)
13938
13960
  if (!spendRequest.shared_payment_token) {
13939
13961
  throw new Error("Spend request does not have a shared payment token");
13940
13962
  }
13941
- const spt = spendRequest.shared_payment_token.id;
13963
+ return payWithSpt(
13964
+ url,
13965
+ spendRequest.shared_payment_token.id,
13966
+ method,
13967
+ data,
13968
+ headers
13969
+ );
13970
+ }
13971
+ async function payWithSpt(url, spt, method, data, headers) {
13942
13972
  const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
13943
13973
  const requestHeaders = buildHeaders(data, headers);
13944
13974
  const initialResponse = await fetch(url, {
@@ -13960,72 +13990,156 @@ async function runMppPay(url, spendRequestId, method, data, headers, repository)
13960
13990
  });
13961
13991
  return readPayResult(retryResponse);
13962
13992
  }
13993
+ async function runMppPayFullFlow(opts) {
13994
+ const {
13995
+ url,
13996
+ method,
13997
+ data,
13998
+ headers,
13999
+ context,
14000
+ amountOverride,
14001
+ paymentMethodId,
14002
+ test,
14003
+ repository,
14004
+ paymentMethodsFactory,
14005
+ onStep,
14006
+ onApprovalUrl
14007
+ } = opts;
14008
+ const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
14009
+ const requestHeaders = buildHeaders(data, headers);
14010
+ onStep?.("probing");
14011
+ const probeResponse = await fetch(url, {
14012
+ method: httpMethod,
14013
+ body: data,
14014
+ headers: requestHeaders
14015
+ });
14016
+ if (probeResponse.status !== 402) {
14017
+ return readPayResult(probeResponse);
14018
+ }
14019
+ const wwwAuth = probeResponse.headers.get("www-authenticate");
14020
+ if (!wwwAuth) {
14021
+ throw new Error("URL returned 402 but no WWW-Authenticate header");
14022
+ }
14023
+ const decoded = decodeStripeChallenge(wwwAuth);
14024
+ const networkId = decoded.network_id;
14025
+ const challengeAmount = decoded.request_json.amount ? Number(decoded.request_json.amount) : void 0;
14026
+ const challengeCurrency = decoded.request_json.currency ?? "usd";
14027
+ const amount = amountOverride ?? challengeAmount;
14028
+ if (!amount) {
14029
+ throw new Error(
14030
+ "Could not determine amount from 402 challenge. Pass --amount explicitly."
14031
+ );
14032
+ }
14033
+ let pmId = paymentMethodId;
14034
+ if (!pmId) {
14035
+ onStep?.("creating");
14036
+ const pmResource = paymentMethodsFactory();
14037
+ const methods = await pmResource.list();
14038
+ if (!methods.length) {
14039
+ throw new Error(
14040
+ "No payment methods found. Add one with `link-cli payment-methods add`."
14041
+ );
14042
+ }
14043
+ pmId = methods[0].id;
14044
+ }
14045
+ onStep?.("creating");
14046
+ const spendRequest = await repository.createSpendRequest({
14047
+ payment_details: pmId,
14048
+ credential_type: "shared_payment_token",
14049
+ network_id: networkId,
14050
+ amount,
14051
+ currency: challengeCurrency,
14052
+ context,
14053
+ request_approval: true,
14054
+ test: test || void 0
14055
+ });
14056
+ onStep?.("approving");
14057
+ if (spendRequest.approval_url) {
14058
+ onApprovalUrl?.(spendRequest.approval_url);
14059
+ }
14060
+ const approved = await pollUntilApproved(repository, spendRequest.id);
14061
+ if (approved.status !== "approved") {
14062
+ throw new Error(
14063
+ `Spend request was not approved (status: ${approved.status})`
14064
+ );
14065
+ }
14066
+ onStep?.("signing");
14067
+ let withSpt = await repository.getSpendRequest(spendRequest.id, {
14068
+ include: ["shared_payment_token"]
14069
+ });
14070
+ for (let i = 0; i < 3 && withSpt && !withSpt.shared_payment_token; i++) {
14071
+ await new Promise((r) => setTimeout(r, 1e3));
14072
+ withSpt = await repository.getSpendRequest(spendRequest.id, {
14073
+ include: ["shared_payment_token"]
14074
+ });
14075
+ }
14076
+ if (!withSpt?.shared_payment_token) {
14077
+ throw new Error("Failed to retrieve shared payment token");
14078
+ }
14079
+ onStep?.("submitting");
14080
+ return payWithSpt(
14081
+ url,
14082
+ withSpt.shared_payment_token.id,
14083
+ method,
14084
+ data,
14085
+ headers
14086
+ );
14087
+ }
13963
14088
  function MppPay({
13964
14089
  url,
13965
14090
  spendRequestId,
13966
14091
  method,
13967
14092
  data,
13968
14093
  headers,
14094
+ context,
14095
+ amountOverride,
14096
+ paymentMethodId,
14097
+ test,
13969
14098
  repository,
14099
+ paymentMethodsFactory,
13970
14100
  onComplete
13971
14101
  }) {
13972
- const [step, setStep] = useState5("retrieving");
14102
+ const [step, setStep] = useState5(
14103
+ spendRequestId ? "signing" : "probing"
14104
+ );
13973
14105
  const [result, setResult] = useState5(null);
13974
14106
  const [error, setError] = useState5(null);
14107
+ const [approvalUrl, setApprovalUrl] = useState5(null);
13975
14108
  useEffect5(() => {
13976
14109
  (async () => {
13977
14110
  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}')`
14111
+ let payResult;
14112
+ if (spendRequestId) {
14113
+ setStep("signing");
14114
+ payResult = await runMppPayWithSpendRequest(
14115
+ url,
14116
+ spendRequestId,
14117
+ method,
14118
+ data,
14119
+ headers,
14120
+ repository
13989
14121
  );
13990
- }
13991
- if (spendRequest.status !== "approved") {
13992
- throw new Error(
13993
- `Spend request must be approved (current status: ${spendRequest.status})`
13994
- );
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
14122
+ } else {
14123
+ if (!context) {
14124
+ throw new Error(
14125
+ "--context is required for the full MPP flow (min 100 chars)"
14126
+ );
14026
14127
  }
14027
- });
14028
- const payResult = await readPayResult(retryResponse);
14128
+ payResult = await runMppPayFullFlow({
14129
+ url,
14130
+ method,
14131
+ data,
14132
+ headers,
14133
+ context,
14134
+ amountOverride,
14135
+ paymentMethodId,
14136
+ test: test ?? false,
14137
+ repository,
14138
+ paymentMethodsFactory,
14139
+ onStep: setStep,
14140
+ onApprovalUrl: (u) => setApprovalUrl(u)
14141
+ });
14142
+ }
14029
14143
  setResult(payResult);
14030
14144
  setStep("done");
14031
14145
  onComplete(payResult);
@@ -14034,10 +14148,24 @@ function MppPay({
14034
14148
  onComplete(null);
14035
14149
  }
14036
14150
  })();
14037
- }, [url, spendRequestId, method, data, headers, repository, onComplete]);
14151
+ }, [
14152
+ url,
14153
+ spendRequestId,
14154
+ method,
14155
+ data,
14156
+ headers,
14157
+ context,
14158
+ amountOverride,
14159
+ paymentMethodId,
14160
+ test,
14161
+ repository,
14162
+ paymentMethodsFactory,
14163
+ onComplete
14164
+ ]);
14038
14165
  const stepLabels = {
14039
- retrieving: "Retrieving spend request",
14040
- probing: "Probing URL",
14166
+ probing: "Probing URL for 402 challenge",
14167
+ creating: "Creating spend request",
14168
+ approving: "Waiting for approval",
14041
14169
  signing: "Signing credential",
14042
14170
  submitting: "Submitting payment",
14043
14171
  done: "Done"
@@ -14049,12 +14177,19 @@ function MppPay({
14049
14177
  ] });
14050
14178
  }
14051
14179
  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
- ] }) }),
14180
+ step !== "done" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
14181
+ /* @__PURE__ */ jsx10(Box7, { children: /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
14182
+ /* @__PURE__ */ jsx10(Spinner4, { type: "dots" }),
14183
+ " ",
14184
+ stepLabels[step],
14185
+ "..."
14186
+ ] }) }),
14187
+ step === "approving" && approvalUrl && /* @__PURE__ */ jsx10(Box7, { marginTop: 1, paddingX: 2, children: /* @__PURE__ */ jsxs7(Text9, { children: [
14188
+ "Approve in Link app:",
14189
+ " ",
14190
+ /* @__PURE__ */ jsx10(Text9, { bold: true, color: "blue", children: approvalUrl })
14191
+ ] }) })
14192
+ ] }),
14058
14193
  result && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
14059
14194
  /* @__PURE__ */ jsxs7(
14060
14195
  Text9,
@@ -14221,7 +14356,7 @@ var SptFlow = ({
14221
14356
  setStep("mpp-pay-gate");
14222
14357
  await waitForEnter();
14223
14358
  setStep("mpp-pay");
14224
- const payResponse = await runMppPay(
14359
+ const payResponse = await runMppPayWithSpendRequest(
14225
14360
  DEMO_CLIMATE_API_URL,
14226
14361
  result.id,
14227
14362
  "POST",
@@ -14641,12 +14776,22 @@ function DecodeChallengeView({
14641
14776
  // src/commands/mpp/schema.ts
14642
14777
  import { z as z4 } from "incur";
14643
14778
  var payOptions = z4.object({
14644
- spendRequestId: z4.string().describe(
14645
- 'Approved spend request ID with credential_type "shared_payment_token"'
14779
+ spendRequestId: z4.string().optional().describe(
14780
+ '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
14781
  ),
14647
14782
  method: z4.string().optional().describe("HTTP method (default: GET, or POST if --data is provided)"),
14648
14783
  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)')
14784
+ header: z4.array(z4.string()).default([]).describe('Request header in "Name: Value" format (repeatable)'),
14785
+ context: z4.string().min(100).optional().describe(
14786
+ "Min 100 chars \u2014 describe the purchase and rationale; the user reads this when approving. Required when --spend-request-id is not provided."
14787
+ ),
14788
+ amount: z4.coerce.number().int().positive().optional().describe(
14789
+ "Amount in cents (derived from 402 challenge if omitted; required if challenge has no amount)"
14790
+ ),
14791
+ paymentMethodId: z4.string().optional().describe("Payment method ID (uses default if omitted)"),
14792
+ test: z4.boolean().default(false).describe(
14793
+ "Use test mode (creates testmode credentials from test card data)"
14794
+ )
14650
14795
  });
14651
14796
  var decodeOptions = z4.object({
14652
14797
  challenge: z4.string().describe(
@@ -14656,12 +14801,12 @@ var decodeOptions = z4.object({
14656
14801
 
14657
14802
  // src/commands/mpp/index.tsx
14658
14803
  import { jsx as jsx15 } from "react/jsx-runtime";
14659
- function createMppCli(repository, authStorage2, envAccessToken2) {
14804
+ function createMppCli(repository, paymentMethodsFactory, authStorage2, envAccessToken2) {
14660
14805
  const cli2 = Cli4.create("mpp", {
14661
14806
  description: "Machine payment protocol (MPP) commands"
14662
14807
  });
14663
14808
  cli2.command("pay", {
14664
- description: "Complete a machine payment protocol (MPP) payment using an approved spend request",
14809
+ 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
14810
  args: z5.object({
14666
14811
  url: z5.string().describe("URL to pay")
14667
14812
  }),
@@ -14669,7 +14814,7 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
14669
14814
  alias: { method: "X", data: "d", header: "H" },
14670
14815
  outputPolicy: "agent-only",
14671
14816
  middleware: [requireAuth(authStorage2, envAccessToken2)],
14672
- async run(c) {
14817
+ async *run(c) {
14673
14818
  const url = c.args.url;
14674
14819
  const opts = c.options;
14675
14820
  const method = opts.method;
@@ -14686,7 +14831,12 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
14686
14831
  method,
14687
14832
  data,
14688
14833
  headers,
14834
+ context: opts.context,
14835
+ amountOverride: opts.amount,
14836
+ paymentMethodId: opts.paymentMethodId,
14837
+ test: opts.test,
14689
14838
  repository,
14839
+ paymentMethodsFactory,
14690
14840
  onComplete: (result) => {
14691
14841
  capturedResult = result;
14692
14842
  }
@@ -14699,14 +14849,90 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
14699
14849
  }
14700
14850
  );
14701
14851
  }
14702
- return runMppPay(
14703
- url,
14704
- opts.spendRequestId,
14705
- method,
14706
- data,
14707
- headers,
14708
- repository
14709
- );
14852
+ if (opts.spendRequestId) {
14853
+ yield await runMppPayWithSpendRequest(
14854
+ url,
14855
+ opts.spendRequestId,
14856
+ method,
14857
+ data,
14858
+ headers,
14859
+ repository
14860
+ );
14861
+ return;
14862
+ }
14863
+ const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
14864
+ const requestHeaders = buildHeaders(data, headers);
14865
+ const probeResponse = await fetch(url, {
14866
+ method: httpMethod,
14867
+ body: data,
14868
+ headers: requestHeaders
14869
+ });
14870
+ if (probeResponse.status !== 402) {
14871
+ yield await readPayResult(probeResponse);
14872
+ return;
14873
+ }
14874
+ const wwwAuth = probeResponse.headers.get("www-authenticate");
14875
+ if (!wwwAuth) {
14876
+ return c.error({
14877
+ code: "INVALID_RESPONSE",
14878
+ message: "URL returned 402 but no WWW-Authenticate header"
14879
+ });
14880
+ }
14881
+ const decoded = decodeStripeChallenge(wwwAuth);
14882
+ const networkId = decoded.network_id;
14883
+ const challengeAmount = decoded.request_json.amount ? Number(decoded.request_json.amount) : void 0;
14884
+ const challengeCurrency = decoded.request_json.currency ?? "usd";
14885
+ const amount = opts.amount ?? challengeAmount;
14886
+ if (!amount) {
14887
+ return c.error({
14888
+ code: "INVALID_INPUT",
14889
+ message: "Could not determine amount from 402 challenge. Pass --amount explicitly."
14890
+ });
14891
+ }
14892
+ if (!opts.context) {
14893
+ return c.error({
14894
+ code: "INVALID_INPUT",
14895
+ message: "--context is required for the full MPP flow (min 100 chars). Describe the purchase and rationale."
14896
+ });
14897
+ }
14898
+ let pmId = opts.paymentMethodId;
14899
+ if (!pmId) {
14900
+ const pmResource = paymentMethodsFactory();
14901
+ const methods = await pmResource.list();
14902
+ if (!methods.length) {
14903
+ return c.error({
14904
+ code: "NO_PAYMENT_METHOD",
14905
+ message: "No payment methods found. Add one with `link-cli payment-methods add`."
14906
+ });
14907
+ }
14908
+ pmId = methods[0].id;
14909
+ }
14910
+ const spendRequest = await repository.createSpendRequest({
14911
+ payment_details: pmId,
14912
+ credential_type: "shared_payment_token",
14913
+ network_id: networkId,
14914
+ amount,
14915
+ currency: challengeCurrency,
14916
+ context: opts.context,
14917
+ request_approval: true,
14918
+ test: opts.test || void 0
14919
+ });
14920
+ const nextFlags = [`--spend-request-id ${spendRequest.id}`];
14921
+ if (method) nextFlags.push(`-X ${method}`);
14922
+ if (data) nextFlags.push(`-d '${data}'`);
14923
+ if (headers) {
14924
+ for (const h of headers) nextFlags.push(`-H '${h}'`);
14925
+ }
14926
+ const nextCommand = `mpp pay ${url} ${nextFlags.join(" ")}`;
14927
+ yield {
14928
+ ...spendRequest,
14929
+ 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.`,
14930
+ _next: {
14931
+ poll_command: `spend-request retrieve ${spendRequest.id} --interval 2 --max-attempts 300`,
14932
+ pay_command: nextCommand,
14933
+ until: "status changes from pending_approval, then run pay_command"
14934
+ }
14935
+ };
14710
14936
  }
14711
14937
  });
14712
14938
  cli2.command("decode", {
@@ -15083,17 +15309,47 @@ async function sendWebResponse(webRes, res) {
15083
15309
  res.writeHead(webRes.status);
15084
15310
  res.end(Buffer.from(buffer));
15085
15311
  }
15312
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
15313
+ function isLoopbackHost(host) {
15314
+ return LOOPBACK_HOSTS.has(host.toLowerCase());
15315
+ }
15316
+ function isAllowedOrigin(origin) {
15317
+ if (!origin) return true;
15318
+ try {
15319
+ return isLoopbackHost(new URL(origin).hostname);
15320
+ } catch {
15321
+ return false;
15322
+ }
15323
+ }
15324
+ function isAllowedRoute(method, pathname) {
15325
+ if (pathname === "/mcp") return true;
15326
+ if (pathname.startsWith("/.well-known/skills/") && method === "GET")
15327
+ return true;
15328
+ return false;
15329
+ }
15086
15330
  function createServeCli(rootCli) {
15087
15331
  return Cli8.create("serve", {
15088
15332
  description: "Start an HTTP server exposing link-cli as an MCP endpoint at /mcp",
15089
15333
  options: z7.object({
15090
- port: z7.coerce.number().default(54321).describe("Port to listen on")
15334
+ port: z7.coerce.number().default(54321).describe("Port to listen on"),
15335
+ host: z7.string().default("127.0.0.1").describe(
15336
+ "Host/interface to bind. Defaults to loopback; set explicitly (e.g. 0.0.0.0) to expose beyond localhost."
15337
+ )
15091
15338
  }),
15092
15339
  async run(c) {
15093
- const { port } = c.options;
15340
+ const { port, host } = c.options;
15094
15341
  const server = createServer(
15095
15342
  async (req, res) => {
15096
- res.setHeader("Access-Control-Allow-Origin", "*");
15343
+ const origin = req.headers.origin;
15344
+ if (!isAllowedOrigin(origin)) {
15345
+ res.writeHead(403, { "Content-Type": "application/json" });
15346
+ res.end(JSON.stringify({ error: "forbidden origin" }));
15347
+ return;
15348
+ }
15349
+ if (origin) {
15350
+ res.setHeader("Access-Control-Allow-Origin", origin);
15351
+ res.setHeader("Vary", "Origin");
15352
+ }
15097
15353
  res.setHeader(
15098
15354
  "Access-Control-Allow-Methods",
15099
15355
  "GET, POST, DELETE, OPTIONS"
@@ -15107,6 +15363,12 @@ function createServeCli(rootCli) {
15107
15363
  res.end();
15108
15364
  return;
15109
15365
  }
15366
+ const pathname = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
15367
+ if (!isAllowedRoute(req.method ?? "GET", pathname)) {
15368
+ res.writeHead(404, { "Content-Type": "application/json" });
15369
+ res.end(JSON.stringify({ error: "not found" }));
15370
+ return;
15371
+ }
15110
15372
  try {
15111
15373
  const webReq = await nodeRequestToWebRequest(req, port);
15112
15374
  const webRes = await rootCli.fetch(webReq);
@@ -15120,9 +15382,16 @@ function createServeCli(rootCli) {
15120
15382
  );
15121
15383
  await new Promise((resolve, reject) => {
15122
15384
  server.on("error", reject);
15123
- server.listen(port, () => {
15385
+ server.listen(port, host, () => {
15386
+ if (!isLoopbackHost(host)) {
15387
+ process.stderr.write(
15388
+ `WARNING: link-cli serve is bound to ${host}, which may be reachable beyond localhost.
15389
+ Any caller that can reach this port can use the authenticated Link session of this CLI. Only do this on a trusted, isolated network.
15390
+ `
15391
+ );
15392
+ }
15124
15393
  process.stderr.write(
15125
- `link-cli MCP server listening on http://localhost:${port}/mcp
15394
+ `link-cli MCP server listening on http://${host}:${port}/mcp
15126
15395
  `
15127
15396
  );
15128
15397
  });
@@ -15733,6 +16002,7 @@ var CreateSpendRequest = ({
15733
16002
  const [request, setRequest] = useState9(null);
15734
16003
  const [error, setError] = useState9("");
15735
16004
  const [verificationUrl, setVerificationUrl] = useState9("");
16005
+ const [supportUrl, setSupportUrl] = useState9("");
15736
16006
  const [outputFilePath, setOutputFilePath] = useState9(null);
15737
16007
  const [fileError, setFileError] = useState9("");
15738
16008
  const approvalUrl = request?.approval_url ?? "";
@@ -15773,8 +16043,11 @@ var CreateSpendRequest = ({
15773
16043
  } catch (err) {
15774
16044
  setError(err.message);
15775
16045
  if (err instanceof LinkApiError) {
15776
- const url = err.details?.error?.verification_url;
15777
- if (url) setVerificationUrl(url);
16046
+ const errDetail = err.details;
16047
+ if (errDetail?.error?.verification_url)
16048
+ setVerificationUrl(errDetail.error.verification_url);
16049
+ if (errDetail?.error?.support_url)
16050
+ setSupportUrl(errDetail.error.support_url);
15778
16051
  }
15779
16052
  setStatus("error");
15780
16053
  setTimeout(() => completeAndExit(null), DISPLAY_DELAY_MS);
@@ -15807,6 +16080,10 @@ var CreateSpendRequest = ({
15807
16080
  verificationUrl && /* @__PURE__ */ jsxs18(Text20, { color: "red", children: [
15808
16081
  "Complete additional verification at: ",
15809
16082
  verificationUrl
16083
+ ] }),
16084
+ supportUrl && /* @__PURE__ */ jsxs18(Text20, { color: "red", children: [
16085
+ "Identity verification failed. Contact support at: ",
16086
+ supportUrl
15810
16087
  ] })
15811
16088
  ] });
15812
16089
  }
@@ -15976,6 +16253,7 @@ var RequestApproval = ({
15976
16253
  const [result, setResult] = useState10(null);
15977
16254
  const [error, setError] = useState10("");
15978
16255
  const [verificationUrl, setVerificationUrl] = useState10("");
16256
+ const [supportUrl, setSupportUrl] = useState10("");
15979
16257
  const { exit } = useApp6();
15980
16258
  const completeAndExit = useCallback10(
15981
16259
  (result2) => {
@@ -16005,8 +16283,11 @@ var RequestApproval = ({
16005
16283
  } catch (err) {
16006
16284
  setError(err.message);
16007
16285
  if (err instanceof LinkApiError) {
16008
- const url = err.details?.error?.verification_url;
16009
- if (url) setVerificationUrl(url);
16286
+ const errDetail = err.details;
16287
+ if (errDetail?.error?.verification_url)
16288
+ setVerificationUrl(errDetail.error.verification_url);
16289
+ if (errDetail?.error?.support_url)
16290
+ setSupportUrl(errDetail.error.support_url);
16010
16291
  }
16011
16292
  setStatus("error");
16012
16293
  setTimeout(() => {
@@ -16030,6 +16311,10 @@ var RequestApproval = ({
16030
16311
  verificationUrl && /* @__PURE__ */ jsxs20(Text22, { color: "red", children: [
16031
16312
  "Complete additional verification at: ",
16032
16313
  verificationUrl
16314
+ ] }),
16315
+ supportUrl && /* @__PURE__ */ jsxs20(Text22, { color: "red", children: [
16316
+ "Identity verification failed. Contact support at: ",
16317
+ supportUrl
16033
16318
  ] })
16034
16319
  ] });
16035
16320
  }
@@ -16315,7 +16600,16 @@ var RetrieveSpendRequest = ({
16315
16600
  /* @__PURE__ */ jsx30(Text23, { bold: true, children: psd.refund_details.state })
16316
16601
  ] })
16317
16602
  ] })
16318
- ] })
16603
+ ] }),
16604
+ request?.link_transaction_id && /* @__PURE__ */ jsx30(Box21, { marginTop: 1, children: /* @__PURE__ */ jsxs21(Text23, { children: [
16605
+ "Transaction ID: ",
16606
+ /* @__PURE__ */ jsx30(Text23, { bold: true, children: request.link_transaction_id })
16607
+ ] }) }),
16608
+ request?.status === "succeeded" && request?.activity_url && /* @__PURE__ */ jsx30(Box21, { marginTop: 1, children: /* @__PURE__ */ jsxs21(Text23, { children: [
16609
+ "Activity URL:",
16610
+ " ",
16611
+ /* @__PURE__ */ jsx30(Text23, { bold: true, color: "cyan", children: request.activity_url })
16612
+ ] }) })
16319
16613
  ] })
16320
16614
  ] });
16321
16615
  }
@@ -16779,6 +17073,12 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
16779
17073
  message: `${err.message} Verification URL: ${apiErr.error.verification_url}`
16780
17074
  });
16781
17075
  }
17076
+ if (apiErr?.error?.support_url) {
17077
+ return c.error({
17078
+ code: err.code,
17079
+ message: `${err.message} Support URL: ${apiErr.error.support_url}`
17080
+ });
17081
+ }
16782
17082
  }
16783
17083
  throw err;
16784
17084
  }
@@ -16898,6 +17198,12 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
16898
17198
  message: `${err.message} Verification URL: ${apiErr.error.verification_url}`
16899
17199
  });
16900
17200
  }
17201
+ if (apiErr?.error?.support_url) {
17202
+ return c.error({
17203
+ code: err.code,
17204
+ message: `${err.message} Support URL: ${apiErr.error.support_url}`
17205
+ });
17206
+ }
16901
17207
  }
16902
17208
  throw err;
16903
17209
  }
@@ -17850,7 +18156,7 @@ function cacheUpdateInfo(value, ttlMs = UPDATE_CACHE_TTL_MS) {
17850
18156
  }
17851
18157
 
17852
18158
  // src/cli.tsx
17853
- var cliVersion = "0.9.0";
18159
+ var cliVersion = "0.10.1";
17854
18160
  var cliName = "@stripe/link-cli";
17855
18161
  var defaultHeaders = {
17856
18162
  "User-Agent": `link-cli/${cliVersion}`
@@ -17941,7 +18247,14 @@ if (!hiddenCli) {
17941
18247
  envAccessToken
17942
18248
  )
17943
18249
  );
17944
- cli.command(createMppCli(spendRequestRepo, authStorage, envAccessToken));
18250
+ cli.command(
18251
+ createMppCli(
18252
+ spendRequestRepo,
18253
+ () => factory.createPaymentMethodsResource(),
18254
+ authStorage,
18255
+ envAccessToken
18256
+ )
18257
+ );
17945
18258
  cli.command(
17946
18259
  createReportCli(
17947
18260
  () => 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.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-cli": "./dist/cli.js"
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "dist",
10
10
  "!dist/sea",
11
+ "postinstall.mjs",
11
12
  "README.md",
12
13
  "LICENSE"
13
14
  ],
@@ -49,6 +50,7 @@
49
50
  "scripts": {
50
51
  "build": "tsup",
51
52
  "build:sea": "tsup --config tsup.sea.config.ts",
53
+ "postinstall": "node postinstall.mjs",
52
54
  "typecheck": "tsc",
53
55
  "test": "vitest run",
54
56
  "dev": "tsx src/cli.tsx"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Refresh the create-payment-credential skill whenever the CLI is (re)installed
4
+ // or upgraded via npm. Delegates to the openclaw `skills` CLI so the skill file
5
+ // stays in sync with the installed CLI version. Must never fail the install.
6
+
7
+ import { spawnSync } from 'node:child_process';
8
+ import process from 'node:process';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const REPO = 'stripe/link-cli';
12
+
13
+ function run() {
14
+ // Skip when running from the source monorepo (dev `pnpm install`). Installed
15
+ // copies live under node_modules; the dev tree does not.
16
+ if (!fileURLToPath(import.meta.url).includes('node_modules')) {
17
+ return;
18
+ }
19
+
20
+ if (process.env.CI || process.env.LINK_CLI_SKIP_SKILL_INSTALL) {
21
+ return;
22
+ }
23
+
24
+ process.stdout.write(
25
+ 'link-cli: refreshing the create-payment-credential skill…\n',
26
+ );
27
+
28
+ const result = spawnSync(
29
+ 'npx',
30
+ ['--yes', 'skills', 'add', REPO, '-g', '-y'],
31
+ {
32
+ stdio: 'inherit',
33
+ timeout: 60_000,
34
+ },
35
+ );
36
+
37
+ if (result.error || result.status !== 0) {
38
+ process.stdout.write(
39
+ `link-cli: skipped skill refresh; run 'npx skills add ${REPO}' manually.\n`,
40
+ );
41
+ }
42
+ }
43
+
44
+ try {
45
+ run();
46
+ } catch {
47
+ // Never fail the install.
48
+ }
49
+
50
+ process.exit(0);