@xkova/sdk 0.1.0-alpha.6 → 0.1.0-alpha.8

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
@@ -93,6 +93,77 @@ one live successor; either key can be revoked immediately.
93
93
 
94
94
  ## Typed resources
95
95
 
96
+ ### Customer workforce applications
97
+
98
+ Use `@xkova/sdk/workforce-bff` on a confidential server for an institution's
99
+ staff application. It is separate from member login and Console operator
100
+ sessions. The Console administrator first registers an exact issuer, audience,
101
+ JWKS endpoint, application origins/callbacks and dedicated workspace API
102
+ principal. They explicitly admit each immutable provider subject to an existing
103
+ staff membership with narrowed roles and assigned resources. Email and provider
104
+ groups never grant authority. A transport key cannot admit its own users.
105
+
106
+ ```ts
107
+ import { XKOVAWorkforceBffClient } from '@xkova/sdk/workforce-bff';
108
+
109
+ const workforce = new XKOVAWorkforceBffClient({
110
+ baseUrl: 'https://sandbox-api.xkova.com',
111
+ apiKey: process.env.XKOVA_WORKFORCE_API_KEY!,
112
+ workforceKey: process.env.XKOVA_WORKFORCE_KEY!,
113
+ });
114
+ ```
115
+
116
+ Use separate encrypted durable transaction/session stores implementing
117
+ `XKOVAWorkforceStore`. Each write must commit independently of a later callback
118
+ error. Serialize each reference across replicas, and make `replace` fail when
119
+ the record is absent. The included PostgreSQL adapter demonstrates this contract.
120
+ Use a distinct namespace from member sessions. Keep session material, provider
121
+ proofs, PKCE verifiers and refresh attempts on the server.
122
+
123
+ `beginAuthorization` returns the registered provider endpoint, audience, nonce,
124
+ PKCE challenge and policy. Use a short-lived HttpOnly flow cookie for its state.
125
+ Your BFF exchanges the real provider code and verifies its signature, issuer,
126
+ audience, nonce and PKCE through `completeAuthorizationCallback`. XKOVA performs
127
+ its own registered-provider verification before issuing the workforce session.
128
+ An uncertain provider exchange cannot redeem the code again. A staged proof can
129
+ recover the same API exchange attempt. A provider token's actual expiry bounds
130
+ the session; refreshing XKOVA credentials does not refresh upstream identity.
131
+
132
+ Read only `projectXKOVAWorkforceSession` into browser UI. Resolve resource access
133
+ per request with `resolveResources({ reference, sessionStore, browserOrigin })`.
134
+ The browser's Origin and session-bound CSRF proof must be checked before writes.
135
+ The BFF pins its registered origin, workforce application and resolved human
136
+ session. Roles/scopes/resources are intersected by the API, never combined with
137
+ the transport principal's authority. A member credential cannot supply workforce
138
+ authority, and workforce sessions cannot enter Console operator routes.
139
+
140
+ For `human_authorization_required`, preserve the exact request, body, origin and
141
+ idempotency key in a private review record. Use `inspectAction` and
142
+ `beginActionProof` for its exact `authorization_token`. Show the returned review
143
+ before starting real fresh provider authentication with the returned nonce and
144
+ qualified assurance policy. Validate the provider callback's own state, PKCE,
145
+ issuer, audience, nonce and current subject; require fresh authentication rather
146
+ than accepting a cached login. Submit the real token with `completeActionProof`.
147
+ Keep its short-lived grant server-side and pass it as `actionGrant` to
148
+ `resolveResources` for only the unchanged frozen request. A grant never approves
149
+ another request or replaces independent Safe signatures/quorum. These methods
150
+ do not automatically retry an action or an uncertain proof completion.
151
+
152
+ `logoutSession` durably blocks the local session before retiring it at XKOVA.
153
+ Retry only that same logout if the network result is uncertain. It does not
154
+ promise a global logout at the external provider. No request may fall back to a
155
+ machine principal after workforce authentication fails.
156
+
157
+ The resolved `wallets` resource supports `list`, `provision`, `register` and
158
+ `signingBootstrap`. Provisioning and signing need the exact one-use human grant.
159
+ Register only after signing the API's returned enrollment typed data. Signing
160
+ bootstrap requires the existing proposal ID, its frozen Safe digest and nonce.
161
+ Use `@xkova/browser/wallet` for the isolated browser ceremony, with a fresh
162
+ purpose-bound admission and the expected wallet address. Keep the BFF key,
163
+ provider proofs and workforce session credentials on the server. Provider
164
+ admission does not replace wallet possession, current signer eligibility,
165
+ independent-person quorum or confirmed chain settlement.
166
+
96
167
  Call resource methods with typed inputs:
97
168
 
98
169
  ```ts
@@ -124,6 +195,10 @@ const balance = await xkova.accountHolders.selfBalance({
124
195
  });
125
196
  ```
126
197
 
198
+ `selfBalance` can return a partial answer. A chain not known right now has no
199
+ entries in `balances` and a null `through_block` in `data_freshness`, so check
200
+ that before treating a missing token as zero or adding balances together.
201
+
127
202
  ## Register a Hosted Auth application
128
203
 
129
204
  Hosted Auth provides an optional XKOVA-hosted sign-in flow. Register an
@@ -216,6 +291,26 @@ proofs, and manages session refresh and logout. Implement its session,
216
291
  authorization-transaction, and direct proof-exchange store contracts in your
217
292
  own database. It does not include a web framework or database adapter.
218
293
 
294
+ `runExclusive` is an exclusivity boundary, not an all-or-nothing SQL
295
+ transaction. Every awaited `put`, `replace` and `delete` must commit before
296
+ returning, and remain committed if later callback work throws. Wrapping the
297
+ whole callback in a rollback transaction can erase a staged proof after its
298
+ one-time provider code was consumed. The same rule protects refresh-attempt
299
+ checkpoints. Reconcile an uncertain write acknowledgement using the exact
300
+ record and attempt identity, rather than starting a new operation.
301
+
302
+ The package includes [a PostgreSQL checkpoint-store example](examples/postgres-checkpoint-store.mjs)
303
+ and its [application-owned table definition](examples/postgres-checkpoints.sql).
304
+ Install `pg` in your backend and apply the table definition through your own
305
+ migration process. Instantiate separate stores with namespaces such as
306
+ `authorization`, `sessions` and `exchange`, one shared pool and an existing
307
+ 32-byte application encryption key. The example uses session advisory locks
308
+ across replicas, independent autocommit checkpoints, authenticated encryption
309
+ bound to namespace/reference, exact replacement without upsert, and connection
310
+ discard when lock release is uncertain. Do not use a transaction-pooling proxy
311
+ for session locks. Own key rotation, pool deadlines and expired-record cleanup
312
+ in your application; do not log records or encryption material.
313
+
219
314
  Authorization, Account Center, and action transactions expire within ten
220
315
  minutes of starting the request, or at the server's expiry if sooner. Late,
221
316
  expired, or excessively long-lived responses are rejected. Keep your BFF's
@@ -246,6 +341,51 @@ To exchange a proof without a browser callback, call `establishSession` with an
246
341
  `exchangeId`. Persist that ID before the first attempt and reuse it on retries.
247
342
  The store serializes attempts and preserves the exchange and session references.
248
343
 
344
+ ### Enroll an external identity-provider member
345
+
346
+ For a registered OIDC connection, the confidential backend can call
347
+ `memberAuth.enrollExternalMember({ externalSub, applicationConfigVersion,
348
+ idempotencyKey })`. Use the exact opaque subject from a fully verified provider
349
+ proof, the final read-back application configuration version, and one persisted
350
+ operation key. Never link by email. The API derives issuer namespace, tenant,
351
+ workspace and application from the registered connection and its bound
352
+ `member_auth` API principal. The header selector and body selector must match.
353
+ An existing issuer/subject membership is returned without replacing its identity,
354
+ status or wallet bindings. A held or closed member remains held or closed.
355
+
356
+ Enrollment creates or finds a membership, not an authenticated session or wallet.
357
+ The normal nonce-bound provider-proof exchange remains required. For callback
358
+ flows use `completeAuthorizationCallback`'s optional `ensureMemberEnrollment`
359
+ hook. It runs after the one-time provider proof is durably staged, with a stable
360
+ `idempotencyKey` preserved across retries. Verify the staged proof's signature,
361
+ issuer, audience, algorithm, expiry and nonce, then call `enrollExternalMember`
362
+ with its exact `sub`. Return that result from the hook. The helper checks that
363
+ the resulting authenticated session resolves to the enrolled account holder.
364
+ Do not put a fallible enrollment call inside `exchangeProviderCode`, before
365
+ the provider result has been staged. A version conflict requires reviewing the
366
+ changed registration and starting a new authorization flow, not blind retries.
367
+
368
+ ```ts
369
+ const completion = await memberAuth.completeAuthorizationCallback({
370
+ ...callbackInput,
371
+ ensureMemberEnrollment: async ({ proofToken, idempotencyKey }) => {
372
+ // This application function performs full JOSE and exact nonce validation.
373
+ const verified = await verifyRegisteredProviderProof(proofToken);
374
+ return memberAuth.enrollExternalMember({
375
+ externalSub: verified.sub,
376
+ applicationConfigVersion: registration.config_version,
377
+ idempotencyKey,
378
+ });
379
+ },
380
+ });
381
+ ```
382
+
383
+ The verifier and `registration` are application prerequisites in this example,
384
+ not SDK-provided provider validation or discovery functions. The public endpoint
385
+ is `POST /v1/member-auth/enrollments`; it accepts no caller-selected account
386
+ holder, tenant, issuer namespace, email or permissions. Enrollment alone never
387
+ qualifies fresh action assurance, signing authority or settlement.
388
+
249
389
  ### Account Center and action callbacks
250
390
 
251
391
  Account Center accepts a same-origin `applicationReturnUri` and preserves it
@@ -464,6 +604,40 @@ try {
464
604
  }
465
605
  ```
466
606
 
607
+ ## Treasury vaults and transfer proposals
608
+
609
+ Use `client.treasury` from a trusted backend with the workspace's explicitly
610
+ granted treasury scopes and roles. Discover the chain and eligible assets before
611
+ creating a vault or proposal. An existing same-chain vault retains its real
612
+ owner set and threshold.
613
+
614
+ ```ts
615
+ import { isHeldApproval } from '@xkova/sdk';
616
+
617
+ const vaults = await xkova.treasury.listWallets();
618
+ const proposal = await xkova.treasury.getTransfer('trf_...');
619
+ const counterparties = await xkova.treasury.listCounterparties({ limit: 20 });
620
+ ```
621
+
622
+ Vault and counterparty setup can return a governance hold. Check `isHeldApproval`
623
+ before reading entity fields. Keep the original request and idempotency key;
624
+ resume through normal policy after independently eligible approvers complete the
625
+ required approval. The SDK does not approve, sign or retry a held operation.
626
+
627
+ `draftTransfer` creates a proposal. `getTransfer` reads its frozen signing context
628
+ and current quorum. Each eligible owner produces their own signature over that
629
+ exact Safe transaction. `submitTransferSignature` submits an existing signature;
630
+ it does not create a wallet or sign on anyone's behalf. `executeTransfer` retains
631
+ current role, signer, nonce, quorum and policy checks. A returned relay result is
632
+ not settlement; read the proposal until its actual confirmed or failed outcome.
633
+ After a timeout, reconcile the existing proposal before creating another one.
634
+
635
+ The API key's attributed staff ID is audit metadata, not a per-human session or
636
+ permission to bootstrap a staff wallet. This namespace does not expose Console
637
+ credentials, staff-wallet bootstrap credentials, or raw private-key material.
638
+ The supported workforce authentication and independent signer ceremonies remain
639
+ separate requirements for a complete customer treasury application.
640
+
467
641
  ## Webhooks and generated types
468
642
 
469
643
  ```ts
package/dist/client.d.ts CHANGED
@@ -13,6 +13,7 @@ import { RampsResource } from './resources/ramps.js';
13
13
  import { RwaResource } from './resources/rwa.js';
14
14
  import { TenantResource } from './resources/tenant.js';
15
15
  import { TokenizationResource } from './resources/tokenization.js';
16
+ import { TreasuryResource } from './resources/treasury.js';
16
17
  import { TransactionsResource } from './resources/transactions.js';
17
18
  import { WalletsResource } from './resources/wallets.js';
18
19
  import { WebhooksResource } from './resources/webhooks.js';
@@ -47,6 +48,7 @@ export interface XKOVAMutationOptions extends XKOVARequestControl {
47
48
  export declare class XKOVAClient {
48
49
  private readonly config;
49
50
  private readonly memberContext?;
51
+ private readonly workforceContext?;
50
52
  private readonly apiBaseUrl;
51
53
  private readonly fetchImpl;
52
54
  private constructor();
@@ -65,6 +67,8 @@ export declare class XKOVAClient {
65
67
  get rwa(): RwaResource;
66
68
  get tenant(): TenantResource;
67
69
  get tokenization(): TokenizationResource;
70
+ /** Tenant treasury operations retain server-side roles, governance and signer checks. */
71
+ get treasury(): TreasuryResource;
68
72
  get transactions(): TransactionsResource;
69
73
  get wallets(): WalletsResource;
70
74
  get webhooks(): WebhooksResource;
package/dist/client.js CHANGED
@@ -16,6 +16,7 @@ import { RampsResource } from './resources/ramps.js';
16
16
  import { RwaResource } from './resources/rwa.js';
17
17
  import { TenantResource } from './resources/tenant.js';
18
18
  import { TokenizationResource } from './resources/tokenization.js';
19
+ import { TreasuryResource } from './resources/treasury.js';
19
20
  import { TransactionsResource } from './resources/transactions.js';
20
21
  import { WalletsResource } from './resources/wallets.js';
21
22
  import { WebhooksResource } from './resources/webhooks.js';
@@ -25,11 +26,13 @@ import { WorkspacesResource } from './resources/workspaces.js';
25
26
  export class XKOVAClient {
26
27
  config;
27
28
  memberContext;
29
+ workforceContext;
28
30
  apiBaseUrl;
29
31
  fetchImpl;
30
- constructor(config, memberContext) {
32
+ constructor(config, memberContext, workforceContext) {
31
33
  this.config = config;
32
34
  this.memberContext = memberContext;
35
+ this.workforceContext = workforceContext;
33
36
  if (config.timeoutMs !== undefined &&
34
37
  (!Number.isFinite(config.timeoutMs) || config.timeoutMs <= 0)) {
35
38
  throw new Error('XKOVAClient timeoutMs must be a positive finite number');
@@ -83,6 +86,10 @@ export class XKOVAClient {
83
86
  get tokenization() {
84
87
  return (this.namespaces.tokenization ??= new TokenizationResource(this));
85
88
  }
89
+ /** Tenant treasury operations retain server-side roles, governance and signer checks. */
90
+ get treasury() {
91
+ return (this.namespaces.treasury ??= new TreasuryResource(this));
92
+ }
86
93
  get transactions() {
87
94
  return (this.namespaces.transactions ??= new TransactionsResource(this));
88
95
  }
@@ -108,6 +115,20 @@ export class XKOVAClient {
108
115
  static forMemberSession(options, context) {
109
116
  return new XKOVAClient(options, Object.freeze({ ...context }));
110
117
  }
118
+ /** @internal Created only after the workforce BFF resolves its confidential store. */
119
+ static forWorkforceSession(options, context) {
120
+ if (!/^xkova_wf_[A-Za-z0-9_-]{43}$/.test(context.workforceKey) ||
121
+ !/^xwfs_[A-Za-z0-9_-]{43}$/.test(context.sessionToken) ||
122
+ (context.humanAuthorization !== undefined &&
123
+ !/^[A-Za-z0-9_-]{43}$/.test(context.humanAuthorization))) {
124
+ throw new TypeError('Invalid workforce session material');
125
+ }
126
+ const origin = new URL(context.browserOrigin);
127
+ if (origin.origin !== context.browserOrigin || !isAllowedProtocol(origin)) {
128
+ throw new TypeError('Invalid workforce application origin');
129
+ }
130
+ return new XKOVAClient(options, undefined, Object.freeze({ ...context }));
131
+ }
111
132
  /** @internal Resource transport. Use typed namespaces instead. */
112
133
  async requestJson(options) {
113
134
  const prepared = await this.prepareRequest(options);
@@ -211,6 +232,9 @@ export class XKOVAClient {
211
232
  headers.set(name, value);
212
233
  }
213
234
  if (this.memberContext) {
235
+ if (headers.has('X-Workforce-Session') || headers.has('X-XKOVA-Workforce-Key')) {
236
+ throw new TypeError('Member and workforce authority cannot be combined');
237
+ }
214
238
  const authority = {
215
239
  Authorization: `Bearer ${this.config.apiKey}`,
216
240
  'X-Member-Session-Id': this.memberContext.memberSessionId,
@@ -224,6 +248,40 @@ export class XKOVAClient {
224
248
  headers.set(name, value);
225
249
  }
226
250
  }
251
+ if (this.workforceContext) {
252
+ if (headers.has('X-XKOVA-Human-Authorization') && !this.workforceContext.humanAuthorization) {
253
+ throw new TypeError('A workforce action grant must come from the confidential context');
254
+ }
255
+ for (const name of [
256
+ 'Cookie',
257
+ 'X-Member-Session-Id',
258
+ 'X-XKOVA-Publishable-Key',
259
+ 'X-Account-Holder-Id',
260
+ 'X-End-User-Id',
261
+ 'X-OIDC-ID-Token',
262
+ 'X-Active-Scope',
263
+ 'X-Workspace-Id',
264
+ 'X-Workspace-Type',
265
+ ]) {
266
+ if (headers.has(name))
267
+ throw new TypeError('A workforce operation cannot mix identity or workspace contexts');
268
+ }
269
+ const authority = {
270
+ Authorization: `Bearer ${this.config.apiKey}`,
271
+ 'X-XKOVA-Workforce-Key': this.workforceContext.workforceKey,
272
+ 'X-Workforce-Session': this.workforceContext.sessionToken,
273
+ Origin: this.workforceContext.browserOrigin,
274
+ ...(this.workforceContext.humanAuthorization !== undefined && {
275
+ 'X-XKOVA-Human-Authorization': this.workforceContext.humanAuthorization,
276
+ }),
277
+ };
278
+ for (const [name, value] of Object.entries(authority)) {
279
+ if (headers.has(name) && headers.get(name) !== value) {
280
+ throw new TypeError('A workforce-bound operation cannot replace its session authority');
281
+ }
282
+ headers.set(name, value);
283
+ }
284
+ }
227
285
  return headers;
228
286
  }
229
287
  requestSignal(signal) {