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
48 changes: 30 additions & 18 deletions packages/kyc-controller/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`):
`getGeoCountry`, `fetchVendorDisclaimers`, `createSession`, `checkKycRequired`,
`createVendorCustomer`, `submitVendorDisclaimers`, `fetchSessionDisclaimersByCountry`, `fetchSessionDisclaimersBySessionId`, `submitSessionDisclaimers`,
`fetchIdosEnclaveJwks`, `fetchIdosRelayJwks`, `createUkycSession`, `setAuthorizations`,
`createJourney`, `getSessionStatus`.
`createJourney`, `getSessionStatus`, `getLatestSessionStatusForVendor`.

Endpoints:

Expand All @@ -145,6 +145,7 @@ Endpoints:
| `setAuthorizations` | `POST` | `/sessions/{id}/authorizations` | Submit wrapped `data_encryption_key` and wrapped `ukyc_capability_token` |
| `createJourney` | `POST` | `/sessions/{id}/journey` | Create verification journey → applicant token |
| `getSessionStatus` | `GET` | `/sessions/{id}/status` | UKYC session status payload (`KycSessionStatusResponse`; stored on `sessionStatus`) |
| `getLatestSessionStatusForVendor` | `GET` | `/sessions/latest/status/{vendor}` | Same payload as `getSessionStatus`, or `null` when no session exists (HTTP 404) |

### 2.3 `crypto.ts`

Expand Down Expand Up @@ -188,7 +189,7 @@ classDiagram
+Record kycRequiredByProduct [persisted]
+string lastCheckedAt [persisted]
+string sessionId [persisted]
+KycSessionStatusResponse sessionStatus
+KycSessionStatusResponse sessionStatus [persisted]
+SumSubState sumsub
}
class SumSubState {
Expand All @@ -207,9 +208,11 @@ State metadata highlights (`kycControllerMetadata`):

- **Persisted** (`persist: true`): `vendorDisclaimersAccepted`,
`providerDisclaimersAccepted`, `idosDisclaimersAccepted`,
`kycRequiredByProduct`, `lastCheckedAt`, `sessionId`. These survive restarts
so the flow can skip already-accepted terms, reuse cached results, and
resume session-status refresh. Session-scoped `sessionDisclaimers` and
`kycRequiredByProduct`, `lastCheckedAt`, `sessionId`, `sessionStatus`. These
survive restarts so the flow can skip already-accepted terms, reuse cached
results, and resume session-status refresh. `sessionStatus` is always
cleared when `sessionId` is cleared (`reset`, consents rewind).
Session-scoped `sessionDisclaimers` and
`credentialReusabilityConsentGiven` are in-memory only (`persist: false`)
and are cleared on `reset()`.
Acceptance is vendor-scoped: `initialize` (and `createVendorCustomer`) drops
Expand Down Expand Up @@ -285,6 +288,15 @@ stateDiagram-v2
> `kycProvider` document records; `credentialReusabilityConsentGiven` is
> forwarded as well (defaults to `false`).

> **`initialize` hydrates any existing UKYC session for the vendor when no
> `sessionId` is already on state.** After resolving geolocation, `initialize`
> calls `GET /sessions/latest/status/{vendor}`. A 404 continues as a first-time
> flow. Any existing session is reused (`sessionId` / `sessionStatus`). When
> `finalStatus` is already `approved`, `phase` goes to `done` and the rest of
> initialize (vendor customer, terms, MoonPay session, consents) is skipped.
> A non-404 lookup error fails the flow. A persisted `sessionId` skips this
> lookup; a persisted approved `sessionStatus` still finishes at `done`.
>
> **`initialize` and `createVendorCustomer` never tear down an active flow.** If
> `phase` is already one of the in-progress phases (`session`, `check`, `auth`,
> `form`, `submit`), a repeat `initialize` or `createVendorCustomer` is a
Expand Down Expand Up @@ -343,6 +355,10 @@ sequenceDiagram
Ctrl->>Svc: getGeoCountry()
Svc->>Geo: getGeolocation()
Note over Svc: map alpha-2 → alpha-3 locally
Note over Ctrl: skipped when sessionId already set
Ctrl->>Svc: getLatestSessionStatusForVendor({ vendor })
Svc->>API: GET /sessions/latest/status/{vendor}
Note over Ctrl: 404 → first-time flow;<br/>approved → phase = done
Ctrl->>Svc: fetchVendorDisclaimers({ country })
Svc->>API: GET /vendors/moonpay/disclaimers?country=
Ctrl-->>UI: phase = terms (+ vendorDisclaimers)
Expand Down Expand Up @@ -378,11 +394,15 @@ sequenceDiagram
Ctrl-->>UI: phase = done (kycRequiredByProduct[product])

opt kycRequired === true → auto-launch document verification
Ctrl->>Svc: createUkycSession({ jwtToken, sessionClientPublicKey, residenceCountry, vendorMetadata })
Svc->>API: POST /sessions
Note over Ctrl: verify encryptionDataKey vs idOS enclave JWKS,<br/>ukycCapabilityToken vs idOS relay JWKS;<br/>wrap data_encryption_key and ukyc_capability_token
Ctrl->>Svc: setAuthorizations({ sessionId, wrappedEncryptionDataKey, wrappedUkycCapabilityToken })
Svc->>API: POST /sessions/{id}/authorizations
Ctrl->>Svc: getLatestSessionStatusForVendor({ vendor })
Svc->>API: GET /sessions/latest/status/{vendor}
alt no existing session for vendor
Ctrl->>Svc: createUkycSession({ jwtToken, sessionClientPublicKey, residenceCountry, vendorMetadata })
Svc->>API: POST /sessions
Note over Ctrl: verify encryptionDataKey vs idOS enclave JWKS,<br/>ukycCapabilityToken vs idOS relay JWKS;<br/>wrap data_encryption_key and ukyc_capability_token
Ctrl->>Svc: setAuthorizations({ sessionId, wrappedEncryptionDataKey, wrappedUkycCapabilityToken })
Svc->>API: POST /sessions/{id}/authorizations
end
Ctrl->>Svc: createJourney(sessionId)
Svc->>API: POST /sessions/{id}/journey
Ctrl->>Launcher: launch({ applicantAccessToken, onTokenExpiration, onStatusChange })
Expand Down Expand Up @@ -471,7 +491,6 @@ stateDiagram-v2
[*] --> idle
idle --> creatingSession : startSumSub()
creatingSession --> fetchingToken : setAuthorizations() ok
creatingSession --> vendorProcessing : setAuthorizations() kycStatus=approved, finalStatus=pending
fetchingToken --> launching : createJourney() ok
launching --> inProgress : onStatusChange (non-Completed)
launching --> complete : onStatusChange = Completed
Expand All @@ -483,13 +502,6 @@ stateDiagram-v2
launching --> failed : launcher unavailable / error
```

> **Already processing on the vendor.** A user who already finished the journey
> can return to a session the relay has approved (`kycStatus: approved`) while
> the vendor is still finalizing its decision (`finalStatus: pending`). When
> authorizations report this, the sub-flow stops at `vendorProcessing`
> (setting `statusMessage`) instead of launching the SDK, so an already-approved
> applicant is not asked to verify again.

> **Completion is status-driven, not resolution-driven.** A resolved `launch`
> is only recorded as `complete` when the SDK reported the `Completed` status
> via `onStatusChange` at least once. If `launch` resolves without ever having
Expand Down
10 changes: 9 additions & 1 deletion packages/kyc-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `KycService.getLatestSessionStatusForVendor` (`GET /sessions/latest/status/{vendor}`), which returns the same payload as `getSessionStatus` or `null` when no session exists for that vendor (HTTP 404). `KycController.initialize` (when no `sessionId` is on state) and UKYC session creation check this and always reuse that session when one exists — a vendor cannot have more than one. The reused `sessionStatus` is recorded for any `finalStatus`, not only `approved`. An already-`approved` latest session finishes `initialize` at `done`.

### Changed

- **BREAKING:** Drive KYC session decisions from `sessionStatus.finalStatus` only (`new` | `pending` | `approved` | `rejected` | `retry`). `kycStatus` is stored but ignored.
- Terminal values are `approved`, `rejected`, and `retry`. `approved` is the only successful status (`completed` is no longer treated as success).
- **BREAKING:** Move the active UKYC `sessionId` and `sessionStatus` from `sumsub` to the root of `KycControllerState`. ([#10276](https://github.com/MetaMask/core/pull/10276))
- Read `state.sessionId` / `state.sessionStatus` instead of `state.sumsub.sessionId` / `state.sumsub.sessionStatus`. `sessionId` is persisted; `sessionStatus` is not.
- Read `state.sessionId` / `state.sessionStatus` instead of `state.sumsub.sessionId` / `state.sumsub.sessionStatus`. Both are persisted. Clearing `sessionId` also clears `sessionStatus`.
- **BREAKING:** `KycController.refreshKycStatus` now loads status from `GET /sessions/{id}/status` (`getSessionStatus`) instead of `GET /kyc/status`. ([#10276](https://github.com/MetaMask/core/pull/10276))
- Requires an active `sessionId` (throws if missing). Returns and publishes the UKYC `sessionStatus` payload as-is (`null` when none is recorded).
- **BREAKING:** Replace `KycUserStatus` with `KycSessionStatus` (`new` | `pending` | `approved` | `rejected` | `retry`). ([#10276](https://github.com/MetaMask/core/pull/10276))
- **BREAKING:** Rename the `GET /sessions/{id}/status` payload type from `KycSessionStatus` to `KycSessionStatusResponse`. ([#10276](https://github.com/MetaMask/core/pull/10276))
- The session id on that payload is `id`, not `sessionId`. Controller state still uses `sessionId`.
- **BREAKING:** Remove `userStatus`, `userStatusSumsubSessionId`, and `userStatusErrorCode` from `KycControllerState`. ([#10276](https://github.com/MetaMask/core/pull/10276))
- Read `state.sessionStatus`, or use `refreshKycStatus` / `KycController:statusChanged`.
- **BREAKING:** Combine the session-status and user-status poll loops onto one timer. ([#10276](https://github.com/MetaMask/core/pull/10276))
Expand All @@ -23,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

- **BREAKING:** Remove `vendorProcessing` from `KycSumSubStatus`. A pending `finalStatus` continues into the SumSub SDK instead of short-circuiting on `kycStatus: approved`.
- **BREAKING:** Remove `KycService.fetchKycStatus` and the `KycService:fetchKycStatus` messenger action. ([#10276](https://github.com/MetaMask/core/pull/10276))
- **BREAKING:** Remove `KycControllerOptions.userStatusPollIntervalMs`. Use `sessionStatusPollIntervalMs` instead. ([#10276](https://github.com/MetaMask/core/pull/10276))
- **BREAKING:** Remove `KycUserStatusResponse`. Use `KycControllerStatusChangedEvent` / `refreshKycStatus`'s return payload instead. ([#10276](https://github.com/MetaMask/core/pull/10276))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@
import type { KycController } from './KycController.js';

/**
* Resolves persisted terms + geolocation, and auto-creates a session when
* terms are already accepted and an email is available.
* Resolves persisted terms + geolocation, hydrates any existing UKYC
* session for the vendor, and auto-creates a session when terms are already
* accepted and an email is available.
*
* Looks up `GET /sessions/latest/status/{vendor}` after capturing the vendor
* when no `sessionId` is already on state. When a session exists it is reused
* (`sessionId` / `sessionStatus`). When `finalStatus` is already `approved`
* (including a persisted `sessionStatus`), the flow finishes at `done`
* instead of creating a customer or session.
*
* @param params - Optional parameters.
* @param params.email - The account email to associate with the session.
Expand Down Expand Up @@ -211,13 +218,12 @@ export type KycControllerGetCustomerIdentityAction = {
* 5. fetches the SumSub applicant access token; and
* 6. presents the SDK via the injected launcher.
*
* If a UKYC session already exists (the consents path creates it before
* recording session disclaimers), steps 1–4 are skipped.
* If a UKYC session already exists for the vendor (`GET
* /sessions/latest/status/{vendor}`), or `sessionId` is already on state,
* steps 1–4 are skipped. A vendor cannot have more than one session.
*
* If authorizations report the applicant is already approved on the relay
* while the vendor is still finalizing (`kycStatus: approved`,
* `finalStatus: pending`), the sub-flow stops at step 4 with a
* `vendorProcessing` status and a message rather than launching the SDK.
* If the existing session's `finalStatus` is already `approved`, the SDK is
* not launched.
*
* @param params - Optional parameters.
* @param params.locale - BCP-47 locale for the SDK UI.
Expand All @@ -236,7 +242,7 @@ export type KycControllerStartSumSubAction = {
* polling while the status is not terminal.
*
* Throws without an active `sessionId`. Skipped when the recorded
* session status is already successful (`approved` / `completed`): a
* {@link sessionStatus} is already successful (`approved`): a
* follow-up session status can still read a stale `pending` (for example
* after `session_not_in_valid_state`) and must not undo that decision.
*
Expand Down
Loading
Loading