@xkova/sdk 0.1.0-alpha.1 → 0.1.0-alpha.3

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
@@ -6,7 +6,7 @@ holders, wallets, payments, tokenization, and other customer resources.
6
6
  ## Install
7
7
 
8
8
  ```bash
9
- npm install @xkova/sdk@0.1.0-alpha.1
9
+ npm install @xkova/sdk@0.1.0-alpha.2
10
10
  ```
11
11
 
12
12
  Requires Node.js 22 or newer. Keep this package and your API keys on the server.
@@ -197,26 +197,17 @@ For your own OIDC provider, use `beginAuthorization` and
197
197
  code unless your integration explicitly assigns that exchange to XKOVA. Both
198
198
  sign-in paths use the same XKOVA session and store contracts.
199
199
 
200
- ### Sandbox demo sign-in
201
-
202
- For a sandbox application explicitly enabled for demo sign-in, call
203
- `completeSandboxDemoAuthorization({ state: start.state, transactionStore })`
204
- after `beginHostedAuthorization`. The SDK obtains the capability and exact
205
- callback from the server-held transaction, validates the returned callback and
206
- expiry, and returns `{ callbackUrl, expiresAt }`. Complete that callback through
207
- `completeHostedAuthorization` with the original browser flow cookie. The API
208
- reserves a demo account for the session. This operation is not
209
- available to ordinary tenant applications or production workspaces, and it
210
- accepts no member, email, wallet, or callback selector.
211
-
212
- Keep the same pending reservation and transaction after an ambiguous admission
213
- failure. Repeating the method uses the same idempotency key and recovers the
214
- same demo account reservation. The method never retries automatically.
215
- `isDefinitiveXKOVASandboxDemoAdmissionFailure(error)` identifies confirmed
216
- rejection before an account was reserved; network, malformed-response, and
217
- unknown server failures can leave the reservation outcome uncertain.
218
- API errors retain their status, correlation ID, and Retry-After. Both admission
219
- and session requests reject HTTP redirects.
200
+ ### Local development
201
+
202
+ The SDK accepts HTTP API URLs, browser origins, callbacks, and storage URLs on
203
+ `localhost` and `127.0.0.1`. Remote hosts require HTTPS. Register each origin
204
+ and callback with its exact scheme, host, port, and path. These two local hosts
205
+ are distinct origins; cookies and browser storage are not shared between them.
206
+
207
+ For local passkeys, serve the authentication page on `localhost` and use
208
+ `localhost` as its relying-party ID. An IP address cannot be a passkey
209
+ relying-party ID. Keep API keys and member sessions in your BFF, including
210
+ during local development.
220
211
 
221
212
  ### Stores and callback recovery
222
213
 
@@ -344,6 +335,10 @@ Business requests use `no-store` and reject redirects so credentials cannot
344
335
  follow a redirect. Network failures preserve the application session. Reuse the
345
336
  original idempotency key when reconciling an uncertain write.
346
337
 
338
+ `resolved.resources.self.profile()` returns the current member's profile,
339
+ including email and name. These fields are display metadata, not authentication
340
+ proof. Read the profile when displaying it instead of caching it in the session.
341
+
347
342
  `resolved.resources.self.verification()` returns the member's verification
348
343
  requirements and sessions through the public self-verification operation.
349
344
  `resolved.resources.self.uploadAvatar(image)` performs presign, object upload,
@@ -351,7 +346,7 @@ confirmation, and refreshed avatar lookup. Pass a nonempty PNG, JPEG, WebP, or G
351
346
  `Blob` of at most 5 MiB. Storage receives only the image bytes and their content
352
347
  headers, never bearer credentials, request signatures, member context, or cookies.
353
348
  The helper rejects redirects, expired capabilities, URL credentials, and non-HTTPS
354
- storage URLs (localhost HTTP is permitted for local development). It does not
349
+ storage URLs (HTTP on `localhost` or `127.0.0.1` is permitted for local development). It does not
355
350
  retry an upload. `XKOVAUploadError` identifies the object-storage stage without
356
351
  exposing the signed URL; API failures retain `XKOVAApiError` metadata.
357
352
 
@@ -371,7 +366,44 @@ Durable application cookies use the host-only `__Host-` prefix, `Secure`, `HttpO
371
366
 
372
367
  The publishable key is a non-secret application registration identifier. It grants no member or tenant authority by itself. The registered application pins tenant, workspace, environment, provider kind, issuer, audience, origins, redirects, and session policy, and must also match the authenticated workspace principal.
373
368
 
374
- ## Cancellation and deadlines
369
+ ### Recover an existing member payment
370
+
371
+ Read recovery state through the resolved member resources:
372
+
373
+ ```ts
374
+ const recovery = await resolved.resources.payments.recovery(paymentId);
375
+ // Call only after the member confirms cancellation in your app.
376
+ if (recovery.can_cancel) {
377
+ await resolved.resources.payments.cancelForMember(paymentId);
378
+ }
379
+ ```
380
+
381
+ The API permits reads by the sender or recipient. Only the sender can cancel
382
+ or continue. The API principal needs `payment.read` and `payment.cancel`
383
+ permissions for these operations. Your BFF must validate the session, exact
384
+ Origin, and CSRF token before cancellation. Cancellation does not require a
385
+ passkey because it sends no value.
386
+
387
+ When `can_continue` is true, use `beginMemberAction()` with
388
+ `resourceType: 'payment'`, the existing `resourceId`, and `action: 'sign'`.
389
+ Complete fresh authorization through the registered provider. Keep the original
390
+ amount, recipient, fees, and deadline. Do not create a second payment or reuse
391
+ an expired signing credential. Recovery uses the payment ID, so a new valid
392
+ session for the same member can recover it without the original form draft.
393
+
394
+ `approval_expired` means the signing window has closed. It remains cancellable
395
+ until a signature has been accepted. After acceptance, `processing` disables
396
+ both actions. `cancelled`, `completed`, and `failed` are separate outcomes;
397
+ retain all three in history. These actions apply to payments; escrow
398
+ cancellation has its own authorization and settlement rules.
399
+
400
+ Availability can change between a read and a click. After a conflict or an
401
+ uncertain cancellation response, read recovery state again. Repeating an
402
+ already completed member cancellation returns the cancelled payment. Aborting
403
+ an HTTP request or cancelling an authorization ceremony does not cancel the
404
+ payment.
405
+
406
+ ## Request cancellation and deadlines
375
407
 
376
408
  Set `timeoutMs` on the client for a default request deadline. Mutation options and workflow polling also accept an `AbortSignal`:
377
409
 
@@ -386,6 +418,33 @@ await xkova.payments.cancel('pay_...', {
386
418
 
387
419
  The SDK does not automatically retry writes. Use `withRetry` with a stable idempotency key when retry behavior is appropriate for your application.
388
420
 
421
+ ## Product enrollment
422
+
423
+ After a member accepts your displayed terms, call
424
+ `resources.self.acceptProductEnrollment(product, { terms_version })` from the
425
+ BFF. Select the product and terms version on your server. Read the saved result
426
+ with `resources.self.productEnrollment(product)` when the member returns.
427
+ Acceptance does not require an `Idempotency-Key` header. Repeating the same
428
+ acceptance preserves its timestamp. Changed terms or credit parameters with the
429
+ same product are rejected.
430
+
431
+ Sandbox applications can include `sandbox_credit: { token_deployment_id,
432
+ amount }` to issue a test-token welcome credit. Use an active dUSDC deployment.
433
+ The credit is issued once per member, workspace, and product, across application
434
+ registrations. The daily workspace budget is 1000 dUSDC. Keep amounts and token
435
+ selection in the BFF, and give each product a stable identifier.
436
+
437
+ For Hosted Auth, set `require_passkey: true` to hold activation and the credit
438
+ until the member has an active passkey and has acknowledged recovery codes.
439
+ `setup_required` means terms are saved and security setup remains incomplete.
440
+ Open Account Center with the `security` intent, then read the enrollment again
441
+ on return. The setup requirement is fixed when the enrollment is accepted.
442
+
443
+ Poll the enrollment after acceptance. `opening` means delivery is pending;
444
+ `active` means the credit has a successful receipt and an indexed balance.
445
+ `requires_review` needs operator investigation. Do not create another product
446
+ identifier to retry an uncertain credit. Credits do not debit bank accounts.
447
+
389
448
  ## Errors
390
449
 
391
450
  `XKOVAApiError.retryAfter` preserves the upstream `Retry-After` header when
@@ -412,7 +471,7 @@ import { verifyWebhookSignature } from '@xkova/sdk/webhook';
412
471
  import type { components, paths } from '@xkova/sdk/types';
413
472
  ```
414
473
 
415
- Pass the raw request body to `verifyWebhookSignature`. The `types` subpath contains only the customer operations included in this SDK release.
474
+ Pass the raw request body to `verifyWebhookSignature`. Import request and response types from `@xkova/sdk/types`.
416
475
 
417
476
  The package also exposes `@xkova/sdk/rwa-verify` for offline RWA transparency proof verification and `@xkova/sdk/abi` for the documented customer contract ABIs.
418
477
 
@@ -432,14 +491,6 @@ idempotency, and settlement. It links the public customer OpenAPI and expanded
432
491
  documentation. Installed declarations determine the methods available in your
433
492
  version; live documentation may describe a newer release.
434
493
 
435
- ## Thirdweb and dependencies
436
-
437
- The only direct runtime dependency is `viem`, used for offline RWA signature
438
- verification. This SDK does not import Thirdweb. The optional `@xkova/browser`
439
- browser companion has no runtime dependencies. XKOVA manages wallet signing,
440
- and API provider metadata may name Thirdweb. You do not need to install its SDK
441
- or obtain Thirdweb credentials.
442
-
443
494
  ## Contributing
444
495
 
445
496
  Run `pnpm check` from the source repository root before committing. The
package/dist/client.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { XKOVAApiError } from './errors.js';
2
+ import { isAllowedProtocol } from './url.js';
2
3
  import { generateIdempotencyKey } from './idempotency.js';
3
4
  import { signXKOVARequest, validateXKOVARequestSigner, xkovaContentDigest, } from './request-signing.js';
4
5
  import { AccountHoldersResource, } from './resources/account-holders.js';
@@ -240,8 +241,8 @@ function normalizeApiBaseUrl(baseUrl) {
240
241
  if (url.username || url.password || url.search || url.hash) {
241
242
  throw new TypeError('XKOVA SDK baseUrl must be an API origin or /v1 base URL');
242
243
  }
243
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && url.hostname === 'localhost')) {
244
- throw new TypeError('XKOVA SDK baseUrl must use HTTPS or localhost HTTP');
244
+ if (!isAllowedProtocol(url)) {
245
+ throw new TypeError('XKOVA SDK baseUrl must use HTTPS or loopback HTTP');
245
246
  }
246
247
  const pathname = url.pathname.replace(/\/+$/, '');
247
248
  if (pathname !== '' && pathname !== '/v1') {
@@ -1115,6 +1115,24 @@ export interface paths {
1115
1115
  patch?: never;
1116
1116
  trace?: never;
1117
1117
  };
1118
+ "/account-holders/self/product-enrollments/{product}": {
1119
+ parameters: {
1120
+ query?: never;
1121
+ header?: never;
1122
+ path?: never;
1123
+ cookie?: never;
1124
+ };
1125
+ /** Read the member's product enrollment */
1126
+ get: operations["getAccountHolderSelfProductEnrollment"];
1127
+ put?: never;
1128
+ /** Record product terms acceptance */
1129
+ post: operations["acceptAccountHolderSelfProductEnrollment"];
1130
+ delete?: never;
1131
+ options?: never;
1132
+ head?: never;
1133
+ patch?: never;
1134
+ trace?: never;
1135
+ };
1118
1136
  "/account-holders/self/wallet-readiness": {
1119
1137
  parameters: {
1120
1138
  query?: never;
@@ -1234,6 +1252,50 @@ export interface paths {
1234
1252
  patch?: never;
1235
1253
  trace?: never;
1236
1254
  };
1255
+ "/account-holders/self/payments/{id}/recovery": {
1256
+ parameters: {
1257
+ query?: never;
1258
+ header: {
1259
+ "X-Member-Session-Id": components["parameters"]["xMemberSessionId"];
1260
+ "X-XKOVA-Publishable-Key": components["parameters"]["xXkovaPublishableKey"];
1261
+ };
1262
+ path: {
1263
+ id: components["parameters"]["pathId"];
1264
+ };
1265
+ cookie?: never;
1266
+ };
1267
+ /** Read a member payment and its available actions */
1268
+ get: operations["getMemberPaymentRecovery"];
1269
+ put?: never;
1270
+ post?: never;
1271
+ delete?: never;
1272
+ options?: never;
1273
+ head?: never;
1274
+ patch?: never;
1275
+ trace?: never;
1276
+ };
1277
+ "/account-holders/self/payments/{id}/cancel": {
1278
+ parameters: {
1279
+ query?: never;
1280
+ header: {
1281
+ "X-Member-Session-Id": components["parameters"]["xMemberSessionId"];
1282
+ "X-XKOVA-Publishable-Key": components["parameters"]["xXkovaPublishableKey"];
1283
+ };
1284
+ path: {
1285
+ id: components["parameters"]["pathId"];
1286
+ };
1287
+ cookie?: never;
1288
+ };
1289
+ get?: never;
1290
+ put?: never;
1291
+ /** Cancel the current member's unsigned payment */
1292
+ post: operations["cancelMemberPayment"];
1293
+ delete?: never;
1294
+ options?: never;
1295
+ head?: never;
1296
+ patch?: never;
1297
+ trace?: never;
1298
+ };
1237
1299
  "/account-holders/self/transactions": {
1238
1300
  parameters: {
1239
1301
  query?: never;
@@ -2711,23 +2773,6 @@ export interface paths {
2711
2773
  patch?: never;
2712
2774
  trace?: never;
2713
2775
  };
2714
- "/member-auth/hosted/authorizations/sandbox-demo": {
2715
- parameters: {
2716
- query?: never;
2717
- header?: never;
2718
- path?: never;
2719
- cookie?: never;
2720
- };
2721
- get?: never;
2722
- put?: never;
2723
- /** Admit a platform-leased sandbox demo persona */
2724
- post: operations["admitMemberAuthSandboxDemo"];
2725
- delete?: never;
2726
- options?: never;
2727
- head?: never;
2728
- patch?: never;
2729
- trace?: never;
2730
- };
2731
2776
  "/member-auth/hosted/authorizations/exchange": {
2732
2777
  parameters: {
2733
2778
  query?: never;
@@ -4313,6 +4358,22 @@ export interface components {
4313
4358
  /** Format: uri */
4314
4359
  avatar_url?: string | null;
4315
4360
  };
4361
+ MemberProductEnrollment: {
4362
+ product: string;
4363
+ /** @enum {string} */
4364
+ status: "not_opened" | "setup_required" | "opening" | "active" | "requires_review";
4365
+ terms_version: string | null;
4366
+ /** Format: date-time */
4367
+ accepted_at: string | null;
4368
+ credit: {
4369
+ amount: string;
4370
+ /** Format: uuid */
4371
+ token_deployment_id: string;
4372
+ /** @enum {string} */
4373
+ status: "pending" | "submitted" | "settled" | "requires_review";
4374
+ transaction_hash: string | null;
4375
+ } | null;
4376
+ };
4316
4377
  CuPayoutAccountRef: {
4317
4378
  routing_number: string;
4318
4379
  account_number: string;
@@ -4328,6 +4389,13 @@ export interface components {
4328
4389
  AccountHolderSelfBalance: {
4329
4390
  balances: components["schemas"]["AccountHolderSelfBalanceEntry"][];
4330
4391
  };
4392
+ MemberPaymentRecovery: {
4393
+ payment: components["schemas"]["Payment"];
4394
+ /** @enum {string} */
4395
+ state: "awaiting_approval" | "approval_expired" | "on_hold" | "processing" | "completed" | "cancelled" | "failed";
4396
+ can_continue: boolean;
4397
+ can_cancel: boolean;
4398
+ };
4331
4399
  AccountHolderSelfTransactionList: {
4332
4400
  data: components["schemas"]["AccountHolderSelfTransaction"][];
4333
4401
  next_cursor: string | null;
@@ -7533,6 +7601,14 @@ export interface operations {
7533
7601
  };
7534
7602
  };
7535
7603
  400: components["responses"]["BadRequest"];
7604
+ 409: {
7605
+ headers: {
7606
+ [name: string]: unknown;
7607
+ };
7608
+ content: {
7609
+ "application/json": components["schemas"]["ErrorResponse"];
7610
+ };
7611
+ };
7536
7612
  422: {
7537
7613
  headers: {
7538
7614
  [name: string]: unknown;
@@ -8063,6 +8139,74 @@ export interface operations {
8063
8139
  403: components["responses"]["Forbidden"];
8064
8140
  };
8065
8141
  };
8142
+ getAccountHolderSelfProductEnrollment: {
8143
+ parameters: {
8144
+ query?: never;
8145
+ header: {
8146
+ "X-Member-Session-Id": components["parameters"]["xMemberSessionId"];
8147
+ "X-XKOVA-Publishable-Key": components["parameters"]["xXkovaPublishableKey"];
8148
+ };
8149
+ path: {
8150
+ product: string;
8151
+ };
8152
+ cookie?: never;
8153
+ };
8154
+ requestBody?: never;
8155
+ responses: {
8156
+ 200: {
8157
+ headers: {
8158
+ [name: string]: unknown;
8159
+ };
8160
+ content: {
8161
+ "application/json": components["schemas"]["MemberProductEnrollment"];
8162
+ };
8163
+ };
8164
+ 401: components["responses"]["Unauthorized"];
8165
+ 403: components["responses"]["Forbidden"];
8166
+ };
8167
+ };
8168
+ acceptAccountHolderSelfProductEnrollment: {
8169
+ parameters: {
8170
+ query?: never;
8171
+ header: {
8172
+ "X-Member-Session-Id": components["parameters"]["xMemberSessionId"];
8173
+ "X-XKOVA-Publishable-Key": components["parameters"]["xXkovaPublishableKey"];
8174
+ };
8175
+ path: {
8176
+ product: string;
8177
+ };
8178
+ cookie?: never;
8179
+ };
8180
+ requestBody: {
8181
+ content: {
8182
+ "application/json": {
8183
+ terms_version: string;
8184
+ /** @default false */
8185
+ require_passkey?: boolean;
8186
+ sandbox_credit?: {
8187
+ /** Format: uuid */
8188
+ token_deployment_id: string;
8189
+ amount: string;
8190
+ };
8191
+ };
8192
+ };
8193
+ };
8194
+ responses: {
8195
+ 200: {
8196
+ headers: {
8197
+ [name: string]: unknown;
8198
+ };
8199
+ content: {
8200
+ "application/json": components["schemas"]["MemberProductEnrollment"];
8201
+ };
8202
+ };
8203
+ 401: components["responses"]["Unauthorized"];
8204
+ 403: components["responses"]["Forbidden"];
8205
+ 409: components["responses"]["Conflict"];
8206
+ 422: components["responses"]["UnprocessableEntity"];
8207
+ 429: components["responses"]["TooManyRequests"];
8208
+ };
8209
+ };
8066
8210
  getAccountHolderSelfWalletReadiness: {
8067
8211
  parameters: {
8068
8212
  query?: never;
@@ -8265,6 +8409,76 @@ export interface operations {
8265
8409
  403: components["responses"]["Forbidden"];
8266
8410
  };
8267
8411
  };
8412
+ getMemberPaymentRecovery: {
8413
+ parameters: {
8414
+ query?: never;
8415
+ header: {
8416
+ "X-Member-Session-Id": components["parameters"]["xMemberSessionId"];
8417
+ "X-XKOVA-Publishable-Key": components["parameters"]["xXkovaPublishableKey"];
8418
+ };
8419
+ path: {
8420
+ id: components["parameters"]["pathId"];
8421
+ };
8422
+ cookie?: never;
8423
+ };
8424
+ requestBody?: never;
8425
+ responses: {
8426
+ 200: {
8427
+ headers: {
8428
+ [name: string]: unknown;
8429
+ };
8430
+ content: {
8431
+ "application/json": components["schemas"]["MemberPaymentRecovery"];
8432
+ };
8433
+ };
8434
+ 401: components["responses"]["Unauthorized"];
8435
+ 403: components["responses"]["Forbidden"];
8436
+ 404: components["responses"]["NotFound"];
8437
+ };
8438
+ };
8439
+ cancelMemberPayment: {
8440
+ parameters: {
8441
+ query?: never;
8442
+ header: {
8443
+ "X-Member-Session-Id": components["parameters"]["xMemberSessionId"];
8444
+ "X-XKOVA-Publishable-Key": components["parameters"]["xXkovaPublishableKey"];
8445
+ };
8446
+ path: {
8447
+ id: components["parameters"]["pathId"];
8448
+ };
8449
+ cookie?: never;
8450
+ };
8451
+ requestBody?: never;
8452
+ responses: {
8453
+ 200: {
8454
+ headers: {
8455
+ [name: string]: unknown;
8456
+ };
8457
+ content: {
8458
+ "application/json": components["schemas"]["MemberPaymentRecovery"];
8459
+ };
8460
+ };
8461
+ 401: components["responses"]["Unauthorized"];
8462
+ 403: components["responses"]["Forbidden"];
8463
+ 404: components["responses"]["NotFound"];
8464
+ 409: {
8465
+ headers: {
8466
+ [name: string]: unknown;
8467
+ };
8468
+ content: {
8469
+ "application/json": components["schemas"]["ErrorResponse"];
8470
+ };
8471
+ };
8472
+ 422: {
8473
+ headers: {
8474
+ [name: string]: unknown;
8475
+ };
8476
+ content: {
8477
+ "application/json": components["schemas"]["ErrorResponse"];
8478
+ };
8479
+ };
8480
+ };
8481
+ };
8268
8482
  listAccountHolderSelfTransactions: {
8269
8483
  parameters: {
8270
8484
  query?: {
@@ -11574,45 +11788,6 @@ export interface operations {
11574
11788
  429: components["responses"]["TooManyRequests"];
11575
11789
  };
11576
11790
  };
11577
- admitMemberAuthSandboxDemo: {
11578
- parameters: {
11579
- query?: never;
11580
- header: {
11581
- "Idempotency-Key": components["parameters"]["idempotencyKey"];
11582
- "X-XKOVA-Publishable-Key": components["parameters"]["xXkovaPublishableKey"];
11583
- };
11584
- path?: never;
11585
- cookie?: never;
11586
- };
11587
- requestBody: {
11588
- content: {
11589
- "application/json": {
11590
- transaction: string;
11591
- };
11592
- };
11593
- };
11594
- responses: {
11595
- 200: {
11596
- headers: {
11597
- [name: string]: unknown;
11598
- };
11599
- content: {
11600
- "application/json": {
11601
- /** Format: uri */
11602
- redirect_url: string;
11603
- /** Format: date-time */
11604
- expires_at: string;
11605
- };
11606
- };
11607
- };
11608
- 401: components["responses"]["Unauthorized"];
11609
- 403: components["responses"]["Forbidden"];
11610
- 409: components["responses"]["Conflict"];
11611
- 422: components["responses"]["UnprocessableEntity"];
11612
- 429: components["responses"]["TooManyRequests"];
11613
- 503: components["responses"]["ServiceUnavailable"];
11614
- };
11615
- };
11616
11791
  completeMemberAuthHostedAuthorization: {
11617
11792
  parameters: {
11618
11793
  query?: never;
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { isAllowedProtocol } from './url.js';
2
3
  export class XKOVAMemberIntentError extends Error {
3
4
  code;
4
5
  status;
@@ -33,8 +34,7 @@ export class XKOVAMemberActionIntents {
33
34
  location.username ||
34
35
  location.password ||
35
36
  location.hash ||
36
- (location.protocol !== 'https:' &&
37
- !(location.protocol === 'http:' && location.hostname === 'localhost')))
37
+ !isAllowedProtocol(location))
38
38
  throw new TypeError('Action review requires the exact application origin');
39
39
  const snapshot = JSON.parse(canonicalJson({
40
40
  request: input.request,
@@ -350,20 +350,6 @@ export declare class XKOVAMemberBffClient {
350
350
  readonly redirectUri: string;
351
351
  readonly transactionStore: XKOVAMemberAuthorizationTransactionStore<TApplicationData>;
352
352
  }): Promise<XKOVAHostedMemberAuthorizationStart>;
353
- /**
354
- * Available only to sandbox applications enabled for demo sign-in. The stored
355
- * Hosted transaction determines the demo account and callback. An uncertain
356
- * response preserves the key so a retry recovers the same reservation.
357
- * Complete the returned callback using
358
- * completeHostedAuthorization and the original browser flow cookie.
359
- */
360
- completeSandboxDemoAuthorization<TApplicationData>(input: {
361
- readonly state: string;
362
- readonly transactionStore: XKOVAMemberAuthorizationTransactionStore<TApplicationData>;
363
- }): Promise<{
364
- readonly callbackUrl: string;
365
- readonly expiresAt: string;
366
- }>;
367
353
  /**
368
354
  * Completes the one-time Hosted Auth result-code exchange and persists the
369
355
  * provider-neutral XKOVA member session. The returned projection is safe for
@@ -492,8 +478,6 @@ export declare class XKOVAMemberBffClient {
492
478
  private readSessionMaterial;
493
479
  private requestSignal;
494
480
  }
495
- /** Only confirmed admission rejections may release a customer's pending demo reservation. */
496
- export declare function isDefinitiveXKOVASandboxDemoAdmissionFailure(error: unknown): boolean;
497
481
  export declare function projectXKOVAMemberSession(session: XKOVAMemberContext): XKOVABrowserMemberProjection;
498
482
  export declare function serializeXKOVAMemberSessionCookie(input: {
499
483
  readonly cookieName: string;
@@ -1,5 +1,6 @@
1
1
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
2
  import { XKOVAApiError } from './errors.js';
3
+ import { isAllowedProtocol } from './url.js';
3
4
  import { XKOVAClient } from './client.js';
4
5
  import { XKOVAMemberResources } from './member-resources.js';
5
6
  import { XKOVAMemberActionIntents, } from './member-action-intents.js';
@@ -195,54 +196,6 @@ export class XKOVAMemberBffClient {
195
196
  throw error;
196
197
  }
197
198
  }
198
- /**
199
- * Available only to sandbox applications enabled for demo sign-in. The stored
200
- * Hosted transaction determines the demo account and callback. An uncertain
201
- * response preserves the key so a retry recovers the same reservation.
202
- * Complete the returned callback using
203
- * completeHostedAuthorization and the original browser flow cookie.
204
- */
205
- async completeSandboxDemoAuthorization(input) {
206
- if (!this.options.apiKey.startsWith('xkv_test_') ||
207
- !this.options.publishableKey.startsWith('xkova_pk_sandbox_')) {
208
- throw new XKOVAMemberAuthorizationTerminalError('provider_rejected', 'Demo admission requires a sandbox application');
209
- }
210
- const stateHash = hashOpaque(input.state);
211
- return input.transactionStore.runExclusive(stateHash, async () => {
212
- const transaction = await input.transactionStore.get(stateHash);
213
- if (!transaction || transaction.expiresAt <= Date.now()) {
214
- throw new XKOVAMemberAuthorizationTerminalError('transaction_unavailable', 'Demo admission requires an active Hosted transaction');
215
- }
216
- assertMemberBffClientFingerprint(transaction.clientFingerprint, this.clientFingerprint);
217
- if (transaction.status !== 'pending' ||
218
- transaction.completionMode !== 'hosted_result_code' ||
219
- !transaction.authorizationUrl) {
220
- throw new XKOVAMemberAuthorizationTerminalError('transaction_unavailable', 'Demo admission requires a completed Hosted start');
221
- }
222
- const authorization = new URL(normalizeHostedAuthorizationUrl(transaction.authorizationUrl));
223
- const capability = authorization.searchParams.get('transaction');
224
- if (!/^\/authorize\/?$/.test(authorization.pathname) ||
225
- !capability ||
226
- !/^[A-Za-z0-9_-]{43}$/.test(capability)) {
227
- throw new XKOVAMemberAuthorizationTerminalError('transaction_unavailable', 'Demo admission capability is invalid');
228
- }
229
- const value = await this.postJson('/member-auth/hosted/authorizations/sandbox-demo', { transaction: capability }, undefined, `sandbox-demo-${stateHash}`);
230
- const body = requireObject(value, 'Sandbox demo admission');
231
- const { expiresAt } = boundedAuthorizationExpiry(body, transaction.expiresAt, 'Sandbox demo admission');
232
- const callback = new URL(requireString(body, 'redirect_url', 'Sandbox demo admission'));
233
- const code = callback.searchParams.get('code');
234
- if (!sameCallback(callback, new URL(transaction.redirectUri)) ||
235
- callback.searchParams.getAll('state').length !== 1 ||
236
- callback.searchParams.get('state') !== input.state ||
237
- callback.searchParams.getAll('code').length !== 1 ||
238
- !code ||
239
- callback.searchParams.has('error')) {
240
- throw new Error('Sandbox demo admission returned the wrong callback binding');
241
- }
242
- assertHostedAuthorizationResultCode(code);
243
- return { callbackUrl: callback.toString(), expiresAt };
244
- });
245
- }
246
199
  /**
247
200
  * Completes the one-time Hosted Auth result-code exchange and persists the
248
201
  * provider-neutral XKOVA member session. The returned projection is safe for
@@ -560,7 +513,7 @@ export class XKOVAMemberBffClient {
560
513
  clientFingerprint: this.clientFingerprint,
561
514
  codeVerifier,
562
515
  redirectUri: returnUri,
563
- exchangeAttemptId: generateXKOVAMemberOperationAttemptId(),
516
+ exchangeAttemptId: randomUUID(),
564
517
  sessionReference: input.sessionReference,
565
518
  intent: 'action',
566
519
  ...(applicationReturnUri !== undefined && { applicationReturnUri }),
@@ -1210,14 +1163,6 @@ export class XKOVAMemberBffClient {
1210
1163
  : AbortSignal.timeout(this.options.timeoutMs);
1211
1164
  }
1212
1165
  }
1213
- /** Only confirmed admission rejections may release a customer's pending demo reservation. */
1214
- export function isDefinitiveXKOVASandboxDemoAdmissionFailure(error) {
1215
- return (error instanceof XKOVAMemberAuthorizationTerminalError ||
1216
- (error instanceof XKOVAApiError &&
1217
- (error.status < 500 ||
1218
- error.code === 'demo_persona_unavailable' ||
1219
- error.code === 'demo_login_disabled')));
1220
- }
1221
1166
  export function projectXKOVAMemberSession(session) {
1222
1167
  return {
1223
1168
  signedIn: true,
@@ -1339,8 +1284,8 @@ function normalizeApiBaseUrl(baseUrl) {
1339
1284
  if (url.username || url.password || url.search || url.hash) {
1340
1285
  throw new TypeError('XKOVA member BFF baseUrl must be an API origin or /v1 base URL');
1341
1286
  }
1342
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && url.hostname === 'localhost')) {
1343
- throw new TypeError('XKOVA member BFF baseUrl must use HTTPS or localhost HTTP');
1287
+ if (!isAllowedProtocol(url)) {
1288
+ throw new TypeError('XKOVA member BFF baseUrl must use HTTPS or loopback HTTP');
1344
1289
  }
1345
1290
  const pathname = url.pathname.replace(/\/+$/, '');
1346
1291
  if (pathname !== '' && pathname !== '/v1') {
@@ -1353,16 +1298,15 @@ function normalizeCallbackUri(value) {
1353
1298
  if (url.username || url.password || url.search || url.hash) {
1354
1299
  throw new TypeError('member authorization redirectUri must not contain credentials, query, or fragment');
1355
1300
  }
1356
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && url.hostname === 'localhost')) {
1357
- throw new TypeError('member authorization redirectUri must use HTTPS or localhost HTTP');
1301
+ if (!isAllowedProtocol(url)) {
1302
+ throw new TypeError('member authorization redirectUri must use HTTPS or loopback HTTP');
1358
1303
  }
1359
1304
  return url.toString();
1360
1305
  }
1361
1306
  function normalizeBrowserOrigin(value) {
1362
1307
  const url = new URL(value);
1363
- if (url.origin !== value ||
1364
- (url.protocol !== 'https:' && !(url.protocol === 'http:' && url.hostname === 'localhost'))) {
1365
- throw new TypeError('member authorization browserOrigin must be an exact HTTPS or localhost origin');
1308
+ if (url.origin !== value || !isAllowedProtocol(url)) {
1309
+ throw new TypeError('member authorization browserOrigin must be an exact HTTPS or loopback origin');
1366
1310
  }
1367
1311
  return value;
1368
1312
  }
@@ -1372,18 +1316,14 @@ function normalizeApplicationReturnUri(value, browserOrigin) {
1372
1316
  url.username ||
1373
1317
  url.password ||
1374
1318
  url.hash ||
1375
- (url.protocol !== 'https:' && !(url.protocol === 'http:' && url.hostname === 'localhost'))) {
1319
+ !isAllowedProtocol(url)) {
1376
1320
  throw new TypeError('member action applicationReturnUri must use the exact browser origin');
1377
1321
  }
1378
1322
  return url.toString();
1379
1323
  }
1380
1324
  function normalizeHostedAuthorizationUrl(value) {
1381
1325
  const url = new URL(value);
1382
- if (url.username ||
1383
- url.password ||
1384
- url.hash ||
1385
- (url.protocol !== 'https:' &&
1386
- !(url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1')))) {
1326
+ if (url.username || url.password || url.hash || !isAllowedProtocol(url)) {
1387
1327
  throw new Error('XKOVA member Hosted authorization URL must use HTTPS or loopback HTTP without credentials or a fragment');
1388
1328
  }
1389
1329
  return url.toString();
@@ -18,6 +18,56 @@ export declare class XKOVAMemberResources {
18
18
  get paymentRequests(): import("./resources/index.js").PaymentRequestsResource;
19
19
  get wallets(): import("./resources/index.js").WalletsResource;
20
20
  readonly self: {
21
+ profile: () => Promise<{
22
+ id: import("./generated/types.js").components["schemas"]["AccountHolderId"];
23
+ tenant_id?: string;
24
+ state: "active" | "held" | "closed" | "closed_uncooperative" | "provisioning_failed";
25
+ state_reason?: string | null;
26
+ state_changed_at?: import("./generated/types.js").components["schemas"]["TimestampUtc"] | null;
27
+ tier: "tier_1" | "tier_2";
28
+ tier_2_uplift_status?: "not_requested" | "pending" | "passed" | "failed";
29
+ first_use_completed: boolean;
30
+ cip_payload?: {
31
+ name_hash?: string;
32
+ dob_hash?: string;
33
+ id_hash?: string;
34
+ address_hash?: string;
35
+ } | null;
36
+ cip_accepted_at?: import("./generated/types.js").components["schemas"]["TimestampUtc"] | null;
37
+ merchant_role_state?: "none" | "pending_review" | "approved" | "rejected" | "revoked";
38
+ auto_save_contacts?: boolean;
39
+ created_at: import("./generated/types.js").components["schemas"]["TimestampUtc"];
40
+ updated_at?: import("./generated/types.js").components["schemas"]["TimestampUtc"];
41
+ email?: string | null;
42
+ first_name?: string | null;
43
+ last_name?: string | null;
44
+ phone?: string | null;
45
+ avatar_url?: string | null;
46
+ }>;
47
+ productEnrollment: (product: string, control?: XKOVARequestControl) => Promise<{
48
+ product: string;
49
+ status: "not_opened" | "setup_required" | "opening" | "active" | "requires_review";
50
+ terms_version: string | null;
51
+ accepted_at: string | null;
52
+ credit: {
53
+ amount: string;
54
+ token_deployment_id: string;
55
+ status: "pending" | "submitted" | "settled" | "requires_review";
56
+ transaction_hash: string | null;
57
+ } | null;
58
+ }>;
59
+ acceptProductEnrollment: (product: string, body: ReqBody<"acceptAccountHolderSelfProductEnrollment">, opts?: XKOVAMutationOptions) => Promise<{
60
+ product: string;
61
+ status: "not_opened" | "setup_required" | "opening" | "active" | "requires_review";
62
+ terms_version: string | null;
63
+ accepted_at: string | null;
64
+ credit: {
65
+ amount: string;
66
+ token_deployment_id: string;
67
+ status: "pending" | "submitted" | "settled" | "requires_review";
68
+ transaction_hash: string | null;
69
+ } | null;
70
+ }>;
21
71
  actionReadiness: (control?: XKOVARequestControl) => Promise<{
22
72
  authorization_method: "hosted_passkey" | "external_oidc";
23
73
  setup_status: "passkey_required" | "recovery_acknowledgement_required" | "recovery_required" | "ready" | "externally_managed";
@@ -31,6 +31,9 @@ export class XKOVAMemberResources {
31
31
  return this.client.wallets;
32
32
  }
33
33
  self = {
34
+ profile: () => this.client.accountHolders.retrieve(this.accountHolderId),
35
+ productEnrollment: (product, control) => this.client.accountHolders.selfProductEnrollment(this.context, product, control),
36
+ acceptProductEnrollment: (product, body, opts) => this.client.accountHolders.acceptSelfProductEnrollment(this.context, product, body, opts),
34
37
  actionReadiness: (control) => this.client.accountHolders.selfActionReadiness({ ...this.context, accountHolderId: this.accountHolderId }, control),
35
38
  sandboxBankingContext: (control) => this.client.accountHolders.selfSandboxBankingContext(this.context, control),
36
39
  verification: (control) => this.client.accountHolders.selfVerification(this.context, control),
@@ -118,6 +118,30 @@ export declare class AccountHoldersResource extends Resource {
118
118
  }[];
119
119
  verification_url: string | null;
120
120
  }>;
121
+ selfProductEnrollment(context: XKOVAMemberSessionContext, product: string, control?: XKOVARequestControl): Promise<{
122
+ product: string;
123
+ status: "not_opened" | "setup_required" | "opening" | "active" | "requires_review";
124
+ terms_version: string | null;
125
+ accepted_at: string | null;
126
+ credit: {
127
+ amount: string;
128
+ token_deployment_id: string;
129
+ status: "pending" | "submitted" | "settled" | "requires_review";
130
+ transaction_hash: string | null;
131
+ } | null;
132
+ }>;
133
+ acceptSelfProductEnrollment(context: XKOVAMemberSessionContext, product: string, body: ReqBody<'acceptAccountHolderSelfProductEnrollment'>, opts?: XKOVAMutationOptions): Promise<{
134
+ product: string;
135
+ status: "not_opened" | "setup_required" | "opening" | "active" | "requires_review";
136
+ terms_version: string | null;
137
+ accepted_at: string | null;
138
+ credit: {
139
+ amount: string;
140
+ token_deployment_id: string;
141
+ status: "pending" | "submitted" | "settled" | "requires_review";
142
+ transaction_hash: string | null;
143
+ } | null;
144
+ }>;
121
145
  selfBalance(context: XKOVAMemberSessionContext, control?: XKOVARequestControl): Promise<{
122
146
  balances: import("../generated/types.js").components["schemas"]["AccountHolderSelfBalanceEntry"][];
123
147
  }>;
@@ -1,5 +1,6 @@
1
1
  import { Resource } from './base.js';
2
2
  import { XKOVAUploadError } from '../errors.js';
3
+ import { isAllowedProtocol } from '../url.js';
3
4
  function memberSessionHeaders(context) {
4
5
  return {
5
6
  'X-Member-Session-Id': context.memberSessionId,
@@ -30,6 +31,12 @@ export class AccountHoldersResource extends Resource {
30
31
  selfVerification(context, control) {
31
32
  return this.getJson('/account-holders/self/verification', undefined, control, memberSessionHeaders(context));
32
33
  }
34
+ selfProductEnrollment(context, product, control) {
35
+ return this.getJson(`/account-holders/self/product-enrollments/${this.enc(product)}`, undefined, control, memberSessionHeaders(context));
36
+ }
37
+ acceptSelfProductEnrollment(context, product, body, opts) {
38
+ return this.postJson(`/account-holders/self/product-enrollments/${this.enc(product)}`, body, opts, undefined, memberSessionHeaders(context));
39
+ }
33
40
  selfBalance(context, control) {
34
41
  return this.getJson('/account-holders/self/balance', undefined, control, memberSessionHeaders(context));
35
42
  }
@@ -115,8 +122,7 @@ export class AccountHoldersResource extends Resource {
115
122
  if (target.username ||
116
123
  target.password ||
117
124
  target.hash ||
118
- (target.protocol !== 'https:' &&
119
- !(target.protocol === 'http:' && target.hostname === 'localhost')) ||
125
+ !isAllowedProtocol(target) ||
120
126
  !Number.isFinite(Date.parse(upload.expires_at)) ||
121
127
  Date.parse(upload.expires_at) <= Date.now()) {
122
128
  throw new XKOVAUploadError('The avatar upload capability is invalid or expired');
@@ -178,5 +178,18 @@ export declare class PaymentsResource extends Resource {
178
178
  created_at: import("../generated/types.js").components["schemas"]["TimestampUtc"];
179
179
  finalized_at: import("../generated/types.js").components["schemas"]["TimestampUtc"] | null;
180
180
  }>;
181
+ /** Requires a resolved member session. Only the sender can continue or cancel. */
182
+ recovery(id: string): Promise<{
183
+ payment: import("../generated/types.js").components["schemas"]["Payment"];
184
+ state: "awaiting_approval" | "approval_expired" | "on_hold" | "processing" | "completed" | "cancelled" | "failed";
185
+ can_continue: boolean;
186
+ can_cancel: boolean;
187
+ }>;
188
+ cancelForMember(id: string, opts?: XKOVAMutationOptions): Promise<{
189
+ payment: import("../generated/types.js").components["schemas"]["Payment"];
190
+ state: "awaiting_approval" | "approval_expired" | "on_hold" | "processing" | "completed" | "cancelled" | "failed";
191
+ can_continue: boolean;
192
+ can_cancel: boolean;
193
+ }>;
181
194
  }
182
195
  export {};
@@ -30,4 +30,11 @@ export class PaymentsResource extends Resource {
30
30
  cancel(id, opts) {
31
31
  return this.postJson(`/payments/${this.enc(id)}/cancel`, undefined, opts);
32
32
  }
33
+ /** Requires a resolved member session. Only the sender can continue or cancel. */
34
+ recovery(id) {
35
+ return this.getJson(`/account-holders/self/payments/${this.enc(id)}/recovery`);
36
+ }
37
+ cancelForMember(id, opts) {
38
+ return this.postJson(`/account-holders/self/payments/${this.enc(id)}/cancel`, undefined, opts);
39
+ }
33
40
  }
package/dist/url.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function isAllowedProtocol(url: URL): boolean;
package/dist/url.js ADDED
@@ -0,0 +1,4 @@
1
+ export function isAllowedProtocol(url) {
2
+ return (url.protocol === 'https:' ||
3
+ (url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1')));
4
+ }
package/llms.txt CHANGED
@@ -22,7 +22,6 @@ Use this package on a Node.js 22+ server. Read the installed declarations before
22
22
  1. Server business integration: create XKOVAClient.forTenant({ baseUrl, apiKey }). Use https://sandbox-api.xkova.com for sandbox and https://api.xkova.com for production. The API key binds the workspace and mode.
23
23
  2. Member application: use XKOVAMemberBffClient from @xkova/sdk/member-bff in your BFF. Resolve confidential sessions to request-scoped resources with resolveResources(). The browser sends only its application cookie and CSRF proof to your BFF.
24
24
  3. Browser companion: optionally install @xkova/browser. It calls your same-origin routes; it does not create those routes or install a session store.
25
- 4. Institution staff and management: use the Console and its documented setup flows. Do not invent customer SDK methods for API-key lifecycle, treasury administration, staff governance, or platform management.
26
25
 
27
26
  ## Find a capability
28
27
 
@@ -58,7 +57,7 @@ API availability does not grant permission. Product entitlement, selected API-ke
58
57
  ## Requests and results
59
58
 
60
59
  - Start with the README's sandbox read example. Validate required environment variables on the server; never emit their values into logs, generated files, browser bundles, or prompts.
61
- - Use exported package entry points and typed resource methods. Do not import dist files as runtime subpaths, use private endpoints, fabricate request fields, or suppress type errors with any.
60
+ - Use exported package entry points and typed resource methods. Do not import dist files as runtime subpaths, fabricate request fields, or suppress type errors with any.
62
61
  - Production writes require the bearer key plus an active asymmetric requestSigner credential. Its async callback signs through your key provider. The SDK supplies RFC 9421 signatures and RFC 9530 digests. Production reads use bearer authentication.
63
62
  - Generate one idempotency key before a logical mutation and persist it. Reuse it only with the identical request. The SDK does not automatically retry writes. A timeout or disconnect can leave the outcome unknown; reconcile the existing resource before starting another operation.
64
63
  - Use decimal strings for monetary values according to the request type. Preserve asset and chain identity throughout a flow. Do not perform monetary calculations with JavaScript floating-point numbers.
@@ -75,10 +74,26 @@ API availability does not grant permission. Product entitlement, selected API-ke
75
74
  - Reuse one createMemberActionReference() from @xkova/browser for a durable review. Save the draft before setup detours. Expired or missing reviews require checking activity and an explicit new review.
76
75
  - Setup readiness is advisory. beginMemberAction() and completeMemberAction() obtain fresh approval for the exact resource. Browser code must not receive provider wallet credentials or internal signing capabilities. Completion of authorization is distinct from execution and settlement.
77
76
 
78
- ## Dependencies
77
+ ## Payment recovery
79
78
 
80
- The server SDK uses viem for offline RWA signature verification. The browser companion has no runtime dependencies. Neither imports Thirdweb. You may see Thirdweb in API provider metadata, but these integrations require no Thirdweb SDK or credentials.
79
+ Use resolved.resources.payments.recovery(paymentId) to read the current
80
+ member's payment and available actions. Only the sender can continue or cancel.
81
+ On explicit cancellation, call cancelForMember(paymentId) after BFF session,
82
+ Origin and CSRF checks. The principal needs payment.read and payment.cancel.
83
+ Cancellation sends no value and does not require a passkey.
84
+
85
+ Continue only when can_continue is true. Begin a fresh member action for the
86
+ same payment resourceId with action sign, then complete the registered proof
87
+ ceremony. Never create another payment, change its fees or deadline, or reuse
88
+ an expired signing credential. A new session for the same member can recover
89
+ the payment without its original draft. Expired approvals remain cancellable;
90
+ accepted signatures close cancellation. Keep cancelled payments in history.
91
+ After a conflict or uncertain cancellation, re-read state before another action.
81
92
 
82
93
  ## Before handing off code
83
94
 
84
95
  List the Console setup and stores the application needs. Test denied access, uncertain writes, pagination, cancellation, and terminal outcomes. Report a missing method or setup requirement explicitly. Do not invent methods, suppress type errors, or bypass authorization to make an example run.
96
+
97
+ ## Product opt-in
98
+
99
+ Use resolveResources().self.productEnrollment(product) to read acceptance and credit status. After explicit consent, call self.acceptProductEnrollment(product, { terms_version, sandbox_credit? }). The BFF chooses the product, terms, amount, and token. Reads do not enroll members. Acceptance needs no Idempotency-Key header; reuse the same product and parameters when retrying. Sandbox credits are dUSDC only and issued once per workspace/member/product; the workspace daily budget is 1000 dUSDC. Poll opening until active. Treat requires_review as unresolved delivery, never as permission to request a second product credit.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xkova/sdk",
3
- "version": "0.1.0-alpha.1",
4
- "description": "Server-side TypeScript client for the XKOVA customer API, with customer-only generated types and production request signing.",
3
+ "version": "0.1.0-alpha.3",
4
+ "description": "TypeScript SDK for the XKOVA API.",
5
5
  "homepage": "https://docs.xkova.com",
6
6
  "repository": {
7
7
  "type": "git",