@stripe/link-cli 0.4.1 → 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 +87 -37
  2. package/dist/cli.js +315 -61
  3. package/package.json +4 -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`.
33
+
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
+ ```
18
45
 
19
- ### MCP Server
46
+ #### MCP Server
20
47
 
21
- Link CLI can also run as a local MCP server. Add the following to your MCP client config (`.mcp.json`, etc.)
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,27 +100,52 @@ 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.
104
+
105
+ Easily approve requests with the [Link app](https://link.com/download).
106
+
107
+ #### Line items and totals
108
+
109
+ `--line-item` and `--total` use repeatable `key:value` format.
110
+
111
+ **`--line-item` keys:** `name` (required), `quantity`, `unit_amount`, `description`, `sku`, `url`, `image_url`, `product_url`
71
112
 
72
- Users can easily approve requests with the [Link app](https://link.com/download).
113
+ ```bash
114
+ --line-item "name:Running Shoes,unit_amount:12000,quantity:1,description:Trail runners"
115
+ ```
116
+
117
+ **`--total` keys:** `type` (required; one of: `subtotal`, `tax`, `total`), `display_text` (required), `amount` (required)
118
+
119
+ ```bash
120
+ --total "type:subtotal,display_text:Subtotal,amount:12000" \
121
+ --total "type:total,display_text:Total,amount:12000"
122
+ ```
73
123
 
74
124
  #### Credential types
75
125
 
76
- 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"`.
77
127
 
78
128
  ### Execute payment
79
129
 
80
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.
81
131
 
82
132
  ```bash
83
- 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
84
141
  ```
85
- 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.
86
144
 
87
145
  For agent polling, pass `--interval` and optionally `--max-attempts`:
88
146
 
89
147
  ```bash
90
- 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
91
149
  ```
92
150
 
93
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.
@@ -98,8 +156,7 @@ If the merchant supports MPP, use `link-cli mpp pay` instead:
98
156
  link-cli mpp pay https://climate.stripe.dev/api/contribute \
99
157
  --spend-request-id lsrq_001 \
100
158
  --method POST \
101
- --data '{"amount":100}' \
102
- --format json
159
+ --data '{"amount":100}'
103
160
  ```
104
161
 
105
162
  ## Advanced
@@ -107,14 +164,14 @@ link-cli mpp pay https://climate.stripe.dev/api/contribute \
107
164
  ### Authentication
108
165
 
109
166
  ```bash
110
- link-cli auth login --client-name "Claude Code" --format json # identify the connecting agent
111
- link-cli auth status --format json # check auth status
112
- 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
113
170
  ```
114
171
 
115
- 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`.
116
173
 
117
- `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:
118
175
 
119
176
  ```json
120
177
  {
@@ -127,7 +184,7 @@ When `--client-name` is provided, the name is shown in the Link app when the use
127
184
  }
128
185
  ```
129
186
 
130
- 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).
131
188
 
132
189
  ### Spend request lifecycle
133
190
 
@@ -136,44 +193,37 @@ A spend request moves through: **create** → **request approval** → **approve
136
193
  **Required fields for create:** `payment_method_id`, `merchant_name`, `merchant_url`, `context`, `amount`
137
194
 
138
195
  **Constraints:** `context` must be at least 100 characters; `amount` must not exceed 50000 (cents); `currency` must be a 3-letter ISO code.
139
- **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.
140
197
 
141
198
  ```bash
142
199
  # Update before approval
143
200
  link-cli spend-request update lsrq_001 \
144
- --merchant-url https://press.stripe.com/working-in-public \
145
- --format json
201
+ --merchant-url https://press.stripe.com/working-in-public
146
202
 
147
203
  # Request approval separately (alternative to create --request-approval)
148
- link-cli spend-request request-approval lsrq_001 --format json
204
+ link-cli spend-request request-approval lsrq_001
149
205
 
150
- # Retrieve at any time (includes card credentials once approved)
151
- 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
152
208
  ```
153
209
 
154
- ### Output formats
155
-
156
- 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.
157
-
158
210
  ### MPP
159
211
 
160
- 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.
161
213
 
162
214
  ```bash
163
215
  link-cli mpp pay https://climate.stripe.dev/api/contribute \
164
216
  --spend-request-id lsrq_001 \
165
217
  --method POST \
166
218
  --data '{"amount":100}' \
167
- --header "X-Custom: value" \
168
- --format json
219
+ --header "X-Custom: value"
169
220
  ```
170
221
 
171
222
  Use `mpp decode` to validate a raw `WWW-Authenticate` header and extract the `network_id` needed for `shared_payment_token` spend requests:
172
223
 
173
224
  ```bash
174
225
  link-cli mpp decode \
175
- --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."' \
176
- --format json
226
+ --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."'
177
227
  ```
178
228
 
179
229
  ### Environment variables
@@ -186,7 +236,7 @@ link-cli mpp decode \
186
236
 
187
237
  ## Onboard
188
238
 
189
- 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:
190
240
 
191
241
  ```bash
192
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({
@@ -13590,8 +13622,13 @@ function parseKvString(raw) {
13590
13622
  }
13591
13623
  return result;
13592
13624
  }
13593
- function formatZodError(err, prefix) {
13625
+ function formatZodError(err, prefix, schema) {
13626
+ const allowed = Object.keys(schema.shape);
13594
13627
  const messages = err.issues.map((issue) => {
13628
+ if (issue.code === "unrecognized_keys") {
13629
+ const keys = issue.keys.map((k) => `"${k}"`).join(", ");
13630
+ return `${prefix}: unrecognized key ${keys}. Allowed keys: ${allowed.join(", ")}`;
13631
+ }
13595
13632
  const key = issue.path[0]?.toString();
13596
13633
  return key ? `${prefix} ${key}: ${issue.message}` : `${prefix}: ${issue.message}`;
13597
13634
  });
@@ -13602,7 +13639,8 @@ function parseLineItemFlag(raw) {
13602
13639
  try {
13603
13640
  return LineItemSchema.parse(obj);
13604
13641
  } catch (err) {
13605
- if (err instanceof z5.ZodError) throw formatZodError(err, "Line item");
13642
+ if (err instanceof z5.ZodError)
13643
+ throw formatZodError(err, "Line item", LineItemSchema);
13606
13644
  throw err;
13607
13645
  }
13608
13646
  }
@@ -13611,7 +13649,8 @@ function parseTotalFlag(raw) {
13611
13649
  try {
13612
13650
  return TotalSchema.parse(obj);
13613
13651
  } catch (err) {
13614
- if (err instanceof z5.ZodError) throw formatZodError(err, "Total");
13652
+ if (err instanceof z5.ZodError)
13653
+ throw formatZodError(err, "Total", TotalSchema);
13615
13654
  throw err;
13616
13655
  }
13617
13656
  }
@@ -13718,11 +13757,15 @@ var CreateSpendRequest = ({
13718
13757
  repository,
13719
13758
  params,
13720
13759
  requestApproval = false,
13760
+ outputFile,
13761
+ force,
13721
13762
  onComplete
13722
13763
  }) => {
13723
13764
  const [status, setStatus] = useState10("creating");
13724
13765
  const [request, setRequest] = useState10(null);
13725
13766
  const [error, setError] = useState10("");
13767
+ const [outputFilePath, setOutputFilePath] = useState10(null);
13768
+ const [fileError, setFileError] = useState10("");
13726
13769
  const approvalUrl = request?.approval_url ?? "";
13727
13770
  const onSuccess = useCallback2(
13728
13771
  (result) => setRequest(result),
@@ -13758,6 +13801,18 @@ var CreateSpendRequest = ({
13758
13801
  };
13759
13802
  create();
13760
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]);
13761
13816
  if (status === "creating") {
13762
13817
  return /* @__PURE__ */ jsx20(Box14, { children: /* @__PURE__ */ jsxs13(Text15, { color: "cyan", children: [
13763
13818
  /* @__PURE__ */ jsx20(Spinner5, { type: "dots" }),
@@ -13801,6 +13856,49 @@ var CreateSpendRequest = ({
13801
13856
  /* @__PURE__ */ jsx20(Text15, { bold: true, children: request.shared_payment_token.id })
13802
13857
  ] })
13803
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
+ ] }),
13804
13902
  /* @__PURE__ */ jsx20(AppDownloadQrCodes, {})
13805
13903
  ] });
13806
13904
  }
@@ -13918,12 +14016,16 @@ var RetrieveSpendRequest = ({
13918
14016
  id,
13919
14017
  timeout = 300,
13920
14018
  include,
14019
+ outputFile,
14020
+ force,
13921
14021
  onComplete
13922
14022
  }) => {
13923
14023
  const [phase, setPhase] = useState12("fetching");
13924
14024
  const [request, setRequest] = useState12(null);
13925
14025
  const [error, setError] = useState12("");
13926
14026
  const [elapsed, setElapsed] = useState12(0);
14027
+ const [outputFilePath, setOutputFilePath] = useState12(null);
14028
+ const [fileError, setFileError] = useState12("");
13927
14029
  const startTimeRef = useRef4(Date.now());
13928
14030
  const pollRef = useRef4(null);
13929
14031
  const timerRef = useRef4(null);
@@ -13934,6 +14036,18 @@ var RetrieveSpendRequest = ({
13934
14036
  if (timerRef.current) clearInterval(timerRef.current);
13935
14037
  };
13936
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]);
13937
14051
  useEffect12(() => {
13938
14052
  const fetch2 = async () => {
13939
14053
  try {
@@ -14114,7 +14228,7 @@ var RetrieveSpendRequest = ({
14114
14228
  /* @__PURE__ */ jsx22(Text17, { bold: true, children: request.shared_payment_token.id })
14115
14229
  ] })
14116
14230
  ] }),
14117
- request?.card && /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", marginTop: 1, children: [
14231
+ request?.card && !outputFile && /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", marginTop: 1, children: [
14118
14232
  /* @__PURE__ */ jsx22(Text17, { bold: true, children: "Card Details:" }),
14119
14233
  /* @__PURE__ */ jsxs15(Text17, { children: [
14120
14234
  " ",
@@ -14173,6 +14287,16 @@ var RetrieveSpendRequest = ({
14173
14287
  request.card.billing_address.country
14174
14288
  ] })
14175
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
+ ] })
14176
14300
  ] })
14177
14301
  ] })
14178
14302
  ] });
@@ -14199,12 +14323,20 @@ var createOptions = z6.object({
14199
14323
  context: z6.string().min(100).describe(
14200
14324
  "Min 100 chars \u2014 describe the purchase and rationale; the user reads this when approving"
14201
14325
  ),
14202
- lineItem: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe("Line item (repeatable, key:value format)"),
14203
- total: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe("Total (repeatable, key:value format)"),
14326
+ lineItem: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe(
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"'
14328
+ ),
14329
+ total: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe(
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"'
14331
+ ),
14204
14332
  requestApproval: z6.boolean().default(true).describe("Request approval and poll until approved/denied/expired"),
14205
14333
  test: z6.boolean().default(false).describe(
14206
14334
  "Use test mode (creates testmode credentials from test card data)"
14207
- )
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")
14208
14340
  });
14209
14341
  var retrieveOptions = z6.object({
14210
14342
  timeout: z6.coerce.number().default(300).describe(
@@ -14216,7 +14348,11 @@ var retrieveOptions = z6.object({
14216
14348
  maxAttempts: z6.coerce.number().default(0).describe(
14217
14349
  "Max poll attempts. 0 = unlimited. Exhaustion during active polling exits non-zero with POLLING_TIMEOUT."
14218
14350
  ),
14219
- 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")
14220
14356
  });
14221
14357
  var updateOptions = z6.object({
14222
14358
  paymentMethodId: z6.string().optional().describe("Payment method ID"),
@@ -14225,8 +14361,12 @@ var updateOptions = z6.object({
14225
14361
  profileId: z6.string().optional().describe("Profile ID"),
14226
14362
  merchantId: z6.string().optional().describe("Merchant ID"),
14227
14363
  currency: z6.string().optional().describe("Currency code"),
14228
- lineItem: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe("Line item (repeatable, key:value format)"),
14229
- total: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe("Total (repeatable, key:value format)")
14364
+ lineItem: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe(
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"'
14366
+ ),
14367
+ total: z6.array(z6.union([z6.string(), z6.record(z6.string(), z6.unknown())])).default([]).describe(
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"'
14369
+ )
14230
14370
  });
14231
14371
 
14232
14372
  // src/commands/spend-request/update.tsx
@@ -14308,6 +14448,23 @@ var UpdateSpendRequest = ({
14308
14448
 
14309
14449
  // src/commands/spend-request/index.tsx
14310
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
+ }
14311
14468
  function createSpendRequestCli(repository) {
14312
14469
  const cli2 = Cli6.create("spend-request", {
14313
14470
  description: "Spend request management commands"
@@ -14385,6 +14542,8 @@ function createSpendRequestCli(repository) {
14385
14542
  request_approval: requestApproval || void 0,
14386
14543
  test: opts.test ? true : void 0
14387
14544
  };
14545
+ const outputFile = opts.outputFile;
14546
+ const forceOverwrite = opts.force;
14388
14547
  if (!c.agent && !c.formatExplicit) {
14389
14548
  return new Promise((resolve) => {
14390
14549
  let capturedResult = null;
@@ -14395,6 +14554,8 @@ function createSpendRequestCli(repository) {
14395
14554
  repository,
14396
14555
  params: createParams,
14397
14556
  requestApproval,
14557
+ outputFile,
14558
+ force: forceOverwrite,
14398
14559
  onComplete: (result) => {
14399
14560
  capturedResult = result;
14400
14561
  }
@@ -14408,7 +14569,15 @@ function createSpendRequestCli(repository) {
14408
14569
  }
14409
14570
  const created = await repository.createSpendRequest(createParams);
14410
14571
  if (!requestApproval) {
14411
- 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
+ }
14412
14581
  return;
14413
14582
  }
14414
14583
  yield {
@@ -14559,6 +14728,8 @@ function createSpendRequestCli(repository) {
14559
14728
  const maxAttempts = opts.maxAttempts;
14560
14729
  const includeArr = opts.include;
14561
14730
  const include = includeArr?.length ? includeArr : void 0;
14731
+ const outputFile = opts.outputFile;
14732
+ const forceOverwrite = opts.force;
14562
14733
  if (!c.agent && !c.formatExplicit) {
14563
14734
  return new Promise((resolve) => {
14564
14735
  let capturedResult = null;
@@ -14570,6 +14741,8 @@ function createSpendRequestCli(repository) {
14570
14741
  id,
14571
14742
  timeout,
14572
14743
  include,
14744
+ outputFile,
14745
+ force: forceOverwrite,
14573
14746
  onComplete: (result) => {
14574
14747
  capturedResult = result;
14575
14748
  }
@@ -14599,15 +14772,20 @@ function createSpendRequestCli(repository) {
14599
14772
  message: `Spend request ${id} not found`
14600
14773
  });
14601
14774
  }
14602
- if (terminalStatuses.has(request.status)) {
14603
- 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
+ }
14604
14786
  return;
14605
14787
  }
14606
14788
  attempts++;
14607
- if (interval <= 0) {
14608
- yield request;
14609
- return;
14610
- }
14611
14789
  const maxAttemptsExhausted = maxAttempts > 0 && attempts >= maxAttempts;
14612
14790
  const timeoutReached = Date.now() >= deadline;
14613
14791
  if (maxAttemptsExhausted || timeoutReached) {
@@ -14929,8 +15107,78 @@ var ResourceFactory = class {
14929
15107
  }
14930
15108
  };
14931
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
+
14932
15180
  // src/cli.tsx
14933
- var cliVersion = "0.4.1";
15181
+ var cliVersion = "0.4.3";
14934
15182
  var buildNumber = "1";
14935
15183
  var cliName = "@stripe/link-cli";
14936
15184
  var defaultHeaders = {
@@ -14941,14 +15189,24 @@ var verbose = process.argv.includes("--verbose");
14941
15189
  var factory = new ResourceFactory({ verbose, defaultHeaders });
14942
15190
  var authRepo = factory.createAuthResource();
14943
15191
  var spendRequestRepo = factory.createSpendRequestResource();
14944
- var notifier = updateNotifier({
14945
- pkg: { name: cliName, version: cliVersion }
14946
- });
14947
15192
  var cli = Cli7.create("link-cli", {
14948
15193
  description: "Create a secure, one-time payment credential from a Link wallet to let agents complete purchases on behalf of users.",
14949
15194
  version: `${cliVersion} (build ${buildNumber})`
14950
15195
  });
14951
- 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));
14952
15210
  cli.command(createSpendRequestCli(spendRequestRepo));
14953
15211
  cli.command(
14954
15212
  createPaymentMethodsCli(() => factory.createPaymentMethodsResource())
@@ -14968,10 +15226,6 @@ cli.command(
14968
15226
  () => factory.createPaymentMethodsResource()
14969
15227
  )
14970
15228
  );
14971
- var isAgent = process.argv.includes("--format") || process.argv.includes("--mcp");
14972
- if (!isAgent) {
14973
- notifier.notify({ defer: false });
14974
- }
14975
15229
  cli.serve();
14976
15230
  var cli_default = cli;
14977
15231
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stripe/link-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-cli": "./dist/cli.js"
@@ -25,6 +25,7 @@
25
25
  "ink": "^5.2.1",
26
26
  "ink-spinner": "^5.0.0",
27
27
  "mppx": "^0.5.7",
28
+ "viem": "^2.47.5",
28
29
  "qrcode": "^1.5.4",
29
30
  "react": "^18.3.1",
30
31
  "update-notifier": "^7.3.1",
@@ -39,8 +40,8 @@
39
40
  "tsx": "^4.21.0",
40
41
  "typescript": "^5.9.3",
41
42
  "vitest": "^4.1.2",
42
- "@stripe/link-sdk": "1.0.0",
43
- "@stripe/link-typescript-config": "0.0.0"
43
+ "@stripe/link-typescript-config": "0.0.0",
44
+ "@stripe/link-sdk": "1.0.0"
44
45
  },
45
46
  "scripts": {
46
47
  "build": "tsup",