diff --git a/packages/kyc-controller/ARCHITECTURE.md b/packages/kyc-controller/ARCHITECTURE.md index d3601027467..0dac3cc4f3e 100644 --- a/packages/kyc-controller/ARCHITECTURE.md +++ b/packages/kyc-controller/ARCHITECTURE.md @@ -99,7 +99,7 @@ graph TB Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`): -`initialize`, `loadDisclaimers`, `acceptTermsAndStartSession`, +`initialize`, `loadDisclaimers`, `fetchSessionDisclaimers`, `acceptTermsAndStartSession`, `createVendorCustomer`, `clearSavedTerms`, `handleFrameMessage`, `buildCheckFrameUrl`, `buildAuthFrameUrl`, `buildResetFrameUrl`, `checkKycRequired`, `getKycStatus`, `getCustomerIdentity`, `refreshKycStatus`, @@ -122,29 +122,29 @@ Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`): Exposed messenger actions (`MESSENGER_EXPOSED_METHODS`): `getGeoCountry`, `fetchVendorDisclaimers`, `createSession`, `checkKycRequired`, -`createVendorCustomer`, `submitVendorDisclaimers`, `fetchDisclaimersCatalog`, `fetchSessionDisclaimers`, `submitSessionDisclaimers`, +`createVendorCustomer`, `submitVendorDisclaimers`, `fetchSessionDisclaimersByCountry`, `fetchSessionDisclaimersBySessionId`, `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`) | -| `fetchDisclaimersCatalog` | `GET` | `/disclaimers?country=` | Global idOS + KYC-provider catalog (no consent state) | -| `fetchSessionDisclaimers` | `GET` | `/sessions/{id}/disclaimers` | Session-scoped catalog, with `consented` flags + credential-reuse flag | -| `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`) | +| `fetchSessionDisclaimersByCountry` | `GET` | `/disclaimers?country=` | Global idOS + KYC-provider catalog (no consent state) | +| `fetchSessionDisclaimersBySessionId` | `GET` | `/sessions/{id}/disclaimers` | Session-scoped catalog, with `consented` flags + credential-reuse flag | +| `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 928123db219..7f9786fa97c 100644 --- a/packages/kyc-controller/CHANGELOG.md +++ b/packages/kyc-controller/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `KycController.fetchSessionDisclaimers`, which fetches the idOS + KYC-provider disclaimers by `{ sessionId }` or `{ country }`. ([#10162](https://github.com/MetaMask/core/pull/10162)) + +### Changed + +- **BREAKING:** Rename `KycService.fetchDisclaimersCatalog` to `fetchSessionDisclaimersByCountry`. ([#10162](https://github.com/MetaMask/core/pull/10162)) + - Rename `FetchDisclaimersCatalogParams` to `FetchSessionDisclaimersByCountryParams`. + - Rename the messenger action `KycService:fetchDisclaimersCatalog` to `KycService:fetchSessionDisclaimersByCountry`. +- **BREAKING:** Rename `KycService.fetchSessionDisclaimers` to `fetchSessionDisclaimersBySessionId` ([#10162](https://github.com/MetaMask/core/pull/10162)) + - Rename `FetchSessionDisclaimersParams` to `FetchSessionDisclaimersBySessionIdParams`. + - Rename the messenger action `KycService:fetchSessionDisclaimers` to `KycService:fetchSessionDisclaimersBySessionId`. + ## [0.2.0] ### Changed diff --git a/packages/kyc-controller/src/KycController-method-action-types.ts b/packages/kyc-controller/src/KycController-method-action-types.ts index cbb56684f85..c03a617de6b 100644 --- a/packages/kyc-controller/src/KycController-method-action-types.ts +++ b/packages/kyc-controller/src/KycController-method-action-types.ts @@ -53,6 +53,29 @@ export type KycControllerLoadDisclaimersAction = { handler: KycController['loadDisclaimers']; }; +/** + * Fetches the idOS + KYC-provider disclaimer catalog. Pass exactly one of + * `sessionId` or `country`: + * + * - `{ sessionId }` → {@link KycService.fetchSessionDisclaimersBySessionId} + * (`GET /sessions/{sessionId}/disclaimers`) + * - `{ country }` → {@link KycService.fetchSessionDisclaimersByCountry} + * (`GET /disclaimers?country=`) + * + * A session-id fetch also writes the catalog to `sessionDisclaimers`. + * + * @param params - The parameters. Provide exactly one of `sessionId` or + * `country`. + * @param params.sessionId - The UKYC session id. + * @param params.country - ISO 3166-1 alpha-3 country code. + * @returns The catalog. Session fetches include consent state; country + * fetches do not. + */ +export type KycControllerFetchSessionDisclaimersAction = { + type: `KycController:fetchSessionDisclaimers`; + handler: KycController['fetchSessionDisclaimers']; +}; + /** * Captures terms acceptance for the currently loaded disclaimers and creates * a session. @@ -264,6 +287,7 @@ export type KycControllerMethodActions = | KycControllerInitializeAction | KycControllerCreateVendorCustomerAction | KycControllerLoadDisclaimersAction + | KycControllerFetchSessionDisclaimersAction | KycControllerAcceptTermsAndStartSessionAction | KycControllerClearSavedTermsAction | KycControllerHandleFrameMessageAction diff --git a/packages/kyc-controller/src/KycController.test.ts b/packages/kyc-controller/src/KycController.test.ts index e487d652527..e67e970bcf3 100644 --- a/packages/kyc-controller/src/KycController.test.ts +++ b/packages/kyc-controller/src/KycController.test.ts @@ -13,7 +13,10 @@ import { getDefaultKycControllerState, KycController, } from './KycController.js'; -import type { KycControllerMessenger } from './KycController.js'; +import type { + FetchSessionDisclaimersParams, + KycControllerMessenger, +} from './KycController.js'; import type { KycConsentRecord, KycDisclaimer, @@ -498,6 +501,121 @@ describe('KycController', () => { }); }); + describe('fetchSessionDisclaimers', () => { + const globalCatalog = { + idOS: [ + { + key: 'idos-tos', + version: '1', + title: 'idOS ToS', + url: 'https://idos.example/tos', + }, + ], + kycProvider: [ + { + key: 'sumsub-tos', + version: '1', + title: 'SumSub ToS', + url: 'https://sumsub.example/tos', + }, + ], + }; + + it('fetches the session-scoped catalog by sessionId', async () => { + await withController(async ({ controller, handlers }) => { + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue( + MOCK_SESSION_DISCLAIMERS, + ); + + expect( + await controller.fetchSessionDisclaimers({ sessionId: 'sid' }), + ).toStrictEqual(MOCK_SESSION_DISCLAIMERS); + + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).toHaveBeenCalledWith({ sessionId: 'sid' }); + expect( + handlers.fetchSessionDisclaimersByCountry, + ).not.toHaveBeenCalled(); + expect(controller.state.sessionDisclaimers).toStrictEqual( + MOCK_SESSION_DISCLAIMERS, + ); + }); + }); + + it('fetches the global catalog by country', async () => { + await withController(async ({ controller, handlers }) => { + handlers.fetchSessionDisclaimersByCountry.mockResolvedValue( + globalCatalog, + ); + + expect( + await controller.fetchSessionDisclaimers({ country: 'USA' }), + ).toStrictEqual(globalCatalog); + + expect(handlers.fetchSessionDisclaimersByCountry).toHaveBeenCalledWith({ + country: 'USA', + }); + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).not.toHaveBeenCalled(); + expect(controller.state.sessionDisclaimers).toBeNull(); + }); + }); + + it('throws when neither sessionId nor country is provided', async () => { + await withController(async ({ controller, handlers }) => { + await expect( + controller.fetchSessionDisclaimers( + {} as FetchSessionDisclaimersParams, + ), + ).rejects.toThrow(/exactly one of sessionId or country/u); + + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).not.toHaveBeenCalled(); + expect( + handlers.fetchSessionDisclaimersByCountry, + ).not.toHaveBeenCalled(); + }); + }); + + it('throws when both sessionId and country are provided', async () => { + await withController(async ({ controller, handlers }) => { + await expect( + controller.fetchSessionDisclaimers({ + sessionId: 'sid', + country: 'USA', + } as unknown as FetchSessionDisclaimersParams), + ).rejects.toThrow(/exactly one of sessionId or country/u); + + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).not.toHaveBeenCalled(); + expect( + handlers.fetchSessionDisclaimersByCountry, + ).not.toHaveBeenCalled(); + }); + }); + + it('does not write sessionDisclaimers when reset lands during a sessionId fetch', async () => { + await withController(async ({ controller, handlers }) => { + handlers.fetchSessionDisclaimersBySessionId.mockImplementation( + async () => { + controller.reset(); + return MOCK_SESSION_DISCLAIMERS; + }, + ); + + expect( + await controller.fetchSessionDisclaimers({ sessionId: 'sid' }), + ).toStrictEqual(MOCK_SESSION_DISCLAIMERS); + + expect(controller.state.sessionDisclaimers).toBeNull(); + }); + }); + }); + describe('acceptTermsAndStartSession', () => { it('captures terms and creates a session', async () => { await withController( @@ -2704,7 +2822,9 @@ describe('KycController', () => { product: 'money', }); - expect(handlers.fetchSessionDisclaimers).toHaveBeenCalled(); + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).toHaveBeenCalled(); expect(handlers.submitVendorDisclaimers).toHaveBeenCalledWith({ vendor: 'iron', disclaimerIds: ['d1'], @@ -3038,7 +3158,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers, launcher }) => { - handlers.fetchSessionDisclaimers.mockResolvedValue( + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue( MOCK_SESSION_DISCLAIMERS, ); handlers.submitSessionDisclaimers.mockResolvedValue({ @@ -3071,7 +3191,9 @@ describe('KycController', () => { vendor: 'iron', disclaimerIds: ['d1'], }); - expect(handlers.fetchSessionDisclaimers).toHaveBeenCalledWith({ + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).toHaveBeenCalledWith({ sessionId: 'sid', }); expect(handlers.submitSessionDisclaimers).toHaveBeenCalledWith({ @@ -3089,7 +3211,8 @@ describe('KycController', () => { expect( handlers.createUkycSession.mock.invocationCallOrder[0], ).toBeLessThan( - handlers.fetchSessionDisclaimers.mock.invocationCallOrder[0], + handlers.fetchSessionDisclaimersBySessionId.mock + .invocationCallOrder[0], ); expect(handlers.createUkycSession).toHaveBeenCalledWith( expect.objectContaining({ @@ -3172,7 +3295,7 @@ describe('KycController', () => { consented: true, })), }; - handlers.fetchSessionDisclaimers + handlers.fetchSessionDisclaimersBySessionId .mockResolvedValueOnce(MOCK_SESSION_DISCLAIMERS) .mockResolvedValueOnce(consentedCatalog); handlers.submitSessionDisclaimers.mockRejectedValue( @@ -3193,7 +3316,9 @@ describe('KycController', () => { idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED, }); - expect(handlers.fetchSessionDisclaimers).toHaveBeenCalledTimes(2); + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).toHaveBeenCalledTimes(2); expect(controller.state.phase).toBe('done'); expect(launcher.launch).toHaveBeenCalled(); controller.reset(); @@ -3220,7 +3345,7 @@ describe('KycController', () => { consented: true, })), }; - handlers.fetchSessionDisclaimers + handlers.fetchSessionDisclaimersBySessionId .mockResolvedValueOnce(MOCK_SESSION_DISCLAIMERS) .mockResolvedValueOnce(afterConflict); handlers.submitSessionDisclaimers.mockRejectedValue( @@ -3270,7 +3395,9 @@ describe('KycController', () => { })), credentialReusabilityConsentGiven: false, }; - handlers.fetchSessionDisclaimers.mockResolvedValue(consentedDocs); + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue( + consentedDocs, + ); handlers.submitSessionDisclaimers.mockRejectedValue( new HttpError( 409, @@ -3305,7 +3432,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers, launcher }) => { - handlers.fetchSessionDisclaimers.mockResolvedValue( + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue( MOCK_SESSION_DISCLAIMERS, ); handlers.submitSessionDisclaimers.mockRejectedValue( @@ -3342,7 +3469,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers, launcher }) => { - handlers.fetchSessionDisclaimers.mockResolvedValue({ + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue({ idOS: [ { key: 'idos-tos', @@ -3400,7 +3527,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers, launcher }) => { - handlers.fetchSessionDisclaimers.mockResolvedValue({ + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue({ ...MOCK_SESSION_DISCLAIMERS, idOS: MOCK_SESSION_DISCLAIMERS.idOS.map((doc) => ({ ...doc, @@ -3863,7 +3990,7 @@ describe('KycController', () => { let release: () => void = () => { // placeholder }; - handlers.fetchSessionDisclaimers.mockReturnValue( + handlers.fetchSessionDisclaimersBySessionId.mockReturnValue( new Promise((resolve) => { release = (): void => { resolve(MOCK_SESSION_DISCLAIMERS); @@ -3991,7 +4118,9 @@ describe('KycController', () => { idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED, }); - expect(handlers.fetchSessionDisclaimers).toHaveBeenCalledWith({ + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).toHaveBeenCalledWith({ sessionId: 'sid', }); expect(launcher.launch).not.toHaveBeenCalled(); @@ -4013,7 +4142,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers }) => { - handlers.fetchSessionDisclaimers + handlers.fetchSessionDisclaimersBySessionId .mockResolvedValueOnce(MOCK_SESSION_DISCLAIMERS) .mockImplementationOnce(async () => { controller.reset(); @@ -4049,10 +4178,12 @@ describe('KycController', () => { }, }, async ({ controller, handlers }) => { - handlers.fetchSessionDisclaimers.mockImplementation(async () => { - controller.reset(); - return MOCK_SESSION_DISCLAIMERS; - }); + handlers.fetchSessionDisclaimersBySessionId.mockImplementation( + async () => { + controller.reset(); + return MOCK_SESSION_DISCLAIMERS; + }, + ); await controller.acceptTermsAndStartSession({ email: 'a@b.co', @@ -4170,7 +4301,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers, launcher }) => { - handlers.fetchSessionDisclaimers.mockResolvedValue({ + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue({ idOS: [], kycProvider: MOCK_SESSION_DISCLAIMERS.kycProvider, credentialReusabilityConsentGiven: false, @@ -4205,7 +4336,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers, launcher }) => { - handlers.fetchSessionDisclaimers.mockResolvedValue({ + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue({ idOS: MOCK_SESSION_DISCLAIMERS.idOS, kycProvider: [], credentialReusabilityConsentGiven: false, @@ -4239,7 +4370,7 @@ describe('KycController', () => { }, }, async ({ controller, handlers, launcher }) => { - handlers.fetchSessionDisclaimers + handlers.fetchSessionDisclaimersBySessionId .mockResolvedValueOnce(MOCK_SESSION_DISCLAIMERS) .mockResolvedValueOnce({ idOS: [], @@ -4322,7 +4453,9 @@ describe('KycController', () => { }); expect(controller.state.phase).toBe('idle'); - expect(handlers.fetchSessionDisclaimers).not.toHaveBeenCalled(); + expect( + handlers.fetchSessionDisclaimersBySessionId, + ).not.toHaveBeenCalled(); expect(handlers.submitSessionDisclaimers).not.toHaveBeenCalled(); }, ); @@ -4807,6 +4940,20 @@ describe('KycController', () => { ).toContain('ch_reset'); }); }); + + it('exposes fetchSessionDisclaimers as a messenger action', async () => { + await withController(async ({ rootMessenger, handlers }) => { + handlers.fetchSessionDisclaimersBySessionId.mockResolvedValue( + MOCK_SESSION_DISCLAIMERS, + ); + + expect( + await rootMessenger.call('KycController:fetchSessionDisclaimers', { + sessionId: 'sid', + }), + ).toStrictEqual(MOCK_SESSION_DISCLAIMERS); + }); + }); }); }); @@ -4823,7 +4970,8 @@ type ServiceHandlers = { checkKycRequired: jest.Mock; createVendorCustomer: jest.Mock; submitVendorDisclaimers: jest.Mock; - fetchSessionDisclaimers: jest.Mock; + fetchSessionDisclaimersByCountry: jest.Mock; + fetchSessionDisclaimersBySessionId: jest.Mock; submitSessionDisclaimers: jest.Mock; fetchKycStatus: jest.Mock; fetchIdosEnclaveJwks: jest.Mock; @@ -4860,7 +5008,8 @@ const SERVICE_ACTIONS = [ 'KycService:checkKycRequired', 'KycService:createVendorCustomer', 'KycService:submitVendorDisclaimers', - 'KycService:fetchSessionDisclaimers', + 'KycService:fetchSessionDisclaimersByCountry', + 'KycService:fetchSessionDisclaimersBySessionId', 'KycService:submitSessionDisclaimers', 'KycService:fetchKycStatus', 'KycService:fetchIdosEnclaveJwks', @@ -4969,7 +5118,11 @@ function withController( .mockResolvedValue([ { id: 'sign-1', customer_id: 'cust-1', content_id: 'd1' }, ]), - fetchSessionDisclaimers: jest + fetchSessionDisclaimersByCountry: jest.fn().mockResolvedValue({ + idOS: [], + kycProvider: [], + }), + fetchSessionDisclaimersBySessionId: jest .fn() .mockResolvedValue(MOCK_SESSION_DISCLAIMERS), submitSessionDisclaimers: jest.fn().mockResolvedValue({ @@ -5021,8 +5174,12 @@ function withController( handlers.submitVendorDisclaimers, ); rootMessenger.registerActionHandler( - 'KycService:fetchSessionDisclaimers', - handlers.fetchSessionDisclaimers, + 'KycService:fetchSessionDisclaimersByCountry', + handlers.fetchSessionDisclaimersByCountry, + ); + rootMessenger.registerActionHandler( + 'KycService:fetchSessionDisclaimersBySessionId', + handlers.fetchSessionDisclaimersBySessionId, ); rootMessenger.registerActionHandler( 'KycService:submitSessionDisclaimers', diff --git a/packages/kyc-controller/src/KycController.ts b/packages/kyc-controller/src/KycController.ts index 5f9ed9897f4..1553a8e41be 100644 --- a/packages/kyc-controller/src/KycController.ts +++ b/packages/kyc-controller/src/KycController.ts @@ -26,6 +26,7 @@ import type { KycConsentRecord, KycCustomerIdentity, KycDisclaimer, + KycDisclaimersCatalog, KycPhase, KycProduct, KycProviderDisclaimersAccepted, @@ -614,11 +615,28 @@ function usesConsentsFlow(vendor: KycVendor): boolean { return vendor !== 'moonpay'; } +/** + * Parameters for {@link KycController.fetchSessionDisclaimers}. Provide + * exactly one of `sessionId` or `country`. + */ +export type FetchSessionDisclaimersParams = + | { + /** UKYC session id from `KycService.createUkycSession`. */ + sessionId: string; + country?: never; + } + | { + /** ISO 3166-1 alpha-3 country code for `GET /disclaimers?country=`. */ + country: string; + sessionId?: never; + }; + // === MESSENGER === const MESSENGER_EXPOSED_METHODS = [ 'initialize', 'loadDisclaimers', + 'fetchSessionDisclaimers', 'acceptTermsAndStartSession', 'createVendorCustomer', 'clearSavedTerms', @@ -1075,6 +1093,62 @@ export class KycController extends BaseController< } } + /** + * Fetches the idOS + KYC-provider disclaimer catalog. Pass exactly one of + * `sessionId` or `country`: + * + * - `{ sessionId }` → {@link KycService.fetchSessionDisclaimersBySessionId} + * (`GET /sessions/{sessionId}/disclaimers`) + * - `{ country }` → {@link KycService.fetchSessionDisclaimersByCountry} + * (`GET /disclaimers?country=`) + * + * A session-id fetch also writes the catalog to `sessionDisclaimers`. + * + * @param params - The parameters. Provide exactly one of `sessionId` or + * `country`. + * @param params.sessionId - The UKYC session id. + * @param params.country - ISO 3166-1 alpha-3 country code. + * @returns The catalog. Session fetches include consent state; country + * fetches do not. + */ + async fetchSessionDisclaimers( + params: FetchSessionDisclaimersParams, + ): Promise { + const { sessionId, country } = params as { + sessionId?: string; + country?: string; + }; + if (sessionId && country) { + throw new Error( + 'KycController.fetchSessionDisclaimers: provide exactly one of sessionId or country.', + ); + } + + const generation = this.#generation; + + if (country) { + return this.messenger.call( + 'KycService:fetchSessionDisclaimersByCountry', + { country }, + ); + } + + if (!sessionId) { + throw new Error( + 'KycController.fetchSessionDisclaimers: provide exactly one of sessionId or country.', + ); + } + + const catalog = await this.messenger.call( + 'KycService:fetchSessionDisclaimersBySessionId', + { sessionId }, + ); + this.#updateIfCurrent(generation, (state) => { + state.sessionDisclaimers = catalog; + }); + return catalog; + } + /** * Captures terms acceptance for the currently loaded disclaimers and creates * a session. @@ -1369,7 +1443,7 @@ export class KycController extends BaseController< generation: number, ): Promise { const catalog = await this.messenger.call( - 'KycService:fetchSessionDisclaimers', + 'KycService:fetchSessionDisclaimersBySessionId', { sessionId }, ); if (this.#generation !== generation) { @@ -1429,7 +1503,7 @@ export class KycController extends BaseController< // continue only when every document the user accepted is now consented; // otherwise fail closed so a version bump cannot skip new docs. const latest = await this.messenger.call( - 'KycService:fetchSessionDisclaimers', + 'KycService:fetchSessionDisclaimersBySessionId', { sessionId }, ); if (this.#generation !== generation) { diff --git a/packages/kyc-controller/src/KycService-method-action-types.ts b/packages/kyc-controller/src/KycService-method-action-types.ts index 791e1b4b107..975254525ec 100644 --- a/packages/kyc-controller/src/KycService-method-action-types.ts +++ b/packages/kyc-controller/src/KycService-method-action-types.ts @@ -91,38 +91,38 @@ export type KycServiceSubmitVendorDisclaimersAction = { * Fetches the global idOS + KYC-provider disclaimer catalog * (`GET /disclaimers?country=`). Carries no consent state — per-document * `consented` flags and `credentialReusabilityConsentGiven` are - * session-scoped via {@link fetchSessionDisclaimers}. Vendor T&Cs continue to + * session-scoped via {@link fetchSessionDisclaimersBySessionId}. 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']; +export type KycServiceFetchSessionDisclaimersByCountryAction = { + type: `KycService:fetchSessionDisclaimersByCountry`; + handler: KycService['fetchSessionDisclaimersByCountry']; }; /** * 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}. + * pre-session global catalog use {@link fetchSessionDisclaimersByCountry}. + * 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. */ -export type KycServiceFetchSessionDisclaimersAction = { - type: `KycService:fetchSessionDisclaimers`; - handler: KycService['fetchSessionDisclaimers']; +export type KycServiceFetchSessionDisclaimersBySessionIdAction = { + type: `KycService:fetchSessionDisclaimersBySessionId`; + handler: KycService['fetchSessionDisclaimersBySessionId']; }; /** * Records idOS + KYC-provider consents for a UKYC session * (`POST /sessions/{sessionId}/disclaimers`). `key`/`version` pairs must - * match the current catalog from {@link fetchSessionDisclaimers}. A 409 + * match the current catalog from {@link fetchSessionDisclaimersBySessionId}. A 409 * means those document versions were already recorded for the session. * * @param params - The consent parameters. @@ -241,8 +241,8 @@ export type KycServiceMethodActions = | KycServiceCheckKycRequiredAction | KycServiceCreateVendorCustomerAction | KycServiceSubmitVendorDisclaimersAction - | KycServiceFetchDisclaimersCatalogAction - | KycServiceFetchSessionDisclaimersAction + | KycServiceFetchSessionDisclaimersByCountryAction + | KycServiceFetchSessionDisclaimersBySessionIdAction | KycServiceSubmitSessionDisclaimersAction | KycServiceFetchKycStatusAction | KycServiceFetchIdosEnclaveJwksAction diff --git a/packages/kyc-controller/src/KycService.test.ts b/packages/kyc-controller/src/KycService.test.ts index b6882ffdd48..a6fc864b7bb 100644 --- a/packages/kyc-controller/src/KycService.test.ts +++ b/packages/kyc-controller/src/KycService.test.ts @@ -760,7 +760,7 @@ describe('KycService', () => { }); }); - describe('fetchDisclaimersCatalog', () => { + describe('fetchSessionDisclaimersByCountry', () => { const documents = { idOS: [ { @@ -788,7 +788,7 @@ describe('KycService', () => { const { service } = getService(); expect( - await service.fetchDisclaimersCatalog({ country: 'USA' }), + await service.fetchSessionDisclaimersByCountry({ country: 'USA' }), ).toStrictEqual(documents); }); @@ -796,7 +796,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchDisclaimersCatalog({ country: 'US' }), + service.fetchSessionDisclaimersByCountry({ country: 'US' }), ).rejects.toThrow(/ISO 3166-1 alpha-3/u); }); @@ -808,7 +808,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchDisclaimersCatalog({ country: 'USA' }), + service.fetchSessionDisclaimersByCountry({ country: 'USA' }), ).rejects.toThrow(/Malformed response received from disclaimers API/u); }); @@ -820,7 +820,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchDisclaimersCatalog({ country: 'USA' }), + service.fetchSessionDisclaimersByCountry({ country: 'USA' }), ).rejects.toThrow(/Malformed response received from disclaimers API/u); }); @@ -832,12 +832,12 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchDisclaimersCatalog({ country: 'USA' }), + service.fetchSessionDisclaimersByCountry({ country: 'USA' }), ).rejects.toThrow(/failed with status '500'/u); }); }); - describe('fetchSessionDisclaimers', () => { + describe('fetchSessionDisclaimersBySessionId', () => { const documents = { idOS: [ { @@ -868,7 +868,9 @@ describe('KycService', () => { const { service } = getService(); expect( - await service.fetchSessionDisclaimers({ sessionId: 'sid-1' }), + await service.fetchSessionDisclaimersBySessionId({ + sessionId: 'sid-1', + }), ).toStrictEqual(catalog); }); @@ -877,7 +879,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchSessionDisclaimers({ sessionId: 'sid-1' }), + service.fetchSessionDisclaimersBySessionId({ sessionId: 'sid-1' }), ).rejects.toThrow( /Malformed response received from session disclaimers API/u, ); @@ -890,7 +892,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchSessionDisclaimers({ sessionId: 'sid-1' }), + service.fetchSessionDisclaimersBySessionId({ sessionId: 'sid-1' }), ).rejects.toThrow( /Malformed response received from session disclaimers API/u, ); @@ -914,7 +916,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchSessionDisclaimers({ sessionId: 'sid-1' }), + service.fetchSessionDisclaimersBySessionId({ sessionId: 'sid-1' }), ).rejects.toThrow( /Malformed response received from session disclaimers API/u, ); @@ -925,7 +927,7 @@ describe('KycService', () => { const { service } = getService(); await expect( - service.fetchSessionDisclaimers({ sessionId: 'sid-1' }), + service.fetchSessionDisclaimersBySessionId({ sessionId: 'sid-1' }), ).rejects.toThrow(/failed with status '500'/u); }); }); diff --git a/packages/kyc-controller/src/KycService.ts b/packages/kyc-controller/src/KycService.ts index bdb4f8717e8..ea2afb80ccc 100644 --- a/packages/kyc-controller/src/KycService.ts +++ b/packages/kyc-controller/src/KycService.ts @@ -54,8 +54,8 @@ const MESSENGER_EXPOSED_METHODS = [ 'checkKycRequired', 'createVendorCustomer', 'submitVendorDisclaimers', - 'fetchDisclaimersCatalog', - 'fetchSessionDisclaimers', + 'fetchSessionDisclaimersByCountry', + 'fetchSessionDisclaimersBySessionId', 'submitSessionDisclaimers', 'fetchKycStatus', 'fetchIdosEnclaveJwks', @@ -323,12 +323,12 @@ export type SubmitVendorDisclaimersParams = { disclaimerIds: string[]; }; -export type FetchDisclaimersCatalogParams = { +export type FetchSessionDisclaimersByCountryParams = { /** ISO 3166-1 alpha-3 country code for `GET /disclaimers?country=`. */ country: string; }; -export type FetchSessionDisclaimersParams = { +export type FetchSessionDisclaimersBySessionIdParams = { /** UKYC session id from {@link KycService.createUkycSession}. */ sessionId: string; }; @@ -679,26 +679,26 @@ export class KycService extends BaseDataService< * Fetches the global idOS + KYC-provider disclaimer catalog * (`GET /disclaimers?country=`). Carries no consent state — per-document * `consented` flags and `credentialReusabilityConsentGiven` are - * session-scoped via {@link fetchSessionDisclaimers}. Vendor T&Cs continue to + * session-scoped via {@link fetchSessionDisclaimersBySessionId}. 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. */ - async fetchDisclaimersCatalog({ + async fetchSessionDisclaimersByCountry({ country, - }: FetchDisclaimersCatalogParams): Promise { + }: FetchSessionDisclaimersByCountryParams): Promise { if (country.length !== 3) { throw new Error( - `KycService.fetchDisclaimersCatalog: country must be an ISO 3166-1 alpha-3 code (received "${country}").`, + `KycService.fetchSessionDisclaimersByCountry: 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}:fetchDisclaimersCatalog`, country], + queryKey: [`${this.name}:fetchSessionDisclaimersByCountry`, country], queryFn: async () => this.#requestJson(url, { method: 'GET' }), staleTime: 0, gcTime: 0, @@ -714,22 +714,22 @@ 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}. + * pre-session global catalog use {@link fetchSessionDisclaimersByCountry}. + * 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({ + async fetchSessionDisclaimersBySessionId({ sessionId, - }: FetchSessionDisclaimersParams): Promise { + }: FetchSessionDisclaimersBySessionIdParams): Promise { const url = new URL( `/sessions/${encodeURIComponent(sessionId)}/disclaimers`, this.#baseUrl, ); const data = await this.fetchQuery({ - queryKey: [`${this.name}:fetchSessionDisclaimers`, sessionId], + queryKey: [`${this.name}:fetchSessionDisclaimersBySessionId`, sessionId], queryFn: async () => this.#requestJson(url, { method: 'GET' }), // Consent state can change after a POST, so always re-fetch. staleTime: 0, @@ -745,7 +745,7 @@ export class KycService extends BaseDataService< /** * Records idOS + KYC-provider consents for a UKYC session * (`POST /sessions/{sessionId}/disclaimers`). `key`/`version` pairs must - * match the current catalog from {@link fetchSessionDisclaimers}. A 409 + * match the current catalog from {@link fetchSessionDisclaimersBySessionId}. A 409 * means those document versions were already recorded for the session. * * @param params - The consent parameters. diff --git a/packages/kyc-controller/src/index.ts b/packages/kyc-controller/src/index.ts index f0bc44276d7..911f13e5957 100644 --- a/packages/kyc-controller/src/index.ts +++ b/packages/kyc-controller/src/index.ts @@ -14,6 +14,7 @@ export type { KycControllerState, KycControllerStateChangeEvent, KycControllerStatusChangedEvent, + FetchSessionDisclaimersParams, } from './KycController.js'; export type { KycControllerAcceptTermsAndStartSessionAction, @@ -24,6 +25,7 @@ export type { KycControllerClearSavedTermsAction, KycControllerClearStateAction, KycControllerCreateVendorCustomerAction, + KycControllerFetchSessionDisclaimersAction, KycControllerGetCustomerIdentityAction, KycControllerGetKycStatusAction, KycControllerGetSessionStatusAction, @@ -44,8 +46,8 @@ export type { CreateSessionParams, CreateUkycSessionParams, EncryptionSchema, - FetchDisclaimersCatalogParams, - FetchSessionDisclaimersParams, + FetchSessionDisclaimersByCountryParams, + FetchSessionDisclaimersBySessionIdParams, GetSessionStatusParams, VendorCustomerResponse, JwksResponse, @@ -67,12 +69,12 @@ export type { KycServiceCreateJourneyAction, KycServiceCreateSessionAction, KycServiceCreateUkycSessionAction, - KycServiceFetchVendorDisclaimersAction, - KycServiceFetchDisclaimersCatalogAction, KycServiceFetchIdosEnclaveJwksAction, KycServiceFetchIdosRelayJwksAction, KycServiceFetchKycStatusAction, - KycServiceFetchSessionDisclaimersAction, + KycServiceFetchSessionDisclaimersByCountryAction, + KycServiceFetchSessionDisclaimersBySessionIdAction, + KycServiceFetchVendorDisclaimersAction, KycServiceGetGeoCountryAction, KycServiceGetSessionStatusAction, KycServiceSetAuthorizationsAction,