@capxul/sdk 0.0.0-rc-2cdddbb91

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 ADDED
@@ -0,0 +1,417 @@
1
+ # `@capxul/sdk`
2
+
3
+ The imperative TypeScript client for Capxul. It owns the public
4
+ `createCapxulClient` factory, domain method bundles, production adapter assembly,
5
+ and the internal Effect-based ports and flows that those methods bridge.
6
+
7
+ ```ts
8
+ import { createCapxulClient } from "@capxul/sdk";
9
+
10
+ const created = await createCapxulClient({
11
+ publishableKey: process.env.CAPXUL_PUBLISHABLE_KEY!,
12
+ requirement: "counterfactual",
13
+ });
14
+
15
+ if (!created.ok) throw created.error;
16
+ const client = created.value;
17
+ ```
18
+
19
+ Every public operation resolves a `Promise<CapxulResult<T>>`. Domain failures
20
+ are values, not thrown exceptions.
21
+
22
+ After the Person and personal Account are ready, begin Organization setup and
23
+ store its exact ID before signer interaction:
24
+
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:
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
+ ## Local Effect DevTools
71
+
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",
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));
112
+
113
+ if ("kind" in amount) {
114
+ console.error(amount.reason); // closed code — the app owns the sentence (ADR-0023)
115
+ } else {
116
+ console.log(formatAssetAmount(amount)); // 1,000.25 USDC
117
+ }
118
+ ```
119
+
120
+ `parseAssetAmount` returns validation errors as values. Payment, request,
121
+ Budget, Payroll, and activity amounts require AssetAmount. Fiat Money remains
122
+ separate valuation data.
123
+
124
+ ## Value all asset positions
125
+
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.
129
+
130
+ ```ts
131
+ import { formatMoney, valueIn } from "@capxul/sdk";
132
+
133
+ const valuation = valueIn(amounts, displayCurrency, rates);
134
+ if (valuation.total !== null) {
135
+ console.log(formatMoney(valuation.total));
136
+ }
137
+ ```
138
+
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.
170
+
171
+ ## Observability
172
+
173
+ Hosts can attach the PostHog client they already initialized through one
174
+ product-and-failure module:
175
+
176
+ ```ts
177
+ import { postHogObservability } from "@capxul/sdk";
178
+
179
+ const created = await createCapxulClient({
180
+ publishableKey: "cap_pk_…",
181
+ observability: postHogObservability(posthog, {
182
+ capxulEnv: "production",
183
+ context: () => ({ correlationId, journeyId, organizationId }),
184
+ }),
185
+ });
186
+ ```
187
+
188
+ The SDK initializes no analytics client and preserves host consent. Product
189
+ events and sanitized failures share one enable/context/delivery policy.
190
+ Engineering logs and traces are separate internally, but applications do not
191
+ configure them: the versioned bootstrap response optionally supplies the
192
+ governed PostHog ingest origin, public project token, and environment. The SDK
193
+ derives endpoints, service, producer, and version. Missing or invalid policy
194
+ disables engineering export without failing client creation.
195
+
196
+ Person onboarding is one Core SDK journey. It requires a normalized, globally
197
+ unique handle before it writes the Profile or starts Account setup:
198
+
199
+ ```ts
200
+ const onboarded = await client.onboarding.completePerson({
201
+ displayName: "Ada Lovelace",
202
+ country: "GH",
203
+ handle: "ada_lovelace",
204
+ });
205
+ ```
206
+
207
+ The backend remains the authority for reserved names and global uniqueness.
208
+ The method returns only after the client requirement reaches a ready Account.
209
+ The React frontend does not use this journey yet.
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
+
216
+ ## Published entry points
217
+
218
+ The packed npm package exposes only:
219
+
220
+ - `@capxul/sdk` — the client factory, public method and value types, signers,
221
+ host observability helper, and immutable Base Sepolia Payment contract
222
+ identity. It also re-exports the canonical Capxul operation vocabulary.
223
+ - `@capxul/sdk/node` — Node-only signer and runtime helpers.
224
+ - `@capxul/sdk/testing` — the real identity runtime assembled over
225
+ deterministic in-memory ports, with OTP, advance-only clock, transition,
226
+ observation, seed, and close controls for tests.
227
+
228
+ The many extra subpaths in `package.json#exports` are workspace development
229
+ surfaces. Do not show them in consumer examples unless `publishConfig.exports`
230
+ also contains them.
231
+
232
+ ```ts
233
+ import { createCapxulTestClient } from "@capxul/sdk/testing";
234
+
235
+ const testing = createCapxulTestClient();
236
+ await testing.seedIdentity({
237
+ authUserId: "auth_seeded_test",
238
+ email: "test@example.test",
239
+ });
240
+ await testing.clock.advance(1_000);
241
+ await testing.client.auth.signIn({ email: "test@example.test" });
242
+ const otp = testing.peekOtp("test@example.test");
243
+ if (!otp) throw new Error("expected a test OTP");
244
+ await testing.client.auth.verifyOtp({ email: "test@example.test", code: otp });
245
+ // Drive the real public client, then inspect testing.transitions.
246
+ await testing.close();
247
+ ```
248
+
249
+ The seeded email is bound to the requested auth user ID, so verifying its OTP
250
+ loads that exact seeded Profile. The public clock exposes only `advance(ms)`;
251
+ the internal clock adapter and its Effect/conformance methods remain private.
252
+ The testing entry is identity-scoped and hermetic. Fine-grained
253
+ `@capxul/sdk/testing/*` adapter paths remain workspace-only.
254
+
255
+ ## Documentation
256
+
257
+ - [Maintainer context](./CONTEXT.md) — ownership, boundaries, invariants,
258
+ evidence, and update triggers.
259
+ - [Architecture](./docs/architecture.md) — client assembly, flows, ports,
260
+ adapters, and proof map.
261
+ - [Observation](./docs/observation.md) — unified host observability,
262
+ bootstrap-owned engineering policy, and bounded backend linkage.
263
+ - [Public SDK documentation](../../apps/docs/content/docs/sdk/index.mdx) —
264
+ tutorials, guides, reference, and explanation for npm consumers.
265
+
266
+ React integration is a separate projection owned by `@capxul/sdk-react`.
267
+
268
+ ## Development
269
+
270
+ Run package commands through Vite+:
271
+
272
+ ```bash
273
+ vp run --filter @capxul/sdk check-types
274
+ vp run --filter @capxul/sdk test
275
+ vp run --filter @capxul/sdk build
276
+ vp run --filter @capxul/sdk proofs:live
277
+ ```
278
+
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).
283
+
284
+ The browser production path owns its Openfort signer when no signer is supplied.
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.