diff --git a/packages/kyc-controller/ARCHITECTURE.md b/packages/kyc-controller/ARCHITECTURE.md index 265a538fbc..27ceff651a 100644 --- a/packages/kyc-controller/ARCHITECTURE.md +++ b/packages/kyc-controller/ARCHITECTURE.md @@ -658,6 +658,6 @@ Reference client (metamask-mobile): | `app/core/Engine/controllers/kyc/kyc-service-init.ts` | Construct service. | | `app/core/Engine/controllers/kyc/reactNativeSumSubLauncher.ts` | Native SumSub adapter. | | `app/core/Engine/messengers/kyc/*.ts` | Messenger delegation. | -| `app/components/Views/MoonpayDemo/useKycFlow.ts` | React ↔ controller binding. | +| `app/components/Views/MoonpayDemo/useKycFlow.ts` | React ↔ controller binding. | | `app/components/Views/MoonpayDemo/useMoonpayFrame.ts` | WebView postMessage bridge. | | `app/selectors/kycController.ts` | Redux selectors. | diff --git a/packages/kyc-controller/CHANGELOG.md b/packages/kyc-controller/CHANGELOG.md index b0cc2fbe4c..862fdea025 100644 --- a/packages/kyc-controller/CHANGELOG.md +++ b/packages/kyc-controller/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `KycController.getCustomerIdentity()` method and the `KycController:getCustomerIdentity` messenger action (plus the exported `KycControllerGetCustomerIdentityAction` and `KycCustomerIdentity` types). Returns the vendor-scoped `{ vendor, id }` for the currently authenticated customer, or `null` before authentication and after `reset()`. Lets consumers (e.g. ramps autoramp creation) attach the vendor customer id to downstream calls without reading the full KYC state, which also holds session/access tokens. The id is session-scoped and never persisted. -- Add Iron (Money/VBA) KYC path to `@metamask/kyc-controller`: `vendor: 'iron'` skips MoonPay Check/Auth frames; `KycService` clients for `/vendors/iron/*`, `POST /consents`, and `GET /kyc/status`; `refreshKycStatus` + `statusChanged` for Money toast state +- Add Iron (Money/VBA) KYC path to `@metamask/kyc-controller`: `vendor: 'iron'` skips MoonPay Check/Auth frames; `KycService` clients for `/vendors/iron/*`, `POST /consents`, and `GET /kyc/status`; `refreshKycStatus` + `statusChanged` for Money toast state ([#9852](https://github.com/MetaMask/core/pull/9852)) - Initial release of the `@metamask/kyc-controller` package for managing KYC / identity verification state across MetaMask clients ([#9781](https://github.com/MetaMask/core/pull/9781)) - Add `KycController` and `KycService` for managing KYC / identity verification state across MetaMask clients ([#9615](https://github.com/MetaMask/core/pull/9615)) - `KycController` (`BaseController`) owns the flow state machine, the Check/Auth frame message protocol, X25519 credential decryption, and SumSub orchestration via an injected `KycSumSubLauncher` adapter. diff --git a/packages/kyc-controller/README.md b/packages/kyc-controller/README.md index 32ee194a13..7aaf642375 100644 --- a/packages/kyc-controller/README.md +++ b/packages/kyc-controller/README.md @@ -20,4 +20,4 @@ This watches `src/**/*.ts` and re-runs the build on each change (it also perform ## Contributing -This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). \ No newline at end of file +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/kyc-controller/scripts/mint-ukyc-test-token.ts b/packages/kyc-controller/scripts/mint-ukyc-test-token.ts index 82b51801ac..10b47b1ea4 100644 --- a/packages/kyc-controller/scripts/mint-ukyc-test-token.ts +++ b/packages/kyc-controller/scripts/mint-ukyc-test-token.ts @@ -98,7 +98,9 @@ if (flags['expires-at']) { } else if (flags['expires-in']) { const issuedAt = params.issuedAt ?? new Date(); params.issuedAt = issuedAt; - params.expiresAt = new Date(issuedAt.getTime() + parseDurationMs(flags['expires-in'])); + params.expiresAt = new Date( + issuedAt.getTime() + parseDurationMs(flags['expires-in']), + ); } const result = mintUkycTestToken(params); diff --git a/packages/kyc-controller/src/KycController.test.ts b/packages/kyc-controller/src/KycController.test.ts index fdebac325f..aef1d4f36e 100644 --- a/packages/kyc-controller/src/KycController.test.ts +++ b/packages/kyc-controller/src/KycController.test.ts @@ -1848,7 +1848,9 @@ describe('KycController', () => { await controller.initialize({ email: 'a@b.co', vendor: 'iron' }); expect(controller.state.phase).toBe('error'); - expect(controller.state.error).toMatch(/Iron customer creation failed/u); + expect(controller.state.error).toMatch( + /Iron customer creation failed/u, + ); }); }); @@ -2065,7 +2067,9 @@ describe('KycController', () => { }, }, async ({ controller, handlers }) => { - handlers.createUkycSession.mockRejectedValue(new Error('sumsub down')); + handlers.createUkycSession.mockRejectedValue( + new Error('sumsub down'), + ); handlers.fetchIronDisclaimers.mockResolvedValue([]); await controller.acceptTermsAndStartSession({ email: 'a@b.co' }); diff --git a/packages/kyc-controller/src/KycController.ts b/packages/kyc-controller/src/KycController.ts index bc2c1582f9..29e7f21e12 100644 --- a/packages/kyc-controller/src/KycController.ts +++ b/packages/kyc-controller/src/KycController.ts @@ -14,6 +14,7 @@ import { x25519 } from '@noble/curves/ed25519'; import { decryptCredentials, generateKeyPair } from './crypto.js'; import type { EncryptedCredentialsEnvelope, X25519KeyPair } from './crypto.js'; +import { toBase64Url } from './encoding.js'; import type { KycControllerMethodActions } from './KycController-method-action-types.js'; import type { KycServiceMethodActions } from './KycService-method-action-types.js'; import type { @@ -28,7 +29,6 @@ import type { KycVendor, } from './types.js'; import { deriveClientMaterial } from './ukyc/deriveClientMaterial.js'; -import { toBase64Url } from './encoding.js'; import { verifyJwtChain } from './ukyc/jwtChain.js'; import { getOrCreateLocalUserSecret } from './ukyc/localUserSecret.js'; import type { UkycLocalUserSecretStore } from './ukyc/localUserSecret.js'; @@ -806,6 +806,8 @@ export class KycController extends BaseController< * launch SumSub — skipping MoonPay Check/Auth frames. * * @param consents - T&C2 boolean flags. + * @param consents.sumsubTncSigned - Whether Sumsub T&C were accepted. + * @param consents.idosTncSigned - Whether idOS T&C were accepted. */ async #startIronSession(consents: { sumsubTncSigned: boolean; @@ -848,12 +850,9 @@ export class KycController extends BaseController< if (this.#generation !== generation) { return; } - if ( - sumsubResult && - 'error' in sumsubResult && - typeof sumsubResult.error === 'string' - ) { - throw new Error(sumsubResult.error); + const sumsubError = sumsubResult?.error; + if (typeof sumsubError === 'string') { + throw new Error(sumsubError); } // After SumSub, refresh user-keyed status for the Money toast and start // polling while still pending. Soft-fail: toast refresh must not rewind @@ -1616,6 +1615,9 @@ export class KycController extends BaseController< * value actually changes. * * @param payload - The status payload to apply. + * @param payload.status - User-keyed KYC status from `GET /kyc/status`. + * @param payload.sumsubSessionId - Optional SumSub session id from status. + * @param payload.errorCode - Optional error code from status. */ #applyUserStatus(payload: { status: KycUserStatus; diff --git a/packages/kyc-controller/src/KycService.test.ts b/packages/kyc-controller/src/KycService.test.ts index 80caa8e8cd..c7d5d6c9db 100644 --- a/packages/kyc-controller/src/KycService.test.ts +++ b/packages/kyc-controller/src/KycService.test.ts @@ -497,9 +497,9 @@ describe('KycService', () => { }); const { service } = getService(); - await expect( - service.createIronCustomer({ email: 'a@b.co' }), - ).resolves.toMatchObject({ + expect( + await service.createIronCustomer({ email: 'a@b.co' }), + ).toMatchObject({ id: 'iron-1', email: 'a@b.co', status: 'SigningsRequired', @@ -581,13 +581,13 @@ describe('KycService', () => { .reply(204); const { service } = getService(); - await expect( - service.submitConsents({ + expect( + await service.submitConsents({ ironDisclaimerIds: ['d1'], sumsubTncSigned: true, idosTncSigned: true, }), - ).resolves.toBeUndefined(); + ).toBeUndefined(); }); it('throws an HttpError on a non-ok response', async () => { diff --git a/packages/kyc-controller/src/KycService.ts b/packages/kyc-controller/src/KycService.ts index 9ab5f59247..e155ca8f38 100644 --- a/packages/kyc-controller/src/KycService.ts +++ b/packages/kyc-controller/src/KycService.ts @@ -85,8 +85,9 @@ type AllowedActions = /** * Published when {@link KycService}'s cache is updated. */ -export type KycServiceCacheUpdatedEvent = - DataServiceCacheUpdatedEvent; +export type KycServiceCacheUpdatedEvent = DataServiceCacheUpdatedEvent< + typeof serviceName +>; /** * Published when a single key within {@link KycService}'s cache is updated. @@ -568,7 +569,8 @@ export class KycService extends BaseDataService< const url = new URL('/vendors/iron/kyc-required', this.#baseUrl); const data = await this.fetchQuery({ queryKey: [`${this.name}:checkIronKycRequired`], - queryFn: async () => this.#requestJson(url, { method: 'POST', body: '{}' }), + queryFn: async () => + this.#requestJson(url, { method: 'POST', body: '{}' }), // The requirement can change server-side, so always re-check. staleTime: 0, cacheTime: 0, diff --git a/packages/kyc-controller/src/ukyc/deriveClientMaterial.ts b/packages/kyc-controller/src/ukyc/deriveClientMaterial.ts index 59ffa68d02..b9c8b8e5aa 100644 --- a/packages/kyc-controller/src/ukyc/deriveClientMaterial.ts +++ b/packages/kyc-controller/src/ukyc/deriveClientMaterial.ts @@ -3,8 +3,8 @@ import { ed25519 } from '@noble/curves/ed25519'; import { hkdf } from '@noble/hashes/hkdf'; import { sha256 } from '@noble/hashes/sha2'; -import { UKYC_DERIVED_KEY_SIZES, UKYC_KDF_INFO } from './constants.js'; import { toBase64Url } from '../encoding.js'; +import { UKYC_DERIVED_KEY_SIZES, UKYC_KDF_INFO } from './constants.js'; /** * Derives UKYC client material from the root `local_user_secret` using diff --git a/packages/kyc-controller/src/ukyc/storageAccessToken.ts b/packages/kyc-controller/src/ukyc/storageAccessToken.ts index abc9455a1e..f020337696 100644 --- a/packages/kyc-controller/src/ukyc/storageAccessToken.ts +++ b/packages/kyc-controller/src/ukyc/storageAccessToken.ts @@ -1,12 +1,12 @@ import { stringToBytes } from '@metamask/utils'; import { ed25519 } from '@noble/curves/ed25519'; +import { toBase64Url } from '../encoding.js'; import { UKYC_STORAGE_ACCESS_TOKEN_AUDIENCES, UKYC_STORAGE_ACCESS_TOKEN_VERSION, } from './constants.js'; import type { UkycClientMaterial } from './deriveClientMaterial.js'; -import { toBase64Url } from '../encoding.js'; /** * Mints `storage_access_token` capabilities — the client-signed, scoped, diff --git a/packages/kyc-controller/src/ukyc/testToken.test.ts b/packages/kyc-controller/src/ukyc/testToken.test.ts index 2784d87ba2..0098a6bbbc 100644 --- a/packages/kyc-controller/src/ukyc/testToken.test.ts +++ b/packages/kyc-controller/src/ukyc/testToken.test.ts @@ -1,6 +1,7 @@ -import { ed25519 } from '@noble/curves/ed25519'; import { hexToBytes, stringToBytes } from '@metamask/utils'; +import { ed25519 } from '@noble/curves/ed25519'; +import { base64UrlToBytes } from '../encoding.js'; import { UKYC_CAPABILITY_AUTH_SCHEME, UKYC_STORAGE_ACCESS_TOKEN_AUDIENCE, @@ -8,7 +9,6 @@ import { } from './constants.js'; import { canonicalizeJson } from './storageAccessToken.js'; import { mintUkycTestToken } from './testToken.js'; -import { base64UrlToBytes } from '../encoding.js'; // A fixed 32-byte secret (all 0x42), as hex, so storage_id and keys are stable. const SECRET_HEX = '42'.repeat(32); @@ -101,9 +101,9 @@ describe('UKYC mintUkycTestToken', () => { // 32 bytes hex-encoded. expect(result.localUserSecret).toMatch(/^[0-9a-f]{64}$/u); expect(result.token.payload.operations).toStrictEqual(['read']); - expect(result.authorizationHeader.startsWith( - `${UKYC_CAPABILITY_AUTH_SCHEME} `, - )).toBe(true); + expect( + result.authorizationHeader.startsWith(`${UKYC_CAPABILITY_AUTH_SCHEME} `), + ).toBe(true); }); it('binds session_id for a Relay-presented token', () => { diff --git a/packages/kyc-controller/src/ukyc/wrappedRelayPayload.ts b/packages/kyc-controller/src/ukyc/wrappedRelayPayload.ts index ae28ec4ec2..2fe21e5084 100644 --- a/packages/kyc-controller/src/ukyc/wrappedRelayPayload.ts +++ b/packages/kyc-controller/src/ukyc/wrappedRelayPayload.ts @@ -1,5 +1,5 @@ -import type { UkycClientMaterial } from './deriveClientMaterial.js'; import { toBase64Url } from '../encoding.js'; +import type { UkycClientMaterial } from './deriveClientMaterial.js'; import type { UkycStorageAccessToken } from './storageAccessToken.js'; /**