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
45 changes: 27 additions & 18 deletions packages/kyc-controller/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,29 @@ 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' \| '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. |
| 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 and crypto, and delegates the vendor frame protocol to `MoonPayFrameHandler`. 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. |

---

### 2. Component overview

The package splits cleanly into a **stateful orchestrator** (`KycController`), a
**stateless HTTP client** (`KycService`), and supporting modules (crypto,
**stateless HTTP client** (`KycService`), a **vendor frame protocol handler**
(`MoonPayFrameHandler`), and supporting modules (state, consents, crypto,
selectors, types).

```mermaid
graph TB
subgraph pkg["@metamask/kyc-controller"]
direction TB
Controller["KycController<br/><i>(BaseController)</i><br/>state + orchestration + frame protocol"]
Controller["KycController<br/><i>(BaseController)</i><br/>state + orchestration"]
FrameHandler["vendors/MoonPayFrameHandler.ts<br/>MoonPay Check/Auth frame protocol"]
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"]
Expand All @@ -64,10 +66,11 @@ graph TB
SumSubSDK["SumSub SDK<br/>(native / web)"]
end

Controller -->|"decryptCredentials()"| Crypto
Controller -->|"messenger.call(KycService:*)"| Service
Controller -.->|"injected launcher"| SumSubSDK
Controller -->|"builds frame URLs<br/>handles frame messages"| Frames
Controller -->|"delegates frame protocol"| FrameHandler
FrameHandler -->|"decryptCredentials()"| Crypto
FrameHandler -->|"builds frame URLs<br/>handles frame messages"| Frames

Service -->|"createServicePolicy / HttpError"| CU
Service -->|"messenger.call(GeolocationController:getGeolocation)"| Geo
Expand All @@ -84,8 +87,9 @@ graph TB

- Extends `BaseController<'KycController', KycControllerState, KycControllerMessenger>`.
- Holds **all flow state** (see [§3](#3-state-shape)).
- Owns an ephemeral **X25519 keypair** (`#keypair`) generated at construction —
never persisted, used only for the frame key exchange.
- Delegates the MoonPay Check/Auth frame protocol to `MoonPayFrameHandler`,
which owns the ephemeral **X25519 keypair** (`#frameKeypair`) minted when a
MoonPay flow starts — never persisted, used only for the frame key exchange.
- Registers its public methods as messenger actions via
`registerMethodActionHandlers`.
- Calls `KycService` exclusively **through the messenger** (`KycService:*`
Expand Down Expand Up @@ -202,7 +206,8 @@ classDiagram
> `T | null` in the source; `Record` is `Partial<Record<KycProduct, boolean>>`.
> Types are simplified above for diagram readability.

State metadata highlights (`kycControllerMetadata`):
State metadata highlights (`kycControllerMetadata`, defined alongside the state
type and default-state factories in `src/KycControllerState.ts`):

- **Persisted** (`persist: true`): `vendorDisclaimersAccepted`,
`providerDisclaimersAccepted`, `idosDisclaimersAccepted`,
Expand All @@ -221,10 +226,10 @@ State metadata highlights (`kycControllerMetadata`):
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.
- Additional non-state secrets kept **off** the state object entirely, held by
`MoonPayFrameHandler`: the X25519 private key (`#frameKeypair`) and the
Auth-frame client token (`#authClientToken`). The auth client token is cleared
on the same vendor switch.

---

Expand Down Expand Up @@ -693,8 +698,12 @@ graph LR
| File | Responsibility |
| ------------------------------------ | ------------------------------------------------------------------- |
| `src/KycController.ts` | Stateful orchestrator, phase machine. |
| `src/KycControllerState.ts` | State type, persistence metadata, and default-state factories. |
| `src/vendors/MoonPayFrameHandler.ts` | MoonPay Check/Auth protocol, URLs, and ephemeral frame credentials. |
| `src/KycService.ts` | Stateless UKYC HTTP client + superstruct validation. |
| `src/consents.ts` | Consent-record shaping, conflict/error predicates, vendor routing. |
| `src/vendorDisclaimerAcceptance.ts` | Vendor-scoped terms acceptance records. |
| `src/ukyc/` | UKYC crypto protocol: key derivation, JWT chains, token wrapping. |
| `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. |
Expand Down
1 change: 1 addition & 0 deletions packages/kyc-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump `@metamask/base-data-service` from `^0.1.3` to `^1.0.0` ([#9972](https://github.com/MetaMask/core/pull/9972))
- Replace `KycController` `console.error` tracing with the `@metamask/utils` debug logger (`createProjectLogger` / `createModuleLogger`), so flow diagnostics are opt-in via `DEBUG=kyc-controller*` instead of always printing to the console. ([#10054](https://github.com/MetaMask/core/pull/10054))
- Bump `@metamask/utils` from `^11.11.0` to `^11.12.0` ([#10076](https://github.com/MetaMask/core/pull/10076))
- Split `KycController.ts` into focused modules, leaving it as the orchestrator and phase machine. The state type, persistence metadata, and default-state factories move to `KycControllerState.ts`; consent-record shaping and the conflict/completion error predicates move to `consents.ts`; UKYC attestation and capability-token wrapping move to `ukyc/sessionAuthorizations.ts`. This is an internal reorganization only — every export remains available from the package root under the same name. ([#XXXX](https://github.com/MetaMask/core/pull/XXXX))

### Fixed

Expand Down
6 changes: 2 additions & 4 deletions packages/kyc-controller/src/KycController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ import { areUint8ArraysEqual, bytesToString } from '@metamask/utils';
import { x25519 } from '@noble/curves/ed25519';

import { base64UrlToBytes, toBase64Url } from './encoding.js';
import {
getDefaultKycControllerState,
KycController,
} from './KycController.js';
import { KycController } from './KycController.js';
import type { KycControllerMessenger } from './KycController.js';
import { getDefaultKycControllerState } from './KycControllerState.js';
import type {
KycConsentRecord,
KycDisclaimer,
Expand Down
Loading
Loading