@stripe/link-cli 0.8.1 → 0.8.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 +2 -2
  2. package/dist/cli.js +575 -101
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -105,7 +105,7 @@ Returns the shipping addresses saved to your Link account. The response preserve
105
105
 
106
106
  ### Create a spend request
107
107
 
108
- Create a spend request with a payment method, merchant details, line items, and amounts:
108
+ Create a spend request with merchant details, line items, and amounts. If `--payment-method-id` is omitted, your default payment method will be used, or the first eligible one if no default is set:
109
109
 
110
110
  ```bash
111
111
  link-cli spend-request create \
@@ -214,7 +214,7 @@ All commands accept `--auth <path>` to store auth credentials in a specific file
214
214
 
215
215
  A spend request moves through: **create** → **request approval** → **approved** (with credentials).
216
216
 
217
- **Required fields for create:** `payment_method_id`, `merchant_name`, `merchant_url`, `context`, `amount`
217
+ **Required fields for create:** `merchant_name`, `merchant_url`, `context`, `amount`. `payment_method_id` is optional — if omitted, your default payment method will be used, or the first eligible one if no default is set.
218
218
 
219
219
  **Constraints:** `context` must be at least 100 characters; `amount` must not exceed 500000 (cents); `currency` must be a 3-letter ISO code. The user has 10 minutes from when approval is requested to approve. Approved credentials (card or SPT) are valid for 12 hours from spend request creation.
220
220
  **Test mode:** Pass `--test` to create testmode credentials (uses test card `4242424242424242`), useful for development and integration testing without real payment methods.
package/dist/cli.js CHANGED
@@ -8835,19 +8835,19 @@ var require_range = __commonJS({
8835
8835
  var replaceCaret = (comp, options) => {
8836
8836
  debug("caret", comp, options);
8837
8837
  const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET];
8838
- const z10 = options.includePrerelease ? "-0" : "";
8838
+ const z11 = options.includePrerelease ? "-0" : "";
8839
8839
  return comp.replace(r, (_, M, m, p, pr) => {
8840
8840
  debug("caret", comp, _, M, m, p, pr);
8841
8841
  let ret;
8842
8842
  if (isX(M)) {
8843
8843
  ret = "";
8844
8844
  } else if (isX(m)) {
8845
- ret = `>=${M}.0.0${z10} <${+M + 1}.0.0-0`;
8845
+ ret = `>=${M}.0.0${z11} <${+M + 1}.0.0-0`;
8846
8846
  } else if (isX(p)) {
8847
8847
  if (M === "0") {
8848
- ret = `>=${M}.${m}.0${z10} <${M}.${+m + 1}.0-0`;
8848
+ ret = `>=${M}.${m}.0${z11} <${M}.${+m + 1}.0-0`;
8849
8849
  } else {
8850
- ret = `>=${M}.${m}.0${z10} <${+M + 1}.0.0-0`;
8850
+ ret = `>=${M}.${m}.0${z11} <${+M + 1}.0.0-0`;
8851
8851
  }
8852
8852
  } else if (pr) {
8853
8853
  debug("replaceCaret pr", pr);
@@ -8864,9 +8864,9 @@ var require_range = __commonJS({
8864
8864
  debug("no pr");
8865
8865
  if (M === "0") {
8866
8866
  if (m === "0") {
8867
- ret = `>=${M}.${m}.${p}${z10} <${M}.${m}.${+p + 1}-0`;
8867
+ ret = `>=${M}.${m}.${p}${z11} <${M}.${m}.${+p + 1}-0`;
8868
8868
  } else {
8869
- ret = `>=${M}.${m}.${p}${z10} <${M}.${+m + 1}.0-0`;
8869
+ ret = `>=${M}.${m}.${p}${z11} <${M}.${+m + 1}.0-0`;
8870
8870
  }
8871
8871
  } else {
8872
8872
  ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`;
@@ -11524,6 +11524,216 @@ var SpendRequestResource = class {
11524
11524
  return normalizeSpendRequest(data);
11525
11525
  }
11526
11526
  };
11527
+ function isRecord(value) {
11528
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11529
+ }
11530
+ function requireString(value, field) {
11531
+ if (typeof value !== "string") {
11532
+ throw new TypeError(`Expected ${field} to be a string`);
11533
+ }
11534
+ return value;
11535
+ }
11536
+ function requireNullableString(value, field) {
11537
+ if (value === null) {
11538
+ return null;
11539
+ }
11540
+ if (typeof value !== "string") {
11541
+ throw new TypeError(`Expected ${field} to be a string or null`);
11542
+ }
11543
+ return value;
11544
+ }
11545
+ function requireNumber(value, field) {
11546
+ if (typeof value !== "number" || !Number.isFinite(value)) {
11547
+ throw new TypeError(`Expected ${field} to be a finite number`);
11548
+ }
11549
+ return value;
11550
+ }
11551
+ function requireBoolean(value, field) {
11552
+ if (typeof value !== "boolean") {
11553
+ throw new TypeError(`Expected ${field} to be a boolean`);
11554
+ }
11555
+ return value;
11556
+ }
11557
+ function requireTransactionOrigin(value, field) {
11558
+ if (value === "link" || value === "external_connection") {
11559
+ return value;
11560
+ }
11561
+ throw new TypeError(`Expected ${field} to be a transaction origin`);
11562
+ }
11563
+ function normalizeTransactions(value) {
11564
+ if (!Array.isArray(value)) {
11565
+ throw new TypeError("Expected transactions to be an array");
11566
+ }
11567
+ return value.map((item, index) => {
11568
+ if (!isRecord(item)) {
11569
+ throw new TypeError(`Expected transactions[${index}] to be an object`);
11570
+ }
11571
+ return {
11572
+ id: requireString(item.id, `transactions[${index}].id`),
11573
+ source_id: requireNullableString(
11574
+ item.source_id,
11575
+ `transactions[${index}].source_id`
11576
+ ),
11577
+ amount: requireNumber(item.amount, `transactions[${index}].amount`),
11578
+ currency: requireString(item.currency, `transactions[${index}].currency`),
11579
+ created_date: requireString(
11580
+ item.created_date,
11581
+ `transactions[${index}].created_date`
11582
+ ),
11583
+ description: requireString(
11584
+ item.description,
11585
+ `transactions[${index}].description`
11586
+ ),
11587
+ origin: requireTransactionOrigin(
11588
+ item.origin,
11589
+ `transactions[${index}].origin`
11590
+ ),
11591
+ category: requireNullableString(
11592
+ item.category,
11593
+ `transactions[${index}].category`
11594
+ ),
11595
+ status: requireString(item.status, `transactions[${index}].status`)
11596
+ };
11597
+ });
11598
+ }
11599
+ function normalizeTransactionsPage(value) {
11600
+ if (Array.isArray(value)) {
11601
+ return { data: normalizeTransactions(value) };
11602
+ }
11603
+ if (!isRecord(value)) {
11604
+ throw new TypeError("Expected response body to be an object");
11605
+ }
11606
+ const { data, has_more, ...rest } = value;
11607
+ const normalized = normalizeTransactions(data);
11608
+ return {
11609
+ ...rest,
11610
+ data: normalized,
11611
+ ...has_more !== void 0 ? { has_more: requireBoolean(has_more, "has_more") } : {}
11612
+ };
11613
+ }
11614
+ var TransactionsResource = class {
11615
+ verbose;
11616
+ getAccessToken;
11617
+ fetchImpl;
11618
+ endpoint;
11619
+ logger;
11620
+ constructor(options = {}) {
11621
+ const config = resolveLinkSdkConfig(options);
11622
+ this.verbose = config.verbose;
11623
+ this.getAccessToken = config.getAccessToken;
11624
+ this.fetchImpl = requireFetchImplementation(config);
11625
+ this.endpoint = `${config.apiBaseUrl}/transactions`;
11626
+ this.logger = config.logger;
11627
+ }
11628
+ async rawFetch(opts) {
11629
+ if (this.verbose) {
11630
+ const redactedHeaders = { ...opts.headers };
11631
+ if (redactedHeaders.Authorization)
11632
+ redactedHeaders.Authorization = "Bearer <redacted>";
11633
+ this.logger.debug(`> ${opts.method} ${opts.url}`);
11634
+ this.logger.debug(` Headers: ${JSON.stringify(redactedHeaders)}`);
11635
+ }
11636
+ let response;
11637
+ try {
11638
+ response = await this.fetchImpl(opts.url, {
11639
+ method: opts.method,
11640
+ headers: opts.headers
11641
+ });
11642
+ } catch (error) {
11643
+ throw new LinkTransportError(
11644
+ `Request failed: ${opts.method} ${opts.url}`,
11645
+ { cause: error }
11646
+ );
11647
+ }
11648
+ const rawBody = await response.text();
11649
+ let data = null;
11650
+ try {
11651
+ data = JSON.parse(rawBody);
11652
+ } catch {
11653
+ }
11654
+ if (this.verbose) {
11655
+ this.logger.debug(`< ${response.status} ${response.statusText}`);
11656
+ response.headers.forEach((value, key) => {
11657
+ this.logger.debug(` ${key}: ${value}`);
11658
+ });
11659
+ this.logger.debug(rawBody);
11660
+ }
11661
+ return { status: response.status, data, rawBody };
11662
+ }
11663
+ async apiFetch(opts) {
11664
+ const token = await this.getAccessToken();
11665
+ const authedOpts = {
11666
+ ...opts,
11667
+ headers: {
11668
+ ...opts.headers,
11669
+ Authorization: `Bearer ${token}`
11670
+ }
11671
+ };
11672
+ const res = await this.rawFetch(authedOpts);
11673
+ if (res.status === 401) {
11674
+ const refreshedToken = await this.getAccessToken({ forceRefresh: true });
11675
+ authedOpts.headers.Authorization = `Bearer ${refreshedToken}`;
11676
+ return this.rawFetch(authedOpts);
11677
+ }
11678
+ return res;
11679
+ }
11680
+ buildUrl(params) {
11681
+ const url = new URL(this.endpoint);
11682
+ if (params.limit !== void 0) {
11683
+ url.searchParams.set("limit", String(params.limit));
11684
+ }
11685
+ if (params.starting_after !== void 0) {
11686
+ url.searchParams.set("starting_after", params.starting_after);
11687
+ }
11688
+ if (params.ending_before !== void 0) {
11689
+ url.searchParams.set("ending_before", params.ending_before);
11690
+ }
11691
+ if (params.start_date !== void 0) {
11692
+ url.searchParams.set("date_start", params.start_date);
11693
+ }
11694
+ if (params.end_date !== void 0) {
11695
+ url.searchParams.set("date_end", params.end_date);
11696
+ }
11697
+ if (params.category !== void 0) {
11698
+ url.searchParams.set("category", params.category);
11699
+ }
11700
+ if (params.origin !== void 0) {
11701
+ url.searchParams.set("origin", params.origin);
11702
+ }
11703
+ if (params.sources !== void 0) {
11704
+ for (const source of params.sources) {
11705
+ url.searchParams.append("sources[]", source);
11706
+ }
11707
+ }
11708
+ return url.toString();
11709
+ }
11710
+ list(params = {}) {
11711
+ return this.listTransactions(params);
11712
+ }
11713
+ async listTransactions(params = {}) {
11714
+ const { status, data, rawBody } = await this.apiFetch({
11715
+ method: "GET",
11716
+ url: this.buildUrl(params)
11717
+ });
11718
+ if (status < 200 || status >= 300) {
11719
+ const body = data;
11720
+ const msg = body?.error ?? body?.message ?? (rawBody || "unknown error");
11721
+ throw new LinkApiError(
11722
+ `Failed to list transactions (${status}): ${msg}`,
11723
+ { status, rawBody, details: data }
11724
+ );
11725
+ }
11726
+ try {
11727
+ return normalizeTransactionsPage(data);
11728
+ } catch (error) {
11729
+ const reason = error instanceof Error ? `: ${error.message}` : "";
11730
+ throw new LinkApiError(
11731
+ `Failed to list transactions (200): invalid response shape${reason}`,
11732
+ { status, rawBody, details: data, cause: error }
11733
+ );
11734
+ }
11735
+ }
11736
+ };
11527
11737
  var UserInfoResource = class {
11528
11738
  verbose;
11529
11739
  getAccessToken;
@@ -11843,7 +12053,7 @@ var ReportResource = class {
11843
12053
  };
11844
12054
 
11845
12055
  // src/cli.tsx
11846
- import { Cli as Cli11 } from "incur";
12056
+ import { Cli as Cli12 } from "incur";
11847
12057
 
11848
12058
  // src/commands/auth/index.tsx
11849
12059
  import { Cli } from "incur";
@@ -14791,7 +15001,7 @@ var CancelSpendRequest = ({
14791
15001
  };
14792
15002
 
14793
15003
  // src/commands/spend-request/create.tsx
14794
- import { Box as Box16, Text as Text18 } from "ink";
15004
+ import { Box as Box16, Text as Text18, useApp as useApp4 } from "ink";
14795
15005
  import Spinner8 from "ink-spinner";
14796
15006
  import { useCallback as useCallback6, useEffect as useEffect9, useState as useState9 } from "react";
14797
15007
 
@@ -14906,9 +15116,18 @@ var CreateSpendRequest = ({
14906
15116
  const [status, setStatus] = useState9("creating");
14907
15117
  const [request, setRequest] = useState9(null);
14908
15118
  const [error, setError] = useState9("");
15119
+ const [verificationUrl, setVerificationUrl] = useState9("");
14909
15120
  const [outputFilePath, setOutputFilePath] = useState9(null);
14910
15121
  const [fileError, setFileError] = useState9("");
14911
15122
  const approvalUrl = request?.approval_url ?? "";
15123
+ const { exit } = useApp4();
15124
+ const completeAndExit = useCallback6(
15125
+ (result) => {
15126
+ onComplete(result);
15127
+ exit();
15128
+ },
15129
+ [onComplete, exit]
15130
+ );
14912
15131
  const onSuccess = useCallback6(
14913
15132
  (result) => setRequest(result),
14914
15133
  []
@@ -14920,7 +15139,7 @@ var CreateSpendRequest = ({
14920
15139
  approvalUrl,
14921
15140
  repository,
14922
15141
  requestId: request?.id ?? null,
14923
- onComplete,
15142
+ onComplete: completeAndExit,
14924
15143
  onSuccess,
14925
15144
  onError
14926
15145
  });
@@ -14933,16 +15152,20 @@ var CreateSpendRequest = ({
14933
15152
  setStatus("waiting");
14934
15153
  } else {
14935
15154
  setStatus("success");
14936
- setTimeout(() => onComplete(result), DISPLAY_DELAY_MS);
15155
+ setTimeout(() => completeAndExit(result), DISPLAY_DELAY_MS);
14937
15156
  }
14938
15157
  } catch (err) {
14939
15158
  setError(err.message);
15159
+ if (err instanceof LinkApiError) {
15160
+ const url = err.details?.error?.verification_url;
15161
+ if (url) setVerificationUrl(url);
15162
+ }
14940
15163
  setStatus("error");
14941
- setTimeout(() => onComplete(null), DISPLAY_DELAY_MS);
15164
+ setTimeout(() => completeAndExit(null), DISPLAY_DELAY_MS);
14942
15165
  }
14943
15166
  };
14944
15167
  create();
14945
- }, [repository, params, requestApproval, onComplete]);
15168
+ }, [repository, params, requestApproval, completeAndExit]);
14946
15169
  useEffect9(() => {
14947
15170
  if (status !== "success" || !outputFile || !request?.card) return;
14948
15171
  const fileData = {
@@ -14964,7 +15187,11 @@ var CreateSpendRequest = ({
14964
15187
  if (status === "error") {
14965
15188
  return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
14966
15189
  /* @__PURE__ */ jsx23(Text18, { color: "red", children: "\u2717 Failed to create spend request" }),
14967
- /* @__PURE__ */ jsx23(Text18, { color: "red", children: error })
15190
+ /* @__PURE__ */ jsx23(Text18, { color: "red", children: error }),
15191
+ verificationUrl && /* @__PURE__ */ jsxs16(Text18, { color: "red", children: [
15192
+ "Complete additional verification at: ",
15193
+ verificationUrl
15194
+ ] })
14968
15195
  ] });
14969
15196
  }
14970
15197
  if (status === "success") {
@@ -15061,7 +15288,7 @@ var CreateSpendRequest = ({
15061
15288
  };
15062
15289
 
15063
15290
  // src/commands/spend-request/list.tsx
15064
- import { Box as Box17, Text as Text19, useApp as useApp4 } from "ink";
15291
+ import { Box as Box17, Text as Text19, useApp as useApp5 } from "ink";
15065
15292
  import Spinner9 from "ink-spinner";
15066
15293
  import { useCallback as useCallback7 } from "react";
15067
15294
  import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
@@ -15070,7 +15297,7 @@ var SpendRequestList = ({
15070
15297
  includeHistory = false,
15071
15298
  onComplete
15072
15299
  }) => {
15073
- const { exit } = useApp4();
15300
+ const { exit } = useApp5();
15074
15301
  const action = useCallback7(
15075
15302
  () => repository.listSpendRequests({ includeHistory }),
15076
15303
  [repository, includeHistory]
@@ -15119,7 +15346,7 @@ var SpendRequestList = ({
15119
15346
  };
15120
15347
 
15121
15348
  // src/commands/spend-request/request-approval.tsx
15122
- import { Box as Box18, Text as Text20 } from "ink";
15349
+ import { Box as Box18, Text as Text20, useApp as useApp6 } from "ink";
15123
15350
  import Spinner10 from "ink-spinner";
15124
15351
  import { useCallback as useCallback8, useEffect as useEffect10, useState as useState10 } from "react";
15125
15352
  import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
@@ -15132,6 +15359,15 @@ var RequestApproval = ({
15132
15359
  const [approvalUrl, setApprovalUrl] = useState10("");
15133
15360
  const [result, setResult] = useState10(null);
15134
15361
  const [error, setError] = useState10("");
15362
+ const [verificationUrl, setVerificationUrl] = useState10("");
15363
+ const { exit } = useApp6();
15364
+ const completeAndExit = useCallback8(
15365
+ (result2) => {
15366
+ onComplete(result2);
15367
+ exit();
15368
+ },
15369
+ [onComplete, exit]
15370
+ );
15135
15371
  const onSuccess = useCallback8((r) => setResult(r), []);
15136
15372
  const onError = useCallback8((msg) => setError(msg), []);
15137
15373
  useApprovalPolling({
@@ -15140,7 +15376,7 @@ var RequestApproval = ({
15140
15376
  approvalUrl,
15141
15377
  repository,
15142
15378
  requestId: id,
15143
- onComplete,
15379
+ onComplete: completeAndExit,
15144
15380
  onSuccess,
15145
15381
  onError
15146
15382
  });
@@ -15152,11 +15388,19 @@ var RequestApproval = ({
15152
15388
  setStatus("waiting");
15153
15389
  } catch (err) {
15154
15390
  setError(err.message);
15391
+ if (err instanceof LinkApiError) {
15392
+ const url = err.details?.error?.verification_url;
15393
+ if (url) setVerificationUrl(url);
15394
+ }
15155
15395
  setStatus("error");
15396
+ setTimeout(() => {
15397
+ onComplete(null);
15398
+ exit();
15399
+ }, DISPLAY_DELAY_MS);
15156
15400
  }
15157
15401
  };
15158
15402
  request();
15159
- }, [repository, id]);
15403
+ }, [repository, id, exit, onComplete]);
15160
15404
  if (status === "requesting") {
15161
15405
  return /* @__PURE__ */ jsx25(Box18, { children: /* @__PURE__ */ jsxs18(Text20, { color: "cyan", children: [
15162
15406
  /* @__PURE__ */ jsx25(Spinner10, { type: "dots" }),
@@ -15166,7 +15410,11 @@ var RequestApproval = ({
15166
15410
  if (status === "error") {
15167
15411
  return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
15168
15412
  /* @__PURE__ */ jsx25(Text20, { color: "red", children: "\u2717 Failed to request approval" }),
15169
- /* @__PURE__ */ jsx25(Text20, { color: "red", children: error })
15413
+ /* @__PURE__ */ jsx25(Text20, { color: "red", children: error }),
15414
+ verificationUrl && /* @__PURE__ */ jsxs18(Text20, { color: "red", children: [
15415
+ "Complete additional verification at: ",
15416
+ verificationUrl
15417
+ ] })
15170
15418
  ] });
15171
15419
  }
15172
15420
  if (status === "success") {
@@ -15631,7 +15879,7 @@ var RetrieveSpendRequest = ({
15631
15879
  // src/commands/spend-request/schema.ts
15632
15880
  import { z as z8 } from "incur";
15633
15881
  var createOptions = z8.object({
15634
- paymentMethodId: z8.string().describe("Payment method ID"),
15882
+ paymentMethodId: z8.string().optional().describe("Payment method ID"),
15635
15883
  credentialType: z8.enum(["shared_payment_token", "card"]).default("card").describe(
15636
15884
  '"card" for checkout forms/Stripe Elements; "shared_payment_token" for HTTP 402/machine payment flows'
15637
15885
  ),
@@ -15875,7 +16123,7 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
15875
16123
  const outputFile = opts.outputFile;
15876
16124
  const forceOverwrite = opts.force;
15877
16125
  if (!c.agent && !c.formatExplicit) {
15878
- let capturedResult = null;
16126
+ let capturedResult = void 0;
15879
16127
  return renderInteractive(
15880
16128
  /* @__PURE__ */ jsx28(
15881
16129
  CreateSpendRequest,
@@ -15891,13 +16139,28 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
15891
16139
  }
15892
16140
  ),
15893
16141
  () => {
15894
- if (!capturedResult)
16142
+ if (capturedResult === void 0)
15895
16143
  throw new Error("Component exited without producing a result");
16144
+ if (capturedResult === null) process.exit(1);
15896
16145
  return capturedResult;
15897
16146
  }
15898
16147
  );
15899
16148
  }
15900
- const created = await repository.createSpendRequest(createParams);
16149
+ let created;
16150
+ try {
16151
+ created = await repository.createSpendRequest(createParams);
16152
+ } catch (err) {
16153
+ if (err instanceof LinkApiError) {
16154
+ const apiErr = err.details;
16155
+ if (apiErr?.error?.verification_url) {
16156
+ return c.error({
16157
+ code: err.code,
16158
+ message: `${err.message} Verification URL: ${apiErr.error.verification_url}`
16159
+ });
16160
+ }
16161
+ }
16162
+ throw err;
16163
+ }
15901
16164
  if (!requestApproval) {
15902
16165
  try {
15903
16166
  yield await applyOutputFile(created, outputFile, forceOverwrite);
@@ -15982,7 +16245,7 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
15982
16245
  requireAuthGuard(c, authStorage2, envAccessToken2);
15983
16246
  const id = c.args.id;
15984
16247
  if (!c.agent && !c.formatExplicit) {
15985
- let capturedResult = null;
16248
+ let capturedResult = void 0;
15986
16249
  return renderInteractive(
15987
16250
  /* @__PURE__ */ jsx28(
15988
16251
  RequestApproval,
@@ -15995,13 +16258,28 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
15995
16258
  }
15996
16259
  ),
15997
16260
  () => {
15998
- if (!capturedResult)
16261
+ if (capturedResult === void 0)
15999
16262
  throw new Error("Component exited without producing a result");
16263
+ if (capturedResult === null) process.exit(1);
16000
16264
  return capturedResult;
16001
16265
  }
16002
16266
  );
16003
16267
  }
16004
- const approval = await repository.requestApproval(id);
16268
+ let approval;
16269
+ try {
16270
+ approval = await repository.requestApproval(id);
16271
+ } catch (err) {
16272
+ if (err instanceof LinkApiError) {
16273
+ const apiErr = err.details;
16274
+ if (apiErr?.error?.verification_url) {
16275
+ return c.error({
16276
+ code: err.code,
16277
+ message: `${err.message} Verification URL: ${apiErr.error.verification_url}`
16278
+ });
16279
+ }
16280
+ }
16281
+ throw err;
16282
+ }
16005
16283
  yield {
16006
16284
  ...approval,
16007
16285
  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.`,
@@ -16138,55 +16416,221 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
16138
16416
  return cli2;
16139
16417
  }
16140
16418
 
16141
- // src/commands/user-info/index.tsx
16419
+ // src/commands/transactions/index.tsx
16142
16420
  import { Cli as Cli10 } from "incur";
16143
16421
 
16144
- // src/commands/user-info/retrieve.tsx
16422
+ // src/commands/transactions/list.tsx
16145
16423
  import { Box as Box21, Text as Text23 } from "ink";
16146
16424
  import Spinner13 from "ink-spinner";
16147
16425
  import { useCallback as useCallback10 } from "react";
16148
16426
  import { jsx as jsx29, jsxs as jsxs21 } from "react/jsx-runtime";
16149
- var UserInfoRetrieve = ({
16427
+ var COLUMN_GAP = " ";
16428
+ var DATE_WIDTH = 10;
16429
+ var AMOUNT_WIDTH = 13;
16430
+ var STATUS_WIDTH = 10;
16431
+ var CATEGORY_WIDTH = 16;
16432
+ var MIN_DESCRIPTION_WIDTH = 16;
16433
+ var HORIZONTAL_PADDING = 4;
16434
+ function formatAmount(amount, currency) {
16435
+ const currencyCode = currency.toUpperCase();
16436
+ try {
16437
+ const formatter = new Intl.NumberFormat("en-US", {
16438
+ style: "currency",
16439
+ currency: currencyCode
16440
+ });
16441
+ const fractionDigits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
16442
+ return formatter.format(amount / 10 ** fractionDigits);
16443
+ } catch {
16444
+ return `${amount} ${currency}`;
16445
+ }
16446
+ }
16447
+ function truncateCell(value, width) {
16448
+ if (value.length <= width) {
16449
+ return value;
16450
+ }
16451
+ if (width <= 3) {
16452
+ return value.slice(0, width);
16453
+ }
16454
+ return `${value.slice(0, width - 3)}...`;
16455
+ }
16456
+ function formatCell(value, width, align = "left") {
16457
+ const truncated = truncateCell(value, width);
16458
+ return align === "right" ? truncated.padStart(width) : truncated.padEnd(width);
16459
+ }
16460
+ var TransactionsList = ({
16150
16461
  resource,
16462
+ params,
16151
16463
  onComplete
16152
16464
  }) => {
16153
- const action = useCallback10(() => resource.retrieve(), [resource]);
16154
- const { status, data: userInfo, error } = useAsyncAction(action, onComplete);
16465
+ const action = useCallback10(
16466
+ () => resource.listTransactions(params),
16467
+ [resource, params]
16468
+ );
16469
+ const { status, data: page, error } = useAsyncAction(action, onComplete);
16470
+ const transactions = page?.data ?? [];
16471
+ const nextCursor = page?.has_more && transactions.length > 0 ? transactions[transactions.length - 1].id : null;
16472
+ const terminalWidth = process.stdout.columns ?? 100;
16473
+ const descriptionWidth = Math.max(
16474
+ MIN_DESCRIPTION_WIDTH,
16475
+ terminalWidth - HORIZONTAL_PADDING - DATE_WIDTH - AMOUNT_WIDTH - STATUS_WIDTH - CATEGORY_WIDTH - COLUMN_GAP.length * 4
16476
+ );
16477
+ const headerRow = [
16478
+ formatCell("Date", DATE_WIDTH),
16479
+ formatCell("Amount", AMOUNT_WIDTH, "right"),
16480
+ formatCell("Status", STATUS_WIDTH),
16481
+ formatCell("Category", CATEGORY_WIDTH),
16482
+ formatCell("Description", descriptionWidth)
16483
+ ].join(COLUMN_GAP);
16484
+ const separatorRow = "-".repeat(headerRow.length);
16485
+ const rows = transactions.map(
16486
+ (txn) => [
16487
+ formatCell(txn.created_date, DATE_WIDTH),
16488
+ formatCell(formatAmount(txn.amount, txn.currency), AMOUNT_WIDTH, "right"),
16489
+ formatCell(txn.status, STATUS_WIDTH),
16490
+ formatCell(txn.category ?? "", CATEGORY_WIDTH),
16491
+ formatCell(txn.description, descriptionWidth)
16492
+ ].join(COLUMN_GAP)
16493
+ );
16155
16494
  if (status === "loading") {
16156
16495
  return /* @__PURE__ */ jsx29(Box21, { children: /* @__PURE__ */ jsxs21(Text23, { color: "cyan", children: [
16157
16496
  /* @__PURE__ */ jsx29(Spinner13, { type: "dots" }),
16158
- " Loading user info..."
16497
+ " Loading transactions..."
16159
16498
  ] }) });
16160
16499
  }
16161
16500
  if (status === "error") {
16162
16501
  return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
16163
- /* @__PURE__ */ jsx29(Text23, { color: "red", children: "\u2717 Failed to load user info" }),
16502
+ /* @__PURE__ */ jsx29(Text23, { color: "red", children: "Failed to load transactions" }),
16164
16503
  /* @__PURE__ */ jsx29(Text23, { color: "red", children: error })
16165
16504
  ] });
16166
16505
  }
16506
+ if (transactions.length === 0) {
16507
+ return /* @__PURE__ */ jsx29(Box21, { children: /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: "No transactions found" }) });
16508
+ }
16167
16509
  return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
16168
- /* @__PURE__ */ jsx29(Text23, { bold: true, children: "User Info" }),
16510
+ /* @__PURE__ */ jsx29(Text23, { bold: true, children: "Transactions" }),
16169
16511
  /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
16170
- /* @__PURE__ */ jsxs21(Text23, { children: [
16171
- /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: "Email: " }),
16172
- userInfo?.email ?? /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: "Not set" })
16512
+ /* @__PURE__ */ jsx29(Text23, { bold: true, children: headerRow }),
16513
+ /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: separatorRow }),
16514
+ rows.map((row, index) => /* @__PURE__ */ jsx29(Text23, { children: row }, transactions[index].id))
16515
+ ] }),
16516
+ page?.has_more !== void 0 ? /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", marginTop: 1, children: [
16517
+ /* @__PURE__ */ jsxs21(Text23, { dimColor: true, children: [
16518
+ "has_more: ",
16519
+ String(page.has_more)
16520
+ ] }),
16521
+ nextCursor ? /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: `next page: --starting-after ${nextCursor}` }) : null
16522
+ ] }) : null
16523
+ ] });
16524
+ };
16525
+
16526
+ // src/commands/transactions/schema.ts
16527
+ import { z as z10 } from "incur";
16528
+ var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
16529
+ var listOptions2 = z10.object({
16530
+ limit: z10.coerce.number().int().positive().max(100).optional().describe("Maximum number of transactions to return (1-100)."),
16531
+ startingAfter: z10.string().optional().describe("Cursor: return transactions after this transaction ID."),
16532
+ endingBefore: z10.string().optional().describe("Cursor: return transactions before this transaction ID."),
16533
+ startDate: z10.string().regex(ISO_DATE_REGEX, "Date must be in YYYY-MM-DD format.").optional().describe("Only include transactions on or after this YYYY-MM-DD date."),
16534
+ endDate: z10.string().regex(ISO_DATE_REGEX, "Date must be in YYYY-MM-DD format.").optional().describe("Only include transactions on or before this YYYY-MM-DD date."),
16535
+ category: z10.string().optional().describe("Filter by transaction category."),
16536
+ origin: z10.enum(["link", "external_connection"]).optional().describe("Filter by transaction origin: link or external_connection."),
16537
+ source: z10.array(z10.string()).default([]).describe("Filter by source ID. Repeat to include multiple sources.")
16538
+ });
16539
+
16540
+ // src/commands/transactions/index.tsx
16541
+ import { jsx as jsx30 } from "react/jsx-runtime";
16542
+ function createTransactionsCli(createResource, authStorage2, envAccessToken2) {
16543
+ const cli2 = Cli10.create("transactions", {
16544
+ description: "List transactions from Link and external accounts"
16545
+ });
16546
+ cli2.command("list", {
16547
+ description: "List transactions from Link and external accounts, including non-Link activity",
16548
+ options: listOptions2,
16549
+ outputPolicy: "agent-only",
16550
+ middleware: [requireAuth(authStorage2, envAccessToken2)],
16551
+ async run(c) {
16552
+ const opts = c.options;
16553
+ const resource = createResource();
16554
+ const params = {};
16555
+ if (opts.limit !== void 0) params.limit = opts.limit;
16556
+ if (opts.startingAfter !== void 0)
16557
+ params.starting_after = opts.startingAfter;
16558
+ if (opts.endingBefore !== void 0)
16559
+ params.ending_before = opts.endingBefore;
16560
+ if (opts.startDate !== void 0) params.start_date = opts.startDate;
16561
+ if (opts.endDate !== void 0) params.end_date = opts.endDate;
16562
+ if (opts.category !== void 0) params.category = opts.category;
16563
+ if (opts.origin !== void 0) params.origin = opts.origin;
16564
+ if (opts.source.length > 0) params.sources = opts.source;
16565
+ if (!c.agent && !c.formatExplicit) {
16566
+ return renderInteractive(
16567
+ /* @__PURE__ */ jsx30(
16568
+ TransactionsList,
16569
+ {
16570
+ resource,
16571
+ params,
16572
+ onComplete: () => {
16573
+ }
16574
+ }
16575
+ ),
16576
+ () => resource.listTransactions(params)
16577
+ );
16578
+ }
16579
+ return resource.listTransactions(params);
16580
+ }
16581
+ });
16582
+ return cli2;
16583
+ }
16584
+
16585
+ // src/commands/user-info/index.tsx
16586
+ import { Cli as Cli11 } from "incur";
16587
+
16588
+ // src/commands/user-info/retrieve.tsx
16589
+ import { Box as Box22, Text as Text24 } from "ink";
16590
+ import Spinner14 from "ink-spinner";
16591
+ import { useCallback as useCallback11 } from "react";
16592
+ import { jsx as jsx31, jsxs as jsxs22 } from "react/jsx-runtime";
16593
+ var UserInfoRetrieve = ({
16594
+ resource,
16595
+ onComplete
16596
+ }) => {
16597
+ const action = useCallback11(() => resource.retrieve(), [resource]);
16598
+ const { status, data: userInfo, error } = useAsyncAction(action, onComplete);
16599
+ if (status === "loading") {
16600
+ return /* @__PURE__ */ jsx31(Box22, { children: /* @__PURE__ */ jsxs22(Text24, { color: "cyan", children: [
16601
+ /* @__PURE__ */ jsx31(Spinner14, { type: "dots" }),
16602
+ " Loading user info..."
16603
+ ] }) });
16604
+ }
16605
+ if (status === "error") {
16606
+ return /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", children: [
16607
+ /* @__PURE__ */ jsx31(Text24, { color: "red", children: "\u2717 Failed to load user info" }),
16608
+ /* @__PURE__ */ jsx31(Text24, { color: "red", children: error })
16609
+ ] });
16610
+ }
16611
+ return /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", children: [
16612
+ /* @__PURE__ */ jsx31(Text24, { bold: true, children: "User Info" }),
16613
+ /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
16614
+ /* @__PURE__ */ jsxs22(Text24, { children: [
16615
+ /* @__PURE__ */ jsx31(Text24, { dimColor: true, children: "Email: " }),
16616
+ userInfo?.email ?? /* @__PURE__ */ jsx31(Text24, { dimColor: true, children: "Not set" })
16173
16617
  ] }),
16174
- /* @__PURE__ */ jsxs21(Text23, { children: [
16175
- /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: "Name: " }),
16176
- userInfo?.name ?? /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: "Not set" })
16618
+ /* @__PURE__ */ jsxs22(Text24, { children: [
16619
+ /* @__PURE__ */ jsx31(Text24, { dimColor: true, children: "Name: " }),
16620
+ userInfo?.name ?? /* @__PURE__ */ jsx31(Text24, { dimColor: true, children: "Not set" })
16177
16621
  ] }),
16178
- /* @__PURE__ */ jsxs21(Text23, { children: [
16179
- /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: "Phone: " }),
16180
- userInfo?.phone ?? /* @__PURE__ */ jsx29(Text23, { dimColor: true, children: "Not set" })
16622
+ /* @__PURE__ */ jsxs22(Text24, { children: [
16623
+ /* @__PURE__ */ jsx31(Text24, { dimColor: true, children: "Phone: " }),
16624
+ userInfo?.phone ?? /* @__PURE__ */ jsx31(Text24, { dimColor: true, children: "Not set" })
16181
16625
  ] })
16182
16626
  ] })
16183
16627
  ] });
16184
16628
  };
16185
16629
 
16186
16630
  // src/commands/user-info/index.tsx
16187
- import { jsx as jsx30 } from "react/jsx-runtime";
16631
+ import { jsx as jsx32 } from "react/jsx-runtime";
16188
16632
  function createUserInfoCli(createResource, authStorage2, envAccessToken2) {
16189
- const cli2 = Cli10.create("user-info", {
16633
+ const cli2 = Cli11.create("user-info", {
16190
16634
  description: "User information commands"
16191
16635
  });
16192
16636
  cli2.command("retrieve", {
@@ -16197,7 +16641,7 @@ function createUserInfoCli(createResource, authStorage2, envAccessToken2) {
16197
16641
  const resource = createResource();
16198
16642
  if (!c.agent && !c.formatExplicit) {
16199
16643
  return renderInteractive(
16200
- /* @__PURE__ */ jsx30(UserInfoRetrieve, { resource, onComplete: () => {
16644
+ /* @__PURE__ */ jsx32(UserInfoRetrieve, { resource, onComplete: () => {
16201
16645
  } }),
16202
16646
  () => resource.retrieve()
16203
16647
  );
@@ -16488,6 +16932,7 @@ var ResourceFactory = class {
16488
16932
  paymentMethodsResource;
16489
16933
  shippingAddressResource;
16490
16934
  userInfoResource;
16935
+ transactionsResource;
16491
16936
  webBotAuthResource;
16492
16937
  reportResource;
16493
16938
  constructor(options = {}) {
@@ -16602,6 +17047,20 @@ var ResourceFactory = class {
16602
17047
  );
16603
17048
  return this.userInfoResource;
16604
17049
  }
17050
+ createTransactionsResource() {
17051
+ if (this.transactionsResource) {
17052
+ return this.transactionsResource;
17053
+ }
17054
+ const getAccessToken = this.createSdkAccessTokenProvider();
17055
+ this.transactionsResource = sanitizeResource(
17056
+ new TransactionsResource({
17057
+ verbose: this.verbose,
17058
+ defaultHeaders: this.defaultHeaders,
17059
+ getAccessToken
17060
+ })
17061
+ );
17062
+ return this.transactionsResource;
17063
+ }
16605
17064
  createWebBotAuthResource() {
16606
17065
  if (this.webBotAuthResource) {
16607
17066
  return this.webBotAuthResource;
@@ -16703,12 +17162,16 @@ function cacheUpdateInfo(value, ttlMs = UPDATE_CACHE_TTL_MS) {
16703
17162
  }
16704
17163
 
16705
17164
  // src/cli.tsx
16706
- var cliVersion = "0.8.1";
17165
+ var cliVersion = "0.8.3";
16707
17166
  var cliName = "@stripe/link-cli";
16708
17167
  var defaultHeaders = {
16709
17168
  "User-Agent": `link-cli/${cliVersion}`
16710
17169
  };
16711
- var verbose = process.argv.includes("--verbose");
17170
+ var verboseIndex = process.argv.indexOf("--verbose");
17171
+ var verbose = verboseIndex !== -1;
17172
+ if (verboseIndex !== -1) {
17173
+ process.argv.splice(verboseIndex, 1);
17174
+ }
16712
17175
  var authFileIndex = process.argv.indexOf("--auth");
16713
17176
  var credentialFilePath = authFileIndex !== -1 ? process.argv[authFileIndex + 1] : process.env.LINK_AUTH_FILE;
16714
17177
  if (authFileIndex !== -1) {
@@ -16728,7 +17191,16 @@ var factory = new ResourceFactory({
16728
17191
  });
16729
17192
  var authRepo = factory.createAuthResource();
16730
17193
  var spendRequestRepo = factory.createSpendRequestResource();
16731
- var cli = Cli11.create("link-cli", {
17194
+ var requestedCommand = process.argv[2];
17195
+ var transactionsCli = requestedCommand === "transactions" ? createTransactionsCli(
17196
+ () => factory.createTransactionsResource(),
17197
+ authStorage,
17198
+ envAccessToken
17199
+ ) : null;
17200
+ if (transactionsCli) {
17201
+ process.argv.splice(2, 1);
17202
+ }
17203
+ var cli = transactionsCli ?? Cli12.create("link-cli", {
16732
17204
  description: "Create a secure, one-time payment credential from a Link wallet to let agents complete purchases on behalf of users.",
16733
17205
  version: cliVersion
16734
17206
  });
@@ -16745,58 +17217,60 @@ if (!isAgent && process.stdout.isTTY) {
16745
17217
  process.stderr.write(renderInteractiveUpdateNotice(updateInfo));
16746
17218
  }
16747
17219
  }
16748
- cli.command(
16749
- createAuthCli(authRepo, getUpdateInfo, authStorage, envAccessToken)
16750
- );
16751
- cli.command(
16752
- createSpendRequestCli(spendRequestRepo, authStorage, envAccessToken)
16753
- );
16754
- cli.command(
16755
- createPaymentMethodsCli(
16756
- () => factory.createPaymentMethodsResource(),
16757
- authStorage,
16758
- envAccessToken
16759
- )
16760
- );
16761
- cli.command(
16762
- createShippingAddressCli(
16763
- () => factory.createShippingAddressResource(),
16764
- authStorage,
16765
- envAccessToken
16766
- )
16767
- );
16768
- cli.command(
16769
- createUserInfoCli(
16770
- () => factory.createUserInfoResource(),
16771
- authStorage,
16772
- envAccessToken
16773
- )
16774
- );
16775
- cli.command(createMppCli(spendRequestRepo, authStorage, envAccessToken));
16776
- cli.command(
16777
- createReportCli(
16778
- () => factory.createReportResource(),
16779
- authStorage,
16780
- envAccessToken
16781
- )
16782
- );
16783
- cli.command(
16784
- createDemoCli(
16785
- authRepo,
16786
- spendRequestRepo,
16787
- () => factory.createPaymentMethodsResource(),
16788
- authStorage
16789
- )
16790
- );
16791
- cli.command(
16792
- createOnboardCli(
16793
- authRepo,
16794
- spendRequestRepo,
16795
- () => factory.createPaymentMethodsResource(),
16796
- authStorage
16797
- )
16798
- );
16799
- cli.command(createServeCli(cli));
17220
+ if (!transactionsCli) {
17221
+ cli.command(
17222
+ createAuthCli(authRepo, getUpdateInfo, authStorage, envAccessToken)
17223
+ );
17224
+ cli.command(
17225
+ createSpendRequestCli(spendRequestRepo, authStorage, envAccessToken)
17226
+ );
17227
+ cli.command(
17228
+ createPaymentMethodsCli(
17229
+ () => factory.createPaymentMethodsResource(),
17230
+ authStorage,
17231
+ envAccessToken
17232
+ )
17233
+ );
17234
+ cli.command(
17235
+ createShippingAddressCli(
17236
+ () => factory.createShippingAddressResource(),
17237
+ authStorage,
17238
+ envAccessToken
17239
+ )
17240
+ );
17241
+ cli.command(
17242
+ createUserInfoCli(
17243
+ () => factory.createUserInfoResource(),
17244
+ authStorage,
17245
+ envAccessToken
17246
+ )
17247
+ );
17248
+ cli.command(createMppCli(spendRequestRepo, authStorage, envAccessToken));
17249
+ cli.command(
17250
+ createReportCli(
17251
+ () => factory.createReportResource(),
17252
+ authStorage,
17253
+ envAccessToken
17254
+ )
17255
+ );
17256
+ cli.command(
17257
+ createDemoCli(
17258
+ authRepo,
17259
+ spendRequestRepo,
17260
+ () => factory.createPaymentMethodsResource(),
17261
+ authStorage
17262
+ )
17263
+ );
17264
+ cli.command(
17265
+ createOnboardCli(
17266
+ authRepo,
17267
+ spendRequestRepo,
17268
+ () => factory.createPaymentMethodsResource(),
17269
+ authStorage
17270
+ )
17271
+ );
17272
+ cli.command(createServeCli(cli));
17273
+ }
16800
17274
  cli.serve();
16801
17275
  var cli_default = cli;
16802
17276
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stripe/link-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-cli": "./dist/cli.js"