@conto_finance/sdk 0.8.0 → 0.8.2

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.
@@ -49,6 +49,8 @@ interface LinkSpendRequest {
49
49
  actionResolution?: string;
50
50
  /** Set with requires_action when Link gives the person a page to finish the step. */
51
51
  actionUrl?: string;
52
+ /** The metadata sent with the request, when Link returns it. */
53
+ metadata?: Record<string, string>;
52
54
  }
53
55
  /**
54
56
  * The single-use virtual card Link issued for an approved request. Live card
@@ -85,7 +87,10 @@ interface LinkWallet {
85
87
  waitForApproval(id: string, options?: LinkWaitOptions): Promise<LinkSpendRequest>;
86
88
  /** Read the card of an approved request. */
87
89
  retrieveCard(id: string, options?: LinkCallOptions): Promise<LinkCardCredential>;
88
- /** Cancel a request that is created, pending approval, or approved. */
90
+ /**
91
+ * Cancel a request, or confirm Link already closed it. Throws when its card
92
+ * may still work or may have been used, so keep the hold.
93
+ */
89
94
  cancel(id: string, options?: LinkCallOptions): Promise<void>;
90
95
  }
91
96
  type LinkWalletErrorReason = 'not_installed' | 'not_logged_in' | 'not_found' | 'not_approved' | 'invalid_input' | 'invalid_response' | 'timed_out' | 'aborted' | 'cli_failed';
@@ -98,7 +103,11 @@ declare class LinkWalletError extends Error {
98
103
  readonly reason: LinkWalletErrorReason;
99
104
  /** The error code link-cli reported, when it reported a safe one. */
100
105
  readonly linkCode?: string;
101
- constructor(reason: LinkWalletErrorReason, message: string, linkCode?: string);
106
+ /** The open Link request with the same details that blocks this one. */
107
+ readonly duplicateOf?: string;
108
+ /** The checkpoint Link has on that request, as Link reported it. */
109
+ readonly duplicateCheckpoint?: string;
110
+ constructor(reason: LinkWalletErrorReason, message: string, linkCode?: string, duplicateOf?: string, duplicateCheckpoint?: string);
102
111
  }
103
112
  interface LinkCliWalletOptions {
104
113
  /** The link-cli executable. Defaults to `link-cli` on PATH. */
@@ -49,6 +49,8 @@ interface LinkSpendRequest {
49
49
  actionResolution?: string;
50
50
  /** Set with requires_action when Link gives the person a page to finish the step. */
51
51
  actionUrl?: string;
52
+ /** The metadata sent with the request, when Link returns it. */
53
+ metadata?: Record<string, string>;
52
54
  }
53
55
  /**
54
56
  * The single-use virtual card Link issued for an approved request. Live card
@@ -85,7 +87,10 @@ interface LinkWallet {
85
87
  waitForApproval(id: string, options?: LinkWaitOptions): Promise<LinkSpendRequest>;
86
88
  /** Read the card of an approved request. */
87
89
  retrieveCard(id: string, options?: LinkCallOptions): Promise<LinkCardCredential>;
88
- /** Cancel a request that is created, pending approval, or approved. */
90
+ /**
91
+ * Cancel a request, or confirm Link already closed it. Throws when its card
92
+ * may still work or may have been used, so keep the hold.
93
+ */
89
94
  cancel(id: string, options?: LinkCallOptions): Promise<void>;
90
95
  }
91
96
  type LinkWalletErrorReason = 'not_installed' | 'not_logged_in' | 'not_found' | 'not_approved' | 'invalid_input' | 'invalid_response' | 'timed_out' | 'aborted' | 'cli_failed';
@@ -98,7 +103,11 @@ declare class LinkWalletError extends Error {
98
103
  readonly reason: LinkWalletErrorReason;
99
104
  /** The error code link-cli reported, when it reported a safe one. */
100
105
  readonly linkCode?: string;
101
- constructor(reason: LinkWalletErrorReason, message: string, linkCode?: string);
106
+ /** The open Link request with the same details that blocks this one. */
107
+ readonly duplicateOf?: string;
108
+ /** The checkpoint Link has on that request, as Link reported it. */
109
+ readonly duplicateCheckpoint?: string;
110
+ constructor(reason: LinkWalletErrorReason, message: string, linkCode?: string, duplicateOf?: string, duplicateCheckpoint?: string);
102
111
  }
103
112
  interface LinkCliWalletOptions {
104
113
  /** The link-cli executable. Defaults to `link-cli` on PATH. */
@@ -69,12 +69,14 @@ function linkTime(value) {
69
69
 
70
70
  // src/link-wallet.ts
71
71
  var LinkWalletError = class extends Error {
72
- constructor(reason, message, linkCode) {
72
+ constructor(reason, message, linkCode, duplicateOf, duplicateCheckpoint) {
73
73
  super(message);
74
74
  this.name = "LinkWalletError";
75
75
  this.reason = reason;
76
76
  this.code = reason === "not_installed" || reason === "not_logged_in" ? "LINK_WALLET_UNAVAILABLE" : "LINK_WALLET_FAILED";
77
77
  if (linkCode) this.linkCode = linkCode;
78
+ if (duplicateOf) this.duplicateOf = duplicateOf;
79
+ if (duplicateCheckpoint) this.duplicateCheckpoint = duplicateCheckpoint;
78
80
  }
79
81
  };
80
82
  var LINK_REQUEST_ID = /^lsrq_[A-Za-z0-9]{1,64}$/;
@@ -91,6 +93,7 @@ var KNOWN_STATUSES = /* @__PURE__ */ new Set([
91
93
  "canceled",
92
94
  "failed"
93
95
  ]);
96
+ var CLOSED_STATUSES = /* @__PURE__ */ new Set(["denied", "expired", "canceled", "failed"]);
94
97
  var MAX_OUTPUT_BYTES = 1024 * 1024;
95
98
  var COMMAND_TIMEOUT_MS = 6e4;
96
99
  function record(value) {
@@ -132,6 +135,11 @@ function parseSpendRequest(value) {
132
135
  const amount = data.amount;
133
136
  const approvalUrl = safeLinkApprovalUrl(data.approval_url);
134
137
  const approvalExpiresAt = linkTime(data.approval_window_expires_at);
138
+ const metadata = Object.fromEntries(
139
+ Object.entries(record(data.metadata)).filter(
140
+ (entry) => typeof entry[1] === "string"
141
+ )
142
+ );
135
143
  return {
136
144
  id,
137
145
  status,
@@ -141,7 +149,8 @@ function parseSpendRequest(value) {
141
149
  ...approvalUrl ? { approvalUrl } : {},
142
150
  ...approvalExpiresAt ? { approvalExpiresAt } : {},
143
151
  ...text(nextAction.resolution) ? { actionResolution: text(nextAction.resolution) } : {},
144
- ...httpsUrl(nextAction.action_url) ? { actionUrl: httpsUrl(nextAction.action_url) } : {}
152
+ ...httpsUrl(nextAction.action_url) ? { actionUrl: httpsUrl(nextAction.action_url) } : {},
153
+ ...Object.keys(metadata).length ? { metadata } : {}
145
154
  };
146
155
  }
147
156
  function invalidCard() {
@@ -342,9 +351,28 @@ function linkCliWallet(options = {}) {
342
351
  const existing = typeof message === "string" ? /matching spend request already exists: (lsrq_[A-Za-z0-9]{1,64})/.exec(message)?.[1] : void 0;
343
352
  if (existing) {
344
353
  const current = await readOnce(existing, call);
345
- if (current.amountMinor === input.amountMinor && (current.merchantUrl === void 0 || new URL(current.merchantUrl).origin === new URL(input.merchantUrl).origin)) {
354
+ const owner = current.metadata?.conto_checkpoint;
355
+ const mine = input.metadata?.conto_checkpoint;
356
+ if (owner && owner !== mine) {
357
+ throw new LinkWalletError(
358
+ "invalid_input",
359
+ "Link has an open request for another purchase with the same details.",
360
+ "duplicate_for_other_purchase",
361
+ existing,
362
+ owner
363
+ );
364
+ }
365
+ if (owner === mine && current.amountMinor === input.amountMinor && (current.merchantUrl === void 0 || new URL(current.merchantUrl).origin === new URL(input.merchantUrl).origin)) {
346
366
  return current;
347
367
  }
368
+ if (!owner && mine) {
369
+ throw new LinkWalletError(
370
+ "cli_failed",
371
+ "Link has an open request with the same details and no checkpoint on it.",
372
+ "duplicate_without_checkpoint",
373
+ existing
374
+ );
375
+ }
348
376
  }
349
377
  throw failureFromOutput(result.value);
350
378
  }
@@ -410,7 +438,16 @@ function linkCliWallet(options = {}) {
410
438
  },
411
439
  async cancel(id, call) {
412
440
  requireRequestId(id);
413
- await runOrThrow(["spend-request", "cancel", id], call);
441
+ try {
442
+ await runOrThrow(["spend-request", "cancel", id], call);
443
+ } catch (error) {
444
+ const status = await readOnce(id, call).then(
445
+ (request) => request.status,
446
+ () => void 0
447
+ );
448
+ if (status !== void 0 && CLOSED_STATUSES.has(status)) return;
449
+ throw error;
450
+ }
414
451
  }
415
452
  };
416
453
  }
@@ -11,12 +11,14 @@ import { chmod, mkdtemp, open, rm } from "fs/promises";
11
11
  import { tmpdir } from "os";
12
12
  import path from "path";
13
13
  var LinkWalletError = class extends Error {
14
- constructor(reason, message, linkCode) {
14
+ constructor(reason, message, linkCode, duplicateOf, duplicateCheckpoint) {
15
15
  super(message);
16
16
  this.name = "LinkWalletError";
17
17
  this.reason = reason;
18
18
  this.code = reason === "not_installed" || reason === "not_logged_in" ? "LINK_WALLET_UNAVAILABLE" : "LINK_WALLET_FAILED";
19
19
  if (linkCode) this.linkCode = linkCode;
20
+ if (duplicateOf) this.duplicateOf = duplicateOf;
21
+ if (duplicateCheckpoint) this.duplicateCheckpoint = duplicateCheckpoint;
20
22
  }
21
23
  };
22
24
  var LINK_REQUEST_ID = /^lsrq_[A-Za-z0-9]{1,64}$/;
@@ -33,6 +35,7 @@ var KNOWN_STATUSES = /* @__PURE__ */ new Set([
33
35
  "canceled",
34
36
  "failed"
35
37
  ]);
38
+ var CLOSED_STATUSES = /* @__PURE__ */ new Set(["denied", "expired", "canceled", "failed"]);
36
39
  var MAX_OUTPUT_BYTES = 1024 * 1024;
37
40
  var COMMAND_TIMEOUT_MS = 6e4;
38
41
  function record(value) {
@@ -74,6 +77,11 @@ function parseSpendRequest(value) {
74
77
  const amount = data.amount;
75
78
  const approvalUrl = safeLinkApprovalUrl(data.approval_url);
76
79
  const approvalExpiresAt = linkTime(data.approval_window_expires_at);
80
+ const metadata = Object.fromEntries(
81
+ Object.entries(record(data.metadata)).filter(
82
+ (entry) => typeof entry[1] === "string"
83
+ )
84
+ );
77
85
  return {
78
86
  id,
79
87
  status,
@@ -83,7 +91,8 @@ function parseSpendRequest(value) {
83
91
  ...approvalUrl ? { approvalUrl } : {},
84
92
  ...approvalExpiresAt ? { approvalExpiresAt } : {},
85
93
  ...text(nextAction.resolution) ? { actionResolution: text(nextAction.resolution) } : {},
86
- ...httpsUrl(nextAction.action_url) ? { actionUrl: httpsUrl(nextAction.action_url) } : {}
94
+ ...httpsUrl(nextAction.action_url) ? { actionUrl: httpsUrl(nextAction.action_url) } : {},
95
+ ...Object.keys(metadata).length ? { metadata } : {}
87
96
  };
88
97
  }
89
98
  function invalidCard() {
@@ -284,9 +293,28 @@ function linkCliWallet(options = {}) {
284
293
  const existing = typeof message === "string" ? /matching spend request already exists: (lsrq_[A-Za-z0-9]{1,64})/.exec(message)?.[1] : void 0;
285
294
  if (existing) {
286
295
  const current = await readOnce(existing, call);
287
- if (current.amountMinor === input.amountMinor && (current.merchantUrl === void 0 || new URL(current.merchantUrl).origin === new URL(input.merchantUrl).origin)) {
296
+ const owner = current.metadata?.conto_checkpoint;
297
+ const mine = input.metadata?.conto_checkpoint;
298
+ if (owner && owner !== mine) {
299
+ throw new LinkWalletError(
300
+ "invalid_input",
301
+ "Link has an open request for another purchase with the same details.",
302
+ "duplicate_for_other_purchase",
303
+ existing,
304
+ owner
305
+ );
306
+ }
307
+ if (owner === mine && current.amountMinor === input.amountMinor && (current.merchantUrl === void 0 || new URL(current.merchantUrl).origin === new URL(input.merchantUrl).origin)) {
288
308
  return current;
289
309
  }
310
+ if (!owner && mine) {
311
+ throw new LinkWalletError(
312
+ "cli_failed",
313
+ "Link has an open request with the same details and no checkpoint on it.",
314
+ "duplicate_without_checkpoint",
315
+ existing
316
+ );
317
+ }
290
318
  }
291
319
  throw failureFromOutput(result.value);
292
320
  }
@@ -352,7 +380,16 @@ function linkCliWallet(options = {}) {
352
380
  },
353
381
  async cancel(id, call) {
354
382
  requireRequestId(id);
355
- await runOrThrow(["spend-request", "cancel", id], call);
383
+ try {
384
+ await runOrThrow(["spend-request", "cancel", id], call);
385
+ } catch (error) {
386
+ const status = await readOnce(id, call).then(
387
+ (request) => request.status,
388
+ () => void 0
389
+ );
390
+ if (status !== void 0 && CLOSED_STATUSES.has(status)) return;
391
+ throw error;
392
+ }
356
393
  }
357
394
  };
358
395
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conto_finance/sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Conto SDK for AI agent payment authorization",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",