From 53b799eb21a5906247bf2fa606142d70c1e4f90a Mon Sep 17 00:00:00 2001 From: Jiexi Luan-Huang Date: Fri, 28 Aug 2026 14:36:19 -0700 Subject: [PATCH 1/4] rename fetchDisclaimers to fetchVendorDisclaimers --- packages/kyc-controller/ARCHITECTURE.md | 6 +- packages/kyc-controller/CHANGELOG.md | 3 +- .../kyc-controller/src/KycController.test.ts | 72 +++++++++---------- packages/kyc-controller/src/KycController.ts | 2 +- .../src/KycService-method-action-types.ts | 10 +-- .../kyc-controller/src/KycService.test.ts | 22 +++--- packages/kyc-controller/src/KycService.ts | 12 ++-- packages/kyc-controller/src/index.ts | 2 +- 8 files changed, 65 insertions(+), 64 deletions(-) diff --git a/packages/kyc-controller/ARCHITECTURE.md b/packages/kyc-controller/ARCHITECTURE.md index c81b079e0d..975a59f889 100644 --- a/packages/kyc-controller/ARCHITECTURE.md +++ b/packages/kyc-controller/ARCHITECTURE.md @@ -121,7 +121,7 @@ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`): Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`): -`getGeoCountry`, `fetchDisclaimers`, `createSession`, `checkKycRequired`, +`getGeoCountry`, `fetchVendorDisclaimers`, `createSession`, `checkKycRequired`, `createVendorCustomer`, `submitVendorDisclaimers`, `fetchSessionDisclaimers`, `submitSessionDisclaimers`, `fetchKycStatus`, `fetchIdosEnclaveJwks`, `fetchIdosRelayJwks`, `createUkycSession`, `setAuthorizations`, `createJourney`, `getSessionStatus`. @@ -131,7 +131,7 @@ 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 | @@ -342,7 +342,7 @@ sequenceDiagram Ctrl->>Svc: getGeoCountry() Svc->>Geo: getGeolocation() Note over Svc: map alpha-2 → alpha-3 locally - Ctrl->>Svc: fetchDisclaimers({ country }) + Ctrl->>Svc: fetchVendorDisclaimers({ country }) Svc->>API: GET /disclaimers Ctrl-->>UI: phase = terms (+ disclaimers) diff --git a/packages/kyc-controller/CHANGELOG.md b/packages/kyc-controller/CHANGELOG.md index 6ab5cc1eba..777b83505d 100644 --- a/packages/kyc-controller/CHANGELOG.md +++ b/packages/kyc-controller/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `KycService.submitVendorDisclaimers({ vendor, disclaimerIds })` (`POST /vendors/{vendor}/disclaimers`) to record Iron T&C signings, plus the `KycVendorSigning` response type. The consents path calls this alongside session-scoped disclaimers; vendor T&C ids are no longer sent on the session disclaimer POST. ([#9979](https://github.com/MetaMask/core/pull/9979)) - Add `KycConsentDocument`, `KycConsentRecord`, and `KycSessionDisclaimers` types for that catalog, plus in-memory `credentialReusabilityConsentGiven` and `sessionDisclaimers` controller state. `acceptTermsAndStartSession` forwards optional `credentialReusabilityConsentGiven` (default `false`). ([#9979](https://github.com/MetaMask/core/pull/9979)) - Parameterize Universal KYC vendor HTTP on `KycService` so identity vendors share one client surface instead of vendor-branded methods ([#9908](https://github.com/MetaMask/core/pull/9908)): - - `fetchDisclaimers({ vendor, country })` and `checkKycRequired({ vendor, ... })` call `/vendors/{vendor}/disclaimers` and `/vendors/{vendor}/kyc-required` (`vendor` defaults to `moonpay`) + - `fetchVendorDisclaimers({ vendor, country })` and `checkKycRequired({ vendor, ... })` call `/vendors/{vendor}/disclaimers` and `/vendors/{vendor}/kyc-required` (`vendor` defaults to `moonpay`) - `createVendorCustomer({ vendor, email })` calls `POST /vendors/{vendor}/customers` - `fetchKycStatus()` reads `GET /kyc/status` - Add a consents-path KYC flow on `KycController` for non-MoonPay vendors (currently `iron`): empty-shell customer → disclaimers → consents → SumSub, skipping MoonPay Check/Auth frames. `initialize({ vendor })` and `createVendorCustomer({ vendor, email })` drive the path; `acceptTermsAndStartSession` requires `sumsubTncSigned` / `idosTncSigned`. ([#9908](https://github.com/MetaMask/core/pull/9908)) @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Rename `KycService.fetchDisclaimers` / `KycService:fetchDisclaimers` / `KycServiceFetchDisclaimersAction` to `fetchVendorDisclaimers` / `KycService:fetchVendorDisclaimers` / `KycServiceFetchVendorDisclaimersAction`. - **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)) diff --git a/packages/kyc-controller/src/KycController.test.ts b/packages/kyc-controller/src/KycController.test.ts index d39fa78ae4..5651378d5e 100644 --- a/packages/kyc-controller/src/KycController.test.ts +++ b/packages/kyc-controller/src/KycController.test.ts @@ -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' }); @@ -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' }); @@ -239,7 +239,7 @@ describe('KycController', () => { }, async ({ controller, handlers }) => { handlers.getGeoCountry.mockResolvedValue('USA'); - handlers.fetchDisclaimers.mockResolvedValue([]); + handlers.fetchVendorDisclaimers.mockResolvedValue([]); await controller.initialize(); @@ -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' }); @@ -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' }); @@ -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' }); @@ -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', }); @@ -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', }); @@ -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' }); @@ -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, @@ -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(); }, ); }); @@ -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', @@ -2353,7 +2353,7 @@ describe('KycController', () => { await pending; expect(controller.state.phase).toBe('idle'); - expect(handlers.fetchDisclaimers).not.toHaveBeenCalled(); + expect(handlers.fetchVendorDisclaimers).not.toHaveBeenCalled(); }); }); }); @@ -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(); }); }); @@ -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' }, ]); @@ -2544,7 +2544,7 @@ describe('KycController', () => { vendor: 'iron', email: 'a@b.co', }); - expect(handlers.fetchDisclaimers).toHaveBeenCalledWith({ + expect(handlers.fetchVendorDisclaimers).toHaveBeenCalledWith({ vendor: 'iron', country: 'USA', }); @@ -2709,7 +2709,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers }) => { - handlers.fetchDisclaimers.mockResolvedValue([ + handlers.fetchVendorDisclaimers.mockResolvedValue([ { id: 'iron-d1', display_name: 'T', url: 'u' }, ]); @@ -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', }); @@ -2743,7 +2743,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers }) => { - handlers.fetchDisclaimers.mockResolvedValue([ + handlers.fetchVendorDisclaimers.mockResolvedValue([ { id: 'd1', display_name: 'T', url: 'u' }, ]); @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -4063,7 +4063,7 @@ describe('KycController', () => { kycProvider: [], credentialReusabilityConsentGiven: false, }); - handlers.fetchDisclaimers.mockResolvedValue([]); + handlers.fetchVendorDisclaimers.mockResolvedValue([]); await controller.acceptTermsAndStartSession({ email: 'a@b.co', @@ -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', @@ -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; @@ -4674,7 +4674,7 @@ type WithControllerOptions = { const SERVICE_ACTIONS = [ 'KycService:getGeoCountry', - 'KycService:fetchDisclaimers', + 'KycService:fetchVendorDisclaimers', 'KycService:createSession', 'KycService:checkKycRequired', 'KycService:createVendorCustomer', @@ -4775,7 +4775,7 @@ function withController( 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({ @@ -4820,8 +4820,8 @@ function withController( handlers.getGeoCountry, ); rootMessenger.registerActionHandler( - 'KycService:fetchDisclaimers', - handlers.fetchDisclaimers, + 'KycService:fetchVendorDisclaimers', + handlers.fetchVendorDisclaimers, ); rootMessenger.registerActionHandler( 'KycService:createSession', diff --git a/packages/kyc-controller/src/KycController.ts b/packages/kyc-controller/src/KycController.ts index d1c914178b..b5a9295ab7 100644 --- a/packages/kyc-controller/src/KycController.ts +++ b/packages/kyc-controller/src/KycController.ts @@ -986,7 +986,7 @@ export class KycController extends BaseController< }); } const disclaimers = await this.messenger.call( - 'KycService:fetchDisclaimers', + 'KycService:fetchVendorDisclaimers', { vendor: this.state.activeVendor, country, diff --git a/packages/kyc-controller/src/KycService-method-action-types.ts b/packages/kyc-controller/src/KycService-method-action-types.ts index 4da7d075de..276164a200 100644 --- a/packages/kyc-controller/src/KycService-method-action-types.ts +++ b/packages/kyc-controller/src/KycService-method-action-types.ts @@ -26,9 +26,9 @@ export type KycServiceGetGeoCountryAction = { * @param params.country - ISO 3166-1 alpha-3 country code. * @returns The disclaimers. */ -export type KycServiceFetchDisclaimersAction = { - type: `KycService:fetchDisclaimers`; - handler: KycService['fetchDisclaimers']; +export type KycServiceFetchVendorDisclaimersAction = { + type: `KycService:fetchVendorDisclaimers`; + handler: KycService['fetchVendorDisclaimers']; }; /** @@ -90,7 +90,7 @@ export type KycServiceSubmitVendorDisclaimersAction = { /** * Fetches the session-scoped idOS + KYC-provider disclaimer catalog * (`GET /sessions/{sessionId}/disclaimers`). Requires an existing UKYC - * session; vendor T&Cs continue to come from {@link fetchDisclaimers}. + * session; vendor T&Cs continue to come from {@link fetchVendorDisclaimers}. * * @param params - The parameters. * @param params.sessionId - The UKYC session id. @@ -218,7 +218,7 @@ export type KycServiceGetSessionStatusAction = { */ export type KycServiceMethodActions = | KycServiceGetGeoCountryAction - | KycServiceFetchDisclaimersAction + | KycServiceFetchVendorDisclaimersAction | KycServiceCreateSessionAction | KycServiceCheckKycRequiredAction | KycServiceCreateVendorCustomerAction diff --git a/packages/kyc-controller/src/KycService.test.ts b/packages/kyc-controller/src/KycService.test.ts index dec4ed331b..a90022da51 100644 --- a/packages/kyc-controller/src/KycService.test.ts +++ b/packages/kyc-controller/src/KycService.test.ts @@ -31,7 +31,7 @@ describe('KycService', () => { .reply(200, disclaimers); const { service } = getService({ omitFetch: true }); - expect(await service.fetchDisclaimers({ country: 'USA' })).toStrictEqual( + expect(await service.fetchVendorDisclaimers({ country: 'USA' })).toStrictEqual( disclaimers, ); }); @@ -102,7 +102,7 @@ describe('KycService', () => { }); }); - describe('fetchDisclaimers', () => { + describe('fetchVendorDisclaimers', () => { it('returns the disclaimers for a country', async () => { const disclaimers = [ { id: '1', display_name: 'Terms', url: 'https://t' }, @@ -113,7 +113,7 @@ describe('KycService', () => { .reply(200, disclaimers); const { service } = getService(); - expect(await service.fetchDisclaimers({ country: 'USA' })).toStrictEqual( + expect(await service.fetchVendorDisclaimers({ country: 'USA' })).toStrictEqual( disclaimers, ); }); @@ -126,14 +126,14 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchDisclaimers({ country: 'USA' }), + service.fetchVendorDisclaimers({ country: 'USA' }), ).rejects.toThrow(/Malformed response received from disclaimers API/u); }); it('throws when no bearer token is available', async () => { const { service } = getService({ bearerToken: '' }); await expect( - service.fetchDisclaimers({ country: 'USA' }), + service.fetchVendorDisclaimers({ country: 'USA' }), ).rejects.toThrow(/Unable to obtain an authentication bearer token/u); }); @@ -145,7 +145,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchDisclaimers({ country: 'USA' }), + service.fetchVendorDisclaimers({ country: 'USA' }), ).rejects.toThrow(/failed with status '500'/u); }); }); @@ -672,7 +672,7 @@ describe('KycService', () => { }); }); - describe('fetchDisclaimers for a non-MoonPay vendor', () => { + describe('fetchVendorDisclaimers for a non-MoonPay vendor', () => { it('returns Iron disclaimers for a country', async () => { const disclaimers = [ { id: '1', display_name: 'Iron Terms', url: 'https://t' }, @@ -684,7 +684,7 @@ describe('KycService', () => { const { service } = getService(); expect( - await service.fetchDisclaimers({ vendor: 'iron', country: 'USA' }), + await service.fetchVendorDisclaimers({ vendor: 'iron', country: 'USA' }), ).toStrictEqual(disclaimers); }); @@ -696,7 +696,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchDisclaimers({ vendor: 'iron', country: 'USA' }), + service.fetchVendorDisclaimers({ vendor: 'iron', country: 'USA' }), ).rejects.toThrow(/Malformed response received from disclaimers API/u); }); }); @@ -983,7 +983,7 @@ describe('KycService', () => { .reply(200, disclaimers); const { service } = getService({ baseUrl: customUrl }); - expect(await service.fetchDisclaimers({ country: 'USA' })).toStrictEqual( + expect(await service.fetchVendorDisclaimers({ country: 'USA' })).toStrictEqual( disclaimers, ); }); @@ -1004,7 +1004,7 @@ describe('KycService', () => { const { rootMessenger } = getService(); expect( - await rootMessenger.call('KycService:fetchDisclaimers', { + await rootMessenger.call('KycService:fetchVendorDisclaimers', { country: 'USA', }), ).toStrictEqual([]); diff --git a/packages/kyc-controller/src/KycService.ts b/packages/kyc-controller/src/KycService.ts index 5d028c34e6..f039537a96 100644 --- a/packages/kyc-controller/src/KycService.ts +++ b/packages/kyc-controller/src/KycService.ts @@ -48,7 +48,7 @@ export const serviceName = 'KycService'; const MESSENGER_EXPOSED_METHODS = [ 'getGeoCountry', - 'fetchDisclaimers', + 'fetchVendorDisclaimers', 'createSession', 'checkKycRequired', 'createVendorCustomer', @@ -300,7 +300,7 @@ export type CreateVendorCustomerParams = { export type SubmitVendorDisclaimersParams = { /** Identity vendor whose T&Cs were accepted (currently `iron`). */ vendor: KycVendor; - /** Disclaimer ids from {@link KycService.fetchDisclaimers}. */ + /** Disclaimer ids from {@link KycService.fetchVendorDisclaimers}. */ disclaimerIds: string[]; }; @@ -381,7 +381,7 @@ export type GetSessionStatusParams = { * It extends {@link BaseDataService}, so every request is routed through * `fetchQuery`: it is wrapped in the shared service policy (retries, circuit * breaker) and its result is exposed via the service's `QueryClient`. Read-only - * endpoints (`fetchDisclaimers`, `fetchIdosEnclaveJwks`, `fetchIdosRelayJwks`) are cached + * endpoints (`fetchVendorDisclaimers`, `fetchIdosEnclaveJwks`, `fetchIdosRelayJwks`) are cached * with a `staleTime`; vendor-disclaimer, session-scoped disclaimer, * session-creating, and status-polling endpoints opt out of caching * (`staleTime`/`gcTime` of `0`) so they never serve a stale result. @@ -497,7 +497,7 @@ export class KycService extends BaseDataService< * @param params.country - ISO 3166-1 alpha-3 country code. * @returns The disclaimers. */ - async fetchDisclaimers({ + async fetchVendorDisclaimers({ vendor = 'moonpay', country, }: { @@ -507,7 +507,7 @@ export class KycService extends BaseDataService< const url = new URL(`/vendors/${vendor}/disclaimers`, this.#baseUrl); url.searchParams.set('country', country); const data = await this.fetchQuery({ - queryKey: [`${this.name}:fetchDisclaimers`, vendor, country], + queryKey: [`${this.name}:fetchVendorDisclaimers`, vendor, country], queryFn: async () => this.#requestJson(url, { method: 'GET' }), staleTime: inMilliseconds(5, Duration.Minute), }); @@ -692,7 +692,7 @@ export class KycService extends BaseDataService< /** * Fetches the session-scoped idOS + KYC-provider disclaimer catalog * (`GET /sessions/{sessionId}/disclaimers`). Requires an existing UKYC - * session; vendor T&Cs continue to come from {@link fetchDisclaimers}. + * session; vendor T&Cs continue to come from {@link fetchVendorDisclaimers}. * * @param params - The parameters. * @param params.sessionId - The UKYC session id. diff --git a/packages/kyc-controller/src/index.ts b/packages/kyc-controller/src/index.ts index 985991a7d3..8fbd396347 100644 --- a/packages/kyc-controller/src/index.ts +++ b/packages/kyc-controller/src/index.ts @@ -64,7 +64,7 @@ export type { KycServiceCreateJourneyAction, KycServiceCreateSessionAction, KycServiceCreateUkycSessionAction, - KycServiceFetchDisclaimersAction, + KycServiceFetchVendorDisclaimersAction, KycServiceFetchIdosEnclaveJwksAction, KycServiceFetchIdosRelayJwksAction, KycServiceFetchKycStatusAction, From d58a971435fa49a73bcec4a3b8f94a435e7cffa2 Mon Sep 17 00:00:00 2001 From: Jiexi Luan-Huang Date: Fri, 28 Aug 2026 14:57:11 -0700 Subject: [PATCH 2/4] add GET `/disclaimers` --- packages/kyc-controller/ARCHITECTURE.md | 4 +- packages/kyc-controller/CHANGELOG.md | 3 +- packages/kyc-controller/src/KycController.ts | 6 +- .../src/KycService-method-action-types.ts | 17 ++- .../kyc-controller/src/KycService.test.ts | 108 ++++++++++++++++- packages/kyc-controller/src/KycService.ts | 111 +++++++++++++++--- packages/kyc-controller/src/index.ts | 1 + packages/kyc-controller/src/types.ts | 22 +++- 8 files changed, 235 insertions(+), 37 deletions(-) diff --git a/packages/kyc-controller/ARCHITECTURE.md b/packages/kyc-controller/ARCHITECTURE.md index 975a59f889..35b2ed84a4 100644 --- a/packages/kyc-controller/ARCHITECTURE.md +++ b/packages/kyc-controller/ARCHITECTURE.md @@ -136,7 +136,7 @@ Endpoints: | `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`) | -| `fetchSessionDisclaimers` | `GET` | `/sessions/{id}/disclaimers` | Session-scoped idOS + KYC-provider catalog | +| `fetchSessionDisclaimers` | `GET` | `/disclaimers?country=` or `/sessions/{id}/disclaimers` | idOS + KYC-provider catalog (`country` XOR `sessionId`; global omits credential-reuse) | | `submitSessionDisclaimers` | `POST` | `/sessions/{id}/disclaimers` | Record `{ idOS, kycProvider, credentialReusabilityConsentGiven }` consents | | `fetchKycStatus` | `GET` | `/kyc/status` | User-keyed simplified KYC status | | `fetchIdosEnclaveJwks` | `GET` | `{idosEnclaveBaseUrl}/.well-known/jwks.json` | idOS enclave JWKS for `encryptionDataKey` attestation | @@ -343,7 +343,7 @@ sequenceDiagram Svc->>Geo: getGeolocation() Note over Svc: map alpha-2 → alpha-3 locally Ctrl->>Svc: fetchVendorDisclaimers({ country }) - Svc->>API: GET /disclaimers + Svc->>API: GET /vendors/moonpay/disclaimers?country= Ctrl-->>UI: phase = terms (+ disclaimers) User->>Ctrl: acceptTermsAndStartSession({ email, sumsubTncSigned, idosTncSigned }) diff --git a/packages/kyc-controller/CHANGELOG.md b/packages/kyc-controller/CHANGELOG.md index 777b83505d..a6b0631946 100644 --- a/packages/kyc-controller/CHANGELOG.md +++ b/packages/kyc-controller/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `idosRelayBaseUrl` on `KycService` and `KycService.fetchIdosRelayJwks()` to fetch the idOS relay well-known JWKS used to verify the `ukycCapabilityToken` encryption schema. `encryptionDataKey` continues to verify against idOS enclave JWKS via `fetchIdosEnclaveJwks` / `idosEnclaveBaseUrl`. ([#10008](https://github.com/MetaMask/core/pull/10008)) - Add session-scoped disclaimer APIs on `KycService` for the idOS / KYC-provider catalog ([#9979](https://github.com/MetaMask/core/pull/9979)): - - `fetchSessionDisclaimers({ sessionId })` calls `GET /sessions/{sessionId}/disclaimers` + - `fetchSessionDisclaimers({ sessionId })` calls `GET /sessions/{sessionId}/disclaimers` (includes `credentialReusabilityConsentGiven`), or `fetchSessionDisclaimers({ country })` calls `GET /disclaimers?country=` (catalog only; `country` is ISO 3166-1 alpha-3) - `submitSessionDisclaimers({ sessionId, idOS, kycProvider, credentialReusabilityConsentGiven })` calls `POST /sessions/{sessionId}/disclaimers` - Add `KycService.submitVendorDisclaimers({ vendor, disclaimerIds })` (`POST /vendors/{vendor}/disclaimers`) to record Iron T&C signings, plus the `KycVendorSigning` response type. The consents path calls this alongside session-scoped disclaimers; vendor T&C ids are no longer sent on the session disclaimer POST. ([#9979](https://github.com/MetaMask/core/pull/9979)) - Add `KycConsentDocument`, `KycConsentRecord`, and `KycSessionDisclaimers` types for that catalog, plus in-memory `credentialReusabilityConsentGiven` and `sessionDisclaimers` controller state. `acceptTermsAndStartSession` forwards optional `credentialReusabilityConsentGiven` (default `false`). ([#9979](https://github.com/MetaMask/core/pull/9979)) @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Make `KycService.fetchSessionDisclaimers` accept either `{ sessionId }` (`GET /sessions/{sessionId}/disclaimers`) or `{ country }` (`GET /disclaimers?country=`, ISO 3166-1 alpha-3). The two params are mutually exclusive; `country` is required for the global catalog. Adds the `KycDisclaimersCatalog` type for the global response (no `credentialReusabilityConsentGiven`); `KycSessionDisclaimers` remains the session-scoped shape. - **BREAKING:** Rename `KycService.fetchDisclaimers` / `KycService:fetchDisclaimers` / `KycServiceFetchDisclaimersAction` to `fetchVendorDisclaimers` / `KycService:fetchVendorDisclaimers` / `KycServiceFetchVendorDisclaimersAction`. - **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)) diff --git a/packages/kyc-controller/src/KycController.ts b/packages/kyc-controller/src/KycController.ts index b5a9295ab7..94a7af58c5 100644 --- a/packages/kyc-controller/src/KycController.ts +++ b/packages/kyc-controller/src/KycController.ts @@ -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; diff --git a/packages/kyc-controller/src/KycService-method-action-types.ts b/packages/kyc-controller/src/KycService-method-action-types.ts index 276164a200..ef4ed25fec 100644 --- a/packages/kyc-controller/src/KycService-method-action-types.ts +++ b/packages/kyc-controller/src/KycService-method-action-types.ts @@ -88,13 +88,18 @@ export type KycServiceSubmitVendorDisclaimersAction = { }; /** - * Fetches the session-scoped idOS + KYC-provider disclaimer catalog - * (`GET /sessions/{sessionId}/disclaimers`). Requires an existing UKYC - * session; vendor T&Cs continue to come from {@link fetchVendorDisclaimers}. + * Fetches the idOS + KYC-provider disclaimer catalog. * - * @param params - The parameters. - * @param params.sessionId - The UKYC session id. - * @returns The catalog, including which documents are already consented. + * - With `sessionId`: `GET /sessions/{sessionId}/disclaimers` (includes + * `credentialReusabilityConsentGiven`). Do not pass `country`. + * - With `country` (ISO 3166-1 alpha-3): `GET /disclaimers?country=` (catalog + * only). Do not pass `sessionId`. + * + * Vendor T&Cs continue to come from {@link fetchVendorDisclaimers}. + * + * @param params - Either `{ sessionId }` or `{ country }`. + * @returns The catalog. Session fetches include + * `credentialReusabilityConsentGiven`; global fetches do not. */ export type KycServiceFetchSessionDisclaimersAction = { type: `KycService:fetchSessionDisclaimers`; diff --git a/packages/kyc-controller/src/KycService.test.ts b/packages/kyc-controller/src/KycService.test.ts index a90022da51..0454210a85 100644 --- a/packages/kyc-controller/src/KycService.test.ts +++ b/packages/kyc-controller/src/KycService.test.ts @@ -740,7 +740,7 @@ describe('KycService', () => { }); describe('fetchSessionDisclaimers', () => { - const catalog = { + const documents = { idOS: [ { key: 'idos-tos', @@ -759,6 +759,9 @@ describe('KycService', () => { consented: false, }, ], + }; + const catalog = { + ...documents, credentialReusabilityConsentGiven: false, }; @@ -771,6 +774,96 @@ describe('KycService', () => { ).toStrictEqual(catalog); }); + it('returns the global disclaimer catalog for a country', async () => { + nock(MOCK_API_URL) + .get('/disclaimers') + .query({ country: 'USA' }) + .reply(200, documents); + const { service } = getService(); + + expect( + await service.fetchSessionDisclaimers({ country: 'USA' }), + ).toStrictEqual(documents); + }); + + it('uses the global catalog for an empty sessionId when country is set', async () => { + nock(MOCK_API_URL) + .get('/disclaimers') + .query({ country: 'FRA' }) + .reply(200, documents); + const { service } = getService(); + + expect( + await service.fetchSessionDisclaimers({ + sessionId: '', + country: 'FRA', + }), + ).toStrictEqual(documents); + }); + + it('throws when neither sessionId nor country is provided', async () => { + const { service } = getService(); + + await expect(service.fetchSessionDisclaimers()).rejects.toThrow( + /country.*required/iu, + ); + }); + + it('throws when country is not a 3-character code', async () => { + const { service } = getService(); + + await expect( + service.fetchSessionDisclaimers({ country: 'US' }), + ).rejects.toThrow(/ISO 3166-1 alpha-3/u); + }); + + it('throws when both sessionId and country are provided', async () => { + const { service } = getService(); + + await expect( + service.fetchSessionDisclaimers({ + sessionId: 'sid-1', + country: 'USA', + }), + ).rejects.toThrow(/must not be provided together/iu); + }); + + it('throws on a malformed global catalog response', async () => { + nock(MOCK_API_URL) + .get('/disclaimers') + .query({ country: 'USA' }) + .reply(200, {}); + const { service } = getService(); + + await expect( + service.fetchSessionDisclaimers({ country: 'USA' }), + ).rejects.toThrow(/Malformed response received from disclaimers API/u); + }); + + it('throws when the global catalog is missing kycProvider', async () => { + nock(MOCK_API_URL) + .get('/disclaimers') + .query({ country: 'USA' }) + .reply(200, { idOS: [] }); + const { service } = getService(); + + await expect( + service.fetchSessionDisclaimers({ country: 'USA' }), + ).rejects.toThrow(/Malformed response received from disclaimers API/u); + }); + + it('throws an HttpError on a non-ok global catalog response', async () => { + nock(MOCK_API_URL) + .get('/disclaimers') + .query({ country: 'USA' }) + .reply(500); + const { service } = getService(); + + await expect( + service.fetchSessionDisclaimers({ country: 'USA' }), + ).rejects.toThrow(/failed with status '500'/u); + }); + it('throws on a malformed response', async () => { nock(MOCK_API_URL).get('/sessions/sid-1/disclaimers').reply(200, {}); const { service } = getService(); @@ -782,6 +875,19 @@ describe('KycService', () => { ); }); + it('throws when the session catalog omits credentialReusabilityConsentGiven', async () => { + nock(MOCK_API_URL) + .get('/sessions/sid-1/disclaimers') + .reply(200, documents); + const { service } = getService(); + + await expect( + service.fetchSessionDisclaimers({ sessionId: 'sid-1' }), + ).rejects.toThrow( + /Malformed response received from session disclaimers API/u, + ); + }); + it('throws an HttpError on a non-ok response', async () => { nock(MOCK_API_URL).get('/sessions/sid-1/disclaimers').reply(500); const { service } = getService(); diff --git a/packages/kyc-controller/src/KycService.ts b/packages/kyc-controller/src/KycService.ts index f039537a96..10e35ced38 100644 --- a/packages/kyc-controller/src/KycService.ts +++ b/packages/kyc-controller/src/KycService.ts @@ -29,6 +29,7 @@ import type { KycServiceMethodActions } from './KycService-method-action-types.j import type { KycConsentRecord, KycDisclaimer, + KycDisclaimersCatalog, KycSessionDisclaimers, KycSessionStatus, KycUserStatusResponse, @@ -261,9 +262,17 @@ const ConsentDocumentStruct = type({ consented: boolean(), }); -const SessionDisclaimersResponseStruct = type({ +const DisclaimersCatalogFields = { idOS: array(ConsentDocumentStruct), kycProvider: array(ConsentDocumentStruct), +} as const; + +/** Global catalog from `GET /disclaimers` (no credential-reuse flag). */ +const GlobalDisclaimersResponseStruct = type(DisclaimersCatalogFields); + +/** Session catalog from `GET`/`POST /sessions/{id}/disclaimers`. */ +const SessionDisclaimersResponseStruct = type({ + ...DisclaimersCatalogFields, credentialReusabilityConsentGiven: boolean(), }); @@ -305,8 +314,18 @@ export type SubmitVendorDisclaimersParams = { }; export type FetchSessionDisclaimersParams = { - /** UKYC session id from {@link KycService.createUkycSession}. */ - sessionId: string; + /** + * UKYC session id from {@link KycService.createUkycSession}. When set, + * fetches `GET /sessions/{sessionId}/disclaimers`. Mutually exclusive with + * `country`. + */ + sessionId?: string; + /** + * ISO 3166-1 alpha-3 country code. Required for the global catalog + * (`GET /disclaimers?country=`). Must not be set when `sessionId` is + * provided. + */ + country?: string; }; export type SubmitSessionDisclaimersParams = { @@ -690,32 +709,88 @@ export class KycService extends BaseDataService< } /** - * Fetches the session-scoped idOS + KYC-provider disclaimer catalog - * (`GET /sessions/{sessionId}/disclaimers`). Requires an existing UKYC - * session; vendor T&Cs continue to come from {@link fetchVendorDisclaimers}. + * Fetches the idOS + KYC-provider disclaimer catalog. + * + * - With `sessionId`: `GET /sessions/{sessionId}/disclaimers` (includes + * per-session `consented` flags and `credentialReusabilityConsentGiven`). + * Do not pass `country`. + * - With `country` (ISO 3166-1 alpha-3): `GET /disclaimers?country=` for the + * global catalog (no credential-reuse field). Do not pass `sessionId`. + * + * Vendor T&Cs continue to come from {@link fetchVendorDisclaimers}. * * @param params - The parameters. - * @param params.sessionId - The UKYC session id. - * @returns The catalog, including which documents are already consented. + * @returns The catalog. Session fetches include + * `credentialReusabilityConsentGiven`; global fetches do not. */ + fetchSessionDisclaimers(params: { + sessionId: string; + }): Promise; + + fetchSessionDisclaimers(params: { + country: string; + }): Promise; + async fetchSessionDisclaimers( - params: FetchSessionDisclaimersParams, - ): Promise { - const url = new URL( - `/sessions/${encodeURIComponent(params.sessionId)}/disclaimers`, - this.#baseUrl, - ); + params: FetchSessionDisclaimersParams = {}, + ): Promise { + const sessionId = + typeof params.sessionId === 'string' && params.sessionId.length > 0 + ? params.sessionId + : undefined; + const country = + typeof params.country === 'string' && params.country.length > 0 + ? params.country + : undefined; + + if (sessionId && country) { + throw new Error( + 'KycService.fetchSessionDisclaimers: sessionId and country must not be provided together.', + ); + } + + if (sessionId) { + const url = new URL( + `/sessions/${encodeURIComponent(sessionId)}/disclaimers`, + this.#baseUrl, + ); + const data = await this.fetchQuery({ + queryKey: [`${this.name}:fetchSessionDisclaimers`, sessionId], + queryFn: async () => this.#requestJson(url, { method: 'GET' }), + // Consent state can change after a POST, so always re-fetch. + staleTime: 0, + gcTime: 0, + }); + return this.#validateResponse( + data, + SessionDisclaimersResponseStruct, + 'session disclaimers', + ); + } + + if (!country) { + throw new Error( + 'KycService.fetchSessionDisclaimers: country is required when sessionId is omitted.', + ); + } + if (country.length !== 3) { + throw new Error( + `KycService.fetchSessionDisclaimers: country must be an ISO 3166-1 alpha-3 code (received "${country}").`, + ); + } + + const url = new URL('/disclaimers', this.#baseUrl); + url.searchParams.set('country', country); const data = await this.fetchQuery({ - queryKey: [`${this.name}:fetchSessionDisclaimers`, params.sessionId], + queryKey: [`${this.name}:fetchSessionDisclaimers`, country], queryFn: async () => this.#requestJson(url, { method: 'GET' }), - // Consent state can change after a POST, so always re-fetch. staleTime: 0, gcTime: 0, }); return this.#validateResponse( data, - SessionDisclaimersResponseStruct, - 'session disclaimers', + GlobalDisclaimersResponseStruct, + 'disclaimers', ); } diff --git a/packages/kyc-controller/src/index.ts b/packages/kyc-controller/src/index.ts index 8fbd396347..bb03d07c11 100644 --- a/packages/kyc-controller/src/index.ts +++ b/packages/kyc-controller/src/index.ts @@ -96,6 +96,7 @@ export type { KycConsentRecord, KycCustomerIdentity, KycDisclaimer, + KycDisclaimersCatalog, KycPhase, KycProduct, KycSessionDisclaimers, diff --git a/packages/kyc-controller/src/types.ts b/packages/kyc-controller/src/types.ts index ef4198ad0d..240f8d6eb0 100644 --- a/packages/kyc-controller/src/types.ts +++ b/packages/kyc-controller/src/types.ts @@ -164,8 +164,8 @@ export type KycVendorSigning = { }; /** - * A legal document in the session-scoped idOS / KYC-provider catalog - * (`GET`/`POST /sessions/{sessionId}/disclaimers`). + * A legal document in the idOS / KYC-provider catalog + * (`GET /disclaimers`, or `GET`/`POST /sessions/{sessionId}/disclaimers`). */ export type KycConsentDocument = { /** Stable identifier of the legal document. */ @@ -176,7 +176,10 @@ export type KycConsentDocument = { title: string; /** URL the document body is hosted at. */ url: string; - /** Whether this session already consented to this document version. */ + /** + * Whether the document version has already been consented to (session-scoped + * fetches). For the global catalog this is typically `false`. + */ consented: boolean; }; @@ -190,14 +193,21 @@ export type KycConsentRecord = { }; /** - * Session-scoped disclaimer catalog returned by - * `GET`/`POST /sessions/{sessionId}/disclaimers`. + * idOS / KYC-provider disclaimer catalog returned by + * `GET /disclaimers?country=` (no session — no credential-reuse consent state). */ -export type KycSessionDisclaimers = { +export type KycDisclaimersCatalog = { /** idOS legal documents. */ idOS: KycConsentDocument[]; /** KYC provider (SumSub) legal documents. */ kycProvider: KycConsentDocument[]; +}; + +/** + * Session-scoped disclaimer catalog returned by + * `GET`/`POST /sessions/{sessionId}/disclaimers`. + */ +export type KycSessionDisclaimers = KycDisclaimersCatalog & { /** Whether the user consented to reuse existing idOS credentials. */ credentialReusabilityConsentGiven: boolean; }; From 91372274818066c363ed11cd594a81e8deb8712c Mon Sep 17 00:00:00 2001 From: Jiexi Luan-Huang Date: Mon, 31 Aug 2026 11:24:45 -0700 Subject: [PATCH 3/4] lint --- packages/kyc-controller/ARCHITECTURE.md | 30 ++++++++++++------------- packages/kyc-controller/CHANGELOG.md | 8 +++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/kyc-controller/ARCHITECTURE.md b/packages/kyc-controller/ARCHITECTURE.md index 35b2ed84a4..b14b3150d2 100644 --- a/packages/kyc-controller/ARCHITECTURE.md +++ b/packages/kyc-controller/ARCHITECTURE.md @@ -128,22 +128,22 @@ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`): Endpoints: -| Method | HTTP | Endpoint | Purpose | -| -------------------------- | ------ | -------------------------------------------- | -------------------------------------------------------------------------------------- | -| `getGeoCountry` | — | (geolocation action) | Resolve alpha-3 country | -| `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`) | +| Method | HTTP | Endpoint | Purpose | +| -------------------------- | ------ | ------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| `getGeoCountry` | — | (geolocation action) | Resolve alpha-3 country | +| `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`) | | `fetchSessionDisclaimers` | `GET` | `/disclaimers?country=` or `/sessions/{id}/disclaimers` | idOS + KYC-provider catalog (`country` XOR `sessionId`; global omits credential-reuse) | -| `submitSessionDisclaimers` | `POST` | `/sessions/{id}/disclaimers` | Record `{ idOS, kycProvider, credentialReusabilityConsentGiven }` consents | -| `fetchKycStatus` | `GET` | `/kyc/status` | User-keyed simplified KYC status | -| `fetchIdosEnclaveJwks` | `GET` | `{idosEnclaveBaseUrl}/.well-known/jwks.json` | idOS enclave JWKS for `encryptionDataKey` attestation | -| `fetchIdosRelayJwks` | `GET` | `{idosRelayBaseUrl}/.well-known/jwks.json` | idOS relay JWKS for `ukycCapabilityToken` attestation | -| `createUkycSession` | `POST` | `/sessions` | Start SumSub sub-flow; registers session client public key; returns encryption schemas | -| `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 | +| `submitSessionDisclaimers` | `POST` | `/sessions/{id}/disclaimers` | Record `{ idOS, kycProvider, credentialReusabilityConsentGiven }` consents | +| `fetchKycStatus` | `GET` | `/kyc/status` | User-keyed simplified KYC status | +| `fetchIdosEnclaveJwks` | `GET` | `{idosEnclaveBaseUrl}/.well-known/jwks.json` | idOS enclave JWKS for `encryptionDataKey` attestation | +| `fetchIdosRelayJwks` | `GET` | `{idosRelayBaseUrl}/.well-known/jwks.json` | idOS relay JWKS for `ukycCapabilityToken` attestation | +| `createUkycSession` | `POST` | `/sessions` | Start SumSub sub-flow; registers session client public key; returns encryption schemas | +| `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 | ### 2.3 `crypto.ts` diff --git a/packages/kyc-controller/CHANGELOG.md b/packages/kyc-controller/CHANGELOG.md index a6b0631946..23478a9ee3 100644 --- a/packages/kyc-controller/CHANGELOG.md +++ b/packages/kyc-controller/CHANGELOG.md @@ -11,12 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `idosRelayBaseUrl` on `KycService` and `KycService.fetchIdosRelayJwks()` to fetch the idOS relay well-known JWKS used to verify the `ukycCapabilityToken` encryption schema. `encryptionDataKey` continues to verify against idOS enclave JWKS via `fetchIdosEnclaveJwks` / `idosEnclaveBaseUrl`. ([#10008](https://github.com/MetaMask/core/pull/10008)) - Add session-scoped disclaimer APIs on `KycService` for the idOS / KYC-provider catalog ([#9979](https://github.com/MetaMask/core/pull/9979)): - - `fetchSessionDisclaimers({ sessionId })` calls `GET /sessions/{sessionId}/disclaimers` (includes `credentialReusabilityConsentGiven`), or `fetchSessionDisclaimers({ country })` calls `GET /disclaimers?country=` (catalog only; `country` is ISO 3166-1 alpha-3) + - `fetchSessionDisclaimers({ sessionId })` calls `GET /sessions/{sessionId}/disclaimers` - `submitSessionDisclaimers({ sessionId, idOS, kycProvider, credentialReusabilityConsentGiven })` calls `POST /sessions/{sessionId}/disclaimers` - Add `KycService.submitVendorDisclaimers({ vendor, disclaimerIds })` (`POST /vendors/{vendor}/disclaimers`) to record Iron T&C signings, plus the `KycVendorSigning` response type. The consents path calls this alongside session-scoped disclaimers; vendor T&C ids are no longer sent on the session disclaimer POST. ([#9979](https://github.com/MetaMask/core/pull/9979)) - Add `KycConsentDocument`, `KycConsentRecord`, and `KycSessionDisclaimers` types for that catalog, plus in-memory `credentialReusabilityConsentGiven` and `sessionDisclaimers` controller state. `acceptTermsAndStartSession` forwards optional `credentialReusabilityConsentGiven` (default `false`). ([#9979](https://github.com/MetaMask/core/pull/9979)) - Parameterize Universal KYC vendor HTTP on `KycService` so identity vendors share one client surface instead of vendor-branded methods ([#9908](https://github.com/MetaMask/core/pull/9908)): - - `fetchVendorDisclaimers({ vendor, country })` and `checkKycRequired({ vendor, ... })` call `/vendors/{vendor}/disclaimers` and `/vendors/{vendor}/kyc-required` (`vendor` defaults to `moonpay`) + - `fetchDisclaimers({ vendor, country })` and `checkKycRequired({ vendor, ... })` call `/vendors/{vendor}/disclaimers` and `/vendors/{vendor}/kyc-required` (`vendor` defaults to `moonpay`) - `createVendorCustomer({ vendor, email })` calls `POST /vendors/{vendor}/customers` - `fetchKycStatus()` reads `GET /kyc/status` - Add a consents-path KYC flow on `KycController` for non-MoonPay vendors (currently `iron`): empty-shell customer → disclaimers → consents → SumSub, skipping MoonPay Check/Auth frames. `initialize({ vendor })` and `createVendorCustomer({ vendor, email })` drive the path; `acceptTermsAndStartSession` requires `sumsubTncSigned` / `idosTncSigned`. ([#9908](https://github.com/MetaMask/core/pull/9908)) @@ -30,8 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Make `KycService.fetchSessionDisclaimers` accept either `{ sessionId }` (`GET /sessions/{sessionId}/disclaimers`) or `{ country }` (`GET /disclaimers?country=`, ISO 3166-1 alpha-3). The two params are mutually exclusive; `country` is required for the global catalog. Adds the `KycDisclaimersCatalog` type for the global response (no `credentialReusabilityConsentGiven`); `KycSessionDisclaimers` remains the session-scoped shape. -- **BREAKING:** Rename `KycService.fetchDisclaimers` / `KycService:fetchDisclaimers` / `KycServiceFetchDisclaimersAction` to `fetchVendorDisclaimers` / `KycService:fetchVendorDisclaimers` / `KycServiceFetchVendorDisclaimersAction`. +- Make `KycService.fetchSessionDisclaimers` accept either `{ sessionId }` (`GET /sessions/{sessionId}/disclaimers`) or `{ country }` (`GET /disclaimers?country=`, ISO 3166-1 alpha-3). The two params are mutually exclusive; `country` is required for the global catalog. Adds the `KycDisclaimersCatalog` type for the global response (no `credentialReusabilityConsentGiven`); `KycSessionDisclaimers` remains the session-scoped shape. ([#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)) From f8d8cfd710b10ab62dd9ecde8361ebceec153b0b Mon Sep 17 00:00:00 2001 From: Jiexi Luan-Huang Date: Mon, 31 Aug 2026 12:14:27 -0700 Subject: [PATCH 4/4] fix(kyc-controller): split global vs session disclaimer fetches Messenger action typing collapses method overloads, so sessionId callers were typed as the country catalog path and broke the build. Expose fetchDisclaimersCatalog separately from fetchSessionDisclaimers. Co-authored-by: Cursor --- packages/kyc-controller/ARCHITECTURE.md | 35 ++--- packages/kyc-controller/CHANGELOG.md | 2 +- .../src/KycService-method-action-types.ts | 34 +++-- .../kyc-controller/src/KycService.test.ts | 118 ++++++++-------- packages/kyc-controller/src/KycService.ts | 127 +++++++----------- packages/kyc-controller/src/index.ts | 2 + 6 files changed, 152 insertions(+), 166 deletions(-) diff --git a/packages/kyc-controller/ARCHITECTURE.md b/packages/kyc-controller/ARCHITECTURE.md index b14b3150d2..a8035e8777 100644 --- a/packages/kyc-controller/ARCHITECTURE.md +++ b/packages/kyc-controller/ARCHITECTURE.md @@ -122,28 +122,29 @@ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`): Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`): `getGeoCountry`, `fetchVendorDisclaimers`, `createSession`, `checkKycRequired`, -`createVendorCustomer`, `submitVendorDisclaimers`, `fetchSessionDisclaimers`, `submitSessionDisclaimers`, +`createVendorCustomer`, `submitVendorDisclaimers`, `fetchDisclaimersCatalog`, `fetchSessionDisclaimers`, `submitSessionDisclaimers`, `fetchKycStatus`, `fetchIdosEnclaveJwks`, `fetchIdosRelayJwks`, `createUkycSession`, `setAuthorizations`, `createJourney`, `getSessionStatus`. Endpoints: -| Method | HTTP | Endpoint | Purpose | -| -------------------------- | ------ | ------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| `getGeoCountry` | — | (geolocation action) | Resolve alpha-3 country | -| `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`) | -| `fetchSessionDisclaimers` | `GET` | `/disclaimers?country=` or `/sessions/{id}/disclaimers` | idOS + KYC-provider catalog (`country` XOR `sessionId`; global omits credential-reuse) | -| `submitSessionDisclaimers` | `POST` | `/sessions/{id}/disclaimers` | Record `{ idOS, kycProvider, credentialReusabilityConsentGiven }` consents | -| `fetchKycStatus` | `GET` | `/kyc/status` | User-keyed simplified KYC status | -| `fetchIdosEnclaveJwks` | `GET` | `{idosEnclaveBaseUrl}/.well-known/jwks.json` | idOS enclave JWKS for `encryptionDataKey` attestation | -| `fetchIdosRelayJwks` | `GET` | `{idosRelayBaseUrl}/.well-known/jwks.json` | idOS relay JWKS for `ukycCapabilityToken` attestation | -| `createUkycSession` | `POST` | `/sessions` | Start SumSub sub-flow; registers session client public key; returns encryption schemas | -| `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 | +| Method | HTTP | Endpoint | Purpose | +| -------------------------- | ------ | -------------------------------------------- | -------------------------------------------------------------------------------------- | +| `getGeoCountry` | — | (geolocation action) | Resolve alpha-3 country | +| `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 | +| `fetchIdosEnclaveJwks` | `GET` | `{idosEnclaveBaseUrl}/.well-known/jwks.json` | idOS enclave JWKS for `encryptionDataKey` attestation | +| `fetchIdosRelayJwks` | `GET` | `{idosRelayBaseUrl}/.well-known/jwks.json` | idOS relay JWKS for `ukycCapabilityToken` attestation | +| `createUkycSession` | `POST` | `/sessions` | Start SumSub sub-flow; registers session client public key; returns encryption schemas | +| `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 | ### 2.3 `crypto.ts` diff --git a/packages/kyc-controller/CHANGELOG.md b/packages/kyc-controller/CHANGELOG.md index 23478a9ee3..40bf326665 100644 --- a/packages/kyc-controller/CHANGELOG.md +++ b/packages/kyc-controller/CHANGELOG.md @@ -30,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Make `KycService.fetchSessionDisclaimers` accept either `{ sessionId }` (`GET /sessions/{sessionId}/disclaimers`) or `{ country }` (`GET /disclaimers?country=`, ISO 3166-1 alpha-3). The two params are mutually exclusive; `country` is required for the global catalog. Adds the `KycDisclaimersCatalog` type for the global response (no `credentialReusabilityConsentGiven`); `KycSessionDisclaimers` remains the session-scoped shape. ([#10011](https://github.com/MetaMask/core/pull/10011)) +- 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)) diff --git a/packages/kyc-controller/src/KycService-method-action-types.ts b/packages/kyc-controller/src/KycService-method-action-types.ts index ef4ed25fec..be557552ed 100644 --- a/packages/kyc-controller/src/KycService-method-action-types.ts +++ b/packages/kyc-controller/src/KycService-method-action-types.ts @@ -88,18 +88,31 @@ export type KycServiceSubmitVendorDisclaimersAction = { }; /** - * Fetches the idOS + KYC-provider disclaimer catalog. + * Fetches the global idOS + KYC-provider disclaimer catalog + * (`GET /disclaimers?country=`). Does not include + * `credentialReusabilityConsentGiven` — that is session-scoped via + * {@link fetchSessionDisclaimers}. Vendor T&Cs continue to come from + * {@link fetchVendorDisclaimers}. * - * - With `sessionId`: `GET /sessions/{sessionId}/disclaimers` (includes - * `credentialReusabilityConsentGiven`). Do not pass `country`. - * - With `country` (ISO 3166-1 alpha-3): `GET /disclaimers?country=` (catalog - * only). Do not pass `sessionId`. - * - * Vendor T&Cs continue to come from {@link fetchVendorDisclaimers}. + * @param params - The parameters. + * @param params.country - ISO 3166-1 alpha-3 country code. + * @returns The catalog documents. + */ +export type KycServiceFetchDisclaimersCatalogAction = { + type: `KycService:fetchDisclaimersCatalog`; + handler: KycService['fetchDisclaimersCatalog']; +}; + +/** + * Fetches the session-scoped idOS + KYC-provider disclaimer catalog + * (`GET /sessions/{sessionId}/disclaimers`), including per-session + * `consented` flags and `credentialReusabilityConsentGiven`. For the + * pre-session global catalog use {@link fetchDisclaimersCatalog}. Vendor + * T&Cs continue to come from {@link fetchVendorDisclaimers}. * - * @param params - Either `{ sessionId }` or `{ country }`. - * @returns The catalog. Session fetches include - * `credentialReusabilityConsentGiven`; global fetches do not. + * @param params - The parameters. + * @param params.sessionId - The UKYC session id. + * @returns The catalog, including which documents are already consented. */ export type KycServiceFetchSessionDisclaimersAction = { type: `KycService:fetchSessionDisclaimers`; @@ -228,6 +241,7 @@ export type KycServiceMethodActions = | KycServiceCheckKycRequiredAction | KycServiceCreateVendorCustomerAction | KycServiceSubmitVendorDisclaimersAction + | KycServiceFetchDisclaimersCatalogAction | KycServiceFetchSessionDisclaimersAction | KycServiceSubmitSessionDisclaimersAction | KycServiceFetchKycStatusAction diff --git a/packages/kyc-controller/src/KycService.test.ts b/packages/kyc-controller/src/KycService.test.ts index 0454210a85..d7dee565e1 100644 --- a/packages/kyc-controller/src/KycService.test.ts +++ b/packages/kyc-controller/src/KycService.test.ts @@ -31,9 +31,9 @@ describe('KycService', () => { .reply(200, disclaimers); const { service } = getService({ omitFetch: true }); - expect(await service.fetchVendorDisclaimers({ country: 'USA' })).toStrictEqual( - disclaimers, - ); + expect( + await service.fetchVendorDisclaimers({ country: 'USA' }), + ).toStrictEqual(disclaimers); }); it('throws when fetch is not globally available and not provided', () => { @@ -113,9 +113,9 @@ describe('KycService', () => { .reply(200, disclaimers); const { service } = getService(); - expect(await service.fetchVendorDisclaimers({ country: 'USA' })).toStrictEqual( - disclaimers, - ); + expect( + await service.fetchVendorDisclaimers({ country: 'USA' }), + ).toStrictEqual(disclaimers); }); it('throws on a malformed response', async () => { @@ -684,7 +684,10 @@ describe('KycService', () => { const { service } = getService(); expect( - await service.fetchVendorDisclaimers({ vendor: 'iron', country: 'USA' }), + await service.fetchVendorDisclaimers({ + vendor: 'iron', + country: 'USA', + }), ).toStrictEqual(disclaimers); }); @@ -739,7 +742,7 @@ describe('KycService', () => { }); }); - describe('fetchSessionDisclaimers', () => { + describe('fetchDisclaimersCatalog', () => { const documents = { idOS: [ { @@ -760,19 +763,6 @@ describe('KycService', () => { }, ], }; - const catalog = { - ...documents, - credentialReusabilityConsentGiven: false, - }; - - it('returns the session-scoped disclaimer catalog', async () => { - nock(MOCK_API_URL).get('/sessions/sid-1/disclaimers').reply(200, catalog); - const { service } = getService(); - - expect( - await service.fetchSessionDisclaimers({ sessionId: 'sid-1' }), - ).toStrictEqual(catalog); - }); it('returns the global disclaimer catalog for a country', async () => { nock(MOCK_API_URL) @@ -782,52 +772,18 @@ describe('KycService', () => { const { service } = getService(); expect( - await service.fetchSessionDisclaimers({ country: 'USA' }), - ).toStrictEqual(documents); - }); - - it('uses the global catalog for an empty sessionId when country is set', async () => { - nock(MOCK_API_URL) - .get('/disclaimers') - .query({ country: 'FRA' }) - .reply(200, documents); - const { service } = getService(); - - expect( - await service.fetchSessionDisclaimers({ - sessionId: '', - country: 'FRA', - }), + await service.fetchDisclaimersCatalog({ country: 'USA' }), ).toStrictEqual(documents); }); - it('throws when neither sessionId nor country is provided', async () => { - const { service } = getService(); - - await expect(service.fetchSessionDisclaimers()).rejects.toThrow( - /country.*required/iu, - ); - }); - it('throws when country is not a 3-character code', async () => { const { service } = getService(); await expect( - service.fetchSessionDisclaimers({ country: 'US' }), + service.fetchDisclaimersCatalog({ country: 'US' }), ).rejects.toThrow(/ISO 3166-1 alpha-3/u); }); - it('throws when both sessionId and country are provided', async () => { - const { service } = getService(); - - await expect( - service.fetchSessionDisclaimers({ - sessionId: 'sid-1', - country: 'USA', - }), - ).rejects.toThrow(/must not be provided together/iu); - }); - it('throws on a malformed global catalog response', async () => { nock(MOCK_API_URL) .get('/disclaimers') @@ -836,7 +792,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchSessionDisclaimers({ country: 'USA' }), + service.fetchDisclaimersCatalog({ country: 'USA' }), ).rejects.toThrow(/Malformed response received from disclaimers API/u); }); @@ -848,7 +804,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchSessionDisclaimers({ country: 'USA' }), + service.fetchDisclaimersCatalog({ country: 'USA' }), ).rejects.toThrow(/Malformed response received from disclaimers API/u); }); @@ -860,9 +816,45 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchSessionDisclaimers({ country: 'USA' }), + service.fetchDisclaimersCatalog({ country: 'USA' }), ).rejects.toThrow(/failed with status '500'/u); }); + }); + + describe('fetchSessionDisclaimers', () => { + const documents = { + idOS: [ + { + key: 'idos-tos', + version: '1', + title: 'idOS ToS', + url: 'https://idos.example/tos', + consented: false, + }, + ], + kycProvider: [ + { + key: 'sumsub-tos', + version: '1', + title: 'SumSub ToS', + url: 'https://sumsub.example/tos', + consented: false, + }, + ], + }; + const catalog = { + ...documents, + credentialReusabilityConsentGiven: false, + }; + + it('returns the session-scoped disclaimer catalog', async () => { + nock(MOCK_API_URL).get('/sessions/sid-1/disclaimers').reply(200, catalog); + const { service } = getService(); + + expect( + await service.fetchSessionDisclaimers({ sessionId: 'sid-1' }), + ).toStrictEqual(catalog); + }); it('throws on a malformed response', async () => { nock(MOCK_API_URL).get('/sessions/sid-1/disclaimers').reply(200, {}); @@ -1089,9 +1081,9 @@ describe('KycService', () => { .reply(200, disclaimers); const { service } = getService({ baseUrl: customUrl }); - expect(await service.fetchVendorDisclaimers({ country: 'USA' })).toStrictEqual( - disclaimers, - ); + expect( + await service.fetchVendorDisclaimers({ country: 'USA' }), + ).toStrictEqual(disclaimers); }); it('throws when baseUrl is empty', () => { diff --git a/packages/kyc-controller/src/KycService.ts b/packages/kyc-controller/src/KycService.ts index 10e35ced38..919a41d8bc 100644 --- a/packages/kyc-controller/src/KycService.ts +++ b/packages/kyc-controller/src/KycService.ts @@ -54,6 +54,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'checkKycRequired', 'createVendorCustomer', 'submitVendorDisclaimers', + 'fetchDisclaimersCatalog', 'fetchSessionDisclaimers', 'submitSessionDisclaimers', 'fetchKycStatus', @@ -313,19 +314,14 @@ export type SubmitVendorDisclaimersParams = { disclaimerIds: string[]; }; +export type FetchDisclaimersCatalogParams = { + /** ISO 3166-1 alpha-3 country code for `GET /disclaimers?country=`. */ + country: string; +}; + export type FetchSessionDisclaimersParams = { - /** - * UKYC session id from {@link KycService.createUkycSession}. When set, - * fetches `GET /sessions/{sessionId}/disclaimers`. Mutually exclusive with - * `country`. - */ - sessionId?: string; - /** - * ISO 3166-1 alpha-3 country code. Required for the global catalog - * (`GET /disclaimers?country=`). Must not be set when `sessionId` is - * provided. - */ - country?: string; + /** UKYC session id from {@link KycService.createUkycSession}. */ + sessionId: string; }; export type SubmitSessionDisclaimersParams = { @@ -709,80 +705,29 @@ export class KycService extends BaseDataService< } /** - * Fetches the idOS + KYC-provider disclaimer catalog. - * - * - With `sessionId`: `GET /sessions/{sessionId}/disclaimers` (includes - * per-session `consented` flags and `credentialReusabilityConsentGiven`). - * Do not pass `country`. - * - With `country` (ISO 3166-1 alpha-3): `GET /disclaimers?country=` for the - * global catalog (no credential-reuse field). Do not pass `sessionId`. - * - * Vendor T&Cs continue to come from {@link fetchVendorDisclaimers}. + * Fetches the global idOS + KYC-provider disclaimer catalog + * (`GET /disclaimers?country=`). Does not include + * `credentialReusabilityConsentGiven` — that is session-scoped via + * {@link fetchSessionDisclaimers}. Vendor T&Cs continue to come from + * {@link fetchVendorDisclaimers}. * * @param params - The parameters. - * @returns The catalog. Session fetches include - * `credentialReusabilityConsentGiven`; global fetches do not. + * @param params.country - ISO 3166-1 alpha-3 country code. + * @returns The catalog documents. */ - fetchSessionDisclaimers(params: { - sessionId: string; - }): Promise; - - fetchSessionDisclaimers(params: { - country: string; - }): Promise; - - async fetchSessionDisclaimers( - params: FetchSessionDisclaimersParams = {}, - ): Promise { - const sessionId = - typeof params.sessionId === 'string' && params.sessionId.length > 0 - ? params.sessionId - : undefined; - const country = - typeof params.country === 'string' && params.country.length > 0 - ? params.country - : undefined; - - if (sessionId && country) { - throw new Error( - 'KycService.fetchSessionDisclaimers: sessionId and country must not be provided together.', - ); - } - - if (sessionId) { - const url = new URL( - `/sessions/${encodeURIComponent(sessionId)}/disclaimers`, - this.#baseUrl, - ); - const data = await this.fetchQuery({ - queryKey: [`${this.name}:fetchSessionDisclaimers`, sessionId], - queryFn: async () => this.#requestJson(url, { method: 'GET' }), - // Consent state can change after a POST, so always re-fetch. - staleTime: 0, - gcTime: 0, - }); - return this.#validateResponse( - data, - SessionDisclaimersResponseStruct, - 'session disclaimers', - ); - } - - if (!country) { - throw new Error( - 'KycService.fetchSessionDisclaimers: country is required when sessionId is omitted.', - ); - } + async fetchDisclaimersCatalog({ + country, + }: FetchDisclaimersCatalogParams): Promise { if (country.length !== 3) { throw new Error( - `KycService.fetchSessionDisclaimers: country must be an ISO 3166-1 alpha-3 code (received "${country}").`, + `KycService.fetchDisclaimersCatalog: country must be an ISO 3166-1 alpha-3 code (received "${country}").`, ); } const url = new URL('/disclaimers', this.#baseUrl); url.searchParams.set('country', country); const data = await this.fetchQuery({ - queryKey: [`${this.name}:fetchSessionDisclaimers`, country], + queryKey: [`${this.name}:fetchDisclaimersCatalog`, country], queryFn: async () => this.#requestJson(url, { method: 'GET' }), staleTime: 0, gcTime: 0, @@ -794,6 +739,38 @@ export class KycService extends BaseDataService< ); } + /** + * Fetches the session-scoped idOS + KYC-provider disclaimer catalog + * (`GET /sessions/{sessionId}/disclaimers`), including per-session + * `consented` flags and `credentialReusabilityConsentGiven`. For the + * pre-session global catalog use {@link fetchDisclaimersCatalog}. Vendor + * T&Cs continue to come from {@link fetchVendorDisclaimers}. + * + * @param params - The parameters. + * @param params.sessionId - The UKYC session id. + * @returns The catalog, including which documents are already consented. + */ + async fetchSessionDisclaimers({ + sessionId, + }: FetchSessionDisclaimersParams): Promise { + const url = new URL( + `/sessions/${encodeURIComponent(sessionId)}/disclaimers`, + this.#baseUrl, + ); + const data = await this.fetchQuery({ + queryKey: [`${this.name}:fetchSessionDisclaimers`, sessionId], + queryFn: async () => this.#requestJson(url, { method: 'GET' }), + // Consent state can change after a POST, so always re-fetch. + staleTime: 0, + gcTime: 0, + }); + return this.#validateResponse( + data, + SessionDisclaimersResponseStruct, + 'session disclaimers', + ); + } + /** * Records idOS + KYC-provider consents for a UKYC session * (`POST /sessions/{sessionId}/disclaimers`). `key`/`version` pairs must diff --git a/packages/kyc-controller/src/index.ts b/packages/kyc-controller/src/index.ts index bb03d07c11..fdfb9d3fe7 100644 --- a/packages/kyc-controller/src/index.ts +++ b/packages/kyc-controller/src/index.ts @@ -42,6 +42,7 @@ export type { CreateSessionParams, CreateUkycSessionParams, EncryptionSchema, + FetchDisclaimersCatalogParams, FetchSessionDisclaimersParams, GetSessionStatusParams, VendorCustomerResponse, @@ -65,6 +66,7 @@ export type { KycServiceCreateSessionAction, KycServiceCreateUkycSessionAction, KycServiceFetchVendorDisclaimersAction, + KycServiceFetchDisclaimersCatalogAction, KycServiceFetchIdosEnclaveJwksAction, KycServiceFetchIdosRelayJwksAction, KycServiceFetchKycStatusAction,