@metamask-previews/kyc-controller 0.0.0-preview-11d78534e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +6 -0
  3. package/LICENSE.APACHE2 +201 -0
  4. package/LICENSE.MIT +21 -0
  5. package/README.md +648 -0
  6. package/dist/KycController-method-action-types.cjs +7 -0
  7. package/dist/KycController-method-action-types.cjs.map +1 -0
  8. package/dist/KycController-method-action-types.d.cts +143 -0
  9. package/dist/KycController-method-action-types.d.cts.map +1 -0
  10. package/dist/KycController-method-action-types.d.mts +143 -0
  11. package/dist/KycController-method-action-types.d.mts.map +1 -0
  12. package/dist/KycController-method-action-types.mjs +6 -0
  13. package/dist/KycController-method-action-types.mjs.map +1 -0
  14. package/dist/KycController.cjs +800 -0
  15. package/dist/KycController.cjs.map +1 -0
  16. package/dist/KycController.d.cts +219 -0
  17. package/dist/KycController.d.cts.map +1 -0
  18. package/dist/KycController.d.mts +219 -0
  19. package/dist/KycController.d.mts.map +1 -0
  20. package/dist/KycController.mjs +795 -0
  21. package/dist/KycController.mjs.map +1 -0
  22. package/dist/KycService-method-action-types.cjs +7 -0
  23. package/dist/KycService-method-action-types.cjs.map +1 -0
  24. package/dist/KycService-method-action-types.d.cts +74 -0
  25. package/dist/KycService-method-action-types.d.cts.map +1 -0
  26. package/dist/KycService-method-action-types.d.mts +74 -0
  27. package/dist/KycService-method-action-types.d.mts.map +1 -0
  28. package/dist/KycService-method-action-types.mjs +6 -0
  29. package/dist/KycService-method-action-types.mjs.map +1 -0
  30. package/dist/KycService.cjs +238 -0
  31. package/dist/KycService.cjs.map +1 -0
  32. package/dist/KycService.d.cts +169 -0
  33. package/dist/KycService.d.cts.map +1 -0
  34. package/dist/KycService.d.mts +169 -0
  35. package/dist/KycService.d.mts.map +1 -0
  36. package/dist/KycService.mjs +234 -0
  37. package/dist/KycService.mjs.map +1 -0
  38. package/dist/countryCodes.cjs +274 -0
  39. package/dist/countryCodes.cjs.map +1 -0
  40. package/dist/countryCodes.d.cts +18 -0
  41. package/dist/countryCodes.d.cts.map +1 -0
  42. package/dist/countryCodes.d.mts +18 -0
  43. package/dist/countryCodes.d.mts.map +1 -0
  44. package/dist/countryCodes.mjs +270 -0
  45. package/dist/countryCodes.mjs.map +1 -0
  46. package/dist/crypto.cjs +165 -0
  47. package/dist/crypto.cjs.map +1 -0
  48. package/dist/crypto.d.cts +83 -0
  49. package/dist/crypto.d.cts.map +1 -0
  50. package/dist/crypto.d.mts +83 -0
  51. package/dist/crypto.d.mts.map +1 -0
  52. package/dist/crypto.mjs +160 -0
  53. package/dist/crypto.mjs.map +1 -0
  54. package/dist/index.cjs +21 -0
  55. package/dist/index.cjs.map +1 -0
  56. package/dist/index.d.cts +12 -0
  57. package/dist/index.d.cts.map +1 -0
  58. package/dist/index.d.mts +12 -0
  59. package/dist/index.d.mts.map +1 -0
  60. package/dist/index.mjs +6 -0
  61. package/dist/index.mjs.map +1 -0
  62. package/dist/selectors.cjs +30 -0
  63. package/dist/selectors.cjs.map +1 -0
  64. package/dist/selectors.d.cts +24 -0
  65. package/dist/selectors.d.cts.map +1 -0
  66. package/dist/selectors.d.mts +24 -0
  67. package/dist/selectors.d.mts.map +1 -0
  68. package/dist/selectors.mjs +24 -0
  69. package/dist/selectors.mjs.map +1 -0
  70. package/dist/types.cjs +10 -0
  71. package/dist/types.cjs.map +1 -0
  72. package/dist/types.d.cts +93 -0
  73. package/dist/types.d.cts.map +1 -0
  74. package/dist/types.d.mts +93 -0
  75. package/dist/types.d.mts.map +1 -0
  76. package/dist/types.mjs +9 -0
  77. package/dist/types.mjs.map +1 -0
  78. package/package.json +89 -0
package/README.md ADDED
@@ -0,0 +1,648 @@
1
+ # KYC Controller `@metamask/kyc-controller`
2
+
3
+ Shared KYC / identity verification controller used across MetaMask clients
4
+
5
+ ## Installation
6
+
7
+ `yarn add @metamask/kyc-controller`
8
+
9
+ or
10
+
11
+ `npm install @metamask/kyc-controller`
12
+
13
+ ## Development
14
+
15
+ To rebuild the package automatically whenever you change a source file, run the `build:watch` script:
16
+
17
+ `yarn workspace @metamask/kyc-controller run build:watch`
18
+
19
+ This watches `src/**/*.ts` and re-runs the build on each change (it also performs an initial build on start), which is useful when developing against a client that consumes this package locally.
20
+
21
+ ## Contributing
22
+
23
+ This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
24
+
25
+
26
+ ## Architecture
27
+
28
+ `@metamask/kyc-controller` is a shared, **platform-agnostic** package that owns
29
+ the end-to-end KYC / identity-verification flow used across MetaMask clients
30
+ (mobile, extension, web). It hides the vendor implementation (currently
31
+ **MoonPay** for identity + **SumSub** for document verification) behind a
32
+ vendor-neutral, per-product surface consumed by features such as **ramps** and
33
+ **card**.
34
+
35
+ This document explains:
36
+
37
+ - The package's internal building blocks and responsibilities.
38
+ - How the pieces communicate (messenger actions, injected adapters).
39
+ - The identity flow as a state machine and an end-to-end sequence.
40
+ - The encrypted frame message protocol and crypto.
41
+ - How the **metamask-mobile** client wires everything together on the client
42
+ side.
43
+
44
+ ---
45
+
46
+ ### 1. Design principles
47
+
48
+ The package is built around a few deliberate constraints:
49
+
50
+ | Principle | How it shows up in the code |
51
+ | --- | --- |
52
+ | **Vendor-neutral surface** | Consumers deal with `KycProduct` (`'ramps' \| 'card'`) and a phase machine, never with MoonPay/SumSub specifics. `KycVendor` is internal. |
53
+ | **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. |
54
+ | **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. |
55
+ | **Stateless service** | `KycService` performs HTTP only; it holds no state and derives auth/geolocation from other controllers via the messenger. |
56
+ | **Everything through the messenger** | Both classes register their public methods as messenger actions, and reach external capabilities (auth token, geolocation) via delegated actions. |
57
+
58
+ ---
59
+
60
+ ### 2. Component overview
61
+
62
+ The package splits cleanly into a **stateful orchestrator** (`KycController`), a
63
+ **stateless HTTP client** (`KycService`), and supporting modules (crypto,
64
+ selectors, types).
65
+
66
+ ```mermaid
67
+ graph TB
68
+ subgraph pkg["@metamask/kyc-controller"]
69
+ direction TB
70
+ Controller["KycController<br/><i>(BaseController)</i><br/>state + orchestration + frame protocol"]
71
+ Service["KycService<br/><i>(stateless)</i><br/>HTTP + response validation"]
72
+ Crypto["crypto.ts<br/>X25519 ECDH + AES-256-GCM"]
73
+ Selectors["selectors.ts<br/>memoized reselect selectors"]
74
+ Types["types.ts<br/>KycPhase, KycProduct,<br/>KycSumSubLauncher, ..."]
75
+ Country["countryCodes.ts<br/>alpha-2 → alpha-3"]
76
+ end
77
+
78
+ subgraph deps["External MetaMask dependencies"]
79
+ Base["@metamask/base-controller"]
80
+ Msgr["@metamask/messenger"]
81
+ CU["@metamask/controller-utils<br/>createServicePolicy, HttpError"]
82
+ Geo["GeolocationController"]
83
+ Auth["AuthenticationController<br/>(profile-sync)"]
84
+ end
85
+
86
+ subgraph vendor["Vendor backends (HTTP / frames)"]
87
+ UKYC["Universal KYC API<br/>kyc-api.cx.metamask.io"]
88
+ Frames["MoonPay frames<br/>blocks.moonpay.com"]
89
+ SumSubSDK["SumSub SDK<br/>(native / web)"]
90
+ end
91
+
92
+ Controller -->|"decryptCredentials()"| Crypto
93
+ Controller -->|"messenger.call(KycService:*)"| Service
94
+ Controller -.->|"injected launcher"| SumSubSDK
95
+ Controller -->|"builds frame URLs<br/>handles frame messages"| Frames
96
+
97
+ Service -->|"createServicePolicy / HttpError"| CU
98
+ Service -->|"messenger.call(GeolocationController:getGeolocation)"| Geo
99
+ Service -->|"messenger.call(AuthenticationController:getBearerToken)"| Auth
100
+ Service -->|"fetch()"| UKYC
101
+
102
+ Controller --- Base
103
+ Controller --- Msgr
104
+ Service --- Msgr
105
+ Selectors -.->|"read"| Controller
106
+ ```
107
+
108
+ #### 2.1 `KycController`
109
+
110
+ - Extends `BaseController<'KycController', KycControllerState, KycControllerMessenger>`.
111
+ - Holds **all flow state** (see [§3](#3-state-shape)).
112
+ - Owns an ephemeral **X25519 keypair** (`#keypair`) generated at construction —
113
+ never persisted, used only for the frame key exchange.
114
+ - Registers its public methods as messenger actions via
115
+ `registerMethodActionHandlers`.
116
+ - Calls `KycService` exclusively **through the messenger** (`KycService:*`
117
+ actions), never a direct reference.
118
+ - Delegates SumSub SDK presentation to an injected `sumsubLauncher`
119
+ (`KycSumSubLauncher`).
120
+ - When the flow is scoped to a product (passed to `initialize` /
121
+ `acceptTermsAndStartSession` and stored as `activeProduct`), automatically
122
+ runs the KYC-required check once authenticated and chains into document
123
+ verification when KYC is required — no extra consumer calls needed.
124
+
125
+ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`):
126
+
127
+ `initialize`, `loadDisclaimers`, `acceptTermsAndStartSession`,
128
+ `clearSavedTerms`, `handleFrameMessage`, `buildCheckFrameUrl`,
129
+ `buildAuthFrameUrl`, `buildResetFrameUrl`, `checkKycRequired`, `getKycStatus`,
130
+ `startSumSub`, `reset`.
131
+
132
+ #### 2.2 `KycService`
133
+
134
+ - **Stateless**, platform-agnostic HTTP client for the Universal KYC (UKYC)
135
+ backend.
136
+ - Base URL derived from `env` (`production` / `development`) or an explicit
137
+ `baseUrl` override.
138
+ - Every request is wrapped in a **service policy** (`createServicePolicy`) for
139
+ retries/circuit-breaking, and carries a **bearer token** obtained from
140
+ `AuthenticationController:getBearerToken`.
141
+ - Every response is validated with **superstruct** before being returned;
142
+ malformed responses throw a descriptive error.
143
+ - Resolves the customer's country from `GeolocationController:getGeolocation`
144
+ and maps alpha-2 → alpha-3.
145
+
146
+ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`):
147
+
148
+ `getGeoCountry`, `fetchDisclaimers`, `createSession`, `checkKycRequired`,
149
+ `createUkycSession`, `submitWrappedKey`.
150
+
151
+ Endpoints:
152
+
153
+ | Method | HTTP | Endpoint | Purpose |
154
+ | --- | --- | --- | --- |
155
+ | `getGeoCountry` | — | (geolocation action) | Resolve alpha-3 country |
156
+ | `fetchDisclaimers` | `GET` | `/vendors/moonpay/disclaimers?country=` | Terms to accept |
157
+ | `createSession` | `POST` | `/vendors/moonpay/sessions` | Create vendor session |
158
+ | `checkKycRequired` | `POST` | `/vendors/moonpay/kyc-required` | Is KYC required? (normalizes `required` → `kycRequired`) |
159
+ | `createUkycSession` | `POST` | `/sessions` | Start SumSub sub-flow |
160
+ | `submitWrappedKey` | `POST` | `/sessions/{id}/wrapped-key` | Exchange wrapped key → applicant token |
161
+
162
+ ### 2.3 `crypto.ts`
163
+
164
+ Implements the Check/Auth frame credential decryption:
165
+
166
+ 1. Client generates an X25519 keypair; the public key (hex) is added to the
167
+ frame URL.
168
+ 2. The frame returns `{ ephemeralPublicKey, iv|nonce, ciphertext }`.
169
+ 3. Client derives `shared = X25519(ourPriv, theirEphemeralPub)`, then
170
+ `key = HKDF-SHA256(shared, 32 bytes)`, then AES-256-GCM decrypts the
171
+ ciphertext (which includes the 16-byte tag). IV must be 12 bytes.
172
+
173
+ It tolerates envelopes delivered as an object, a JSON string, or base64(JSON),
174
+ and hex-or-base64 binary fields.
175
+
176
+ #### 2.4 `selectors.ts`
177
+
178
+ Memoized `reselect` selectors over `KycControllerState`:
179
+ `selectKycPhase`, `selectKycSumSub`, and the parametric
180
+ `selectIsKycRequiredForProduct(product)`.
181
+
182
+ ---
183
+
184
+ ### 3. State shape
185
+
186
+ ```mermaid
187
+ classDiagram
188
+ class KycControllerState {
189
+ +KycPhase phase
190
+ +string statusMessage
191
+ +string error
192
+ +string email
193
+ +string termsAcceptedAt [persisted]
194
+ +string[] acceptedDisclaimerIds [persisted]
195
+ +KycDisclaimer[] disclaimers
196
+ +string disclaimersError
197
+ +string geoCountry
198
+ +string sessionToken [secret]
199
+ +string accessToken [secret]
200
+ +string moonpayCustomerId
201
+ +KycProduct activeProduct
202
+ +Record kycRequiredByProduct [persisted]
203
+ +string lastCheckedAt [persisted]
204
+ +SumSubState sumsub
205
+ }
206
+ class SumSubState {
207
+ +KycSumSubStatus status
208
+ +Json result
209
+ +string sessionId
210
+ +string applicantAccessToken
211
+ }
212
+ KycControllerState --> SumSubState : sumsub
213
+ ```
214
+
215
+ > Note: nullable fields (`error`, `email`, `sessionToken`, …) are typed as
216
+ > `T | null` in the source; `Record` is `Partial<Record<KycProduct, boolean>>`.
217
+ > Types are simplified above for diagram readability.
218
+
219
+ State metadata highlights (`kycControllerMetadata`):
220
+
221
+ - **Persisted** (`persist: true`): `termsAcceptedAt`, `acceptedDisclaimerIds`,
222
+ `kycRequiredByProduct`, `lastCheckedAt`. These survive restarts so the flow
223
+ can skip already-accepted terms and reuse cached results.
224
+ - **Secrets, never persisted / never logged**: `sessionToken`, `accessToken`,
225
+ `moonpayCustomerId`, `email`, `disclaimers`, and the whole `sumsub` sub-tree.
226
+ - Additional non-state secrets kept **off** the state object entirely: the
227
+ X25519 private key (`#keypair`) and the Auth-frame client token
228
+ (`#authClientToken`).
229
+
230
+ ---
231
+
232
+ ### 4. The identity flow (phase state machine)
233
+
234
+ `KycPhase` models the linear identity flow. Each transition is driven by a
235
+ controller method or an incoming frame message.
236
+
237
+ ```mermaid
238
+ stateDiagram-v2
239
+ [*] --> idle
240
+ idle --> terms : initialize() (no saved terms)
241
+ idle --> session : initialize() (saved terms + email)
242
+
243
+ terms --> session : acceptTermsAndStartSession()
244
+ session --> check : createSession() ok
245
+ session --> terms : createSession() fails (clears saved terms)
246
+
247
+ check --> form : Check frame → active (already authenticated)
248
+ check --> auth : Check frame → connectionRequired (needs OTP)
249
+ check --> terms : Check frame → termsAcceptanceRequired
250
+
251
+ auth --> form : Auth frame → active (OTP verified)
252
+ auth --> terms : Auth frame → termsAcceptanceRequired
253
+
254
+ form --> submit : checkKycRequired()<br/>(auto when a product is set)
255
+ submit --> done : kyc-required response ok
256
+ submit --> error : request failed
257
+
258
+ check --> error : unexpected status / decrypt failure
259
+ auth --> error : unexpected status
260
+ done --> [*]
261
+ error --> idle : reset()
262
+ done --> idle : reset()
263
+ ```
264
+
265
+ > When the flow is scoped to a product (a `product` is passed to `initialize`
266
+ > or `acceptTermsAndStartSession`), reaching `form` **automatically** runs the
267
+ > KYC-required check (`form → submit → done`) with no user interaction, and — if
268
+ > KYC is required — automatically launches the SumSub document-verification
269
+ > sub-flow (see [§7](#7-sumsub-sub-flow)). When no product is set the flow stops
270
+ > at `form` and the consumer drives `checkKycRequired` / `startSumSub` manually.
271
+
272
+ Phase meanings (from `types.ts`):
273
+
274
+ | Phase | Meaning |
275
+ | --- | --- |
276
+ | `idle` | Nothing started. |
277
+ | `terms` | Waiting for the customer to accept vendor terms. |
278
+ | `session` | Creating the vendor session. |
279
+ | `check` | Running the **invisible** connection-check frame. |
280
+ | `auth` | Running the **visible** authentication (email OTP) frame. |
281
+ | `form` | Authenticated. Auto-runs the KYC-required check when a product is set; otherwise waits for the consumer. |
282
+ | `submit` | Submitting the KYC-required check. |
283
+ | `done` | Complete — see `kycRequiredByProduct` / `sumsub`. Document verification auto-launches when KYC is required. |
284
+ | `error` | Halted — see `error`. |
285
+
286
+ ---
287
+
288
+ ### 5. End-to-end sequence
289
+
290
+ This sequence shows the full happy path including the two frames and the SumSub
291
+ hand-off. The **client transport** (WebView on mobile, iframe on web) is
292
+ generic — it only forwards raw frame messages to `handleFrameMessage` and posts
293
+ back any returned `reply`.
294
+
295
+ ```mermaid
296
+ sequenceDiagram
297
+ autonumber
298
+ actor User
299
+ participant UI as Client UI + transport<br/>(WebView/iframe)
300
+ participant Ctrl as KycController
301
+ participant Svc as KycService
302
+ participant Geo as GeolocationController
303
+ participant API as UKYC API
304
+ participant Frame as MoonPay Check/Auth frame
305
+ participant Launcher as SumSub launcher (injected)
306
+
307
+ User->>Ctrl: initialize({ email, product })
308
+ Ctrl->>Svc: getGeoCountry()
309
+ Svc->>Geo: getGeolocation()
310
+ Note over Svc: map alpha-2 → alpha-3 locally
311
+ Ctrl->>Svc: fetchDisclaimers({ country })
312
+ Svc->>API: GET /disclaimers
313
+ Ctrl-->>UI: phase = terms (+ disclaimers)
314
+
315
+ User->>Ctrl: acceptTermsAndStartSession({ email })
316
+ Ctrl->>Svc: createSession({ email, termsAcceptedAt, disclaimerIds })
317
+ Svc->>API: POST /sessions
318
+ Ctrl-->>UI: phase = check (+ sessionToken)
319
+
320
+ UI->>Ctrl: buildCheckFrameUrl()
321
+ Ctrl-->>UI: URL (sessionToken + publicKey)
322
+ UI->>Frame: load Check frame (invisible)
323
+ Frame-->>UI: handshake
324
+ UI->>Ctrl: handleFrameMessage(handshake)
325
+ Ctrl-->>UI: reply = ack
326
+ UI->>Frame: post ack
327
+ Frame-->>UI: complete (status + encrypted credentials)
328
+ UI->>Ctrl: handleFrameMessage(complete)
329
+ Note over Ctrl: decryptCredentials() → accessToken / clientToken
330
+
331
+ alt Check → connectionRequired
332
+ Ctrl-->>UI: phase = auth
333
+ UI->>Frame: load Auth frame (visible, OTP)
334
+ Frame-->>UI: complete (active + credentials)
335
+ UI->>Ctrl: handleFrameMessage(complete)
336
+ end
337
+
338
+ Ctrl-->>UI: phase = form (accessToken set)
339
+
340
+ Note over Ctrl: activeProduct set at initialize →<br/>continue automatically (no user action)
341
+ Ctrl->>Svc: checkKycRequired({ accessToken, country, capabilities })
342
+ Svc->>API: POST /kyc-required
343
+ Ctrl-->>UI: phase = done (kycRequiredByProduct[product])
344
+
345
+ opt kycRequired === true → auto-launch document verification
346
+ Ctrl->>Svc: createUkycSession({ jwtToken, vendorMetadata })
347
+ Svc->>API: POST /sessions
348
+ Ctrl->>Svc: submitWrappedKey({ sessionId, wrappedUserKey, ... })
349
+ Svc->>API: POST /sessions/{id}/wrapped-key
350
+ Ctrl->>Launcher: launch({ applicantAccessToken, onTokenExpiration, onStatusChange })
351
+ Launcher-->>Ctrl: SDK result
352
+ Ctrl-->>UI: sumsub.status = complete (+ result)
353
+ end
354
+ ```
355
+
356
+ > The KYC-required check and the document-verification launch after `form` are
357
+ > driven by the controller itself, not the user — the flow captures the
358
+ > `product` at `initialize` and continues automatically. If `initialize` is
359
+ > called without a `product`, the flow stops at `form` and the consumer triggers
360
+ > `checkKycRequired` (and later `startSumSub`) explicitly.
361
+
362
+ ---
363
+
364
+ ### 6. Frame message protocol & crypto
365
+
366
+ The Check, Auth and Reset frames all speak a small `postMessage` protocol.
367
+ `KycController.handleFrameMessage` implements the identity portion; the client
368
+ transport is responsible only for delivering messages and injecting replies.
369
+
370
+ ```mermaid
371
+ sequenceDiagram
372
+ autonumber
373
+ participant Frame as MoonPay frame
374
+ participant UI as Client transport
375
+ participant Ctrl as KycController
376
+
377
+ Frame->>UI: { kind: "handshake", meta:{channelId} }
378
+ UI->>Ctrl: handleFrameMessage({ message })
379
+ Ctrl-->>UI: { reply: { version:2, meta:{channelId}, kind:"ack" } }
380
+ UI->>Frame: postMessage(ack)
381
+
382
+ Frame->>UI: { kind:"complete", meta:{channelId},<br/>payload:{ status, credentials, customer } }
383
+ UI->>Ctrl: handleFrameMessage({ message })
384
+ Note over Ctrl: 1. store customer.id (moonpayCustomerId)<br/>2. decryptCredentials(envelope, privKey)<br/>3. route by channelId (ch_1 Check / ch_2 Auth)
385
+ Ctrl->>Ctrl: apply outcome → next phase
386
+ ```
387
+
388
+ Channels: `ch_1` = Check, `ch_2` = Auth, `ch_reset` = Reset.
389
+
390
+ Credential decryption (`crypto.ts`):
391
+
392
+ ```mermaid
393
+ graph LR
394
+ A["envelope<br/>{ ephemeralPublicKey, iv|nonce, ciphertext }"] --> B["X25519 ECDH<br/>shared = f(ourPriv, theirPub)"]
395
+ B --> C["HKDF-SHA256<br/>key (32 bytes)"]
396
+ C --> D["AES-256-GCM decrypt<br/>(iv = 12 bytes)"]
397
+ D --> E["JSON credentials<br/>{ accessToken?, clientToken? }"]
398
+ ```
399
+
400
+ Check-frame outcomes (`#handleCheckOutcome`):
401
+
402
+ - `active` + `accessToken` → phase `form` (already authenticated).
403
+ - `connectionRequired` + `clientToken` → store `#authClientToken`, phase `auth`.
404
+ - `termsAcceptanceRequired` → clear saved terms, phase `terms`.
405
+ - anything else → `error`.
406
+
407
+ Auth-frame outcomes (`#handleAuthOutcome`):
408
+
409
+ - `active` + `accessToken` → phase `form`.
410
+ - `termsAcceptanceRequired` → clear saved terms, phase `terms`.
411
+ - anything else → `error`.
412
+
413
+ ---
414
+
415
+ ### 7. SumSub sub-flow
416
+
417
+ The document-verification sub-flow tracks its own status independently of the
418
+ identity `phase`, and delegates the actual SDK presentation to the injected
419
+ launcher.
420
+
421
+ ```mermaid
422
+ stateDiagram-v2
423
+ [*] --> idle
424
+ idle --> creatingSession : startSumSub()
425
+ creatingSession --> fetchingToken : createUkycSession() ok
426
+ fetchingToken --> launching : submitWrappedKey() ok
427
+ launching --> inProgress : launcher.onStatusChange
428
+ inProgress --> complete : status = Completed
429
+ launching --> complete : SDK resolves
430
+ creatingSession --> failed : error
431
+ fetchingToken --> failed : error
432
+ launching --> failed : launcher unavailable / error
433
+ ```
434
+
435
+ The `KycSumSubLauncher` interface (injected per client):
436
+
437
+ ```ts
438
+ type KycSumSubLauncher = {
439
+ isAvailable(): boolean;
440
+ launch(params: KycSumSubLaunchParams): Promise<Record<string, unknown>>;
441
+ };
442
+ ```
443
+
444
+ `launch` receives `applicantAccessToken`, an `onTokenExpiration` callback (the
445
+ controller re-runs `submitWrappedKey` to refresh), and an `onStatusChange`
446
+ callback that the controller maps into `sumsub.status`.
447
+
448
+ ---
449
+
450
+ ### 8. Messenger wiring
451
+
452
+ Both classes are messenger-driven. The controller depends on the service's
453
+ actions; the service depends on auth + geolocation actions from other
454
+ controllers.
455
+
456
+ ```mermaid
457
+ graph LR
458
+ subgraph CtrlMsgr["KycControllerMessenger"]
459
+ C_own["Own actions:<br/>KycController:getState + 12 methods"]
460
+ C_ext["Allowed (delegated):<br/>KycService:*"]
461
+ end
462
+ subgraph SvcMsgr["KycServiceMessenger"]
463
+ S_own["Own actions:<br/>KycService: 6 methods"]
464
+ S_ext["Allowed (delegated):<br/>AuthenticationController:getBearerToken<br/>GeolocationController:getGeolocation"]
465
+ end
466
+
467
+ C_ext -.delegates.-> S_own
468
+ S_ext -.delegates.-> Auth["AuthenticationController"]
469
+ S_ext -.delegates.-> Geo["GeolocationController"]
470
+ ```
471
+
472
+ - `KycController` emits `KycController:stateChange` and exposes
473
+ `KycController:getState` plus its method actions.
474
+ - `KycController`'s `AllowedActions` = `KycServiceMethodActions` — it can call
475
+ the service.
476
+ - `KycService`'s `AllowedActions` = the auth bearer-token and geolocation
477
+ actions.
478
+
479
+ ---
480
+
481
+ ### 9. Client-side usage (metamask-mobile)
482
+
483
+ The mobile app is a reference consumer. It wires the controller/service into the
484
+ Engine, injects a React Native SumSub launcher, bridges WebView frame messages,
485
+ and reads state through Redux selectors. The **package stays free of any of
486
+ this** — all React/native/WebView code lives in the app.
487
+
488
+ ```mermaid
489
+ graph TB
490
+ subgraph app["metamask-mobile"]
491
+ direction TB
492
+ subgraph engine["Engine wiring"]
493
+ CInit["kyc-controller-init.ts<br/>new KycController({ messenger, state, sumsubLauncher })"]
494
+ SInit["kyc-service-init.ts<br/>new KycService({ fetch, env, messenger, baseUrl })"]
495
+ CMsgr["kyc-controller-messenger.ts<br/>delegates KycService:*"]
496
+ SMsgr["kyc-service-messenger.ts<br/>delegates Auth + Geolocation"]
497
+ Launcher["reactNativeSumSubLauncher.ts<br/>lazy-loads @sumsub/react-native-mobilesdk-module"]
498
+ end
499
+ subgraph ui["UI layer"]
500
+ Hook["useKycFlow.ts<br/>binds controller ↔ React"]
501
+ Frame["MoonpayFrame + useMoonpayFrame<br/>WebView postMessage bridge"]
502
+ Reset["useMoonpayReset.ts<br/>Reset frame"]
503
+ Demo["MoonpayDemo / SumSubDemo / KYCDemo<br/>screens"]
504
+ end
505
+ subgraph redux["Redux"]
506
+ Sel["selectors/kycController.ts<br/>wraps core selectors"]
507
+ end
508
+ end
509
+
510
+ subgraph core["@metamask/kyc-controller"]
511
+ KC["KycController"]
512
+ KS["KycService"]
513
+ end
514
+
515
+ CInit --> KC
516
+ SInit --> KS
517
+ CInit --> Launcher
518
+ Launcher -. injected .-> KC
519
+ CMsgr --> KC
520
+ SMsgr --> KS
521
+
522
+ Hook -->|"Engine.context.KycController.*"| KC
523
+ Hook -->|"useSelector"| Sel
524
+ Sel -->|"state.engine.backgroundState.KycController"| KC
525
+ Frame -->|"raw frame message"| Hook
526
+ Hook -->|"handleFrameMessage()"| KC
527
+ Demo --> Hook
528
+ Demo --> Frame
529
+ Demo --> Reset
530
+ ```
531
+
532
+ #### 9.1 Engine wiring
533
+
534
+ - **`kyc-controller-init.ts`** constructs `KycController` with the persisted
535
+ state slice and injects `reactNativeSumSubLauncher`.
536
+ - **`kyc-service-init.ts`** constructs `KycService` with the global `fetch`, an
537
+ `env` derived from `isProduction()`, and (currently) a dev `baseUrl` override.
538
+ - **`kyc-controller-messenger.ts`** delegates the six `KycService:*` actions to
539
+ the controller's messenger.
540
+ - **`kyc-service-messenger.ts`** delegates
541
+ `AuthenticationController:getBearerToken` and
542
+ `GeolocationController:getGeolocation` to the service's messenger.
543
+
544
+ #### 9.2 SumSub launcher adapter
545
+
546
+ `reactNativeSumSubLauncher` implements `KycSumSubLauncher`:
547
+
548
+ - `isAvailable()` checks for the native module (`NativeModules.SNSMobileSDKModule`).
549
+ - `launch()` **lazily imports** `@sumsub/react-native-mobilesdk-module` (so
550
+ merely wiring the controller never loads the native module — important for
551
+ Jest / Expo Go), initializes the SDK with the applicant token, and forwards
552
+ `onStatusChanged` / token-expiration callbacks back to the controller.
553
+
554
+ #### 9.3 React binding — `useKycFlow`
555
+
556
+ A thin hook that:
557
+
558
+ - Reads controller state from Redux via the `selectors/kycController.ts`
559
+ selectors.
560
+ - Forwards user intents to controller actions through
561
+ `Engine.context.KycController.*` (`initialize`, `acceptTermsAndStartSession`,
562
+ `checkKycRequired`, `startSumSub`, `clearSavedTerms`, `reset`).
563
+ - Builds frame URLs on demand (`buildCheckFrameUrl` / `buildAuthFrameUrl`) as
564
+ the phase changes.
565
+ - Bridges WebView frame messages into `handleFrameMessage` and posts back the
566
+ returned `reply`.
567
+ - Keeps view-only concerns (email input, debug log, frame visibility) in local
568
+ React state.
569
+
570
+ #### 9.4 WebView transport — `useMoonpayFrame` / `MoonpayFrame`
571
+
572
+ - Injects a `postMessage` bridge into the frame that forwards the frame's
573
+ outbound messages to React Native via `window.ReactNativeWebView.postMessage`.
574
+ - **Validates the origin** (`https://blocks.moonpay.com`) before handing a
575
+ message to the controller.
576
+ - Implements `reply()` by dispatching a `MessageEvent` back into the WebView on
577
+ both `document` and `window` (platform quirk between iOS WKWebView and Android
578
+ System WebView).
579
+ - The Check frame is rendered **invisible** (1×1, opacity 0) unless the user
580
+ toggles it in the debug panel; the Auth frame is rendered visibly for OTP.
581
+
582
+ #### 9.5 Redux selectors
583
+
584
+ `selectors/kycController.ts` wraps the package's core selectors and reads the
585
+ slice at `state.engine.backgroundState.KycController`, exposing app-friendly
586
+ selectors (`selectKycPhase`, `selectKycSumSub`,
587
+ `selectIsKycRequiredForProduct(product)`, plus per-field selectors).
588
+
589
+ ---
590
+
591
+ ### 10. Boundaries & responsibilities summary
592
+
593
+ ```mermaid
594
+ graph LR
595
+ subgraph shared["Shared package (platform-agnostic)"]
596
+ A1["Flow orchestration + state"]
597
+ A2["HTTP + response validation"]
598
+ A3["Crypto (X25519 / AES-GCM)"]
599
+ A4["Frame message protocol"]
600
+ A5["Selectors + vendor-neutral types"]
601
+ end
602
+ subgraph client["Client (per platform)"]
603
+ B1["Engine/DI wiring"]
604
+ B2["WebView / iframe transport"]
605
+ B3["SumSub SDK launcher"]
606
+ B4["Auth token + geolocation providers"]
607
+ B5["UI + Redux binding"]
608
+ end
609
+ shared -. injected adapters .- client
610
+ ```
611
+
612
+ | Concern | Owner |
613
+ | --- | --- |
614
+ | Flow phase machine & state | `KycController` (shared) |
615
+ | UKYC HTTP + validation + retries | `KycService` (shared) |
616
+ | Credential decryption / key exchange | `crypto.ts` (shared) |
617
+ | Frame message semantics | `KycController.handleFrameMessage` (shared) |
618
+ | Frame **transport** (WebView/iframe) | Client |
619
+ | SumSub SDK presentation | Client (via `KycSumSubLauncher`) |
620
+ | Auth bearer token / geolocation | Other controllers (via messenger) |
621
+ | Persistence of state | Client (base-controller persistence) |
622
+
623
+ ---
624
+
625
+ ### Appendix — key source files
626
+
627
+ | File | Responsibility |
628
+ | --- | --- |
629
+ | `src/KycController.ts` | Stateful orchestrator, phase machine, frame protocol. |
630
+ | `src/KycService.ts` | Stateless UKYC HTTP client + superstruct validation. |
631
+ | `src/crypto.ts` | X25519 ECDH + AES-256-GCM credential decryption. |
632
+ | `src/selectors.ts` | Memoized selectors over controller state. |
633
+ | `src/types.ts` | `KycPhase`, `KycProduct`, `KycSumSubLauncher`, etc. |
634
+ | `src/countryCodes.ts` | ISO alpha-2 → alpha-3 mapping. |
635
+ | `src/index.ts` | Public exports (no barrel wildcards). |
636
+
637
+ Reference client (metamask-mobile):
638
+
639
+ | File | Responsibility |
640
+ | --- | --- |
641
+ | `app/core/Engine/controllers/kyc/kyc-controller-init.ts` | Construct controller + inject launcher. |
642
+ | `app/core/Engine/controllers/kyc/kyc-service-init.ts` | Construct service. |
643
+ | `app/core/Engine/controllers/kyc/reactNativeSumSubLauncher.ts` | Native SumSub adapter. |
644
+ | `app/core/Engine/messengers/kyc/*.ts` | Messenger delegation. |
645
+ | `app/components/Views/MoonpayDemo/useKycFlow.ts` | React ↔ controller binding. |
646
+ | `app/components/Views/MoonpayDemo/useMoonpayFrame.ts` | WebView postMessage bridge. |
647
+ | `app/selectors/kycController.ts` | Redux selectors. |
648
+
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * This file is auto generated.
4
+ * Do not edit manually.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ //# sourceMappingURL=KycController-method-action-types.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KycController-method-action-types.cjs","sourceRoot":"","sources":["../src/KycController-method-action-types.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { KycController } from './KycController';\n\n/**\n * Resolves persisted terms + geolocation, and auto-creates a session when\n * terms are already accepted and an email is available.\n *\n * @param params - Optional parameters.\n * @param params.email - The account email to associate with the session.\n * @param params.product - The consuming feature the flow runs for. When\n * provided, the controller automatically runs the KYC-required check once\n * authentication completes (and chains into document verification when KYC\n * is required). When omitted, the flow stops at `form` and the consumer must\n * call `checkKycRequired` manually.\n */\nexport type KycControllerInitializeAction = {\n type: `KycController:initialize`;\n handler: KycController['initialize'];\n};\n\n/**\n * Loads the disclaimers for the resolved (or provided) country.\n *\n * @param params - Optional parameters.\n * @param params.country - ISO 3166-1 alpha-3 country code override.\n */\nexport type KycControllerLoadDisclaimersAction = {\n type: `KycController:loadDisclaimers`;\n handler: KycController['loadDisclaimers'];\n};\n\n/**\n * Captures terms acceptance for the currently loaded disclaimers and creates\n * a session.\n *\n * @param params - Optional parameters.\n * @param params.email - The account email to associate with the session.\n * @param params.product - The consuming feature the flow runs for. See\n * {@link initialize} for how the product drives the automatic post\n * authentication continuation.\n */\nexport type KycControllerAcceptTermsAndStartSessionAction = {\n type: `KycController:acceptTermsAndStartSession`;\n handler: KycController['acceptTermsAndStartSession'];\n};\n\n/**\n * Clears the persisted terms acceptance.\n */\nexport type KycControllerClearSavedTermsAction = {\n type: `KycController:clearSavedTerms`;\n handler: KycController['clearSavedTerms'];\n};\n\n/**\n * Handles a message posted by a Check/Auth frame and advances the flow.\n *\n * The transport-agnostic caller (WebView on mobile, iframe on web) forwards\n * the raw message and injects the returned `reply` back into the frame.\n *\n * @param params - The parameters.\n * @param params.message - The raw message posted by the frame.\n * @returns An object whose optional `reply` should be posted back.\n */\nexport type KycControllerHandleFrameMessageAction = {\n type: `KycController:handleFrameMessage`;\n handler: KycController['handleFrameMessage'];\n};\n\n/**\n * Builds the Check-frame URL, or `null` when no session exists yet.\n *\n * @returns The Check-frame URL or `null`.\n */\nexport type KycControllerBuildCheckFrameUrlAction = {\n type: `KycController:buildCheckFrameUrl`;\n handler: KycController['buildCheckFrameUrl'];\n};\n\n/**\n * Builds the Auth-frame URL, or `null` when no client token is available.\n *\n * @returns The Auth-frame URL or `null`.\n */\nexport type KycControllerBuildAuthFrameUrlAction = {\n type: `KycController:buildAuthFrameUrl`;\n handler: KycController['buildAuthFrameUrl'];\n};\n\n/**\n * Builds the Reset-frame URL.\n *\n * @returns The Reset-frame URL.\n */\nexport type KycControllerBuildResetFrameUrlAction = {\n type: `KycController:buildResetFrameUrl`;\n handler: KycController['buildResetFrameUrl'];\n};\n\n/**\n * Checks whether KYC is required for a product and caches the result.\n *\n * @param params - The parameters.\n * @param params.product - The consuming feature.\n * @param params.country - Optional alpha-3 country override.\n * @returns Whether KYC is required.\n */\nexport type KycControllerCheckKycRequiredAction = {\n type: `KycController:checkKycRequired`;\n handler: KycController['checkKycRequired'];\n};\n\n/**\n * Reads the cached \"is KYC required\" result for a product.\n *\n * @param params - The parameters.\n * @param params.product - The consuming feature.\n * @returns The cached value, or `undefined` if not yet checked.\n */\nexport type KycControllerGetKycStatusAction = {\n type: `KycController:getKycStatus`;\n handler: KycController['getKycStatus'];\n};\n\n/**\n * Runs the SumSub document-verification sub-flow: creates a UKYC session,\n * exchanges the wrapped key for an applicant access token, and presents the\n * SDK via the injected launcher.\n *\n * @param params - Optional parameters.\n * @param params.locale - BCP-47 locale for the SDK UI.\n * @param params.debug - Enables SDK debug logging.\n * @returns The SDK result.\n */\nexport type KycControllerStartSumSubAction = {\n type: `KycController:startSumSub`;\n handler: KycController['startSumSub'];\n};\n\n/**\n * Resets the flow to idle, clearing session tokens and sub-flow state while\n * preserving persisted terms acceptance and the per-product cache.\n */\nexport type KycControllerResetAction = {\n type: `KycController:reset`;\n handler: KycController['reset'];\n};\n\n/**\n * Union of all KycController action types.\n */\nexport type KycControllerMethodActions =\n | KycControllerInitializeAction\n | KycControllerLoadDisclaimersAction\n | KycControllerAcceptTermsAndStartSessionAction\n | KycControllerClearSavedTermsAction\n | KycControllerHandleFrameMessageAction\n | KycControllerBuildCheckFrameUrlAction\n | KycControllerBuildAuthFrameUrlAction\n | KycControllerBuildResetFrameUrlAction\n | KycControllerCheckKycRequiredAction\n | KycControllerGetKycStatusAction\n | KycControllerStartSumSubAction\n | KycControllerResetAction;\n"]}