@capxul/sdk 4.20.0-beta.3 → 4.20.0-beta.31

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
@@ -19,51 +19,131 @@ const client = created.value;
19
19
  Every public operation resolves a `Promise<CapxulResult<T>>`. Domain failures
20
20
  are values, not thrown exceptions.
21
21
 
22
- ## Money input and display
23
-
24
- Use the public money helpers at form and display boundaries. They do not
25
- convert a money value to a JavaScript number.
22
+ After the Person and personal Account are ready, begin Organization setup and
23
+ store its exact ID before signer interaction:
26
24
 
27
25
  ```ts
28
- import { formatMoney, parseMoney } from "@capxul/sdk";
26
+ const begun = await client.onboarding.beginOrganization({
27
+ name: "Analytical Engines",
28
+ handle: "analytical-engines",
29
+ country: "GH",
30
+ });
31
+ if (!begun.ok) throw begun.error;
32
+
33
+ const { orgId } = begun.value;
34
+ if (begun.value.setup.status === "awaitingAuthorization") {
35
+ const authorized = await client.org(orgId).setup.authorize();
36
+ if (!authorized.ok) throw authorized.error;
37
+ }
29
38
 
30
- const amount = parseMoney("1,000.25", {
31
- currency: account.available.currency,
32
- decimals: account.available.decimals,
39
+ const observed = await client.org(orgId).setup.subscribe((snapshot) => {
40
+ if (snapshot.status === "value") console.log(snapshot.value.status);
33
41
  });
42
+ if (!observed.ok) throw observed.error;
43
+ // Call observed.value() when the screen no longer needs updates.
44
+ ```
45
+
46
+ `setup.resume()` requests a backend check for the same Organization. It does
47
+ not ask the signer to submit again. `onboarding.completeOrganization()` runs
48
+ begin, authorization when needed, and observation through `ready`.
49
+
50
+ An authenticated client can find unfinished founder setups without a local
51
+ recovery file. Read pages until `isDone` is true; omit `cursor` on the first
52
+ request:
53
+
54
+ ```ts
55
+ let cursor: string | undefined;
56
+ do {
57
+ const listed = await client.onboarding.listOrganizationSetups(
58
+ cursor === undefined ? {} : { cursor },
59
+ );
60
+ if (!listed.ok) throw listed.error;
61
+ for (const setup of listed.value.page) console.log(setup.orgId, setup.status);
62
+ if (listed.value.isDone) break;
63
+ if (listed.value.cursor === null) throw new Error("Missing setup cursor");
64
+ cursor = listed.value.cursor;
65
+ } while (true);
66
+ ```
67
+
68
+ `listOrganizationSetups` is a read. It does not authorize, resume, or sign.
69
+
70
+ ## Invitation updates
71
+
72
+ Use `client.org(orgId).invitations.subscribe({ invitationId }, listener, { signal })`
73
+ to observe one offer. The listener receives `loading`, then validated `value`
74
+ snapshots or a terminal typed `error`. The successful result holds an idempotent
75
+ unsubscribe function. Abort, sign-out, account replacement, and client close
76
+ stop delivery. A late registration handle is released when it arrives.
77
+ This read never accepts an offer or retries a write. Bound a wait through the
78
+ caller’s AbortSignal; per-call timeout options do not define stream lifetime.
79
+
80
+ ## Asset amount input and display
81
+
82
+ Use the asset helpers at Payment form and display boundaries. They keep the
83
+ exact AssetId and do not convert a value to a JavaScript number.
84
+
85
+ ```ts
86
+ import { formatAssetAmount, parseAssetAmount, requireAsset, TOKENS } from "@capxul/sdk";
87
+
88
+ const amount = parseAssetAmount("1,000.25", requireAsset(TOKENS.BASE_SEPOLIA.CAPXUL_TEST_USDC));
34
89
 
35
90
  if ("kind" in amount) {
36
91
  console.error(amount.reason); // closed code — the app owns the sentence (ADR-0023)
37
92
  } else {
38
- console.log(formatMoney(amount)); // $1,000.25
39
- console.log(formatMoney(amount, { grammar: "code" })); // 1,000.25 USD
93
+ console.log(formatAssetAmount(amount)); // 1,000.25 USDC
40
94
  }
41
95
  ```
42
96
 
43
- `parseMoney` returns validation errors as values. `formatMoney` rejects a
44
- malformed `Money` record with `INVALID_INPUT`.
97
+ `parseAssetAmount` returns validation errors as values. Payment, request,
98
+ Budget, Payroll, and activity amounts require AssetAmount. Fiat Money remains
99
+ separate valuation data.
45
100
 
46
- ## The asset a screen shows
101
+ ## Value all asset positions
47
102
 
48
- `holdings.primary` picks the one asset a hero balance or a summary total
49
- should show. It returns the renderable holding with the largest available
50
- balance. A chain row that carries no `decimals`, no `symbol`, or a symbol that
51
- stands for no supported currency is skipped, because it cannot be rendered.
103
+ Use `holdings.current` for raw positions. Use `valueIn` to value exact asset
104
+ quantities in a display currency. A token symbol does not identify an asset.
105
+ Quantities from different assets cannot be compared as a headline balance.
52
106
 
53
107
  ```ts
54
- import { formatMoney, type Holding } from "@capxul/sdk";
55
-
56
- const result = await client.holdings.primary();
108
+ import { formatMoney, valueIn } from "@capxul/sdk";
57
109
 
58
- if (result.ok && result.value !== null) {
59
- const holding: Holding = result.value;
60
- console.log(holding.symbol); // "USDX" — the token symbol the chain reported
61
- console.log(formatMoney(holding.available, { grammar: "code" })); // e.g. "1,250.50 USD"
110
+ const valuation = valueIn(amounts, displayCurrency, rates);
111
+ if (valuation.total !== null) {
112
+ console.log(formatMoney(valuation.total));
62
113
  }
63
114
  ```
64
115
 
65
- `null` means no row is renderable. It is a value, not an error, so give the
66
- screen its own rule for showing nothing.
116
+ Keep `valuation.unrated` visible beside the known subtotal. A missing rate
117
+ does not mean a zero balance.
118
+
119
+ ## Approvals
120
+
121
+ Every signature a person makes goes through one approval. A caller with no
122
+ key, such as the CLI or an agent, prepares the Payment and hands the person
123
+ the link. Nothing is signed or sent until the person approves it there.
124
+
125
+ ```ts
126
+ const prepared = await client.approvals.prepare({
127
+ to: { kind: "email", email: "ada@example.com" },
128
+ amount: { asset: usdc.assetId, value: "25" },
129
+ });
130
+ if (!prepared.ok) throw prepared.error;
131
+ console.log(prepared.value.url, prepared.value.summary);
132
+
133
+ // Sends the approved operation once, then waits until its Payment settles.
134
+ const outcome = await client.approvals.wait(prepared.value.approvalId, { signal });
135
+ ```
136
+
137
+ The approval page runs as the same person, with their own signer:
138
+
139
+ ```ts
140
+ await client.approvals.review(approvalId); // the first open is recorded
141
+ const approved = await client.approvals.approve(approvalId); // or reject(approvalId)
142
+ ```
143
+
144
+ `approve` signs only the digest the approval's operation describes, and only
145
+ with the approval's signer. The backend checks the signature and never signs.
146
+ A rejected or expired approval ends `wait` with that state.
67
147
 
68
148
  ## Observability
69
149
 
@@ -105,6 +185,11 @@ The backend remains the authority for reserved names and global uniqueness.
105
185
  The method returns only after the client requirement reaches a ready Account.
106
186
  The React frontend does not use this journey yet.
107
187
 
188
+ A Node host that keeps login outside the browser can pass `openfortSignerFactory`.
189
+ The factory receives only the validated Openfort and auth bootstrap fields. Its
190
+ browser page can use `createOpenfortBrowserSigner` with host-owned access-token
191
+ and encryption-session callbacks. It does not need a second login or private SDK import.
192
+
108
193
  ## Published entry points
109
194
 
110
195
  The packed npm package exposes only:
@@ -125,7 +210,10 @@ also contains them.
125
210
  import { createCapxulTestClient } from "@capxul/sdk/testing";
126
211
 
127
212
  const testing = createCapxulTestClient();
128
- await testing.seedIdentity({ authUserId: "auth_seeded_test", email: "test@example.test" });
213
+ await testing.seedIdentity({
214
+ authUserId: "auth_seeded_test",
215
+ email: "test@example.test",
216
+ });
129
217
  await testing.clock.advance(1_000);
130
218
  await testing.client.auth.signIn({ email: "test@example.test" });
131
219
  const otp = testing.peekOtp("test@example.test");
@@ -165,11 +253,142 @@ vp run --filter @capxul/sdk build
165
253
  vp run --filter @capxul/sdk proofs:live
166
254
  ```
167
255
 
168
- `proofs:live` runs the deterministic Organization Core SDK journey against the
169
- configured live Convex deployment and Base Sepolia. Live proofs require
170
- operator secrets and prove only the path named by the command. See
171
- [maintainer context](./CONTEXT.md#capability-and-proof-boundaries).
256
+ `proofs:live` runs the Organization Core SDK journey against the configured
257
+ Convex deployment and Base Sepolia. Run it with the synchronized candidate.
258
+ The package tests prove local SDK behavior.
259
+ See [maintainer context](./CONTEXT.md#capability-and-proof-boundaries).
172
260
 
173
261
  The browser production path owns its Openfort signer when no signer is supplied.
174
- Current integration recovery evidence and its published-package boundary are
175
- tracked in the [#870 receipt](../../docs/receipts/issue-870-openfort-signer-recovery-RECEIPT-2026-07-19.md).
262
+ Integration recovery evidence and its published-package boundary were recorded
263
+ in the #870 receipt (since deleted; see git history).
264
+
265
+ ### Node session storage
266
+
267
+ A Node host can pass `nodeSessionStorage: { load, save }` to
268
+ `createCapxulClient`. Both methods return promises. `load` returns an opaque
269
+ BetterAuth credential or `null`; `save` receives the credential or `null` on
270
+ logout. Store it with owner-only access and isolate it by application and
271
+ environment. Do not print it or use a cached `AuthSession` as backend authority.
272
+ The SDK validates the credential and expiry with BetterAuth on process restart.
273
+ With durable storage, each adapter serializes session reads, OTP verification,
274
+ sign-out, and Convex token reads. A delayed response cannot clear or rotate a
275
+ credential from a later operation on that adapter. Cancellation while queued
276
+ returns immediately and prevents that operation from reaching the provider.
277
+ Hosts that share storage across clients or processes must fence writes, for
278
+ example with an atomic compare-and-set; the adapter cannot order other clients.
279
+
280
+ For separate OTP request and verification processes, retain the email and
281
+ original request timestamp. After `auth.getSession()` confirms signed-out state,
282
+ call `auth.resumeOtp({ email, requestedAt })`, then `auth.verifyOtp({ email,
283
+ code })`. Resume restores pending input only. It does not send another OTP or
284
+ authenticate the caller. Keep OTP values out of arguments, logs, and stored data.
285
+
286
+ ### Existing financial documents
287
+
288
+ Use `paymentDocuments.verify({ documentHash, contentHash })` to verify stored
289
+ content. Use `paymentDocuments.render` with the same pair for safe semantic HTML.
290
+ Call `paymentDocuments.get` only for a deliberate download. Its `bytesBase64`
291
+ contains the exact stored bytes. Do not log document results or error payloads.
292
+ Get references from Payment or Movement activity detail. References include
293
+ kind, format, and role. A hash is not permission to read a document.
294
+ Reads check current Payment participants or current Movement actor access.
295
+ Missing and unauthorized pairs return the same refusal. These methods do not
296
+ create documents or add Payment links.
297
+
298
+ ## Inbox documents
299
+
300
+ Personal Account and Organization Inbox reads include `documents` on each
301
+ item. An unpaid issued Invoice can have document references before a Payment
302
+ exists. Requests without an attached document return an empty array. The
303
+ references preserve the document and content hashes needed by
304
+ `paymentDocuments.get`, `verify`, and `render`. Inbox listing does not render
305
+ or download documents. Existing issuer and payer access checks still apply.
306
+
307
+ Issued requests and Inbox items retain a linked `paymentId` when preparation
308
+ has started. Invoice Inbox items also retain the stored `issuerSafe`. These
309
+ fields do not mean that the Payment has settled.
310
+
311
+ `inbox.paymentTerms(requestId)` reads exact private terms without a signer or
312
+ write. It returns the issued Invoice instruction or a deterministic Memo
313
+ instruction for a generic request. Prepared requests retain their stored
314
+ instruction. Only the addressed payer can read these terms.
315
+
316
+ `inbox.fulfill(confirmation, { onPrepared })` executes the confirmed request
317
+ through the existing signer. `onPrepared({ paymentIds })` receives verified
318
+ durable IDs before signing. If the callback fails, signing stops. Do not await
319
+ the fulfillment call inside this callback. Organization confirmation requires
320
+ an admitted Permission; personal confirmation does not accept one. The backend
321
+ checks the payer, exact amount, issuer Safe, release, and frozen instruction.
322
+
323
+ ## Fixed offers and checkout
324
+
325
+ `account.offers` and `org(id).offers` manage reusable fixed-price offers with
326
+ explicit quantity limits. Creation uses a caller request key. Revisions append
327
+ immutable prices and require the expected revision.
328
+
329
+ `checkouts.offer(id)` is a public read and creates no purchase. `checkouts.create`
330
+ uses a UUID v4 purchase key, selected revision, quantity, and funding wallet to
331
+ freeze one purchase. `checkouts.get` restores its exact snapshot. `checkouts.prepare`
332
+ returns unsigned approval and Payments-contract transactions for that wallet.
333
+ The consumer checks chain, balance, allowance, and gas. Preparation does not sign,
334
+ submit, or prove settlement. No checkout URL is returned before the page exists.
335
+
336
+ ## Fixed offer checkout candidate
337
+
338
+ The checkout APIs below are source candidate APIs. The published beta17 baseline
339
+ does not include them. Consume a matching packed candidate for local integration.
340
+
341
+ `account.offers` and `org(id).offers` provide create, list, get, revise, and
342
+ deactivate. Create uses a caller replay key. Revision and deactivation require the
343
+ current revision. Each offer has one asset, an exact unit amount, and bounded whole
344
+ unit quantities. An offer revision does not change an existing purchase.
345
+
346
+ Public `checkouts.offer(id)` reads approved offer fields. `checkouts.create(input)`
347
+ explicitly freezes the offer revision, quantity, funding wallet, recipient, amount,
348
+ network, contract, instruction, and settlement identity. Reuse the same UUID v4
349
+ purchase key after a lost reply. Different terms with the same key refuse.
350
+ `checkouts.get(id)` recovers the frozen snapshot. These methods do not sign.
351
+
352
+ `checkouts.prepare({ checkoutId, sourceWallet })` returns exact unsigned approval
353
+ and payment transactions. The consumer checks network, balance, gas, and allowance
354
+ before requesting wallet action. A connection or submission is not settlement.
355
+
356
+ `checkouts.state(id)` performs a checked read. It returns historical receipt data
357
+ separately from current verification: `unpaid`, `verified`, `orphaned`, or
358
+ `unavailable`. Provider failure cannot prove that a receipt was removed.
359
+ `checkouts.reconcile({ checkoutId, transactionHash })` returns the candidate result
360
+ and independently checked stored state. An unrelated candidate cannot change a
361
+ valid stored payment. Canonical occurrence correction retains logical identity.
362
+
363
+ These offer methods do not implement recurring billing.
364
+ `checkouts.payRegistered({ checkoutId, payer, permissionId? })` prepares, verifies,
365
+ and submits through the existing registered signer. Account and Organization
366
+ authority remain mandatory. Submission does not establish settlement. Public
367
+ Invoice funding and frontend registered-payer adoption remain separate work.
368
+
369
+ `isCheckoutSettlementConsumed(error, settlementId)` decodes the contract's refusal
370
+ for the exact frozen ID. Use it for structured or raw RPC errors. Error message
371
+ text alone does not establish that an ID was consumed.
372
+
373
+ ## Invoice checkout candidate
374
+
375
+ `checkouts.invoice(linkToken)` exposes only the approved payment summary.
376
+ Itemization, notes, billing identity and document bytes remain restricted to the
377
+ issuer and named debtor. Funding does not change that debtor or grant document
378
+ access. Existing link tokens resolve the original obligation.
379
+
380
+ `prepareInvoice({ linkToken, sourceWallet })` returns unsigned transactions.
381
+ `invoiceState(linkToken)` checks current receipt evidence. `reconcileInvoice`
382
+ verifies an explicitly supplied transaction against the fixed terms. Late exact
383
+ payments remain visible after expiry or cancellation. Conflicting consumption is
384
+ unavailable, never paid, and does not create another settlement identity.
385
+
386
+ `payInvoiceRegistered({ linkToken, payer, sourceWallet, permissionId? })` uses the
387
+ existing registered signer. The selected Safe must match the backend's current
388
+ spending authority. The response omits private source commands and document
389
+ bundles. Existing private preparations retain their original settlement ID.
390
+
391
+ `checkoutUrl(frontendOrigin, reference)` constructs an offer or Invoice route.
392
+ The caller supplies the selected environment's frontend origin. The consumer
393
+ still validates the reference and reads authoritative backend terms. Constructing
394
+ a URL does not prove that its consumer is deployed.