@stripe/link-cli 0.4.2 → 0.4.3

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 +71 -38
  2. package/dist/cli.js +295 -56
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Link CLI
2
2
 
3
- Link CLI lets agents get secure, one-time-use payment credentials from a Link wallet so they can complete purchases on your behalf without ever storing your real card details.
3
+ Link CLI lets agents get secure, one-time-use payment credentials from a Link wallet to complete purchases on your behalf without storing your real card details.
4
+
5
+ The CLI can produce one of two credential types:
6
+
7
+ - A virtual card (PAN) for use with a standard web checkout form. The issued card works anywhere, and is not restricted to Link-enabled sellers or sellers that use Stripe.
8
+ - A [Shared Payment Token](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens) (SPT) when the seller accepts programmatic payments through [Machine Payment Protocols](https://mpp.dev) (MPP)
9
+
10
+ For now, this is only available to US Link accounts.
4
11
 
5
12
  ## Installation
6
13
 
@@ -14,11 +21,31 @@ Or run directly with `npx`:
14
21
  npx @stripe/link-cli
15
22
  ```
16
23
 
17
- You can install the skill via `npx skills add stripe/link-cli`.
24
+ ### Use with agents
25
+
26
+ Install the skill:
27
+
28
+ ```bash
29
+ npx skills add stripe/link-cli
30
+ ```
31
+
32
+ By default when called from an agent (non-TTY), all commands use `toon` output — a compact, LLM-friendly text format. All commands accept `--format [format]` for structured output. Other formats: `json`, `yaml`, `md`, `jsonl`.
18
33
 
19
- ### MCP Server
34
+ List available commands:
35
+
36
+ ```bash
37
+ link-cli --llms-full
38
+ ```
39
+
40
+ Get a command's full schema with `--schema`. Example:
41
+
42
+ ```bash
43
+ link-cli spend-request create --schema
44
+ ```
20
45
 
21
- Link CLI can also run as a local MCP server. Add the following to your MCP client config (`.mcp.json`, etc.)
46
+ #### MCP Server
47
+
48
+ Link CLI can run as a local MCP server. Add the following to your MCP client config (`.mcp.json`, etc.)
22
49
 
23
50
  ```json
24
51
  {
@@ -33,6 +60,12 @@ Link CLI can also run as a local MCP server. Add the following to your MCP clien
33
60
 
34
61
  ## Quickstart
35
62
 
63
+ Run a guided onboarding and demo flow:
64
+
65
+ ```bash
66
+ link-cli onboard
67
+ ```
68
+
36
69
  ### Login
37
70
 
38
71
  The `link-cli` requires a Link account. You can log in to your existing one or [register online](https://app.link.com).
@@ -41,7 +74,7 @@ The `link-cli` requires a Link account. You can log in to your existing one or [
41
74
  link-cli auth login
42
75
  ```
43
76
 
44
- You'll receive a verification URL and a short phrase. Visit the URL, log in to your Link account, and enter the phrase to approve the connection.
77
+ You receive a verification URL and a short phrase. Visit the URL, log in to your Link account, and enter the phrase to approve the connection.
45
78
 
46
79
  ### List payment methods
47
80
 
@@ -49,11 +82,11 @@ You'll receive a verification URL and a short phrase. Visit the URL, log in to y
49
82
  link-cli payment-methods list
50
83
  ```
51
84
 
52
- Returns the cards and bank accounts saved to your Link account. Use the `id` field as `payment_method_id` in the next step. If you have no payment methods, you can [add new ones in Link](https://app.link.com/wallet).
85
+ Returns the cards and bank accounts saved to your Link account. Use the `id` field as `payment_method_id` in the next step. If you have no payment methods, [add new ones in Link](https://app.link.com/wallet).
53
86
 
54
87
  ### Create a spend request
55
88
 
56
- To request a secure, one-time payment credential from your Link wallet, you create a spend request. You specify a payment method in your account, as well as some merchant details, line items, and amounts.
89
+ Create a spend request with a payment method, merchant details, line items, and amounts:
57
90
 
58
91
  ```bash
59
92
  link-cli spend-request create \
@@ -67,9 +100,9 @@ link-cli spend-request create \
67
100
  --request-approval
68
101
  ```
69
102
 
70
- The `--request-approval` flag triggers a push notification (or email) to the user for approval, then polls until the request is approved or denied.
103
+ The `--request-approval` flag triggers a push notification to the user for approval, then polls until the request is approved or denied.
71
104
 
72
- Users can easily approve requests with the [Link app](https://link.com/download).
105
+ Easily approve requests with the [Link app](https://link.com/download).
73
106
 
74
107
  #### Line items and totals
75
108
 
@@ -81,7 +114,7 @@ Users can easily approve requests with the [Link app](https://link.com/download)
81
114
  --line-item "name:Running Shoes,unit_amount:12000,quantity:1,description:Trail runners"
82
115
  ```
83
116
 
84
- **`--total` keys:** `type` (required), `display_text` (required), `amount` (required)
117
+ **`--total` keys:** `type` (required; one of: `subtotal`, `tax`, `total`), `display_text` (required), `amount` (required)
85
118
 
86
119
  ```bash
87
120
  --total "type:subtotal,display_text:Subtotal,amount:12000" \
@@ -90,21 +123,29 @@ Users can easily approve requests with the [Link app](https://link.com/download)
90
123
 
91
124
  #### Credential types
92
125
 
93
- By default, a spend request provisions a virtual card. For merchants that support the [Machine Payments Protocol](https://mpp.dev) (HTTP 402) and the Stripe payment method, you can instead include `--credential-type "shared_payment_token"`.
126
+ By default, a spend request provisions a virtual card. For merchants that support the [Machine Payments Protocol](https://mpp.dev) (HTTP 402) and the Stripe payment method, instead pass `--credential-type "shared_payment_token"`.
94
127
 
95
128
  ### Execute payment
96
129
 
97
130
  The approved spend request includes a `card` object with `number`, `cvc`, `exp_month`, `exp_year`, `billing_address`, and `valid_until`. Enter these into the merchant's checkout form.
98
131
 
99
132
  ```bash
100
- link-cli spend-request retrieve lsrq_001 --format json
133
+ link-cli spend-request retrieve lsrq_001
134
+ ```
135
+ By default, retrieving a spend request doesn't include card details. Pass `--include card` to see unmasked card details.
136
+
137
+ To avoid leaking card credentials into agent transcripts or logs, use `--output-file` to write the full card to a secure local file while stdout shows only redacted data (brand, last4, expiry):
138
+
139
+ ```bash
140
+ link-cli spend-request retrieve lsrq_001 --include card --output-file /tmp/link-card.json --format json
101
141
  ```
102
- By default, retrieving a spend request will not include card details. Use the `--include=card` to see unmasked card details.
142
+
143
+ The file is created with `0600` permissions. If the file already exists, the command fails unless `--force` is passed. When `--output-file` is set, the JSON output replaces the `card` object with redacted fields and adds a `card_output_file` path.
103
144
 
104
145
  For agent polling, pass `--interval` and optionally `--max-attempts`:
105
146
 
106
147
  ```bash
107
- link-cli spend-request retrieve lsrq_001 --interval 2 --max-attempts 150 --format json
148
+ link-cli spend-request retrieve lsrq_001 --interval 2 --max-attempts 150
108
149
  ```
109
150
 
110
151
  Polling exits successfully only after the request reaches a terminal status such as `approved`, `denied`, or `expired`. If polling reaches `--timeout` or exhausts `--max-attempts` while the request is still non-terminal, the command exits non-zero with `code: "POLLING_TIMEOUT"` so callers do not treat a still-pending request as complete.
@@ -115,8 +156,7 @@ If the merchant supports MPP, use `link-cli mpp pay` instead:
115
156
  link-cli mpp pay https://climate.stripe.dev/api/contribute \
116
157
  --spend-request-id lsrq_001 \
117
158
  --method POST \
118
- --data '{"amount":100}' \
119
- --format json
159
+ --data '{"amount":100}'
120
160
  ```
121
161
 
122
162
  ## Advanced
@@ -124,14 +164,14 @@ link-cli mpp pay https://climate.stripe.dev/api/contribute \
124
164
  ### Authentication
125
165
 
126
166
  ```bash
127
- link-cli auth login --client-name "Claude Code" --format json # identify the connecting agent
128
- link-cli auth status --format json # check auth status
129
- link-cli auth logout --format json # disconnect
167
+ link-cli auth login --client-name "Claude Code" # identify the connecting agent
168
+ link-cli auth status # check auth status
169
+ link-cli auth logout # disconnect
130
170
  ```
131
171
 
132
- When `--client-name` is provided, the name is shown in the Link app when the user approves the connection — e.g. `Claude Code on my-macbook` instead of `link-cli on my-macbook`.
172
+ When you provide `--client-name`, the Link app displays it when you approve the connection — for example, `Claude Code on my-macbook` instead of `link-cli on my-macbook`.
133
173
 
134
- `auth status --format json` includes an `update` field when a newer version is available:
174
+ `auth status` includes an `update` field when a newer version is available:
135
175
 
136
176
  ```json
137
177
  {
@@ -144,7 +184,7 @@ When `--client-name` is provided, the name is shown in the Link app when the use
144
184
  }
145
185
  ```
146
186
 
147
- Set `NO_UPDATE_NOTIFIER=1` to suppress update checks (e.g. in CI).
187
+ Set `NO_UPDATE_NOTIFIER=1` to suppress update checks (for example, in CI).
148
188
 
149
189
  ### Spend request lifecycle
150
190
 
@@ -153,44 +193,37 @@ A spend request moves through: **create** → **request approval** → **approve
153
193
  **Required fields for create:** `payment_method_id`, `merchant_name`, `merchant_url`, `context`, `amount`
154
194
 
155
195
  **Constraints:** `context` must be at least 100 characters; `amount` must not exceed 50000 (cents); `currency` must be a 3-letter ISO code.
156
- **Test mode:** Pass `--test` to create testmode credentials (uses test card `4242424242424242`). Useful for development and integration testing without using real payment methods.
196
+ **Test mode:** Pass `--test` to create testmode credentials (uses test card `4242424242424242`), useful for development and integration testing without real payment methods.
157
197
 
158
198
  ```bash
159
199
  # Update before approval
160
200
  link-cli spend-request update lsrq_001 \
161
- --merchant-url https://press.stripe.com/working-in-public \
162
- --format json
201
+ --merchant-url https://press.stripe.com/working-in-public
163
202
 
164
203
  # Request approval separately (alternative to create --request-approval)
165
- link-cli spend-request request-approval lsrq_001 --format json
204
+ link-cli spend-request request-approval lsrq_001
166
205
 
167
- # Retrieve at any time (includes card credentials once approved)
168
- link-cli spend-request retrieve lsrq_001 --format json
206
+ # Retrieve at any time (includes card credentials after approval)
207
+ link-cli spend-request retrieve lsrq_001
169
208
  ```
170
209
 
171
- ### Output formats
172
-
173
- All commands accept `--format json` for structured JSON output. Other formats: `yaml`, `md`, `jsonl`, `toon` (default). Errors are returned as JSON with `code` and `message` fields, with exit code 1.
174
-
175
210
  ### MPP
176
211
 
177
- 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 be approved. The SPT is one-time-use — if payment fails, create a new spend request.
212
+ 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.
178
213
 
179
214
  ```bash
180
215
  link-cli mpp pay https://climate.stripe.dev/api/contribute \
181
216
  --spend-request-id lsrq_001 \
182
217
  --method POST \
183
218
  --data '{"amount":100}' \
184
- --header "X-Custom: value" \
185
- --format json
219
+ --header "X-Custom: value"
186
220
  ```
187
221
 
188
222
  Use `mpp decode` to validate a raw `WWW-Authenticate` header and extract the `network_id` needed for `shared_payment_token` spend requests:
189
223
 
190
224
  ```bash
191
225
  link-cli mpp decode \
192
- --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."' \
193
- --format json
226
+ --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."'
194
227
  ```
195
228
 
196
229
  ### Environment variables
@@ -203,7 +236,7 @@ link-cli mpp decode \
203
236
 
204
237
  ## Onboard
205
238
 
206
- Run the guided setup flow — authenticates, checks payment methods, shows the app download QR, and walks through both demo flows:
239
+ Run the guided setup flow — authenticates, checks payment methods, shows the app download QR, and runs both demo flows:
207
240
 
208
241
  ```bash
209
242
  link-cli onboard
package/dist/cli.js CHANGED
@@ -3218,8 +3218,8 @@ var require_utils = __commonJS({
3218
3218
  }
3219
3219
  return ind;
3220
3220
  }
3221
- function removeDotSegments(path5) {
3222
- let input = path5;
3221
+ function removeDotSegments(path6) {
3222
+ let input = path6;
3223
3223
  const output = [];
3224
3224
  let nextSlash = -1;
3225
3225
  let len = 0;
@@ -3418,8 +3418,8 @@ var require_schemes = __commonJS({
3418
3418
  wsComponent.secure = void 0;
3419
3419
  }
3420
3420
  if (wsComponent.resourceName) {
3421
- const [path5, query] = wsComponent.resourceName.split("?");
3422
- wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
3421
+ const [path6, query] = wsComponent.resourceName.split("?");
3422
+ wsComponent.path = path6 && path6 !== "/" ? path6 : void 0;
3423
3423
  wsComponent.query = query;
3424
3424
  wsComponent.resourceName = void 0;
3425
3425
  }
@@ -7605,12 +7605,12 @@ var require_dist = __commonJS({
7605
7605
  throw new Error(`Unknown format "${name}"`);
7606
7606
  return f;
7607
7607
  };
7608
- function addFormats(ajv, list, fs4, exportName) {
7608
+ function addFormats(ajv, list, fs5, exportName) {
7609
7609
  var _a;
7610
7610
  var _b;
7611
7611
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
7612
7612
  for (const f of list)
7613
- ajv.addFormat(f, fs4[f]);
7613
+ ajv.addFormat(f, fs5[f]);
7614
7614
  }
7615
7615
  module.exports = exports = formatsPlugin;
7616
7616
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -9550,7 +9550,6 @@ var require_semver2 = __commonJS({
9550
9550
 
9551
9551
  // src/cli.tsx
9552
9552
  import { Cli as Cli7 } from "incur";
9553
- import updateNotifier from "update-notifier";
9554
9553
 
9555
9554
  // ../sdk/dist/index.js
9556
9555
  import fs3 from "fs";
@@ -9574,12 +9573,12 @@ var disallowedKeys = /* @__PURE__ */ new Set([
9574
9573
  "constructor"
9575
9574
  ]);
9576
9575
  var digits = new Set("0123456789");
9577
- function getPathSegments(path5) {
9576
+ function getPathSegments(path6) {
9578
9577
  const parts = [];
9579
9578
  let currentSegment = "";
9580
9579
  let currentPart = "start";
9581
9580
  let isIgnoring = false;
9582
- for (const character of path5) {
9581
+ for (const character of path6) {
9583
9582
  switch (character) {
9584
9583
  case "\\": {
9585
9584
  if (currentPart === "index") {
@@ -9701,11 +9700,11 @@ function assertNotStringIndex(object, key) {
9701
9700
  throw new Error("Cannot use string index");
9702
9701
  }
9703
9702
  }
9704
- function getProperty(object, path5, value) {
9705
- if (!isObject(object) || typeof path5 !== "string") {
9703
+ function getProperty(object, path6, value) {
9704
+ if (!isObject(object) || typeof path6 !== "string") {
9706
9705
  return value === void 0 ? object : value;
9707
9706
  }
9708
- const pathArray = getPathSegments(path5);
9707
+ const pathArray = getPathSegments(path6);
9709
9708
  if (pathArray.length === 0) {
9710
9709
  return value;
9711
9710
  }
@@ -9725,12 +9724,12 @@ function getProperty(object, path5, value) {
9725
9724
  }
9726
9725
  return object === void 0 ? value : object;
9727
9726
  }
9728
- function setProperty(object, path5, value) {
9729
- if (!isObject(object) || typeof path5 !== "string") {
9727
+ function setProperty(object, path6, value) {
9728
+ if (!isObject(object) || typeof path6 !== "string") {
9730
9729
  return object;
9731
9730
  }
9732
9731
  const root = object;
9733
- const pathArray = getPathSegments(path5);
9732
+ const pathArray = getPathSegments(path6);
9734
9733
  for (let index = 0; index < pathArray.length; index++) {
9735
9734
  const key = pathArray[index];
9736
9735
  assertNotStringIndex(object, key);
@@ -9743,11 +9742,11 @@ function setProperty(object, path5, value) {
9743
9742
  }
9744
9743
  return root;
9745
9744
  }
9746
- function deleteProperty(object, path5) {
9747
- if (!isObject(object) || typeof path5 !== "string") {
9745
+ function deleteProperty(object, path6) {
9746
+ if (!isObject(object) || typeof path6 !== "string") {
9748
9747
  return false;
9749
9748
  }
9750
- const pathArray = getPathSegments(path5);
9749
+ const pathArray = getPathSegments(path6);
9751
9750
  for (let index = 0; index < pathArray.length; index++) {
9752
9751
  const key = pathArray[index];
9753
9752
  assertNotStringIndex(object, key);
@@ -9761,11 +9760,11 @@ function deleteProperty(object, path5) {
9761
9760
  }
9762
9761
  }
9763
9762
  }
9764
- function hasProperty(object, path5) {
9765
- if (!isObject(object) || typeof path5 !== "string") {
9763
+ function hasProperty(object, path6) {
9764
+ if (!isObject(object) || typeof path6 !== "string") {
9766
9765
  return false;
9767
9766
  }
9768
- const pathArray = getPathSegments(path5);
9767
+ const pathArray = getPathSegments(path6);
9769
9768
  if (pathArray.length === 0) {
9770
9769
  return false;
9771
9770
  }
@@ -10861,12 +10860,19 @@ function withComputedExpiry(auth) {
10861
10860
  expires_at: auth.expires_at ?? Date.now() + auth.expires_in * 1e3
10862
10861
  };
10863
10862
  }
10863
+ var CONFIG_FILE_MODE = 384;
10864
10864
  var Storage = class {
10865
10865
  config;
10866
+ options;
10867
+ constructor(options = {}) {
10868
+ this.options = options;
10869
+ }
10866
10870
  getConfig() {
10867
10871
  if (!this.config) {
10868
10872
  this.config = new Conf({
10869
10873
  projectName: "link-cli",
10874
+ configFileMode: CONFIG_FILE_MODE,
10875
+ ...this.options.cwd ? { cwd: this.options.cwd } : {},
10870
10876
  defaults: {
10871
10877
  auth: null,
10872
10878
  pendingDeviceAuth: null
@@ -11488,7 +11494,7 @@ var AuthStatus = ({ onComplete }) => {
11488
11494
 
11489
11495
  // src/commands/auth/index.tsx
11490
11496
  import { jsx as jsx4 } from "react/jsx-runtime";
11491
- function createAuthCli(authResource, updateInfo) {
11497
+ function createAuthCli(authResource, getUpdateInfo2) {
11492
11498
  const cli2 = Cli.create("auth", {
11493
11499
  description: "Authentication commands"
11494
11500
  });
@@ -11574,6 +11580,18 @@ function createAuthCli(authResource, updateInfo) {
11574
11580
  options: statusOptions,
11575
11581
  outputPolicy: "agent-only",
11576
11582
  async *run(c) {
11583
+ const opts = c.options;
11584
+ const interval = opts.interval;
11585
+ const maxAttempts = opts.maxAttempts;
11586
+ const deadline = Date.now() + opts.timeout * 1e3;
11587
+ const updateInfo = await getUpdateInfo2?.({
11588
+ polling: interval > 0
11589
+ });
11590
+ const update = updateInfo ? {
11591
+ current_version: updateInfo.current,
11592
+ latest_version: updateInfo.latest,
11593
+ update_command: "npm install -g @stripe/link-cli"
11594
+ } : void 0;
11577
11595
  if (!c.agent && !c.formatExplicit) {
11578
11596
  return new Promise((resolve) => {
11579
11597
  const { waitUntilExit } = render(
@@ -11588,15 +11606,12 @@ function createAuthCli(authResource, updateInfo) {
11588
11606
  access_token: `${auth.access_token.substring(0, 20)}...`,
11589
11607
  token_type: auth.token_type
11590
11608
  } : {},
11591
- credentials_path: storage.getPath()
11609
+ credentials_path: storage.getPath(),
11610
+ ...update && { update }
11592
11611
  });
11593
11612
  });
11594
11613
  });
11595
11614
  }
11596
- const opts = c.options;
11597
- const interval = opts.interval;
11598
- const maxAttempts = opts.maxAttempts;
11599
- const deadline = Date.now() + opts.timeout * 1e3;
11600
11615
  let attempts = 0;
11601
11616
  let previousAttemptData;
11602
11617
  while (true) {
@@ -11609,11 +11624,6 @@ function createAuthCli(authResource, updateInfo) {
11609
11624
  }
11610
11625
  }
11611
11626
  const auth = storage.getAuth();
11612
- const update = updateInfo ? {
11613
- current_version: updateInfo.current,
11614
- latest_version: updateInfo.latest,
11615
- update_command: "npm install -g @stripe/link-cli"
11616
- } : void 0;
11617
11627
  if (auth) {
11618
11628
  yield {
11619
11629
  authenticated: true,
@@ -12293,16 +12303,16 @@ import { useEffect as useEffect6, useRef as useRef2, useState as useState6 } fro
12293
12303
 
12294
12304
  // src/commands/mpp/decode.ts
12295
12305
  import { Challenge } from "mppx";
12296
- function getString(value, path5, required = true) {
12306
+ function getString(value, path6, required = true) {
12297
12307
  if (value == null) {
12298
12308
  if (required) {
12299
- throw new Error(`Invalid stripe challenge request: ${path5}: missing`);
12309
+ throw new Error(`Invalid stripe challenge request: ${path6}: missing`);
12300
12310
  }
12301
12311
  return void 0;
12302
12312
  }
12303
12313
  if (typeof value !== "string") {
12304
12314
  throw new Error(
12305
- `Invalid stripe challenge request: ${path5}: expected string, received ${typeof value}`
12315
+ `Invalid stripe challenge request: ${path6}: expected string, received ${typeof value}`
12306
12316
  );
12307
12317
  }
12308
12318
  return value;
@@ -13562,6 +13572,28 @@ function createPaymentMethodsCli(createResource) {
13562
13572
  import { Cli as Cli6, z as z7 } from "incur";
13563
13573
  import { render as render6 } from "ink";
13564
13574
 
13575
+ // src/utils/credential-output.ts
13576
+ import fs4 from "fs/promises";
13577
+ import path5 from "path";
13578
+ async function writeCredentialFile(filePath, data, force) {
13579
+ const resolved = path5.resolve(filePath);
13580
+ if (!force) {
13581
+ try {
13582
+ await fs4.access(resolved);
13583
+ throw new Error(
13584
+ `OUTPUT_FILE_EXISTS: ${resolved} already exists. Use --force to overwrite.`
13585
+ );
13586
+ } catch (err) {
13587
+ if (err.code !== "ENOENT") throw err;
13588
+ }
13589
+ }
13590
+ await fs4.writeFile(resolved, JSON.stringify(data, null, 2), {
13591
+ mode: 384
13592
+ });
13593
+ await fs4.chmod(resolved, 384);
13594
+ return resolved;
13595
+ }
13596
+
13565
13597
  // src/utils/line-item-parser.ts
13566
13598
  import { z as z5 } from "zod";
13567
13599
  var LineItemSchema = z5.object({
@@ -13725,11 +13757,15 @@ var CreateSpendRequest = ({
13725
13757
  repository,
13726
13758
  params,
13727
13759
  requestApproval = false,
13760
+ outputFile,
13761
+ force,
13728
13762
  onComplete
13729
13763
  }) => {
13730
13764
  const [status, setStatus] = useState10("creating");
13731
13765
  const [request, setRequest] = useState10(null);
13732
13766
  const [error, setError] = useState10("");
13767
+ const [outputFilePath, setOutputFilePath] = useState10(null);
13768
+ const [fileError, setFileError] = useState10("");
13733
13769
  const approvalUrl = request?.approval_url ?? "";
13734
13770
  const onSuccess = useCallback2(
13735
13771
  (result) => setRequest(result),
@@ -13765,6 +13801,18 @@ var CreateSpendRequest = ({
13765
13801
  };
13766
13802
  create();
13767
13803
  }, [repository, params, requestApproval, onComplete]);
13804
+ useEffect10(() => {
13805
+ if (status !== "success" || !outputFile || !request?.card) return;
13806
+ const fileData = {
13807
+ spend_request_id: request.id,
13808
+ merchant_name: request.merchant_name,
13809
+ merchant_url: request.merchant_url,
13810
+ context: request.context,
13811
+ created_at: request.created_at,
13812
+ card: request.card
13813
+ };
13814
+ writeCredentialFile(outputFile, fileData, force ?? false).then((path6) => setOutputFilePath(path6)).catch((err) => setFileError(err.message));
13815
+ }, [status, outputFile, force, request]);
13768
13816
  if (status === "creating") {
13769
13817
  return /* @__PURE__ */ jsx20(Box14, { children: /* @__PURE__ */ jsxs13(Text15, { color: "cyan", children: [
13770
13818
  /* @__PURE__ */ jsx20(Spinner5, { type: "dots" }),
@@ -13808,6 +13856,49 @@ var CreateSpendRequest = ({
13808
13856
  /* @__PURE__ */ jsx20(Text15, { bold: true, children: request.shared_payment_token.id })
13809
13857
  ] })
13810
13858
  ] }),
13859
+ request?.card && !outputFile && /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", marginTop: 1, children: [
13860
+ /* @__PURE__ */ jsx20(Text15, { bold: true, children: "Card Details:" }),
13861
+ /* @__PURE__ */ jsxs13(Text15, { children: [
13862
+ " ",
13863
+ "Number: ",
13864
+ /* @__PURE__ */ jsx20(Text15, { bold: true, children: request.card.number })
13865
+ ] }),
13866
+ /* @__PURE__ */ jsxs13(Text15, { children: [
13867
+ " ",
13868
+ "Brand: ",
13869
+ /* @__PURE__ */ jsx20(Text15, { bold: true, children: request.card.brand })
13870
+ ] }),
13871
+ /* @__PURE__ */ jsxs13(Text15, { children: [
13872
+ " ",
13873
+ "Expiry:",
13874
+ " ",
13875
+ /* @__PURE__ */ jsxs13(Text15, { bold: true, children: [
13876
+ String(request.card.exp_month).padStart(2, "0"),
13877
+ "/",
13878
+ request.card.exp_year
13879
+ ] })
13880
+ ] }),
13881
+ request.card.cvc && /* @__PURE__ */ jsxs13(Text15, { children: [
13882
+ " ",
13883
+ "CVC: ",
13884
+ /* @__PURE__ */ jsx20(Text15, { bold: true, children: request.card.cvc })
13885
+ ] }),
13886
+ request.card.valid_until && /* @__PURE__ */ jsxs13(Text15, { children: [
13887
+ " ",
13888
+ "Valid Until: ",
13889
+ /* @__PURE__ */ jsx20(Text15, { bold: true, children: request.card.valid_until })
13890
+ ] })
13891
+ ] }),
13892
+ request?.card && outputFile && /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", marginTop: 1, children: [
13893
+ outputFilePath && /* @__PURE__ */ jsxs13(Text15, { color: "green", children: [
13894
+ "Card credentials written to ",
13895
+ /* @__PURE__ */ jsx20(Text15, { bold: true, children: outputFilePath })
13896
+ ] }),
13897
+ fileError && /* @__PURE__ */ jsxs13(Text15, { color: "red", children: [
13898
+ "Failed to write card file: ",
13899
+ fileError
13900
+ ] })
13901
+ ] }),
13811
13902
  /* @__PURE__ */ jsx20(AppDownloadQrCodes, {})
13812
13903
  ] });
13813
13904
  }
@@ -13925,12 +14016,16 @@ var RetrieveSpendRequest = ({
13925
14016
  id,
13926
14017
  timeout = 300,
13927
14018
  include,
14019
+ outputFile,
14020
+ force,
13928
14021
  onComplete
13929
14022
  }) => {
13930
14023
  const [phase, setPhase] = useState12("fetching");
13931
14024
  const [request, setRequest] = useState12(null);
13932
14025
  const [error, setError] = useState12("");
13933
14026
  const [elapsed, setElapsed] = useState12(0);
14027
+ const [outputFilePath, setOutputFilePath] = useState12(null);
14028
+ const [fileError, setFileError] = useState12("");
13934
14029
  const startTimeRef = useRef4(Date.now());
13935
14030
  const pollRef = useRef4(null);
13936
14031
  const timerRef = useRef4(null);
@@ -13941,6 +14036,18 @@ var RetrieveSpendRequest = ({
13941
14036
  if (timerRef.current) clearInterval(timerRef.current);
13942
14037
  };
13943
14038
  }, []);
14039
+ useEffect12(() => {
14040
+ if (phase !== "success" || !outputFile || !request?.card) return;
14041
+ const fileData = {
14042
+ spend_request_id: request.id,
14043
+ merchant_name: request.merchant_name,
14044
+ merchant_url: request.merchant_url,
14045
+ context: request.context,
14046
+ created_at: request.created_at,
14047
+ card: request.card
14048
+ };
14049
+ writeCredentialFile(outputFile, fileData, force ?? false).then((path6) => setOutputFilePath(path6)).catch((err) => setFileError(err.message));
14050
+ }, [phase, outputFile, force, request]);
13944
14051
  useEffect12(() => {
13945
14052
  const fetch2 = async () => {
13946
14053
  try {
@@ -14121,7 +14228,7 @@ var RetrieveSpendRequest = ({
14121
14228
  /* @__PURE__ */ jsx22(Text17, { bold: true, children: request.shared_payment_token.id })
14122
14229
  ] })
14123
14230
  ] }),
14124
- request?.card && /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", marginTop: 1, children: [
14231
+ request?.card && !outputFile && /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", marginTop: 1, children: [
14125
14232
  /* @__PURE__ */ jsx22(Text17, { bold: true, children: "Card Details:" }),
14126
14233
  /* @__PURE__ */ jsxs15(Text17, { children: [
14127
14234
  " ",
@@ -14180,6 +14287,16 @@ var RetrieveSpendRequest = ({
14180
14287
  request.card.billing_address.country
14181
14288
  ] })
14182
14289
  ] })
14290
+ ] }),
14291
+ request?.card && outputFile && /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", marginTop: 1, children: [
14292
+ outputFilePath && /* @__PURE__ */ jsxs15(Text17, { color: "green", children: [
14293
+ "Card credentials written to ",
14294
+ /* @__PURE__ */ jsx22(Text17, { bold: true, children: outputFilePath })
14295
+ ] }),
14296
+ fileError && /* @__PURE__ */ jsxs15(Text17, { color: "red", children: [
14297
+ "Failed to write card file: ",
14298
+ fileError
14299
+ ] })
14183
14300
  ] })
14184
14301
  ] })
14185
14302
  ] });
@@ -14210,12 +14327,16 @@ var createOptions = z6.object({
14210
14327
  'Line item (repeatable, key:value format). Keys: name (required), quantity, unit_amount, description, sku, url, image_url, product_url. Example: "name:Shoes,unit_amount:5000,quantity:2"'
14211
14328
  ),
14212
14329
  total: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe(
14213
- 'Total (repeatable, key:value format). Keys: type (required), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"'
14330
+ 'Total (repeatable, key:value format). Keys: type (required; one of: subtotal, tax, total), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"'
14214
14331
  ),
14215
14332
  requestApproval: z6.boolean().default(true).describe("Request approval and poll until approved/denied/expired"),
14216
14333
  test: z6.boolean().default(false).describe(
14217
14334
  "Use test mode (creates testmode credentials from test card data)"
14218
- )
14335
+ ),
14336
+ outputFile: z6.string().optional().describe(
14337
+ "Write full card credentials to this file path; stdout shows redacted card data only"
14338
+ ),
14339
+ force: z6.boolean().default(false).describe("Overwrite output file if it already exists")
14219
14340
  });
14220
14341
  var retrieveOptions = z6.object({
14221
14342
  timeout: z6.coerce.number().default(300).describe(
@@ -14227,7 +14348,11 @@ var retrieveOptions = z6.object({
14227
14348
  maxAttempts: z6.coerce.number().default(0).describe(
14228
14349
  "Max poll attempts. 0 = unlimited. Exhaustion during active polling exits non-zero with POLLING_TIMEOUT."
14229
14350
  ),
14230
- include: z6.array(z6.string()).default([]).describe("Include extra data (repeatable, e.g. --include card)")
14351
+ include: z6.array(z6.string()).default([]).describe("Include extra data (repeatable, e.g. --include card)"),
14352
+ outputFile: z6.string().optional().describe(
14353
+ "Write full card credentials to this file path; stdout shows redacted card data only"
14354
+ ),
14355
+ force: z6.boolean().default(false).describe("Overwrite output file if it already exists")
14231
14356
  });
14232
14357
  var updateOptions = z6.object({
14233
14358
  paymentMethodId: z6.string().optional().describe("Payment method ID"),
@@ -14240,7 +14365,7 @@ var updateOptions = z6.object({
14240
14365
  'Line item (repeatable, key:value format). Keys: name (required), quantity, unit_amount, description, sku, url, image_url, product_url. Example: "name:Shoes,unit_amount:5000,quantity:2"'
14241
14366
  ),
14242
14367
  total: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe(
14243
- 'Total (repeatable, key:value format). Keys: type (required), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"'
14368
+ 'Total (repeatable, key:value format). Keys: type (required; one of: subtotal, tax, total), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"'
14244
14369
  )
14245
14370
  });
14246
14371
 
@@ -14323,6 +14448,23 @@ var UpdateSpendRequest = ({
14323
14448
 
14324
14449
  // src/commands/spend-request/index.tsx
14325
14450
  import { jsx as jsx24 } from "react/jsx-runtime";
14451
+ async function applyOutputFile(request, outputFile, force) {
14452
+ if (!outputFile || !request.card) return request;
14453
+ const fileData = {
14454
+ spend_request_id: request.id,
14455
+ merchant_name: request.merchant_name,
14456
+ merchant_url: request.merchant_url,
14457
+ context: request.context,
14458
+ created_at: request.created_at,
14459
+ card: request.card
14460
+ };
14461
+ const resolvedPath = await writeCredentialFile(outputFile, fileData, force);
14462
+ const { card: _, ...withoutCard } = request;
14463
+ return {
14464
+ ...withoutCard,
14465
+ card_output_file: resolvedPath
14466
+ };
14467
+ }
14326
14468
  function createSpendRequestCli(repository) {
14327
14469
  const cli2 = Cli6.create("spend-request", {
14328
14470
  description: "Spend request management commands"
@@ -14400,6 +14542,8 @@ function createSpendRequestCli(repository) {
14400
14542
  request_approval: requestApproval || void 0,
14401
14543
  test: opts.test ? true : void 0
14402
14544
  };
14545
+ const outputFile = opts.outputFile;
14546
+ const forceOverwrite = opts.force;
14403
14547
  if (!c.agent && !c.formatExplicit) {
14404
14548
  return new Promise((resolve) => {
14405
14549
  let capturedResult = null;
@@ -14410,6 +14554,8 @@ function createSpendRequestCli(repository) {
14410
14554
  repository,
14411
14555
  params: createParams,
14412
14556
  requestApproval,
14557
+ outputFile,
14558
+ force: forceOverwrite,
14413
14559
  onComplete: (result) => {
14414
14560
  capturedResult = result;
14415
14561
  }
@@ -14423,7 +14569,15 @@ function createSpendRequestCli(repository) {
14423
14569
  }
14424
14570
  const created = await repository.createSpendRequest(createParams);
14425
14571
  if (!requestApproval) {
14426
- yield created;
14572
+ try {
14573
+ yield await applyOutputFile(created, outputFile, forceOverwrite);
14574
+ } catch (err) {
14575
+ const message = err.message;
14576
+ if (message.startsWith("OUTPUT_FILE_EXISTS")) {
14577
+ return c.error({ code: "OUTPUT_FILE_EXISTS", message });
14578
+ }
14579
+ return c.error({ code: "OUTPUT_FILE_WRITE_ERROR", message });
14580
+ }
14427
14581
  return;
14428
14582
  }
14429
14583
  yield {
@@ -14574,6 +14728,8 @@ function createSpendRequestCli(repository) {
14574
14728
  const maxAttempts = opts.maxAttempts;
14575
14729
  const includeArr = opts.include;
14576
14730
  const include = includeArr?.length ? includeArr : void 0;
14731
+ const outputFile = opts.outputFile;
14732
+ const forceOverwrite = opts.force;
14577
14733
  if (!c.agent && !c.formatExplicit) {
14578
14734
  return new Promise((resolve) => {
14579
14735
  let capturedResult = null;
@@ -14585,6 +14741,8 @@ function createSpendRequestCli(repository) {
14585
14741
  id,
14586
14742
  timeout,
14587
14743
  include,
14744
+ outputFile,
14745
+ force: forceOverwrite,
14588
14746
  onComplete: (result) => {
14589
14747
  capturedResult = result;
14590
14748
  }
@@ -14614,15 +14772,20 @@ function createSpendRequestCli(repository) {
14614
14772
  message: `Spend request ${id} not found`
14615
14773
  });
14616
14774
  }
14617
- if (terminalStatuses.has(request.status)) {
14618
- yield request;
14775
+ const shouldEmitFinal = terminalStatuses.has(request.status) || interval <= 0;
14776
+ if (shouldEmitFinal) {
14777
+ try {
14778
+ yield await applyOutputFile(request, outputFile, forceOverwrite);
14779
+ } catch (err) {
14780
+ const message = err.message;
14781
+ if (message.startsWith("OUTPUT_FILE_EXISTS")) {
14782
+ return c.error({ code: "OUTPUT_FILE_EXISTS", message });
14783
+ }
14784
+ return c.error({ code: "OUTPUT_FILE_WRITE_ERROR", message });
14785
+ }
14619
14786
  return;
14620
14787
  }
14621
14788
  attempts++;
14622
- if (interval <= 0) {
14623
- yield request;
14624
- return;
14625
- }
14626
14789
  const maxAttemptsExhausted = maxAttempts > 0 && attempts >= maxAttempts;
14627
14790
  const timeoutReached = Date.now() >= deadline;
14628
14791
  if (maxAttemptsExhausted || timeoutReached) {
@@ -14944,8 +15107,78 @@ var ResourceFactory = class {
14944
15107
  }
14945
15108
  };
14946
15109
 
15110
+ // src/utils/update-info.ts
15111
+ import process8 from "process";
15112
+ import updateNotifier from "update-notifier";
15113
+ var UPDATE_CACHE_TTL_MS = 60 * 60 * 1e3;
15114
+ var FAILURE_CACHE_TTL_MS = 60 * 1e3;
15115
+ var cachedUpdateInfo;
15116
+ var inflightUpdateInfo;
15117
+ function createAgentUpdateInfoProvider(packageName, cliVersion2) {
15118
+ return async ({ polling }) => {
15119
+ if (polling) {
15120
+ return cachedUpdateInfo?.value;
15121
+ }
15122
+ const now = Date.now();
15123
+ if (cachedUpdateInfo && cachedUpdateInfo.expiresAt > now) {
15124
+ return cachedUpdateInfo.value;
15125
+ }
15126
+ if (!inflightUpdateInfo) {
15127
+ inflightUpdateInfo = fetchLatestVersion(packageName, cliVersion2).finally(
15128
+ () => {
15129
+ inflightUpdateInfo = void 0;
15130
+ }
15131
+ );
15132
+ }
15133
+ return inflightUpdateInfo;
15134
+ };
15135
+ }
15136
+ function createInteractiveUpdateInfoProvider(updateInfo) {
15137
+ return async () => updateInfo;
15138
+ }
15139
+ function renderInteractiveUpdateNotice(updateInfo) {
15140
+ return [
15141
+ "",
15142
+ `Update available for @stripe/link-cli: ${updateInfo.current} -> ${updateInfo.latest}`,
15143
+ "Run: npm install -g @stripe/link-cli",
15144
+ ""
15145
+ ].join("\n");
15146
+ }
15147
+ async function fetchLatestVersion(packageName, cliVersion2) {
15148
+ const previousDisableFlag = process8.env.NO_UPDATE_NOTIFIER;
15149
+ try {
15150
+ process8.env.NO_UPDATE_NOTIFIER = "1";
15151
+ const notifier = updateNotifier({
15152
+ pkg: {
15153
+ name: packageName,
15154
+ version: cliVersion2
15155
+ }
15156
+ });
15157
+ const payload = await notifier.fetchInfo();
15158
+ const latest = payload.latest?.trim();
15159
+ const value = latest && latest.length > 0 && latest !== cliVersion2 ? { current: cliVersion2, latest } : void 0;
15160
+ cacheUpdateInfo(value);
15161
+ return value;
15162
+ } catch {
15163
+ cacheUpdateInfo(void 0, FAILURE_CACHE_TTL_MS);
15164
+ return void 0;
15165
+ } finally {
15166
+ if (previousDisableFlag === void 0) {
15167
+ process8.env.NO_UPDATE_NOTIFIER = void 0;
15168
+ } else {
15169
+ process8.env.NO_UPDATE_NOTIFIER = previousDisableFlag;
15170
+ }
15171
+ }
15172
+ }
15173
+ function cacheUpdateInfo(value, ttlMs = UPDATE_CACHE_TTL_MS) {
15174
+ cachedUpdateInfo = {
15175
+ expiresAt: Date.now() + ttlMs,
15176
+ value
15177
+ };
15178
+ }
15179
+
14947
15180
  // src/cli.tsx
14948
- var cliVersion = "0.4.2";
15181
+ var cliVersion = "0.4.3";
14949
15182
  var buildNumber = "1";
14950
15183
  var cliName = "@stripe/link-cli";
14951
15184
  var defaultHeaders = {
@@ -14956,14 +15189,24 @@ var verbose = process.argv.includes("--verbose");
14956
15189
  var factory = new ResourceFactory({ verbose, defaultHeaders });
14957
15190
  var authRepo = factory.createAuthResource();
14958
15191
  var spendRequestRepo = factory.createSpendRequestResource();
14959
- var notifier = updateNotifier({
14960
- pkg: { name: cliName, version: cliVersion }
14961
- });
14962
15192
  var cli = Cli7.create("link-cli", {
14963
15193
  description: "Create a secure, one-time payment credential from a Link wallet to let agents complete purchases on behalf of users.",
14964
15194
  version: `${cliVersion} (build ${buildNumber})`
14965
15195
  });
14966
- cli.command(createAuthCli(authRepo, notifier.update));
15196
+ var isAgent = process.argv.includes("--format") || process.argv.includes("--mcp");
15197
+ var agentUpdateInfoProvider = createAgentUpdateInfoProvider(
15198
+ cliName,
15199
+ cliVersion
15200
+ );
15201
+ var getUpdateInfo = agentUpdateInfoProvider;
15202
+ if (!isAgent && process.stdout.isTTY) {
15203
+ const updateInfo = await agentUpdateInfoProvider({ polling: false });
15204
+ getUpdateInfo = createInteractiveUpdateInfoProvider(updateInfo);
15205
+ if (updateInfo) {
15206
+ process.stderr.write(renderInteractiveUpdateNotice(updateInfo));
15207
+ }
15208
+ }
15209
+ cli.command(createAuthCli(authRepo, getUpdateInfo));
14967
15210
  cli.command(createSpendRequestCli(spendRequestRepo));
14968
15211
  cli.command(
14969
15212
  createPaymentMethodsCli(() => factory.createPaymentMethodsResource())
@@ -14983,10 +15226,6 @@ cli.command(
14983
15226
  () => factory.createPaymentMethodsResource()
14984
15227
  )
14985
15228
  );
14986
- var isAgent = process.argv.includes("--format") || process.argv.includes("--mcp");
14987
- if (!isAgent) {
14988
- notifier.notify({ defer: false });
14989
- }
14990
15229
  cli.serve();
14991
15230
  var cli_default = cli;
14992
15231
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stripe/link-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-cli": "./dist/cli.js"
@@ -40,8 +40,8 @@
40
40
  "tsx": "^4.21.0",
41
41
  "typescript": "^5.9.3",
42
42
  "vitest": "^4.1.2",
43
- "@stripe/link-sdk": "1.0.0",
44
- "@stripe/link-typescript-config": "0.0.0"
43
+ "@stripe/link-typescript-config": "0.0.0",
44
+ "@stripe/link-sdk": "1.0.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup",