Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 42 additions & 24 deletions packages/kyc-controller/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ This document explains:

The package is built around a few deliberate constraints:

| Principle | How it shows up in the code |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Vendor-neutral surface** | Consumers deal with `KycProduct` (`'ramps' \| 'card'`) and a phase machine, never with MoonPay/SumSub specifics. `KycVendor` is internal. |
| **Platform-agnostic core** | No React, no `Buffer`/`atob`, no native SDK imports. Crypto uses `@noble/*` + `@scure/base`. WebView/iframe presentation and the SumSub SDK are **injected** by each client. |
| **Controller owns orchestration; clients own presentation** | `KycController` owns all state, HTTP orchestration, crypto and the frame protocol. Clients only render frames, forward raw messages, and present the SumSub SDK. |
| **Stateless service** | `KycService` performs HTTP only; it holds no state and derives auth/geolocation from other controllers via the messenger. |
| **Everything through the messenger** | Both classes register their public methods as messenger actions, and reach external capabilities (auth token, geolocation) via delegated actions. |
| Principle | How it shows up in the code |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Vendor-neutral surface** | Consumers deal with `KycProduct` (`'ramps' \| 'card' \| 'money'`) and a phase machine. Identity vendor is a parameterized `KycVendor` (`initialize({ vendor })`), not vendor-branded public methods. |
| **Platform-agnostic core** | No React, no `Buffer`/`atob`, no native SDK imports. Crypto uses `@noble/*` + `@scure/base`. WebView/iframe presentation and the SumSub SDK are **injected** by each client. |
| **Controller owns orchestration; clients own presentation** | `KycController` owns all state, HTTP orchestration, crypto and the frame protocol. Clients only render frames, forward raw messages, and present the SumSub SDK. |
| **Stateless service** | `KycService` performs HTTP only; it holds no state and derives auth/geolocation from other controllers via the messenger. |
| **Everything through the messenger** | Both classes register their public methods as messenger actions, and reach external capabilities (auth token, geolocation) via delegated actions. |

---

Expand Down Expand Up @@ -100,8 +100,9 @@ graph TB
Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`):

`initialize`, `loadDisclaimers`, `acceptTermsAndStartSession`,
`clearSavedTerms`, `handleFrameMessage`, `buildCheckFrameUrl`,
`buildAuthFrameUrl`, `buildResetFrameUrl`, `checkKycRequired`, `getKycStatus`,
`createVendorCustomer`, `clearSavedTerms`, `handleFrameMessage`,
`buildCheckFrameUrl`, `buildAuthFrameUrl`, `buildResetFrameUrl`,
`checkKycRequired`, `getKycStatus`, `getCustomerIdentity`, `refreshKycStatus`,
`startSumSub`, `reset`.

#### 2.2 `KycService`
Expand All @@ -121,18 +122,22 @@ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`):
Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`):

`getGeoCountry`, `fetchDisclaimers`, `createSession`, `checkKycRequired`,
`createUkycSession`, `createJourney`.
`createVendorCustomer`, `submitConsents`, `fetchKycStatus`, `createUkycSession`,
`createJourney`.

Endpoints:

| Method | HTTP | Endpoint | Purpose |
| ------------------- | ------ | --------------------------------------- | ----------------------------------------------------------------------- |
| `getGeoCountry` | — | (geolocation action) | Resolve alpha-3 country |
| `fetchDisclaimers` | `GET` | `/vendors/moonpay/disclaimers?country=` | Terms to accept |
| `createSession` | `POST` | `/vendors/moonpay/sessions` | Create vendor session |
| `checkKycRequired` | `POST` | `/vendors/moonpay/kyc-required` | Is KYC required? (normalizes `required` → `kycRequired`) |
| `createUkycSession` | `POST` | `/sessions` | Start SumSub sub-flow (wrapped key + read-only `ukyc_capability_token`) |
| `createJourney` | `POST` | `/sessions/{id}/journey` | Create verification journey → applicant token |
| Method | HTTP | Endpoint | Purpose |
| ---------------------- | ------ | ---------------------------------------- | ----------------------------------------------------------------------- |
| `getGeoCountry` | — | (geolocation action) | Resolve alpha-3 country |
| `fetchDisclaimers` | `GET` | `/vendors/{vendor}/disclaimers?country=` | Terms to accept (`vendor` defaults to `moonpay`) |
| `createSession` | `POST` | `/vendors/moonpay/sessions` | Create MoonPay vendor session |
| `checkKycRequired` | `POST` | `/vendors/{vendor}/kyc-required` | Is KYC required? (normalizes `required` → `kycRequired`) |
| `createVendorCustomer` | `POST` | `/vendors/{vendor}/customers` | Create or resume an empty-shell vendor customer |
| `submitConsents` | `POST` | `/consents` | Post T&C1 + T&C2 consents (204 No Content) |
| `fetchKycStatus` | `GET` | `/kyc/status` | User-keyed simplified KYC status |
| `createUkycSession` | `POST` | `/sessions` | Start SumSub sub-flow (wrapped key + read-only `ukyc_capability_token`) |
| `createJourney` | `POST` | `/sessions/{id}/journey` | Create verification journey → applicant token |

### 2.3 `crypto.ts`

Expand Down Expand Up @@ -167,6 +172,7 @@ classDiagram
+string email
+string termsAcceptedAt [persisted]
+string[] acceptedDisclaimerIds [persisted]
+KycVendor termsAcceptedVendor [persisted]
+KycDisclaimer[] disclaimers
+string disclaimersError
+string geoCountry
Expand Down Expand Up @@ -194,8 +200,11 @@ classDiagram
State metadata highlights (`kycControllerMetadata`):

- **Persisted** (`persist: true`): `termsAcceptedAt`, `acceptedDisclaimerIds`,
`kycRequiredByProduct`, `lastCheckedAt`. These survive restarts so the flow
can skip already-accepted terms and reuse cached results.
`termsAcceptedVendor`, `kycRequiredByProduct`, `lastCheckedAt`. These survive
restarts so the flow can skip already-accepted terms and reuse cached results.
Acceptance is vendor-scoped: `initialize` (and `createVendorCustomer`) drops
the stored acceptance when it belongs to a different vendor, so one vendor's
disclaimer ids are never submitted to another.
- **Secrets, never persisted / never logged**: `sessionToken`, `accessToken`,
`moonpayCustomerId`, `email`, `disclaimers`, and the whole `sumsub` sub-tree.
- Additional non-state secrets kept **off** the state object entirely: the
Expand All @@ -215,7 +224,7 @@ stateDiagram-v2
idle --> terms : initialize() (no saved terms)
idle --> session : initialize() (saved terms + email)

terms --> session : acceptTermsAndStartSession()
terms --> session : acceptTermsAndStartSession({ sumsubTncSigned, idosTncSigned })
session --> check : createSession() ok
session --> terms : createSession() fails<br/>(clears saved terms, activeProduct + stale tokens)

Expand Down Expand Up @@ -244,6 +253,14 @@ stateDiagram-v2
> sub-flow (see [§7](#7-sumsub-sub-flow)). When no product is set the flow stops
> at `form` and the consumer drives `checkKycRequired` / `startSumSub` manually.

> **Non-MoonPay vendors use a consents path.** `initialize({ vendor: 'iron' })`
> creates an empty-shell customer, loads vendor disclaimers, and — after terms
> are accepted — posts consents and launches SumSub. MoonPay Check/Auth frames
> are skipped; `phase` moves `terms → session → submit → done`.
> `acceptTermsAndStartSession` requires `sumsubTncSigned` and `idosTncSigned`
> (T&C2) for every vendor; omitted flags fail the flow instead of defaulting to
> `true`.

> **`initialize` never tears down an active flow.** If `phase` is already one of
> the in-progress phases (`session`, `check`, `auth`, `form`, `submit`), a
> repeat `initialize` is a **no-op** — it will not create a new session, clear
Expand Down Expand Up @@ -299,7 +316,7 @@ sequenceDiagram
Svc->>API: GET /disclaimers
Ctrl-->>UI: phase = terms (+ disclaimers)

User->>Ctrl: acceptTermsAndStartSession({ email })
User->>Ctrl: acceptTermsAndStartSession({ email, sumsubTncSigned, idosTncSigned })
Ctrl->>Svc: createSession({ email, termsAcceptedAt, disclaimerIds })
Svc->>API: POST /sessions
Ctrl-->>UI: phase = check (+ sessionToken)
Expand Down Expand Up @@ -549,8 +566,9 @@ graph TB

- **`kyc-controller-init.ts`** constructs `KycController` with the persisted
state slice and injects `reactNativeSumSubLauncher`.
- **`kyc-service-init.ts`** constructs `KycService` with the global `fetch`, an
`env` derived from `isProduction()`, and (currently) a dev `baseUrl` override.
- **`kyc-service-init.ts`** constructs `KycService` with an `env` derived from
`isProduction()` and (currently) a dev `baseUrl` override. It does not inject
a `fetch`; `KycService` defaults to the runtime's native `fetch`.
- **`kyc-controller-messenger.ts`** delegates the six `KycService:*` actions to
the controller's messenger.
- **`kyc-service-messenger.ts`** delegates
Expand Down
Loading
Loading