@nexus-cross/kyc
v1.3.6
Published
cross-auth KYC integration (identity + Sumsub status). Framework-agnostic core + React adapter.
Readme
@nexus-cross/kyc
KYC integration for cross-auth. A framework-agnostic core plus a thin React
adapter that wrap the cross-auth /kyc endpoints:
GET /kyc— read-only. Returns the caller's identity (SIWE or social) and the identity-core-api KYC status. No side effects.POST /kyc— links the caller's wallets to the project (idempotent) and, when KYC isn't yet approved, starts/resumes verification (idempotent) and surfaces either a hostedverification_url(preferred) or a Sumsub SDK token. The package enters the flow accordingly — see Launching verification.
The package follows the same hexagonal shape and env-resolution style as
@nexus-cross/onramp: pure core (ports/types/use cases) →
adapters (HTTP repository + env-based endpoints) → react (Provider + hooks),
with a createKyc() facade on top.
Install
pnpm add @nexus-cross/kyc
# React Provider/hooks (the "./react" entry) additionally need:
pnpm add react react-domreact is an optional peer — only the @nexus-cross/kyc/react sub-entry
requires it. The main entry (createKyc, HttpKycRepository, …) is React-free
and works in any framework or vanilla JS.
Environment / base URL
Base URL resolution mirrors @nexus-cross/onramp. You normally don't set
anything — it picks the cross-auth host from the environment identifier:
| Environment | Base URL |
|---|---|
| production (default) | https://cross-auth.crosstoken.io/cross-auth |
| stage | https://stg-cross-auth.crosstoken.io/cross-auth |
| dev | https://dev-cross-auth.crosstoken.io/cross-auth |
Resolution priority:
- Explicit
baseUrloption passed tocreateKyc/HttpKycRepository. VITE_CROSSX_AUTH_BASE_URL(Vite) orNEXT_PUBLIC_CROSSX_AUTH_BASE_URL(Next.js).- Environment identifier
VITE_CROSSX_ENVIRONMENT/NEXT_PUBLIC_CROSSX_ENVIRONMENT/CROSSX_ENVIRONMENT(dev|stage|production) → the table above.
Non-https URLs are rejected (except http://localhost for local dev).
Authentication
The /kyc endpoints require auth. Two ways, used together:
- Bearer token — pass
getAccessToken; the repository sendsAuthorization: Bearer <token>. It's called on every request, so a token refresh is picked up automatically without re-creating the client (don't bake a stale token in). ABearerprefix in the returned value is de-duplicated. - Cookie session — every request is sent with
credentials: 'include', so an HttpOnly cross-auth session cookie is attached automatically. If you rely only on cookies, you can omitgetAccessToken.
Social login extra headers
For social logins the backend additionally requires X-Project-Id plus a
client identifier, otherwise it returns 401:
- Web — the client identifier is the
Originheader, which the browser sends automatically on cross-origin requests (it's a forbidden header, so this package never sets it). Just make sureprojectIdis set. - Native SDK — pass
appId(X-App-Id) andappType(X-App-Type) instead.
projectId is sent as X-Project-Id. Use your embedded project id (falling back
to the cross project id), the same value connect-kit uses for the embedded wallet.
SIWE logins are authenticated by the token/cookie alone and don't need the
client identifier.
Quick start — with connect-kit (recommended)
If you use @nexus-cross/connect-kit-react, KYC is auto-wired. Set
kycEnabled: true in the kit config; connect-kit mounts the provider and injects
the access token from the connected crossx 2.0 SDK session — no token
plumbing, no KycProvider:
// config
createConnectKitConfig({ crossProjectId, kycEnabled: true /* … */ });import { useKyc } from '@nexus-cross/connect-kit-react';
function KycButton() {
const { verified, status, isLoading, isStarting, refresh, startVerification } =
useKyc();
if (verified) return <span>KYC verified ✓</span>;
return (
<>
<button onClick={() => void refresh()} disabled={isLoading}>Check KYC</button>
<button onClick={() => void startVerification()} disabled={isStarting}>
Start KYC
</button>
<p>status: {status ?? '—'}</p>
</>
);
}X-Project-Id uses embeddedProjectId (falling back to crossProjectId). The
token is read per request, so refreshes are picked up automatically.
Quick start — standalone (without connect-kit)
Mount <KycProvider> yourself and supply getAccessToken:
// App.tsx
import { KycProvider } from '@nexus-cross/kyc/react';
export function App() {
return (
<KycProvider
config={{
projectId: EMBEDDED_PROJECT_ID,
getAccessToken: () => accessToken, // read latest token here
}}
>
<KycButton />
</KycProvider>
);
}// KycButton.tsx
import { useKyc } from '@nexus-cross/kyc/react';
function KycButton() {
const { status, verified, isLoading, isStarting, error, refresh, startVerification } =
useKyc(); // autoFetch: true → GET /kyc on mount
if (verified) return <span>KYC verified ✓</span>;
return (
<div>
<button onClick={() => void refresh()} disabled={isLoading}>
{isLoading ? 'Checking…' : 'Check KYC'}
</button>
<button onClick={() => void startVerification()} disabled={isStarting}>
{isStarting ? 'Starting…' : 'Start KYC'}
</button>
<p>status: {status ?? '—'}</p>
{error && <p>error: {error.message}</p>}
</div>
);
}startVerification() runs POST /kyc and then enters the verification flow
automatically (see Launching verification). Use the
lower-level start() if you want the raw KycIdentity back without launching.
Pass useKyc({ autoFetch: false }) to skip the on-mount GET /kyc and trigger
it manually via refresh(). useOptionalKyc() returns the facade or null
outside a Provider (for "enable only if mounted" UIs).
Launching verification
POST /kyc (via start() / startVerification()) returns a KycIdentity. How
the verification flow is entered depends on what the backend put in it, and the
package picks the right path automatically:
| Response field | Behavior | Notes |
|---|---|---|
| verificationUrl (verification_url) | default — opens the hosted URL in a new tab (window.open) | vendor-neutral; no CDN/CSP. This is the recommended backend contract. |
| sdkToken (kyc_token), no URL | fallback — loads the Sumsub WebSDK and launches it in a fullscreen modal | Sumsub CDN coupling is isolated to the browser adapter; token refresh re-calls POST /kyc. |
| neither | throws KycError('LAUNCH_FAILED') | — |
- New tab vs current window: pass
target(URL mode only).'newWindow'(default) opens the URL in a new tab;'currentWindow'navigates the current tab vialocation.assign(rely on the backendredirectto return). The WebSDK fallback always renders as a modal in the current window.await startVerification({ target: 'currentWindow' }); - React:
useKyc().startVerification(opts?)=start()+ launch. Returns{ identity, handle };handle.close()dismisses the WebSDK modal (no-op in URL mode). - Framework-agnostic:
launchKycVerification(identity, opts)from@nexus-cross/kyc. - Completion is authoritative via webhook; poll
GET /kyc(refresh()) for the final status regardless of which path was taken.
Quick start — framework-agnostic
import { createKyc, launchKycVerification } from '@nexus-cross/kyc';
const kyc = createKyc({
projectId: EMBEDDED_PROJECT_ID,
getAccessToken: () => accessToken,
});
const me = await kyc.getStatus(); // GET /kyc
if (!me.verified) {
const started = await kyc.start(); // POST /kyc
// verificationUrl → new tab, else sdkToken → Sumsub WebSDK modal
await launchKycVerification(started);
}API
createKyc(options): Kyc
options (CreateKycOptions):
| Option | Type | Notes |
|---|---|---|
| projectId | string? | Sent as X-Project-Id. Required for social login. |
| getAccessToken | () => string \| null \| undefined \| Promise<…> | Bearer token getter, called per request. |
| appId | string? | Native SDK flow only → X-App-Id. |
| appType | string? | 'android' \| 'ios' \| 'windows' → X-App-Type. |
| baseUrl | string? | Override the resolved cross-auth base URL. |
| repository | KycRepository? | Inject a custom/mock transport. |
Returns { port, getStatus(), start() }.
KycIdentity
Normalized (camelCase) form of cross-auth KYCResp:
| Field | Type | Source |
|---|---|---|
| status | 'none' \| 'pending' \| 'approved' \| 'rejected_retry' \| 'rejected_final' \| 'wallet_required' | status |
| verified | boolean | kyc_verified |
| loginType | 'siwe' \| 'social' \| undefined | login_type |
| walletAddress | string? | wallet_address |
| rejectType | 'RETRY' \| 'FINAL' \| undefined | reject_type (rejected states) |
| provider | string? | provider (e.g. "sumsub") |
| verificationUrl | string? | verification_url (hosted link → opened directly) |
| sdkToken | string? | kyc_token (Sumsub SDK token, WebSDK fallback) |
| sdkTokenExpiresAt | string? | kyc_expires_at (ISO) |
| email / nickname / sub / uuid | string? | social only |
Errors
Failures throw a KycError with a code: MISSING_PROJECT_ID,
UNAUTHORIZED (401/403), STATUS_FAILED, START_FAILED, INVALID_RESPONSE,
NETWORK_ERROR, LAUNCH_FAILED (verification could not be opened — popup
blocked, SSR, or neither verificationUrl nor sdkToken present). In React, the
last error is surfaced via useKyc().error (reads/refresh() capture it;
start()/startVerification() also throw so you can try/catch).
Advanced — custom transport
Implement KycRepository to talk to a different gateway or to mock in tests,
then inject it:
import { createKyc, type KycRepository } from '@nexus-cross/kyc';
const mock: KycRepository = {
fetchStatus: async () => ({ status: 'approved', verified: true }),
initVerification: async () => ({ status: 'approved', verified: true }),
};
const kyc = createKyc({ repository: mock });License
MIT
