Skip to content
Merged
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
11 changes: 6 additions & 5 deletions packages/kyc-controller/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`):

Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`):

`getGeoCountry`, `fetchDisclaimers`, `createSession`, `checkKycRequired`,
`createVendorCustomer`, `submitVendorDisclaimers`, `fetchSessionDisclaimers`, `submitSessionDisclaimers`,
`getGeoCountry`, `fetchVendorDisclaimers`, `createSession`, `checkKycRequired`,
`createVendorCustomer`, `submitVendorDisclaimers`, `fetchDisclaimersCatalog`, `fetchSessionDisclaimers`, `submitSessionDisclaimers`,
`fetchKycStatus`, `fetchIdosEnclaveJwks`, `fetchIdosRelayJwks`, `createUkycSession`, `setAuthorizations`,
`createJourney`, `getSessionStatus`.

Expand All @@ -131,11 +131,12 @@ Endpoints:
| Method | HTTP | Endpoint | Purpose |
| -------------------------- | ------ | -------------------------------------------- | -------------------------------------------------------------------------------------- |
| `getGeoCountry` | — | (geolocation action) | Resolve alpha-3 country |
| `fetchDisclaimers` | `GET` | `/vendors/{vendor}/disclaimers?country=` | Vendor T&Cs to accept (`vendor` defaults to `moonpay`) |
| `fetchVendorDisclaimers` | `GET` | `/vendors/{vendor}/disclaimers?country=` | Vendor T&Cs 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 |
| `submitVendorDisclaimers` | `POST` | `/vendors/{vendor}/disclaimers` | Record vendor T&C signings (`disclaimerIds`) |
| `fetchDisclaimersCatalog` | `GET` | `/disclaimers?country=` | Global idOS + KYC-provider catalog (no credential-reuse flag) |
| `fetchSessionDisclaimers` | `GET` | `/sessions/{id}/disclaimers` | Session-scoped idOS + KYC-provider catalog |
| `submitSessionDisclaimers` | `POST` | `/sessions/{id}/disclaimers` | Record `{ idOS, kycProvider, credentialReusabilityConsentGiven }` consents |
| `fetchKycStatus` | `GET` | `/kyc/status` | User-keyed simplified KYC status |
Expand Down Expand Up @@ -342,8 +343,8 @@ sequenceDiagram
Ctrl->>Svc: getGeoCountry()
Svc->>Geo: getGeolocation()
Note over Svc: map alpha-2 → alpha-3 locally
Ctrl->>Svc: fetchDisclaimers({ country })
Svc->>API: GET /disclaimers
Ctrl->>Svc: fetchVendorDisclaimers({ country })
Svc->>API: GET /vendors/moonpay/disclaimers?country=
Ctrl-->>UI: phase = terms (+ disclaimers)

User->>Ctrl: acceptTermsAndStartSession({ email, sumsubTncSigned, idosTncSigned })
Expand Down
2 changes: 2 additions & 0 deletions packages/kyc-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Add `KycService.fetchDisclaimersCatalog` / `KycService:fetchDisclaimersCatalog` (`GET /disclaimers?country=`, ISO 3166-1 alpha-3) for the pre-session idOS + KYC-provider catalog, plus the `KycDisclaimersCatalog` type (no `credentialReusabilityConsentGiven`). `fetchSessionDisclaimers` remains session-scoped (`GET /sessions/{sessionId}/disclaimers` → `KycSessionDisclaimers`). ([#10011](https://github.com/MetaMask/core/pull/10011))
- **BREAKING:** Rename `KycService.fetchDisclaimers` / `KycService:fetchDisclaimers` / `KycServiceFetchDisclaimersAction` to `fetchVendorDisclaimers` / `KycService:fetchVendorDisclaimers` / `KycServiceFetchVendorDisclaimersAction`. ([#10011](https://github.com/MetaMask/core/pull/10011))
- **BREAKING:** Rename `fractalEncryptionBaseUrl` to `idosEnclaveBaseUrl`, `KycService.fetchJwks` / `KycService:fetchJwks` / `KycServiceFetchJwksAction` to `fetchIdosEnclaveJwks` / `KycService:fetchIdosEnclaveJwks` / `KycServiceFetchIdosEnclaveJwksAction`, and related Fractal encryption naming to idOS enclave. ([#10008](https://github.com/MetaMask/core/pull/10008))
- **BREAKING:** Verify `encryptionDataKey` against idOS enclave JWKS (`KycService:fetchIdosEnclaveJwks` / `idosEnclaveBaseUrl`) and `ukycCapabilityToken` against idOS relay JWKS (`KycService:fetchIdosRelayJwks` / `idosRelayBaseUrl`) when wrapping UKYC authorizations, instead of validating both schemas against Fractal. Hosts must supply `idosRelayBaseUrl` on `KycService` construction (same class of requirement as `idosEnclaveBaseUrl`). ([#10008](https://github.com/MetaMask/core/pull/10008))
- **BREAKING:** Require `sessionClientPublicKey` (unpadded base64url X25519 public key) and `residenceCountry` (ISO 3166-1 alpha-3) on `KycService.createUkycSession` (`POST /sessions`). The controller generates the per-session keypair before creating the session and uses the private half to wrap authorizations; residence country is taken from the resolved geo country. ([#9993](https://github.com/MetaMask/core/pull/9993))
Expand Down
72 changes: 36 additions & 36 deletions packages/kyc-controller/src/KycController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe('KycController', () => {
it('captures the active product for the automatic post-auth continuation', async () => {
await withController(async ({ controller, handlers }) => {
handlers.getGeoCountry.mockResolvedValue('USA');
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.initialize({ product: 'card' });

Expand All @@ -182,7 +182,7 @@ describe('KycController', () => {
{ options: { state: { activeProduct: 'card' } } },
async ({ controller, handlers }) => {
handlers.getGeoCountry.mockResolvedValue('USA');
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.initialize({ email: 'a@b.co' });

Expand Down Expand Up @@ -239,7 +239,7 @@ describe('KycController', () => {
},
async ({ controller, handlers }) => {
handlers.getGeoCountry.mockResolvedValue('USA');
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.initialize();

Expand All @@ -253,7 +253,7 @@ describe('KycController', () => {
it('loads disclaimers for a provided country', async () => {
await withController(async ({ controller, handlers }) => {
const disclaimers = [{ id: '1', display_name: 'T', url: 'u' }];
handlers.fetchDisclaimers.mockResolvedValue(disclaimers);
handlers.fetchVendorDisclaimers.mockResolvedValue(disclaimers);

await controller.loadDisclaimers({ country: 'USA' });

Expand All @@ -264,7 +264,7 @@ describe('KycController', () => {

it('caches the provided country override in geoCountry', async () => {
await withController(async ({ controller, handlers }) => {
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.loadDisclaimers({ country: 'USA' });

Expand All @@ -276,7 +276,7 @@ describe('KycController', () => {
await withController(
{ options: { state: { accessToken: 'a' } } },
async ({ controller, handlers }) => {
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);
handlers.checkKycRequired.mockResolvedValue({ kycRequired: true });

await controller.loadDisclaimers({ country: 'USA' });
Expand All @@ -297,12 +297,12 @@ describe('KycController', () => {
await withController(
{ options: { state: { geoCountry: 'USA' } } },
async ({ controller, handlers }) => {
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.loadDisclaimers();

expect(handlers.getGeoCountry).not.toHaveBeenCalled();
expect(handlers.fetchDisclaimers).toHaveBeenCalledWith({
expect(handlers.fetchVendorDisclaimers).toHaveBeenCalledWith({
vendor: 'moonpay',
country: 'USA',
});
Expand All @@ -313,12 +313,12 @@ describe('KycController', () => {
it('resolves the country when neither param nor cache is available', async () => {
await withController(async ({ controller, handlers }) => {
handlers.getGeoCountry.mockResolvedValue('FRA');
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.loadDisclaimers();

expect(controller.state.geoCountry).toBe('FRA');
expect(handlers.fetchDisclaimers).toHaveBeenCalledWith({
expect(handlers.fetchVendorDisclaimers).toHaveBeenCalledWith({
vendor: 'moonpay',
country: 'FRA',
});
Expand All @@ -327,7 +327,7 @@ describe('KycController', () => {

it('records an error when loading fails', async () => {
await withController(async ({ controller, handlers }) => {
handlers.fetchDisclaimers.mockRejectedValue(new Error('boom'));
handlers.fetchVendorDisclaimers.mockRejectedValue(new Error('boom'));

await controller.loadDisclaimers({ country: 'USA' });

Expand Down Expand Up @@ -516,7 +516,7 @@ describe('KycController', () => {
},
async ({ controller, handlers }) => {
handlers.createSession.mockRejectedValue(new Error('nope'));
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
sumsubTncSigned: true,
Expand Down Expand Up @@ -569,7 +569,7 @@ describe('KycController', () => {

expect(controller.state.phase).toBe('idle');
expect(controller.state.error).toBeNull();
expect(handlers.fetchDisclaimers).not.toHaveBeenCalled();
expect(handlers.fetchVendorDisclaimers).not.toHaveBeenCalled();
},
);
});
Expand All @@ -587,7 +587,7 @@ describe('KycController', () => {
},
async ({ controller, handlers }) => {
handlers.createSession.mockRejectedValue(new Error('nope'));
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
product: 'ramps',
Expand Down Expand Up @@ -2353,7 +2353,7 @@ describe('KycController', () => {
await pending;

expect(controller.state.phase).toBe('idle');
expect(handlers.fetchDisclaimers).not.toHaveBeenCalled();
expect(handlers.fetchVendorDisclaimers).not.toHaveBeenCalled();
});
});
});
Expand Down Expand Up @@ -2424,7 +2424,7 @@ describe('KycController', () => {

expect(controller.state).toStrictEqual(getDefaultKycControllerState());
// The superseded flow must not resume the terms step either.
expect(handlers.fetchDisclaimers).not.toHaveBeenCalled();
expect(handlers.fetchVendorDisclaimers).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -2530,7 +2530,7 @@ describe('KycController', () => {
it('creates an Iron customer and loads Iron disclaimers on initialize', async () => {
await withController(async ({ controller, handlers }) => {
handlers.getGeoCountry.mockResolvedValue('USA');
handlers.fetchDisclaimers.mockResolvedValue([
handlers.fetchVendorDisclaimers.mockResolvedValue([
{ id: 'd1', display_name: 'Iron T&C', url: 'https://t' },
]);

Expand All @@ -2544,7 +2544,7 @@ describe('KycController', () => {
vendor: 'iron',
email: 'a@b.co',
});
expect(handlers.fetchDisclaimers).toHaveBeenCalledWith({
expect(handlers.fetchVendorDisclaimers).toHaveBeenCalledWith({
vendor: 'iron',
country: 'USA',
});
Expand Down Expand Up @@ -2709,7 +2709,7 @@ describe('KycController', () => {
},
},
async ({ controller, handlers }) => {
handlers.fetchDisclaimers.mockResolvedValue([
handlers.fetchVendorDisclaimers.mockResolvedValue([
{ id: 'iron-d1', display_name: 'T', url: 'u' },
]);

Expand All @@ -2719,7 +2719,7 @@ describe('KycController', () => {
expect(controller.state.termsAcceptedAt).toBeNull();
expect(controller.state.acceptedDisclaimerIds).toStrictEqual([]);
expect(controller.state.termsAcceptedVendor).toBeNull();
expect(handlers.fetchDisclaimers).toHaveBeenCalledWith({
expect(handlers.fetchVendorDisclaimers).toHaveBeenCalledWith({
vendor: 'iron',
country: 'USA',
});
Expand All @@ -2743,7 +2743,7 @@ describe('KycController', () => {
},
},
async ({ controller, handlers }) => {
handlers.fetchDisclaimers.mockResolvedValue([
handlers.fetchVendorDisclaimers.mockResolvedValue([
{ id: 'd1', display_name: 'T', url: 'u' },
]);

Expand Down Expand Up @@ -3255,7 +3255,7 @@ describe('KycController', () => {
"Fetching 'disclaimers' failed with status '409'",
),
);
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -3292,7 +3292,7 @@ describe('KycController', () => {
"Fetching 'disclaimers' failed with status '409'",
),
);
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -3553,7 +3553,7 @@ describe('KycController', () => {
handlers.createUkycSession.mockRejectedValue(
new Error('sumsub down'),
);
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand All @@ -3580,7 +3580,7 @@ describe('KycController', () => {
},
async ({ controller, handlers }) => {
handlers.createJourney.mockRejectedValue(new Error('journey down'));
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -3610,7 +3610,7 @@ describe('KycController', () => {
onStatusChange?.('idle', 'InProgress');
return { ok: false };
});
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -3933,7 +3933,7 @@ describe('KycController', () => {
handlers.submitVendorDisclaimers.mockRejectedValue(
new Error('iron signings down'),
);
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -3995,7 +3995,7 @@ describe('KycController', () => {
"Fetching 'disclaimers' failed with status '500'",
),
);
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -4028,7 +4028,7 @@ describe('KycController', () => {
kycProvider: MOCK_SESSION_DISCLAIMERS.kycProvider,
credentialReusabilityConsentGiven: false,
});
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -4063,7 +4063,7 @@ describe('KycController', () => {
kycProvider: [],
credentialReusabilityConsentGiven: false,
});
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -4108,7 +4108,7 @@ describe('KycController', () => {
"Fetching 'disclaimers' failed with status '409'",
),
);
handlers.fetchDisclaimers.mockResolvedValue([]);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
Expand Down Expand Up @@ -4638,7 +4638,7 @@ type RootMessenger = Messenger<

type ServiceHandlers = {
getGeoCountry: jest.Mock;
fetchDisclaimers: jest.Mock;
fetchVendorDisclaimers: jest.Mock;
createSession: jest.Mock;
checkKycRequired: jest.Mock;
createVendorCustomer: jest.Mock;
Expand Down Expand Up @@ -4674,7 +4674,7 @@ type WithControllerOptions = {

const SERVICE_ACTIONS = [
'KycService:getGeoCountry',
'KycService:fetchDisclaimers',
'KycService:fetchVendorDisclaimers',
'KycService:createSession',
'KycService:checkKycRequired',
'KycService:createVendorCustomer',
Expand Down Expand Up @@ -4775,7 +4775,7 @@ function withController<ReturnValue>(

const handlers: ServiceHandlers = {
getGeoCountry: jest.fn().mockResolvedValue('USA'),
fetchDisclaimers: jest.fn().mockResolvedValue([]),
fetchVendorDisclaimers: jest.fn().mockResolvedValue([]),
createSession: jest.fn().mockResolvedValue({ sessionToken: 'sess' }),
checkKycRequired: jest.fn().mockResolvedValue({ kycRequired: false }),
createVendorCustomer: jest.fn().mockResolvedValue({
Expand Down Expand Up @@ -4820,8 +4820,8 @@ function withController<ReturnValue>(
handlers.getGeoCountry,
);
rootMessenger.registerActionHandler(
'KycService:fetchDisclaimers',
handlers.fetchDisclaimers,
'KycService:fetchVendorDisclaimers',
handlers.fetchVendorDisclaimers,
);
rootMessenger.registerActionHandler(
'KycService:createSession',
Expand Down
8 changes: 4 additions & 4 deletions packages/kyc-controller/src/KycController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ export type KycControllerState = {
/** Error encountered while loading disclaimers, or `null`. */
disclaimersError: string | null;
/**
* Session-scoped idOS / KYC-provider disclaimer catalog from
* `GET /sessions/{sessionId}/disclaimers`. `null` until a UKYC session
* exists and the catalog has been fetched.
* idOS / KYC-provider disclaimer catalog from `GET /disclaimers` or
* `GET /sessions/{sessionId}/disclaimers`. `null` until the catalog has
* been fetched (typically after a UKYC session exists).
*/
sessionDisclaimers: KycSessionDisclaimers | null;

Expand Down Expand Up @@ -986,7 +986,7 @@ export class KycController extends BaseController<
});
}
const disclaimers = await this.messenger.call(
'KycService:fetchDisclaimers',
'KycService:fetchVendorDisclaimers',
{
vendor: this.state.activeVendor,
country,
Expand Down
Loading