@stripe/link-cli 0.15.1 → 0.17.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 +18 -0
  2. package/dist/cli.js +130 -33
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -310,6 +310,24 @@ card SpendRequest instead; do not create an LPT request.
310
310
  | Rolling creation rate | 200 per 60 days |
311
311
 
312
312
 
313
+ Use `mpp pay` to complete purchases on merchants that use the [Machine Payments Protocol](https://mpp.dev). The spend request must use `credential_type: "shared_payment_token"` and you must approve it before paying. The SPT is one-time-use — if payment fails, create a new spend request.
314
+
315
+ ```bash
316
+ link-cli mpp pay https://climate.stripe.dev/api/contribute \
317
+ --spend-request-id lsrq_001 \
318
+ --method POST \
319
+ --data '{"amount":100}' \
320
+ --header "X-Custom: value"
321
+ ```
322
+
323
+ In agent mode (`--format json`), the full flow returns the payment continuation twice: as `_next.pay_argv` (`{ "command": "mpp", "args": [...] }`) and as `_next.pay_command`. Prefer `pay_argv` and invoke it directly, passing each `args` entry as its own process argument. The URL, body and headers can carry merchant-controlled text, so `pay_command` is shell-quoted for callers that must go through a shell — pass it to the shell verbatim, without unquoting or re-splitting it.
324
+
325
+ Use `mpp decode` to validate a raw `WWW-Authenticate` header and extract the `network_id` needed for `shared_payment_token` spend requests:
326
+
327
+ ```bash
328
+ link-cli mpp decode \
329
+ --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."'
330
+ ```
313
331
 
314
332
  ### Report outcomes
315
333
 
package/dist/cli.js CHANGED
@@ -593,7 +593,9 @@ var BalancesResource = class extends BaseResource {
593
593
  var paymentMethodSchema = z2.looseObject({
594
594
  id: z2.string(),
595
595
  type: z2.string(),
596
- is_default: z2.boolean()
596
+ is_default: z2.boolean(),
597
+ name: z2.string(),
598
+ nickname: z2.optional(z2.string().nullable())
597
599
  });
598
600
  var paymentMethodsResponseSchema = z2.looseObject({
599
601
  payment_details: z2.array(paymentMethodSchema)
@@ -800,11 +802,13 @@ var SpendRequestResource = class extends BaseResource {
800
802
  );
801
803
  }
802
804
  async update(id, params) {
805
+ const { approve, ...body } = params;
806
+ const url = approve ? `${this.endpoint}/${id}/update_delegated` : `${this.endpoint}/${id}`;
803
807
  const { status, data, rawBody } = await this.apiFetch({
804
808
  method: "POST",
805
- url: `${this.endpoint}/${id}`,
809
+ url,
806
810
  headers: { "Content-Type": "application/json" },
807
- body: JSON.stringify(params)
811
+ body: JSON.stringify(body)
808
812
  });
809
813
  if (status < 200 || status >= 300) {
810
814
  this.throwApiError("update spend request", status, data, rawBody);
@@ -2799,7 +2803,7 @@ function buildHeaders(data, headers) {
2799
2803
  if (key) result[key] = value;
2800
2804
  }
2801
2805
  if (!Object.keys(result).some((key) => key.toLowerCase() === "user-agent")) {
2802
- result["User-Agent"] = `link-cli/${"0.15.1"}`;
2806
+ result["User-Agent"] = `link-cli/${"0.17.0"}`;
2803
2807
  }
2804
2808
  return result;
2805
2809
  }
@@ -3658,6 +3662,21 @@ function createDemoCli(authRepo2, spendRequestRepo2, createPaymentMethodsResourc
3658
3662
  // src/commands/mpp/index.tsx
3659
3663
  import { Cli as Cli4, z as z14 } from "incur";
3660
3664
 
3665
+ // src/utils/shell-quote.ts
3666
+ var SAFE_RE = /^[A-Za-z0-9_@%+=:,./-]+$/;
3667
+ function shellQuote(value) {
3668
+ if (value === "") {
3669
+ return "''";
3670
+ }
3671
+ if (SAFE_RE.test(value)) {
3672
+ return value;
3673
+ }
3674
+ return `'${value.replaceAll("'", `'\\''`)}'`;
3675
+ }
3676
+ function shellCommand(parts) {
3677
+ return parts.map(shellQuote).join(" ");
3678
+ }
3679
+
3661
3680
  // src/commands/mpp/decode-view.tsx
3662
3681
  import { Box as Box10, Text as Text12 } from "ink";
3663
3682
  import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -3829,20 +3848,22 @@ function createMppCli(repository, paymentMethodsFactory, authStorage2, envAccess
3829
3848
  request_approval: true,
3830
3849
  test: opts.test || void 0
3831
3850
  });
3832
- const nextFlags = [`--spend-request-id ${spendRequest.id}`];
3833
- if (method) nextFlags.push(`-X ${method}`);
3834
- if (data) nextFlags.push(`-d '${data}'`);
3851
+ const nextArgs = ["pay", url, "--spend-request-id", spendRequest.id];
3852
+ if (method) nextArgs.push("-X", method);
3853
+ if (data) nextArgs.push("-d", data);
3835
3854
  if (headers) {
3836
- for (const h of headers) nextFlags.push(`-H '${h}'`);
3855
+ for (const h of headers) nextArgs.push("-H", h);
3837
3856
  }
3838
- const nextCommand = `mpp pay ${url} ${nextFlags.join(" ")}`;
3857
+ const nextCommand = `mpp ${shellCommand(nextArgs)}`;
3858
+ const pollCommand = `spend-request retrieve ${shellQuote(spendRequest.id)} --interval 2 --max-attempts 300`;
3839
3859
  yield {
3840
3860
  ...spendRequest,
3841
- 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.`,
3861
+ instruction: `Present the approval_url to the user and ask them to approve in the Link app. Then call \`${pollCommand}\` to poll until approved. Once approved, run _next.pay_argv (preferred \u2014 invoke it directly without a shell) or _next.pay_command to complete payment. Do not wait for the user to reply \u2014 start polling immediately.`,
3842
3862
  _next: {
3843
- poll_command: `spend-request retrieve ${spendRequest.id} --interval 2 --max-attempts 300`,
3863
+ poll_command: pollCommand,
3844
3864
  pay_command: nextCommand,
3845
- until: "status changes from pending_approval, then run pay_command"
3865
+ pay_argv: { command: "mpp", args: nextArgs },
3866
+ until: "status changes from pending_approval, then run pay_argv"
3846
3867
  }
3847
3868
  };
3848
3869
  }
@@ -4094,7 +4115,7 @@ var PaymentMethodsList = ({
4094
4115
  return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
4095
4116
  /* @__PURE__ */ jsx19(Text15, { bold: true, children: "Payment Methods" }),
4096
4117
  /* @__PURE__ */ jsx19(Box13, { flexDirection: "column", marginTop: 1, children: methods.map((pm) => {
4097
- const label = pm.card_details?.brand ?? pm.bank_account_details?.bank_name ?? "Bank account";
4118
+ const label = pm.name ?? pm.card_details?.brand ?? pm.bank_account_details?.bank_name ?? "Bank account";
4098
4119
  const last4 = pm.card_details?.last4 ?? pm.bank_account_details?.last4;
4099
4120
  const suffix = pm.nickname ? `(${pm.nickname})` : "";
4100
4121
  const agenticCap = pm.capabilities?.agentic_payments;
@@ -4920,6 +4941,7 @@ var CreateSpendRequest = ({
4920
4941
  repository,
4921
4942
  params,
4922
4943
  requestApproval = false,
4944
+ approve = false,
4923
4945
  outputFile,
4924
4946
  force,
4925
4947
  onComplete
@@ -5058,7 +5080,7 @@ var CreateSpendRequest = ({
5058
5080
  result.status_details?.requires_action?.next_action ?? null
5059
5081
  );
5060
5082
  setStatus("requires_action");
5061
- } else if (requestApproval) {
5083
+ } else if (requestApproval && result.status !== "approved") {
5062
5084
  setStatus("waiting");
5063
5085
  } else {
5064
5086
  setStatus("success");
@@ -5333,7 +5355,7 @@ var CreateSpendRequest = ({
5333
5355
  fileError
5334
5356
  ] })
5335
5357
  ] }),
5336
- /* @__PURE__ */ jsx27(AppDownloadQrCodes, {})
5358
+ !approve && /* @__PURE__ */ jsx27(AppDownloadQrCodes, {})
5337
5359
  ] });
5338
5360
  }
5339
5361
  return /* @__PURE__ */ jsxs18(Fragment4, { children: [
@@ -6171,13 +6193,39 @@ var updateOptions = z19.object({
6171
6193
  ),
6172
6194
  total: z19.array(z19.union([z19.string(), z19.record(z19.string(), z19.unknown())])).default([]).describe(
6173
6195
  'Total (repeatable, key:value format). Keys: type (required; one of: subtotal, tax, total, items_base_amount, items_discount, discount, fulfillment, shipping, fee, gift_wrap, tip, store_credit), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"'
6174
- )
6196
+ ),
6197
+ approve: z19.boolean().default(false).describe("Use the delegated approval flow for this update")
6175
6198
  });
6176
6199
 
6177
6200
  // src/commands/spend-request/update.tsx
6178
- import { Box as Box22, Text as Text24 } from "ink";
6201
+ import { Box as Box22, Text as Text24, useInput as useInput10 } from "ink";
6179
6202
  import Spinner14 from "ink-spinner";
6180
- import { useCallback as useCallback11 } from "react";
6203
+ import { useCallback as useCallback11, useState as useState12 } from "react";
6204
+
6205
+ // src/utils/poll-until-spend-request-update.ts
6206
+ async function pollUntilSpendRequestUpdate(repository, id, pendingAmount, options = {}) {
6207
+ const pollIntervalMs = options.pollIntervalMs ?? 2e3;
6208
+ const timeoutMs = options.timeoutMs ?? 3e5;
6209
+ const startTime = Date.now();
6210
+ while (true) {
6211
+ if (Date.now() - startTime > timeoutMs) {
6212
+ throw new Error("Spend request update polling timed out");
6213
+ }
6214
+ const request = await repository.retrieve(id);
6215
+ if (!request) {
6216
+ throw new Error(`Spend request ${id} not found`);
6217
+ }
6218
+ if (request.amount === pendingAmount) {
6219
+ return { request, outcome: "approved" };
6220
+ }
6221
+ if (!request.approval_url) {
6222
+ return { request, outcome: "denied" };
6223
+ }
6224
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
6225
+ }
6226
+ }
6227
+
6228
+ // src/commands/spend-request/update.tsx
6181
6229
  import { jsx as jsx31, jsxs as jsxs22 } from "react/jsx-runtime";
6182
6230
  var UpdateSpendRequest = ({
6183
6231
  repository,
@@ -6185,12 +6233,45 @@ var UpdateSpendRequest = ({
6185
6233
  params,
6186
6234
  onComplete
6187
6235
  }) => {
6188
- const action = useCallback11(
6189
- () => repository.update(id, params),
6190
- [repository, id, params]
6236
+ const [pendingApproval, setPendingApproval] = useState12(
6237
+ null
6238
+ );
6239
+ const action = useCallback11(async () => {
6240
+ const request2 = await repository.update(id, params);
6241
+ if (params.amount !== void 0 && request2.status === "approved" && request2.approval_url) {
6242
+ setPendingApproval(request2);
6243
+ return pollUntilSpendRequestUpdate(repository, id, params.amount);
6244
+ }
6245
+ return { request: request2, outcome: "updated" };
6246
+ }, [repository, id, params]);
6247
+ const handleComplete = useCallback11(
6248
+ (result2) => onComplete(result2?.request ?? null),
6249
+ [onComplete]
6250
+ );
6251
+ const {
6252
+ status,
6253
+ data: result,
6254
+ error
6255
+ } = useAsyncAction(action, handleComplete);
6256
+ const request = result?.request ?? pendingApproval;
6257
+ useInput10(
6258
+ (_input, key) => {
6259
+ if (key.return && pendingApproval?.approval_url) {
6260
+ openUrl(pendingApproval.approval_url);
6261
+ }
6262
+ },
6263
+ { isActive: status === "loading" && pendingApproval !== null }
6191
6264
  );
6192
- const { status, data: request, error } = useAsyncAction(action, onComplete);
6193
6265
  if (status === "loading") {
6266
+ if (pendingApproval?.approval_url) {
6267
+ return /* @__PURE__ */ jsx31(
6268
+ ApprovalWaitingView,
6269
+ {
6270
+ status: "polling",
6271
+ approvalUrl: pendingApproval.approval_url
6272
+ }
6273
+ );
6274
+ }
6194
6275
  return /* @__PURE__ */ jsx31(Box22, { children: /* @__PURE__ */ jsxs22(Text24, { color: "cyan", children: [
6195
6276
  /* @__PURE__ */ jsx31(Spinner14, { type: "dots" }),
6196
6277
  " Updating spend request ",
@@ -6204,6 +6285,21 @@ var UpdateSpendRequest = ({
6204
6285
  /* @__PURE__ */ jsx31(Text24, { color: "red", children: error })
6205
6286
  ] });
6206
6287
  }
6288
+ if (result?.outcome === "denied") {
6289
+ return /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", children: [
6290
+ /* @__PURE__ */ jsx31(Text24, { color: "yellow", children: "\u2717 Spend request update denied" }),
6291
+ /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
6292
+ /* @__PURE__ */ jsxs22(Text24, { children: [
6293
+ "ID: ",
6294
+ /* @__PURE__ */ jsx31(Text24, { bold: true, children: request?.id })
6295
+ ] }),
6296
+ /* @__PURE__ */ jsxs22(Text24, { children: [
6297
+ "Amount: ",
6298
+ /* @__PURE__ */ jsx31(Text24, { bold: true, children: request?.amount ?? "N/A" })
6299
+ ] })
6300
+ ] })
6301
+ ] });
6302
+ }
6207
6303
  return /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", children: [
6208
6304
  /* @__PURE__ */ jsx31(Text24, { color: "green", children: "\u2713 Spend request updated" }),
6209
6305
  /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
@@ -6218,10 +6314,7 @@ var UpdateSpendRequest = ({
6218
6314
  /* @__PURE__ */ jsxs22(Text24, { children: [
6219
6315
  "Amount:",
6220
6316
  " ",
6221
- /* @__PURE__ */ jsx31(Text24, { bold: true, children: (() => {
6222
- const t = request?.totals?.find((t2) => t2.type === "total");
6223
- return t ? String(t.amount) : "N/A";
6224
- })() })
6317
+ /* @__PURE__ */ jsx31(Text24, { bold: true, children: request?.amount !== void 0 ? String(request.amount) : "N/A" })
6225
6318
  ] }),
6226
6319
  /* @__PURE__ */ jsxs22(Text24, { children: [
6227
6320
  "Merchant: ",
@@ -6243,9 +6336,9 @@ function buildRequiresActionResult(request) {
6243
6336
  const isAutoResume2 = nextAction?.resolution === "auto_resume";
6244
6337
  return {
6245
6338
  ...request,
6246
- instruction: isAutoResume2 ? `The spend request requires 3D Secure verification. Present action_url (${nextAction?.action_url}) to the user, then call \`spend-request retrieve ${request.id} --interval 2 --max-attempts 300\` to poll until it resolves. Do not create a new spend request \u2014 this one resumes automatically once the challenge is completed.` : `The spend request requires action (${nextAction?.type}): ${nextAction?.display_message}${nextAction?.action_url ? ` URL: ${nextAction.action_url}` : ""} Have the user complete this, then create a new spend request.`,
6339
+ instruction: isAutoResume2 ? `The spend request requires 3D Secure verification. Present action_url (${nextAction?.action_url}) to the user, then call \`spend-request retrieve ${shellQuote(request.id)} --interval 2 --max-attempts 300\` to poll until it resolves. Do not create a new spend request \u2014 this one resumes automatically once the challenge is completed.` : `The spend request requires action (${nextAction?.type}): ${nextAction?.display_message}${nextAction?.action_url ? ` URL: ${nextAction.action_url}` : ""} Have the user complete this, then create a new spend request.`,
6247
6340
  _next: isAutoResume2 ? {
6248
- command: `spend-request retrieve ${request.id} --interval 2 --max-attempts 300`,
6341
+ command: `spend-request retrieve ${shellQuote(request.id)} --interval 2 --max-attempts 300`,
6249
6342
  until: "status changes from requires_action"
6250
6343
  } : void 0
6251
6344
  };
@@ -6433,6 +6526,7 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
6433
6526
  repository,
6434
6527
  params: createParams,
6435
6528
  requestApproval,
6529
+ approve: opts.approve ? true : void 0,
6436
6530
  outputFile,
6437
6531
  force: forceOverwrite,
6438
6532
  onComplete: (result) => {
@@ -6503,9 +6597,9 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
6503
6597
  }
6504
6598
  yield {
6505
6599
  ...created,
6506
- instruction: `Present the approval_url to the user and ask them to approve in the Link app. Then call \`spend-request retrieve ${created.id} --interval 2 --max-attempts 300\` to poll until approved. Do not wait for the user to reply \u2014 start polling immediately.`,
6600
+ instruction: `Present the approval_url to the user and ask them to approve in the Link app. Then call \`spend-request retrieve ${shellQuote(created.id)} --interval 2 --max-attempts 300\` to poll until approved. Do not wait for the user to reply \u2014 start polling immediately.`,
6507
6601
  _next: {
6508
- command: `spend-request retrieve ${created.id} --interval 2 --max-attempts 300`,
6602
+ command: `spend-request retrieve ${shellQuote(created.id)} --interval 2 --max-attempts 300`,
6509
6603
  until: "status changes from pending_approval"
6510
6604
  }
6511
6605
  };
@@ -6539,6 +6633,9 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
6539
6633
  params.totals = opts.total.map(
6540
6634
  (item) => typeof item === "string" ? parseTotalFlag(item) : item
6541
6635
  );
6636
+ if (opts.approve !== void 0) {
6637
+ params.approve = opts.approve;
6638
+ }
6542
6639
  if (!c.agent && !c.formatExplicit) {
6543
6640
  let capturedResult = null;
6544
6641
  return renderInteractive(
@@ -6616,9 +6713,9 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
6616
6713
  }
6617
6714
  yield {
6618
6715
  ...approval,
6619
- instruction: `Present the approval_url to the user and ask them to approve in the Link app. Then call \`spend-request retrieve ${id} --interval 2 --max-attempts 300\` to poll until approved. Do not wait for the user to reply \u2014 start polling immediately.`,
6716
+ instruction: `Present the approval_url to the user and ask them to approve in the Link app. Then call \`spend-request retrieve ${shellQuote(id)} --interval 2 --max-attempts 300\` to poll until approved. Do not wait for the user to reply \u2014 start polling immediately.`,
6620
6717
  _next: {
6621
- command: `spend-request retrieve ${id} --interval 2 --max-attempts 300`,
6718
+ command: `spend-request retrieve ${shellQuote(id)} --interval 2 --max-attempts 300`,
6622
6719
  until: "status changes from pending_approval"
6623
6720
  }
6624
6721
  };
@@ -7580,7 +7677,7 @@ function cacheUpdateInfo(value, ttlMs = UPDATE_CACHE_TTL_MS) {
7580
7677
  }
7581
7678
 
7582
7679
  // src/cli.tsx
7583
- var cliVersion = "0.15.1";
7680
+ var cliVersion = "0.17.0";
7584
7681
  var cliName = "@stripe/link-cli";
7585
7682
  var defaultHeaders = {
7586
7683
  "User-Agent": `link-cli/${cliVersion}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stripe/link-cli",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-cli": "./dist/cli.js"
@@ -45,7 +45,7 @@
45
45
  "tsx": "^4.23.5",
46
46
  "typescript": "^5.9.3",
47
47
  "vitest": "^4.1.10",
48
- "@stripe/link-sdk": "0.2.1",
48
+ "@stripe/link-sdk": "0.3.0",
49
49
  "@stripe/link-typescript-config": "0.0.0"
50
50
  },
51
51
  "scripts": {