Skip to content
Open
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
121 changes: 60 additions & 61 deletions packages/kyc-controller/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ graph TB
Service["KycService<br/><i>(stateless)</i><br/>HTTP + response validation"]
Crypto["crypto.ts<br/>X25519 ECDH + AES-256-GCM"]
Selectors["selectors.ts<br/>memoized reselect selectors"]
Types["types.ts<br/>KycPhase, KycProduct,<br/>KycSumSubLauncher, ..."]
Types["types.ts<br/>KycVendor, KycSessionStatus,<br/>KycSumSubLauncher, ..."]
Country["countryCodes.ts<br/>alpha-2 → alpha-3"]
end

Expand Down Expand Up @@ -162,69 +162,68 @@ and hex-or-base64 binary fields.

#### 2.4 `selectors.ts`

Memoized `reselect` selectors over `KycControllerState`:
`selectKycPhase`, `selectKycSumSub`, and the parametric
`selectIsKycRequiredForProduct(product)`.
Selectors over `KycControllerState` that take the vendor:
`selectKycSession(state, vendor)` and `selectKycSessionStatus(state, vendor)`.
`sessionDecision.ts` exports the pure `getKycSessionDecision(entry)`, which
`KycController.getSessionDecision({ vendor })` wraps.

---

### 3. State shape

The controller holds one session per identity vendor, matching kyc-api, which
keys sessions by canonical user and vendor. There is no active vendor: every
session method takes `{ vendor }`, and features that use the same vendor share
its entry.

```mermaid
classDiagram
class KycControllerState {
+KycPhase phase
+string statusMessage
+string error
+string email
+KycVendorDisclaimersAccepted vendorDisclaimersAccepted [persisted]
+KycDisclaimer[] vendorDisclaimers
+string vendorError
+string geoCountry
+string moonpaySessionToken [secret]
+string moonpayAccessToken [secret]
+string moonpayCustomerId
+KycProduct activeProduct
+Record kycRequiredByProduct [persisted]
+string lastCheckedAt [persisted]
+SumSubState sumsub
+Partial~Record~KycVendor, KycVendorSessionState~~ sessions
}
class KycVendorSessionState {
+KycSessionStatus sessionStatus
+KycProviderFlowStatus providerFlowStatus
+string residenceCountry
+KycVendorDisclaimersAcceptance vendorDisclaimersAccepted
+KycConsentRecord[] providerDisclaimersAccepted
+KycConsentRecord[] idosDisclaimersAccepted
+boolean credentialReusabilityConsentGiven
}
class SumSubState {
+KycSumSubStatus status
+Json result
+string sessionId
+string applicantAccessToken
class KycVendorDisclaimersAcceptance {
+string[] disclaimerIds
+string acceptedAt
+boolean submitted
}
KycControllerState --> SumSubState : sumsub
KycControllerState --> KycVendorSessionState : sessions[vendor]
KycVendorSessionState --> KycVendorDisclaimersAcceptance
```

> Note: nullable fields (`error`, `email`, `moonpaySessionToken`, …) are typed as
> `T | null` in the source; `Record` is `Partial<Record<KycProduct, boolean>>`.
> Types are simplified above for diagram readability.

State metadata highlights (`kycControllerMetadata`):

- **Persisted** (`persist: true`): `vendorDisclaimersAccepted`,
`providerDisclaimersAccepted`, `idosDisclaimersAccepted`,
`kycRequiredByProduct`, `lastCheckedAt`. These survive restarts so the flow
can skip already-accepted terms and reuse cached results. Session-scoped
`sessionDisclaimers` and `credentialReusabilityConsentGiven` are in-memory
only (`persist: false`) and are cleared on `reset()`.
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. The drop waits until the
vendor switch commits (`createVendorCustomer` succeeds, or the MoonPay
path proceeds); a failed or reset switch leaves the previous vendor's
acceptance in place.
- **Secrets, never persisted / never logged**: `moonpaySessionToken`, `moonpayAccessToken`,
`moonpayCustomerId`, `email`, `vendorDisclaimers`, and the whole `sumsub` sub-tree.
Switching away from MoonPay (`initialize` / `createVendorCustomer`) drops
these MoonPay Check/Auth artifacts immediately so `buildCheckFrameUrl` cannot
return a MoonPay URL while `activeVendor` is a consents-path vendor.
- Additional non-state secrets kept **off** the state object entirely: the
X25519 private key (`#keypair`) and the Auth-frame client token
(`#authClientToken`). The auth client token is cleared on the same vendor
switch.
> Nullable fields are typed as `T | null` in the source; the diagram drops that
> for readability.

- An entry is created by whichever comes first: `acceptVendorDisclaimers` for
that vendor or `startSession` for that vendor.
- `residenceCountry` is the country the session was created with, or the
country the vendor's terms were fetched for before a session existed.
Vendor-disclaimer checks read the catalog for it.
- Vendor terms are shown before a session exists but can only be submitted
once one does. `acceptVendorDisclaimers` stores them with
`submitted: false`; `recordVendorDisclaimers({ vendor })` submits them and
sets `submitted: true`. `hasCompletedVendorDisclaimers` only counts
submitted acceptance.
- `getSessionDecision({ vendor })` reduces an entry to `none`, `new`,
`pending`, `approved`, `rejected` or `retry`. It is not a "ready" verdict:
approval says nothing about vendor work after it (e.g. Iron account
activation).
- The whole `sessions` map is persisted. `clearState` / `reset` empty it and
stop every vendor's polling loop.
- The UKYC `local_user_secret` lives in Encrypted User Storage, not on state,
and is shared by every vendor.
- The target model for generalising the flow across vendors is proposed in
[MetaMask/core#11252](https://github.com/MetaMask/core/pull/11252). The
phase-machine sections below describe the earlier MoonPay flow and predate
both.

---

Expand Down Expand Up @@ -690,15 +689,15 @@ graph LR

### Appendix — key source files

| File | Responsibility |
| ---------------------- | ---------------------------------------------------- |
| `src/KycController.ts` | Stateful orchestrator, phase machine. |
| `src/KycService.ts` | Stateless UKYC HTTP client + superstruct validation. |
| `src/crypto.ts` | X25519 ECDH + AES-256-GCM credential decryption. |
| `src/selectors.ts` | Memoized selectors over controller state. |
| `src/types.ts` | `KycPhase`, `KycProduct`, `KycSumSubLauncher`, etc. |
| `src/countryCodes.ts` | ISO alpha-2 → alpha-3 mapping. |
| `src/index.ts` | Public exports (no barrel wildcards). |
| File | Responsibility |
| ---------------------- | ---------------------------------------------------------- |
| `src/KycController.ts` | Stateful orchestrator, phase machine. |
| `src/KycService.ts` | Stateless UKYC HTTP client + superstruct validation. |
| `src/crypto.ts` | X25519 ECDH + AES-256-GCM credential decryption. |
| `src/selectors.ts` | Memoized selectors over controller state. |
| `src/types.ts` | `KycVendor`, `KycSessionStatus`, `KycSumSubLauncher`, etc. |
| `src/countryCodes.ts` | ISO alpha-2 → alpha-3 mapping. |
| `src/index.ts` | Public exports (no barrel wildcards). |

Reference client (metamask-mobile):

Expand Down
26 changes: 26 additions & 0 deletions packages/kyc-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `KycController.getSessionDecision({ vendor })` and the `KycController:getSessionDecision` messenger action, which reduce a vendor's session to one of `none`, `new`, `pending`, `approved`, `rejected` or `retry` ([#11275](https://github.com/MetaMask/core/pull/11275))
- Export the pure `getKycSessionDecision` function, the `KycSessionDecision` type and `KYC_SESSION_DECISIONS`
- Only the vendor's own `finalStatus: approved` counts as approved. It is not a "ready" verdict and says nothing about vendor work after approval
- Add `KycController.acceptVendorDisclaimers({ vendor, country, disclaimerIds })` and its messenger action, which store a vendor's terms acceptance before a session exists, marked not submitted ([#11275](https://github.com/MetaMask/core/pull/11275))
- Add `selectKycSession(state, vendor)`, `getDefaultKycVendorSessionState` and the `KycVendorSessionState` and `KycVendorDisclaimersAcceptance` types ([#11275](https://github.com/MetaMask/core/pull/11275))

### Changed

- **BREAKING:** Replace the single-session state with `sessions`, keyed by vendor ([#11275](https://github.com/MetaMask/core/pull/11275))
- Each entry holds `sessionStatus`, `providerFlowStatus`, `residenceCountry`, `vendorDisclaimersAccepted`, `providerDisclaimersAccepted`, `idosDisclaimersAccepted` and `credentialReusabilityConsentGiven` for that vendor
- The `email`, `vendor` and `geoCountry` fields are removed. Clients must migrate persisted state
- **BREAKING:** Session methods take the vendor: `getProviderFlowStatus`, `readSessionStatus`, `refreshSessionStatus`, `startSessionStatusPolling`, `hasCompletedSessionDisclaimers` and `hasCompletedVendorDisclaimers` take `{ vendor }`; `recordSessionDisclaimers` and `launchProviderFlow` take `vendor` alongside their other parameters ([#11275](https://github.com/MetaMask/core/pull/11275))
- **BREAKING:** `fetchSessionStatusOnce` takes `{ vendor }` instead of a session id, and `fetchSessionDisclaimers` takes `{ vendor }` instead of `{ sessionId }` ([#11275](https://github.com/MetaMask/core/pull/11275))
- **BREAKING:** `recordVendorDisclaimers` takes `{ vendor }` and submits the acceptance stored by `acceptVendorDisclaimers`, instead of taking `disclaimerIds`. It needs an existing session ([#11275](https://github.com/MetaMask/core/pull/11275))
- **BREAKING:** `hasCompletedVendorDisclaimers` only counts acceptance that was submitted to the vendor, checked against the catalog for the entry's `residenceCountry` ([#11275](https://github.com/MetaMask/core/pull/11275))
- **BREAKING:** `vendor` is required on `KycService.fetchVendorDisclaimers` and `KycService.createUkycSession`, which no longer default to `moonpay` ([#11275](https://github.com/MetaMask/core/pull/11275))
- `startSession` no longer throws when called for a vendor, email or country that differs from an earlier session, and only requires a verified email when it creates a session ([#11275](https://github.com/MetaMask/core/pull/11275))
- `launchProviderFlow` returns `failed` without writing state while another vendor's SumSub flow is on screen ([#11275](https://github.com/MetaMask/core/pull/11275))
- `getDataShares` reads the external user id from any vendor's stored session ([#11275](https://github.com/MetaMask/core/pull/11275))

### Removed

- **BREAKING:** Remove `selectKycVendor`, `getDefaultKycVendorDisclaimersAccepted`, `getDefaultKycProviderDisclaimersAccepted` and the `KycPhase`, `KycProduct`, `KycVendorDisclaimersAccepted`, `KycProviderDisclaimersAccepted`, `KycIronVendorDisclaimersAccepted` and `KycMoonpayVendorDisclaimersAccepted` types ([#11275](https://github.com/MetaMask/core/pull/11275))

## [0.15.0]

### Added
Expand Down
Loading
Loading