# @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 hosted `verification_url` (preferred) or a Sumsub SDK token.
  The package enters the flow accordingly — see [Launching verification](#launching-verification).

The package follows the same hexagonal shape and env-resolution style as
[`@nexus-cross/onramp`](../onramp): pure `core` (ports/types/use cases) →
`adapters` (HTTP repository + env-based endpoints) → `react` (Provider + hooks),
with a `createKyc()` facade on top.

## Install

```sh
pnpm add @nexus-cross/kyc
# React Provider/hooks (the "./react" entry) additionally need:
pnpm add react react-dom
```

`react` 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:

1. Explicit `baseUrl` option passed to `createKyc` / `HttpKycRepository`.
2. `VITE_CROSSX_AUTH_BASE_URL` (Vite) or `NEXT_PUBLIC_CROSSX_AUTH_BASE_URL` (Next.js).
3. 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 sends
  `Authorization: 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). A `Bearer ` prefix 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 omit `getAccessToken`.

### 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 `Origin` header, which the browser sends
  automatically on cross-origin requests (it's a forbidden header, so this
  package never sets it). Just make sure `projectId` is set.
- **Native SDK** — pass `appId` (`X-App-Id`) and `appType` (`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`**:

```ts
// config
createConnectKitConfig({ crossProjectId, kycEnabled: true /* … */ });
```

```tsx
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`:

```tsx
// 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>
  );
}
```

```tsx
// 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](#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](../../docs/kyc/01-verification-link-proposal.md). |
| `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 via `location.assign` (rely on the backend
  `redirect` to return). The WebSDK fallback always renders as a modal in the
  current window.
  ```ts
  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

```ts
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:

```ts
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
