Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 55 additions & 27 deletions packages/kyc-controller/src/KycController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,31 @@ function makeEnvelope(
* @param credentials - The plaintext credentials to encrypt.
* @returns The encrypted envelope.
*/
function envelopeFor(
async function envelopeFor(
controller: KycController,
credentials: Record<string, unknown>,
): { ephemeralPublicKey: string; iv: string; ciphertext: string } {
const url = controller.buildCheckFrameUrl();
const publicKeyHex = new URL(url as string).searchParams.get(
'publicKey',
) as string;
): Promise<{ ephemeralPublicKey: string; iv: string; ciphertext: string }> {
let url = controller.buildCheckFrameUrl();
if (!url) {
const inProgressPhases: KycController['state']['phase'][] = [
'session',
'check',
'auth',
'form',
'submit',
];
if (!inProgressPhases.includes(controller.state.phase)) {
throw new Error(
'Controller needs a MoonPay frame keypair; call initialize({ vendor: "moonpay" }) first',
);
}
await controller.initialize({ vendor: 'moonpay' });
url = controller.buildCheckFrameUrl();
}
if (!url) {
throw new Error('Could not build Check frame URL for envelope');
}
const publicKeyHex = new URL(url).searchParams.get('publicKey') as string;
return makeEnvelope(hexToBytes(publicKeyHex), credentials);
}

Expand Down Expand Up @@ -431,7 +448,7 @@ describe('KycController', () => {
});

// Establish an auth-frame client token from a prior authentication.
const envelope = envelopeFor(controller, {
const envelope = await envelopeFor(controller, {
clientToken: 'old-client',
});
await controller.handleFrameMessage({
Expand Down Expand Up @@ -496,6 +513,7 @@ describe('KycController', () => {
await pending;

expect(controller.state.sessionToken).toBe('new-session');
await controller.initialize({ vendor: 'moonpay' });
expect(controller.buildCheckFrameUrl()).toContain(
'sessionToken=new-session',
);
Expand Down Expand Up @@ -713,14 +731,13 @@ describe('KycController', () => {
await withController(
{ options: { state: { phase: 'done', sessionToken: 'tok' } } },
async ({ controller }) => {
const envelope = envelopeFor(controller, { accessToken: 'access-1' });
const result = await controller.handleFrameMessage({
message: {
kind: 'complete',
meta: { channelId: 'ch_1' },
payload: {
status: 'active',
credentials: envelope,
credentials: 'not-used',
customer: { id: 'cust-late' },
},
},
Expand Down Expand Up @@ -770,6 +787,7 @@ describe('KycController', () => {
await withController(
{ options: { state: { phase: 'check', sessionToken: 'tok' } } },
async ({ controller }) => {
await controller.initialize({ vendor: 'moonpay' });
await controller.handleFrameMessage({
message: {
kind: 'complete',
Expand All @@ -788,7 +806,7 @@ describe('KycController', () => {
await withController(
{ options: { state: { phase: 'check', sessionToken: 'tok' } } },
async ({ controller }) => {
const envelope = envelopeFor(controller, {
const envelope = await envelopeFor(controller, {
accessToken: 'access-1',
});
await controller.handleFrameMessage({
Expand All @@ -808,7 +826,7 @@ describe('KycController', () => {
await withController(
{ options: { state: { phase: 'check', sessionToken: 'tok' } } },
async ({ controller }) => {
const envelope = envelopeFor(controller, {
const envelope = await envelopeFor(controller, {
clientToken: 'client-1',
});
await controller.handleFrameMessage({
Expand Down Expand Up @@ -877,7 +895,7 @@ describe('KycController', () => {
await withController(
{ options: { state: { phase: 'auth', sessionToken: 'tok' } } },
async ({ controller }) => {
const envelope = envelopeFor(controller, {
const envelope = await envelopeFor(controller, {
accessToken: 'access-2',
});
await controller.handleFrameMessage({
Expand Down Expand Up @@ -936,7 +954,9 @@ describe('KycController', () => {
},
},
async ({ controller, handlers }) => {
const envelope = envelopeFor(controller, { accessToken: 'access-1' });
const envelope = await envelopeFor(controller, {
accessToken: 'access-1',
});

await controller.handleFrameMessage({
message: {
Expand Down Expand Up @@ -966,7 +986,9 @@ describe('KycController', () => {
},
async ({ controller, handlers, launcher }) => {
handlers.checkKycRequired.mockResolvedValue({ kycRequired: false });
const envelope = envelopeFor(controller, { accessToken: 'access-1' });
const envelope = await envelopeFor(controller, {
accessToken: 'access-1',
});

await controller.handleFrameMessage({
message: {
Expand Down Expand Up @@ -1006,7 +1028,9 @@ describe('KycController', () => {
onStatusChange?.('InProgress', 'Completed');
return { ok: true };
});
const envelope = envelopeFor(controller, { accessToken: 'access-2' });
const envelope = await envelopeFor(controller, {
accessToken: 'access-2',
});

await controller.handleFrameMessage({
message: {
Expand Down Expand Up @@ -1038,7 +1062,9 @@ describe('KycController', () => {
async ({ controller, handlers, launcher }) => {
handlers.checkKycRequired.mockResolvedValue({ kycRequired: true });
launcher.isAvailable.mockReturnValue(false);
const envelope = envelopeFor(controller, { accessToken: 'access-1' });
const envelope = await envelopeFor(controller, {
accessToken: 'access-1',
});

const result = await controller.handleFrameMessage({
message: {
Expand Down Expand Up @@ -1083,7 +1109,9 @@ describe('KycController', () => {
onStatusChange?.('InProgress', 'Completed');
return { ok: true };
});
const envelope = envelopeFor(controller, { accessToken: 'access-1' });
const envelope = await envelopeFor(controller, {
accessToken: 'access-1',
});
const message = {
kind: 'complete',
meta: { channelId: 'ch_2' },
Expand Down Expand Up @@ -1122,15 +1150,9 @@ describe('KycController', () => {
},
},
async ({ controller, handlers }) => {
// The keypair is stable across reset, so both envelopes can be built
// up front while the session token (used only to derive the public
// key here) is still present.
const envelope1 = envelopeFor(controller, {
const envelope1 = await envelopeFor(controller, {
accessToken: 'access-1',
});
const envelope2 = envelopeFor(controller, {
accessToken: 'access-2',
});
const messageFor = (
credentials: unknown,
): {
Expand Down Expand Up @@ -1169,6 +1191,9 @@ describe('KycController', () => {
// returns to phase `check`) and confirm the next completion continues
// again rather than being blocked forever by a stuck guard.
await controller.initialize({ product: 'ramps' });
const envelope2 = await envelopeFor(controller, {
accessToken: 'access-2',
});
handlers.checkKycRequired.mockResolvedValue({ kycRequired: false });
await controller.handleFrameMessage({
message: messageFor(envelope2),
Expand All @@ -1193,7 +1218,9 @@ describe('KycController', () => {
},
async ({ controller, handlers, launcher }) => {
handlers.checkKycRequired.mockRejectedValue(new Error('down'));
const envelope = envelopeFor(controller, { accessToken: 'access-1' });
const envelope = await envelopeFor(controller, {
accessToken: 'access-1',
});

await controller.handleFrameMessage({
message: {
Expand All @@ -1220,7 +1247,8 @@ describe('KycController', () => {
it('builds the check frame URL with a session', async () => {
await withController(
{ options: { state: { sessionToken: 'tok' } } },
({ controller }) => {
async ({ controller }) => {
await controller.initialize({ vendor: 'moonpay' });
const url = controller.buildCheckFrameUrl() as string;
expect(url).toContain('sessionToken=tok');
expect(url).toContain('channelId=ch_1');
Expand Down Expand Up @@ -2432,7 +2460,7 @@ describe('KycController', () => {
await withController(
{ options: { state: { phase: 'check', sessionToken: 'tok' } } },
async ({ controller }) => {
const envelope = envelopeFor(controller, {
const envelope = await envelopeFor(controller, {
clientToken: 'client-1',
});
await controller.handleFrameMessage({
Expand Down
37 changes: 27 additions & 10 deletions packages/kyc-controller/src/KycController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,8 @@ export class KycController extends BaseController<
> {
readonly #sumsubLauncher: KycSumSubLauncher;

/** Ephemeral X25519 keypair for the frame key exchange (never persisted). */
readonly #keypair: X25519KeyPair;
/** MoonPay Check/Auth frame X25519 keypair (never persisted). */
#moonpayFrameKeypair: X25519KeyPair | null = null;

/** Auth-frame client token, kept out of state. */
#authClientToken: string | null = null;
Expand Down Expand Up @@ -739,7 +739,6 @@ export class KycController extends BaseController<
this.#sumsubLauncher = sumsubLauncher;
this.#sessionStatusPollIntervalMs = sessionStatusPollIntervalMs;
this.#userStatusPollIntervalMs = userStatusPollIntervalMs;
this.#keypair = generateKeyPair();

this.messenger.registerMethodActionHandlers(
this,
Expand Down Expand Up @@ -803,11 +802,20 @@ export class KycController extends BaseController<
// forces `phase` back through `session`/`check`, breaking an in-flight
// Check/Auth frame flow. Leave the active flow untouched and let the
// consumer drive it (or call `reset` first to start over).
const vendor = params?.vendor ?? 'moonpay';

if (IN_PROGRESS_PHASES.includes(this.state.phase)) {
if (vendor === 'moonpay' && !this.#moonpayFrameKeypair) {
this.#moonpayFrameKeypair = generateKeyPair();
}
return;
}

const vendor = params?.vendor ?? 'moonpay';
if (vendor === 'moonpay') {
this.#moonpayFrameKeypair = generateKeyPair();
} else {
this.#moonpayFrameKeypair = null;
}
Comment thread
cursor[bot] marked this conversation as resolved.

// `initialize` starts a fresh flow, so `activeProduct` is always reset to
// this call's product (or `null`). Otherwise a prior run's product could
Expand Down Expand Up @@ -943,6 +951,7 @@ export class KycController extends BaseController<
state.activeVendor = params.vendor;
if (params.vendor !== 'moonpay') {
this.#authClientToken = null;
this.#moonpayFrameKeypair = null;
this.#clearMoonPaySession(state);
}
});
Expand Down Expand Up @@ -1542,11 +1551,11 @@ export class KycController extends BaseController<

let accessToken: string | undefined;
let clientToken: string | undefined;
if (credsEnvelope) {
if (credsEnvelope && this.#moonpayFrameKeypair) {
try {
const { credentials } = decryptCredentials(
credsEnvelope,
this.#keypair.privateKey,
this.#moonpayFrameKeypair.privateKey,
);
accessToken = credentials.accessToken;
clientToken = credentials.clientToken;
Expand Down Expand Up @@ -1687,12 +1696,16 @@ export class KycController extends BaseController<
* @returns The Check-frame URL or `null`.
*/
buildCheckFrameUrl(): string | null {
if (this.state.activeVendor !== 'moonpay' || !this.state.sessionToken) {
if (
this.state.activeVendor !== 'moonpay' ||
!this.state.sessionToken ||
!this.#moonpayFrameKeypair
) {
return null;
}
const url = new URL(`${FRAMES_BASE_URL}/check-connection`);
url.searchParams.set('sessionToken', this.state.sessionToken);
url.searchParams.set('publicKey', this.#keypair.publicKeyHex);
url.searchParams.set('publicKey', this.#moonpayFrameKeypair.publicKeyHex);
url.searchParams.set('channelId', CHANNEL_CHECK);
url.searchParams.set('skipKyc', 'true');
return url.toString();
Expand All @@ -1704,12 +1717,16 @@ export class KycController extends BaseController<
* @returns The Auth-frame URL or `null`.
*/
buildAuthFrameUrl(): string | null {
if (this.state.activeVendor !== 'moonpay' || !this.#authClientToken) {
if (
this.state.activeVendor !== 'moonpay' ||
!this.#authClientToken ||
!this.#moonpayFrameKeypair
) {
return null;
}
const url = new URL(`${FRAMES_BASE_URL}/auth`);
url.searchParams.set('clientToken', this.#authClientToken);
url.searchParams.set('publicKey', this.#keypair.publicKeyHex);
url.searchParams.set('publicKey', this.#moonpayFrameKeypair.publicKeyHex);
url.searchParams.set('channelId', CHANNEL_AUTH);
return url.toString();
}
Expand Down
Loading