@conto_finance/sdk 0.6.0 → 0.6.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Stripe Link agent wallet adapters for conto.checkout.getLinkCard().
3
+ *
4
+ * The wallet runs in the builder's environment with the builder's own Link
5
+ * access. Card data stays in this process: it is never sent to Conto, written
6
+ * to a log, or placed in an error.
7
+ */
8
+ /** Statuses a Link spend request reports. `unknown` stands for any status this SDK does not know. */
9
+ type LinkSpendRequestStatus = 'created' | 'pending_approval' | 'requires_action' | 'approved' | 'submitted' | 'succeeded' | 'denied' | 'expired' | 'canceled' | 'failed' | 'unknown';
10
+ /** One Link card request, built by Conto from the approved checkpoint. */
11
+ interface LinkCardRequestInput {
12
+ /** The Conto-approved amount in cents. The card is limited to it. */
13
+ amountMinor: number;
14
+ currency: 'USD';
15
+ merchantName: string;
16
+ /** The store's origin, such as https://shop.example. */
17
+ merchantUrl: string;
18
+ /** What the person reads in Link before approving. At least 100 characters. */
19
+ context: string;
20
+ lineItems: ReadonlyArray<{
21
+ name: string;
22
+ quantity: number;
23
+ unitAmountMinor: number;
24
+ }>;
25
+ totals: ReadonlyArray<{
26
+ type: string;
27
+ label: string;
28
+ amountMinor: number;
29
+ }>;
30
+ /** Opaque key. Reusing it retries the same request instead of creating another. */
31
+ idempotencyKey: string;
32
+ metadata?: Readonly<Record<string, string>>;
33
+ }
34
+ /** A Link spend request as the wallet last read it. Never contains card data. */
35
+ interface LinkSpendRequest {
36
+ id: string;
37
+ status: LinkSpendRequestStatus;
38
+ amountMinor?: number;
39
+ merchantName?: string;
40
+ merchantUrl?: string;
41
+ /** Link page where the person can review the request. */
42
+ approvalUrl?: string;
43
+ /** Set with requires_action. `auto_resume` means Link continues once the person finishes the step. */
44
+ actionResolution?: string;
45
+ /** Set with requires_action when Link gives the person a page to finish the step. */
46
+ actionUrl?: string;
47
+ }
48
+ /**
49
+ * The single-use virtual card Link issued for an approved request. Live card
50
+ * data: use it only in the store's card form for the approved purchase. Never
51
+ * log, print, persist, or send it anywhere else.
52
+ */
53
+ interface LinkCardCredential {
54
+ number: string;
55
+ /** The card's CVC. */
56
+ securityValue: string;
57
+ expMonth: number;
58
+ expYear: number;
59
+ /** When Link stops accepting the card, as an ISO 8601 time. */
60
+ validUntil?: string;
61
+ last4: string;
62
+ brand?: string;
63
+ }
64
+ interface LinkCallOptions {
65
+ signal?: AbortSignal;
66
+ }
67
+ interface LinkWaitOptions extends LinkCallOptions {
68
+ /** How long to wait for the person to decide. 0 reads the request once. */
69
+ timeoutSeconds?: number;
70
+ }
71
+ /** The four Link operations Conto needs. Implement it to use another Link client. */
72
+ interface LinkWallet {
73
+ /** Ask Link for a card for this exact purchase. Link sends the person an approval request. */
74
+ createCardRequest(input: LinkCardRequestInput, options?: LinkCallOptions): Promise<LinkSpendRequest>;
75
+ /**
76
+ * Wait while the request is created or pending approval, up to the timeout,
77
+ * and resolve with its latest state. A waiting status after the timeout means
78
+ * the person has not decided yet.
79
+ */
80
+ waitForApproval(id: string, options?: LinkWaitOptions): Promise<LinkSpendRequest>;
81
+ /** Read the card of an approved request. */
82
+ retrieveCard(id: string, options?: LinkCallOptions): Promise<LinkCardCredential>;
83
+ /** Cancel a request that is created, pending approval, or approved. */
84
+ cancel(id: string, options?: LinkCallOptions): Promise<void>;
85
+ }
86
+ type LinkWalletErrorReason = 'not_installed' | 'not_logged_in' | 'not_found' | 'not_approved' | 'invalid_input' | 'invalid_response' | 'timed_out' | 'aborted' | 'cli_failed';
87
+ /**
88
+ * A Link wallet call failed. The message is fixed text: it never repeats
89
+ * link-cli output, so it can never carry card data.
90
+ */
91
+ declare class LinkWalletError extends Error {
92
+ readonly code: 'LINK_WALLET_UNAVAILABLE' | 'LINK_WALLET_FAILED';
93
+ readonly reason: LinkWalletErrorReason;
94
+ /** The error code link-cli reported, when it reported a safe one. */
95
+ readonly linkCode?: string;
96
+ constructor(reason: LinkWalletErrorReason, message: string, linkCode?: string);
97
+ }
98
+ interface LinkCliWalletOptions {
99
+ /** The link-cli executable. Defaults to `link-cli` on PATH. */
100
+ binary?: string;
101
+ /** Create test-mode requests. Link returns test card data and charges nothing. */
102
+ test?: boolean;
103
+ /** Link payment method to fund the card. Defaults to the person's default method. */
104
+ paymentMethodId?: string;
105
+ /** Default time waitForApproval waits, in seconds. Defaults to 300. */
106
+ timeoutSeconds?: number;
107
+ /** Environment for link-cli. Defaults to this process's environment. */
108
+ env?: Readonly<Record<string, string | undefined>>;
109
+ }
110
+ /** Link spend request ids look like lsrq_ followed by letters and digits. */
111
+ declare function isLinkRequestId(value: unknown): value is string;
112
+ /**
113
+ * A LinkWallet that runs Stripe's link-cli (`npm i -g @stripe/link-cli`,
114
+ * then `link-cli auth login`) on this machine. Each call is one link-cli
115
+ * process run without a shell. Card data is read from a private temporary file
116
+ * that is deleted before the call returns.
117
+ */
118
+ declare function linkCliWallet(options?: LinkCliWalletOptions): LinkWallet;
119
+
120
+ export { type LinkCallOptions, type LinkCardCredential, type LinkCardRequestInput, type LinkCliWalletOptions, type LinkSpendRequest, type LinkSpendRequestStatus, type LinkWaitOptions, type LinkWallet, LinkWalletError, type LinkWalletErrorReason, isLinkRequestId, linkCliWallet };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Stripe Link agent wallet adapters for conto.checkout.getLinkCard().
3
+ *
4
+ * The wallet runs in the builder's environment with the builder's own Link
5
+ * access. Card data stays in this process: it is never sent to Conto, written
6
+ * to a log, or placed in an error.
7
+ */
8
+ /** Statuses a Link spend request reports. `unknown` stands for any status this SDK does not know. */
9
+ type LinkSpendRequestStatus = 'created' | 'pending_approval' | 'requires_action' | 'approved' | 'submitted' | 'succeeded' | 'denied' | 'expired' | 'canceled' | 'failed' | 'unknown';
10
+ /** One Link card request, built by Conto from the approved checkpoint. */
11
+ interface LinkCardRequestInput {
12
+ /** The Conto-approved amount in cents. The card is limited to it. */
13
+ amountMinor: number;
14
+ currency: 'USD';
15
+ merchantName: string;
16
+ /** The store's origin, such as https://shop.example. */
17
+ merchantUrl: string;
18
+ /** What the person reads in Link before approving. At least 100 characters. */
19
+ context: string;
20
+ lineItems: ReadonlyArray<{
21
+ name: string;
22
+ quantity: number;
23
+ unitAmountMinor: number;
24
+ }>;
25
+ totals: ReadonlyArray<{
26
+ type: string;
27
+ label: string;
28
+ amountMinor: number;
29
+ }>;
30
+ /** Opaque key. Reusing it retries the same request instead of creating another. */
31
+ idempotencyKey: string;
32
+ metadata?: Readonly<Record<string, string>>;
33
+ }
34
+ /** A Link spend request as the wallet last read it. Never contains card data. */
35
+ interface LinkSpendRequest {
36
+ id: string;
37
+ status: LinkSpendRequestStatus;
38
+ amountMinor?: number;
39
+ merchantName?: string;
40
+ merchantUrl?: string;
41
+ /** Link page where the person can review the request. */
42
+ approvalUrl?: string;
43
+ /** Set with requires_action. `auto_resume` means Link continues once the person finishes the step. */
44
+ actionResolution?: string;
45
+ /** Set with requires_action when Link gives the person a page to finish the step. */
46
+ actionUrl?: string;
47
+ }
48
+ /**
49
+ * The single-use virtual card Link issued for an approved request. Live card
50
+ * data: use it only in the store's card form for the approved purchase. Never
51
+ * log, print, persist, or send it anywhere else.
52
+ */
53
+ interface LinkCardCredential {
54
+ number: string;
55
+ /** The card's CVC. */
56
+ securityValue: string;
57
+ expMonth: number;
58
+ expYear: number;
59
+ /** When Link stops accepting the card, as an ISO 8601 time. */
60
+ validUntil?: string;
61
+ last4: string;
62
+ brand?: string;
63
+ }
64
+ interface LinkCallOptions {
65
+ signal?: AbortSignal;
66
+ }
67
+ interface LinkWaitOptions extends LinkCallOptions {
68
+ /** How long to wait for the person to decide. 0 reads the request once. */
69
+ timeoutSeconds?: number;
70
+ }
71
+ /** The four Link operations Conto needs. Implement it to use another Link client. */
72
+ interface LinkWallet {
73
+ /** Ask Link for a card for this exact purchase. Link sends the person an approval request. */
74
+ createCardRequest(input: LinkCardRequestInput, options?: LinkCallOptions): Promise<LinkSpendRequest>;
75
+ /**
76
+ * Wait while the request is created or pending approval, up to the timeout,
77
+ * and resolve with its latest state. A waiting status after the timeout means
78
+ * the person has not decided yet.
79
+ */
80
+ waitForApproval(id: string, options?: LinkWaitOptions): Promise<LinkSpendRequest>;
81
+ /** Read the card of an approved request. */
82
+ retrieveCard(id: string, options?: LinkCallOptions): Promise<LinkCardCredential>;
83
+ /** Cancel a request that is created, pending approval, or approved. */
84
+ cancel(id: string, options?: LinkCallOptions): Promise<void>;
85
+ }
86
+ type LinkWalletErrorReason = 'not_installed' | 'not_logged_in' | 'not_found' | 'not_approved' | 'invalid_input' | 'invalid_response' | 'timed_out' | 'aborted' | 'cli_failed';
87
+ /**
88
+ * A Link wallet call failed. The message is fixed text: it never repeats
89
+ * link-cli output, so it can never carry card data.
90
+ */
91
+ declare class LinkWalletError extends Error {
92
+ readonly code: 'LINK_WALLET_UNAVAILABLE' | 'LINK_WALLET_FAILED';
93
+ readonly reason: LinkWalletErrorReason;
94
+ /** The error code link-cli reported, when it reported a safe one. */
95
+ readonly linkCode?: string;
96
+ constructor(reason: LinkWalletErrorReason, message: string, linkCode?: string);
97
+ }
98
+ interface LinkCliWalletOptions {
99
+ /** The link-cli executable. Defaults to `link-cli` on PATH. */
100
+ binary?: string;
101
+ /** Create test-mode requests. Link returns test card data and charges nothing. */
102
+ test?: boolean;
103
+ /** Link payment method to fund the card. Defaults to the person's default method. */
104
+ paymentMethodId?: string;
105
+ /** Default time waitForApproval waits, in seconds. Defaults to 300. */
106
+ timeoutSeconds?: number;
107
+ /** Environment for link-cli. Defaults to this process's environment. */
108
+ env?: Readonly<Record<string, string | undefined>>;
109
+ }
110
+ /** Link spend request ids look like lsrq_ followed by letters and digits. */
111
+ declare function isLinkRequestId(value: unknown): value is string;
112
+ /**
113
+ * A LinkWallet that runs Stripe's link-cli (`npm i -g @stripe/link-cli`,
114
+ * then `link-cli auth login`) on this machine. Each call is one link-cli
115
+ * process run without a shell. Card data is read from a private temporary file
116
+ * that is deleted before the call returns.
117
+ */
118
+ declare function linkCliWallet(options?: LinkCliWalletOptions): LinkWallet;
119
+
120
+ export { type LinkCallOptions, type LinkCardCredential, type LinkCardRequestInput, type LinkCliWalletOptions, type LinkSpendRequest, type LinkSpendRequestStatus, type LinkWaitOptions, type LinkWallet, LinkWalletError, type LinkWalletErrorReason, isLinkRequestId, linkCliWallet };
@@ -0,0 +1,400 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/link-wallet.ts
31
+ var link_wallet_exports = {};
32
+ __export(link_wallet_exports, {
33
+ LinkWalletError: () => LinkWalletError,
34
+ isLinkRequestId: () => isLinkRequestId,
35
+ linkCliWallet: () => linkCliWallet
36
+ });
37
+ module.exports = __toCommonJS(link_wallet_exports);
38
+ var import_node_child_process = require("child_process");
39
+ var import_node_fs = require("fs");
40
+ var import_promises = require("fs/promises");
41
+ var import_node_os = require("os");
42
+ var import_node_path = __toESM(require("path"));
43
+ var LinkWalletError = class extends Error {
44
+ constructor(reason, message, linkCode) {
45
+ super(message);
46
+ this.name = "LinkWalletError";
47
+ this.reason = reason;
48
+ this.code = reason === "not_installed" || reason === "not_logged_in" ? "LINK_WALLET_UNAVAILABLE" : "LINK_WALLET_FAILED";
49
+ if (linkCode) this.linkCode = linkCode;
50
+ }
51
+ };
52
+ var LINK_REQUEST_ID = /^lsrq_[A-Za-z0-9]{1,64}$/;
53
+ var SAFE_LINK_CODE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
54
+ var KNOWN_STATUSES = /* @__PURE__ */ new Set([
55
+ "created",
56
+ "pending_approval",
57
+ "requires_action",
58
+ "approved",
59
+ "submitted",
60
+ "succeeded",
61
+ "denied",
62
+ "expired",
63
+ "canceled",
64
+ "failed"
65
+ ]);
66
+ var MAX_OUTPUT_BYTES = 1024 * 1024;
67
+ var COMMAND_TIMEOUT_MS = 6e4;
68
+ function record(value) {
69
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
70
+ }
71
+ function text(value) {
72
+ return typeof value === "string" && value.length > 0 ? value : void 0;
73
+ }
74
+ function httpsUrl(value) {
75
+ if (typeof value !== "string" || value.length > 2048) return void 0;
76
+ try {
77
+ return new URL(value).protocol === "https:" ? value : void 0;
78
+ } catch {
79
+ return void 0;
80
+ }
81
+ }
82
+ function isLinkRequestId(value) {
83
+ return typeof value === "string" && LINK_REQUEST_ID.test(value);
84
+ }
85
+ function requireRequestId(id) {
86
+ if (!isLinkRequestId(id)) {
87
+ throw new LinkWalletError("invalid_input", "The Link request id is not valid.");
88
+ }
89
+ return id;
90
+ }
91
+ function flagText(value, maxLength) {
92
+ const cleaned = value.replace(/[\r\n\t,]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength).trim();
93
+ return cleaned || "Item";
94
+ }
95
+ function parseSpendRequest(value) {
96
+ const latest = Array.isArray(value) ? value[value.length - 1] : value;
97
+ const data = record(latest);
98
+ const id = data.id;
99
+ if (!isLinkRequestId(id)) {
100
+ throw new LinkWalletError("invalid_response", "link-cli returned an unexpected response.");
101
+ }
102
+ const status = typeof data.status === "string" && KNOWN_STATUSES.has(data.status) ? data.status : "unknown";
103
+ const nextAction = record(record(record(data.status_details).requires_action).next_action);
104
+ const amount = data.amount;
105
+ return {
106
+ id,
107
+ status,
108
+ ...Number.isSafeInteger(amount) ? { amountMinor: amount } : {},
109
+ ...text(data.merchant_name) ? { merchantName: text(data.merchant_name) } : {},
110
+ ...httpsUrl(data.merchant_url) ? { merchantUrl: httpsUrl(data.merchant_url) } : {},
111
+ ...httpsUrl(data.approval_url) ? { approvalUrl: httpsUrl(data.approval_url) } : {},
112
+ ...text(nextAction.resolution) ? { actionResolution: text(nextAction.resolution) } : {},
113
+ ...httpsUrl(nextAction.action_url) ? { actionUrl: httpsUrl(nextAction.action_url) } : {}
114
+ };
115
+ }
116
+ function invalidCard() {
117
+ return new LinkWalletError("invalid_response", "Link returned card data this SDK cannot read.");
118
+ }
119
+ function validUntil(value) {
120
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
121
+ return new Date(value < 1e12 ? value * 1e3 : value).toISOString();
122
+ }
123
+ if (typeof value === "string" && !Number.isNaN(Date.parse(value))) {
124
+ return new Date(value).toISOString();
125
+ }
126
+ return void 0;
127
+ }
128
+ function parseCard(value) {
129
+ const card = record(value);
130
+ const number = typeof card.number === "string" ? card.number.replace(/[\s-]/g, "") : "";
131
+ const rawSecurityValue = card["cvc"];
132
+ const securityValue = typeof rawSecurityValue === "string" ? rawSecurityValue.trim() : "";
133
+ const expMonth = Number(card.exp_month);
134
+ let expYear = Number(card.exp_year);
135
+ if (Number.isInteger(expYear) && expYear >= 0 && expYear < 100) expYear += 2e3;
136
+ if (!/^\d{12,19}$/.test(number) || !/^\d{3,4}$/.test(securityValue) || !Number.isInteger(expMonth) || expMonth < 1 || expMonth > 12 || !Number.isInteger(expYear) || expYear < 2e3) {
137
+ throw invalidCard();
138
+ }
139
+ const until = validUntil(card.valid_until);
140
+ const brand = text(card.brand);
141
+ return {
142
+ number,
143
+ securityValue,
144
+ expMonth,
145
+ expYear,
146
+ ...until ? { validUntil: until } : {},
147
+ last4: number.slice(-4),
148
+ ...brand ? { brand } : {}
149
+ };
150
+ }
151
+ function failureFromOutput(value) {
152
+ const data = record(value);
153
+ const code = typeof data.code === "string" && SAFE_LINK_CODE.test(data.code) ? data.code : void 0;
154
+ const message = typeof data.message === "string" ? data.message.toLowerCase() : "";
155
+ if (code === "NOT_AUTHENTICATED" || message.includes("auth login") || message.includes("not authenticated")) {
156
+ return new LinkWalletError(
157
+ "not_logged_in",
158
+ 'link-cli is not logged in. Run "link-cli auth login" on this machine.',
159
+ code
160
+ );
161
+ }
162
+ if (code === "NOT_FOUND") {
163
+ return new LinkWalletError("not_found", "Link has no spend request with this id.", code);
164
+ }
165
+ if (code === "POLLING_TIMEOUT") {
166
+ return new LinkWalletError("timed_out", "The person has not decided in Link yet.", code);
167
+ }
168
+ return new LinkWalletError("cli_failed", "link-cli could not complete the request.", code);
169
+ }
170
+ function linkCliWallet(options = {}) {
171
+ const binary = options.binary ?? "link-cli";
172
+ const defaultTimeoutSeconds = options.timeoutSeconds ?? 300;
173
+ if (!Number.isFinite(defaultTimeoutSeconds) || defaultTimeoutSeconds < 0) {
174
+ throw new LinkWalletError("invalid_input", "timeoutSeconds must be zero or more.");
175
+ }
176
+ if (options.paymentMethodId !== void 0 && !/^[A-Za-z0-9_]{1,128}$/.test(options.paymentMethodId)) {
177
+ throw new LinkWalletError(
178
+ "invalid_input",
179
+ "paymentMethodId is not a valid Link payment method id."
180
+ );
181
+ }
182
+ const env = {
183
+ ...options.env ?? process.env,
184
+ NO_UPDATE_NOTIFIER: "1",
185
+ NO_COLOR: "1"
186
+ };
187
+ function run(args, call = {}) {
188
+ return new Promise((resolve, reject) => {
189
+ if (call.signal?.aborted) {
190
+ reject(new LinkWalletError("aborted", "The Link request was stopped."));
191
+ return;
192
+ }
193
+ let child;
194
+ try {
195
+ child = (0, import_node_child_process.spawn)(binary, [...args, "--format", "json"], {
196
+ env,
197
+ shell: false,
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ windowsHide: true,
200
+ ...call.signal ? { signal: call.signal } : {}
201
+ });
202
+ } catch {
203
+ reject(new LinkWalletError("cli_failed", "link-cli could not be started."));
204
+ return;
205
+ }
206
+ const chunks = [];
207
+ let size = 0;
208
+ let overflow = false;
209
+ let timedOut = false;
210
+ const timer = setTimeout(() => {
211
+ timedOut = true;
212
+ child.kill("SIGTERM");
213
+ }, call.timeoutMs ?? COMMAND_TIMEOUT_MS);
214
+ child.stdout?.on("data", (chunk) => {
215
+ size += chunk.length;
216
+ if (size > MAX_OUTPUT_BYTES) {
217
+ overflow = true;
218
+ child.kill("SIGTERM");
219
+ return;
220
+ }
221
+ chunks.push(chunk);
222
+ });
223
+ child.stderr?.resume();
224
+ child.on("error", (error) => {
225
+ clearTimeout(timer);
226
+ if (error.code === "ENOENT") {
227
+ reject(
228
+ new LinkWalletError(
229
+ "not_installed",
230
+ 'link-cli is not installed. Run "npm i -g @stripe/link-cli", then "link-cli auth login".'
231
+ )
232
+ );
233
+ } else if (error.name === "AbortError") {
234
+ reject(new LinkWalletError("aborted", "The Link request was stopped."));
235
+ } else {
236
+ reject(new LinkWalletError("cli_failed", "link-cli could not be started."));
237
+ }
238
+ });
239
+ child.on("close", (exitCode) => {
240
+ clearTimeout(timer);
241
+ if (call.signal?.aborted) {
242
+ reject(new LinkWalletError("aborted", "The Link request was stopped."));
243
+ return;
244
+ }
245
+ if (timedOut) {
246
+ reject(new LinkWalletError("cli_failed", "link-cli did not finish in time."));
247
+ return;
248
+ }
249
+ if (overflow) {
250
+ reject(
251
+ new LinkWalletError("invalid_response", "link-cli returned an unexpected response.")
252
+ );
253
+ return;
254
+ }
255
+ let value;
256
+ try {
257
+ value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
258
+ } catch {
259
+ reject(
260
+ exitCode === 0 ? new LinkWalletError("invalid_response", "link-cli returned an unexpected response.") : new LinkWalletError("cli_failed", "link-cli could not complete the request.")
261
+ );
262
+ return;
263
+ }
264
+ resolve({ exitCode, value });
265
+ });
266
+ });
267
+ }
268
+ async function runOrThrow(args, call) {
269
+ const result = await run(args, call);
270
+ if (result.exitCode !== 0) throw failureFromOutput(result.value);
271
+ return result.value;
272
+ }
273
+ async function readOnce(id, call) {
274
+ return parseSpendRequest(await runOrThrow(["spend-request", "retrieve", id], call));
275
+ }
276
+ return {
277
+ async createCardRequest(input, call) {
278
+ if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor <= 0) {
279
+ throw new LinkWalletError(
280
+ "invalid_input",
281
+ "amountMinor must be a positive number of cents."
282
+ );
283
+ }
284
+ if (input.currency !== "USD") {
285
+ throw new LinkWalletError("invalid_input", "Only USD card requests are supported.");
286
+ }
287
+ if (typeof input.context !== "string" || input.context.length < 100) {
288
+ throw new LinkWalletError("invalid_input", "context must contain at least 100 characters.");
289
+ }
290
+ if (!/^[A-Za-z0-9_:.-]{8,255}$/.test(input.idempotencyKey)) {
291
+ throw new LinkWalletError("invalid_input", "idempotencyKey is not valid.");
292
+ }
293
+ if (!httpsUrl(input.merchantUrl)) {
294
+ throw new LinkWalletError("invalid_input", "merchantUrl must be an HTTPS URL.");
295
+ }
296
+ const args = [
297
+ "spend-request",
298
+ "create",
299
+ `--amount=${input.amountMinor}`,
300
+ "--currency=usd",
301
+ `--merchant-name=${input.merchantName}`,
302
+ `--merchant-url=${input.merchantUrl}`,
303
+ `--context=${input.context}`,
304
+ `--idempotency-key=${input.idempotencyKey}`,
305
+ ...input.lineItems.map(
306
+ (item) => `--line-item=name:${flagText(item.name, 200)},unit_amount:${item.unitAmountMinor},quantity:${item.quantity}`
307
+ ),
308
+ ...input.totals.map(
309
+ (total) => `--total=type:${total.type},display_text:${flagText(total.label, 120)},amount:${total.amountMinor}`
310
+ ),
311
+ ...Object.entries(input.metadata ?? {}).map(
312
+ ([key, value]) => `--metadata=${flagText(key, 40)}:${flagText(value, 500)}`
313
+ ),
314
+ ...options.paymentMethodId ? [`--payment-method-id=${options.paymentMethodId}`] : [],
315
+ ...options.test ? ["--test"] : []
316
+ ];
317
+ const result = await run(args, call);
318
+ if (result.exitCode !== 0) {
319
+ const message = record(result.value).message;
320
+ const existing = typeof message === "string" ? /matching spend request already exists: (lsrq_[A-Za-z0-9]{1,64})/.exec(message)?.[1] : void 0;
321
+ if (existing) {
322
+ const current = await readOnce(existing, call);
323
+ if (current.amountMinor === input.amountMinor && (current.merchantUrl === void 0 || new URL(current.merchantUrl).origin === new URL(input.merchantUrl).origin)) {
324
+ return current;
325
+ }
326
+ }
327
+ throw failureFromOutput(result.value);
328
+ }
329
+ return parseSpendRequest(result.value);
330
+ },
331
+ async waitForApproval(id, call = {}) {
332
+ requireRequestId(id);
333
+ const timeoutSeconds = Math.max(0, Math.floor(call.timeoutSeconds ?? defaultTimeoutSeconds));
334
+ if (timeoutSeconds === 0) return readOnce(id, call);
335
+ const result = await run(
336
+ ["spend-request", "retrieve", id, "--interval=2", `--timeout=${timeoutSeconds}`],
337
+ { signal: call.signal, timeoutMs: (timeoutSeconds + 30) * 1e3 }
338
+ );
339
+ if (result.exitCode === 0) return parseSpendRequest(result.value);
340
+ const failure = failureFromOutput(result.value);
341
+ if (failure.reason === "timed_out") return readOnce(id, call);
342
+ throw failure;
343
+ },
344
+ async retrieveCard(id, call) {
345
+ requireRequestId(id);
346
+ const directory = await (0, import_promises.mkdtemp)(import_node_path.default.join((0, import_node_os.tmpdir)(), "conto-link-"));
347
+ try {
348
+ await (0, import_promises.chmod)(directory, 448);
349
+ const file = import_node_path.default.join(directory, "card.json");
350
+ const request = parseSpendRequest(
351
+ await runOrThrow(
352
+ ["spend-request", "retrieve", id, "--include=card", `--output-file=${file}`],
353
+ call
354
+ )
355
+ );
356
+ if (!["approved", "submitted", "succeeded"].includes(request.status)) {
357
+ throw new LinkWalletError(
358
+ "not_approved",
359
+ "The person has not approved this request in Link."
360
+ );
361
+ }
362
+ const handle = await (0, import_promises.open)(
363
+ file,
364
+ import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0) | (import_node_fs.constants.O_NONBLOCK ?? 0)
365
+ ).catch((error) => {
366
+ if (error.code === "ENOENT") return void 0;
367
+ throw new LinkWalletError("invalid_response", "The card file was not private.");
368
+ });
369
+ if (!handle) throw new LinkWalletError("invalid_response", "Link did not return a card.");
370
+ try {
371
+ const stats = await handle.stat();
372
+ if (!stats.isFile() || (stats.mode & 63) !== 0) {
373
+ throw new LinkWalletError("invalid_response", "The card file was not private.");
374
+ }
375
+ let contents;
376
+ try {
377
+ contents = JSON.parse(await handle.readFile("utf8"));
378
+ } catch {
379
+ throw invalidCard();
380
+ }
381
+ return parseCard(record(contents).card);
382
+ } finally {
383
+ await handle.close();
384
+ }
385
+ } finally {
386
+ await (0, import_promises.rm)(directory, { recursive: true, force: true });
387
+ }
388
+ },
389
+ async cancel(id, call) {
390
+ requireRequestId(id);
391
+ await runOrThrow(["spend-request", "cancel", id], call);
392
+ }
393
+ };
394
+ }
395
+ // Annotate the CommonJS export names for ESM import in node:
396
+ 0 && (module.exports = {
397
+ LinkWalletError,
398
+ isLinkRequestId,
399
+ linkCliWallet
400
+ });