@capxul/sdk 4.20.0-beta.4 → 4.20.0-beta.40

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