@conto_finance/sdk 0.7.0 → 0.8.0

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.
package/README.md CHANGED
@@ -155,23 +155,124 @@ const confirmed = await conto.checkout.confirm(result.checkpoint, {
155
155
  or `link-cli` is missing or logged out, `getLinkCard()` cancels the checkpoint to release the
156
156
  held spend and throws `LinkCardDeniedError`, `LinkCardExpiredError`, `LinkCardTimeoutError`, or
157
157
  `LinkWalletUnavailableError`. All extend `LinkCardError`. Do not pay.
158
- - Retrying with the same input resumes the same checkpoint and Link request.
158
+ - Retrying with the same input resumes the same checkpoint and Link request while Conto's approval
159
+ is open. That approval lasts 30 minutes. For about 5 to 10 minutes after it lapses, the stage can
160
+ still read `APPROVED`, so a retry can return the card, but `confirm()` refuses it. Once the stage
161
+ is `OUTCOME_UNKNOWN`, a retry throws `CHECKOUT_CHECKPOINT_NOT_APPROVED`.
162
+ - `getLinkCard()` and `replaceLinkCard()` tell Conto the Link request ID as soon as Stripe Link
163
+ creates it. If Conto's approval then lapses, or a rule stops `confirm()`, Conto keeps the spend held,
164
+ because the card may still work. The checkpoint moves to stage `OUTCOME_UNKNOWN`. Cancel the Link
165
+ request, then call `conto.checkout.cancel(id)` to release the spend. Otherwise the owner gets a
166
+ review alert and records what happened. If Conto cannot record the request, the SDK cancels it in
167
+ Link, then cancels the checkpoint, and throws `LinkCardError` with code `LINK_CARD_NOT_RECORDED`.
168
+ `holdReleased` says whether the spend was released.
169
+ - Pass `onLinkApproval` to get Stripe Link's approval page, so a person without the Link app can
170
+ approve on the web while signed in to Link. It is called once, after Conto approves and Link
171
+ creates the request, with `{ checkpoint, linkRequestId, approvalUrl, approvalExpiresAt }`.
172
+ `approvalUrl` is always an HTTPS link.com address. A `LinkCardError` has it too when the SDK
173
+ stopped while the person had not decided.
159
174
  - The card (`number`, `securityValue` for the CVC, `expMonth`, `expYear`, `validUntil`, `last4`,
160
175
  `brand`) stays in this process. Printing or serializing the result shows only the last four
161
176
  digits. It never appears in an error.
162
- - The card covers the approved total, so `confirm()` with a `paymentReference` refuses a
163
- different total. Call `conto.checkout.replaceLinkCard(result, { wallet, amountMinor })` to
177
+ - The card covers the approved total, so `confirm()` refuses a different total for a checkpoint from
178
+ `getLinkCard()` or `onLinkApproval`, even when `paymentReference` is not passed. Call
179
+ `conto.checkout.replaceLinkCard(result, { wallet, amountMinor })` to
164
180
  cancel the old card and checkpoint and get a card for the final total.
165
181
  - Any `LinkWallet` works in place of `linkCliWallet()`: it creates a card request, waits for
166
182
  approval, retrieves the card, and cancels.
167
183
  - The protection holds only when the agent cannot also run `link-cli`, use a shell, or call Link
168
184
  with your credentials itself.
169
185
  - Pass observed final items and charges to `confirm()` even when the total is unchanged.
170
- The server rejects a changed cart against the approved intent. The trusted host must
186
+ The server checks the items and the final total. Other rows, such as tax and shipping, may
187
+ differ from the ones passed to `begin()`. The trusted host must
171
188
  validate the actual merchant and checkout, own a durable one-use payment dispatch
172
189
  claim, and keep card use outside model tools. Replaying a confirmed checkpoint is
173
190
  not permission to submit payment again.
174
191
 
192
+ ## Shared budgets
193
+
194
+ A shared budget is one limit that several agents draw on. The budget holder sets it in Spending
195
+ controls or with `admin.spending`. Agents no budget covers are not affected, except a buyer linked to
196
+ an owner workspace. Until a budget in that workspace covers it, those paths fail with `reasonCode`
197
+ `LINKED_BUDGET_REQUIRED`.
198
+
199
+ ```typescript
200
+ // Agent key. Nothing on conto.spending can raise a limit, approve a task, or release a hold.
201
+ const { budgets } = await conto.spending.budgets();
202
+ await conto.spending.preview({
203
+ amount: '40',
204
+ currency: 'USDC',
205
+ sourceId: walletId,
206
+ merchant: recipientAddress,
207
+ });
208
+
209
+ // Organization API key.
210
+ await admin.spending.createBudget({
211
+ name: 'Team tools',
212
+ scope: 'AGENTS',
213
+ participantIds: [agentA, agentB],
214
+ amount: '500',
215
+ currency: 'USDC',
216
+ period: 'MONTHLY',
217
+ periodStart: new Date().toISOString(),
218
+ periodEnd: new Date(Date.now() + 365 * 86_400_000).toISOString(),
219
+ });
220
+ ```
221
+
222
+ **A budget stops other payment paths.** While it exists, it stops x402, MPP, external wallet
223
+ payments, agent-to-agent payments, payment grants, checkout relay, hosted Stripe Link, guarded browser
224
+ checkout (`runGuardedCheckout()`), card overlays and checkout checkpoints, including
225
+ `checkout.getLinkCard()`, for every agent it covers. A paused, expired or not yet started budget still
226
+ stops them. Until its start date, while paused, and after its end date, a budget also refuses the
227
+ managed wallet payments and connected card draws it covers, for those agents. A task's budget does
228
+ neither. It applies only to a managed wallet payment or connected card draw that names the task.
229
+ `updateBudget()` can resume a paused budget or extend an
230
+ expired one, and a start date cannot be changed. To end a budget, call `admin.spending.deleteBudget(id, { version })`. It fails with
231
+ `reasonCode` `BUDGET_HAS_HOLDS` while a purchase holds spend against the budget. Try budgets on a test
232
+ organization first.
233
+
234
+ - A budget covers purchases in its own currency, so set it to what the wallets pay in: `USDC` on
235
+ Base, `USDC.e` on Tempo or `pathUSD` on Tempo Testnet. `USD`, `USDC` and `USDC.e` budgets cover one
236
+ another at par when no budget at that level, workspace or agent, is in the exact currency. A
237
+ managed wallet payment or connected card draw is refused, whatever the budget's state, when a level
238
+ has budgets for that agent and none covers its currency. `preview()` gives `reasonCode`
239
+ `BUDGET_CURRENCY_UNSUPPORTED`. A budget's currency cannot be changed.
240
+ - `preview()` needs a managed wallet linked to the agent, and that wallet's stablecoin as `currency`.
241
+ It holds nothing and approves nothing, so keep the request, approval and execute flow.
242
+ - When a budget requires a task, ask for one with `conto.spending.requestTask()`. Once the budget
243
+ holder approves it, pass its id as `context.taskRequestId` on `payments.request()` from a managed
244
+ wallet, or as `taskRequestId` on `connectedCards.draw()`. With a self-custody wallet,
245
+ `payments.request()` returns `DENIED`, and `payments.approve()` refuses the task fields.
246
+ - `context.purchaseKey` identifies one purchase. A key cannot be used again, even after its purchase
247
+ is released, so use a new key for a new attempt.
248
+ - `payments.request()` can return `APPROVED` for a purchase a budget refuses. `payments.execute()`
249
+ then throws status 409 and code `SHARED_BUDGET_BLOCKED`. With `autoExecute`, the request stays
250
+ `APPROVED` and nothing is thrown. To see why, call `preview()` with the same `taskRequestId` and
251
+ `purchaseKey`.
252
+ - `purchases()` lists purchases against budgets in the agent's organization, not Commerce
253
+ `conto.purchases`. It is empty when none of those budgets has applied. A buyer linked to an owner
254
+ workspace has no row for a purchase that only an owner budget covered. The owner reads those with
255
+ `admin.spending.purchases()`.
256
+ - `admin.spending` needs an organization API key. `createBudget()`, `updateBudget()`, `deleteBudget()`
257
+ and `decideTask()` need `policies:write`. `accounting()`, `attachEvidence()` and `reviewEvidence()`
258
+ need `transactions:write`. `reconcile()` needs `spending:reconcile`, which Standard keys do not
259
+ have. An admin SDK key cannot change budgets, decide tasks, or reconcile.
260
+ - `attachEvidence()` takes a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set
261
+ the Blob's `type`, for example `new Blob([bytes], { type: 'application/pdf' })`. A Blob with no type,
262
+ or with a type that does not match the file's first bytes, fails with `reasonCode` `INVALID_FILE`.
263
+ Conto does not take the amount, currency or merchant from the file. The budget holder enters them
264
+ with `reviewEvidence()`.
265
+ - `exportPurchases()` returns CSV bytes and fails with `reasonCode` `EXPORT_TOO_LARGE` above 10,000
266
+ purchases.
267
+ - Budget errors have `code` `POLICY_VIOLATION`, so read `reasonCode` on `ContoError` and
268
+ `ContoAdminError`. Some also have `nextActions`, such as `SHARED_BUDGET_EXCEEDED` with
269
+ `REQUEST_BUDGET_INCREASE`. `checkout.begin()`, `getLinkCard()` and `replaceLinkCard()` have `code`
270
+ `CHECKPOINT_BUDGET_NOT_SUPPORTED` with that `reasonCode` set. `payments.execute()` has `code`
271
+ `SHARED_BUDGET_BLOCKED` and no `reasonCode`.
272
+ - A `VALIDATION_ERROR` on a spending call usually lists the fields in `fieldErrors`. It does not when
273
+ the call got a field it does not accept, such as `periodStart` or `currency` on `updateBudget()`. A
274
+ `payments.request()` validation failure never has `fieldErrors`.
275
+
175
276
  ## External-card Policy Overlay
176
277
 
177
278
  The admin client registers only a masked card alias and assigns cooperative
@@ -317,9 +418,10 @@ Shop Pay, or PayPal browser action without requiring private provider integratio
317
418
  privileged browser executor must be available only through this wrapper. An unrestricted click or
318
419
  payment tool would bypass the control. Persist its stable dispatch attempt before calling the
319
420
  helper. The helper owns authorization, one-use claim, the privileged callback, and finalization.
320
- The
321
- [Stripe Link guide](https://conto.finance/docs/guides/stripe-link#make-the-check-enforceable)
322
- describes the runtime boundary and includes an example.
421
+ When a rule requires the owner's approval, `runGuardedCheckout` stops with status 409 and code
422
+ `APPROVAL_REQUIRED`. Checkpoints wait for the owner instead, so for Stripe Link prefer the
423
+ [checkpoints in the Stripe Link guide](https://conto.finance/docs/guides/stripe-link#add-the-checkpoints)
424
+ or `getLinkCard()`.
323
425
 
324
426
  Isolated browser processes can implement `PrivilegedBrowserCheckoutDriver` and use
325
427
  `runGuardedCheckoutWithDriver`. The agent model must receive only that guarded wrapper, never the
@@ -522,7 +624,8 @@ and other non-idempotent mutations are never retried automatically. If execution
522
624
  payment until an operator has resolved it.
523
625
 
524
626
  `ContoError` and `ContoAdminError` expose a stable error contract: `code`, `status`, `requestId`,
525
- `retryAfter`, and locally generated `retryGuidance` where applicable. Arbitrary server details,
627
+ `retryAfter`, `fieldErrors` on most spending validation failures, `reasonCode` and `nextActions` on a
628
+ budget refusal, and locally generated `retryGuidance` where applicable. Arbitrary server details,
526
629
  context, hints, and workflow steps are not forwarded through SDK errors:
527
630
 
528
631
  ```typescript
@@ -0,0 +1,30 @@
1
+ // src/link-approval.ts
2
+ function safeLinkApprovalUrl(value) {
3
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048) return void 0;
4
+ let url;
5
+ try {
6
+ url = new URL(value);
7
+ } catch {
8
+ return void 0;
9
+ }
10
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.port !== "" || !(url.hostname === "link.com" || url.hostname.endsWith(".link.com"))) {
11
+ return void 0;
12
+ }
13
+ url.hash = "";
14
+ return url.toString();
15
+ }
16
+ function linkTime(value) {
17
+ let ms = Number.NaN;
18
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
19
+ ms = value < 1e12 ? value * 1e3 : value;
20
+ } else if (typeof value === "string" && value.length > 0 && value.length <= 64) {
21
+ ms = Date.parse(value);
22
+ }
23
+ const date = new Date(ms);
24
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
25
+ }
26
+
27
+ export {
28
+ safeLinkApprovalUrl,
29
+ linkTime
30
+ };