Skip to content

Commit

Permalink
wip: implement pending request queue
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewkmin committed Jan 22, 2025
1 parent 62b4d3e commit e9aeca7
Showing 1 changed file with 112 additions and 131 deletions.
243 changes: 112 additions & 131 deletions packages/iframe-stamper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,46 +179,81 @@ export class IframeStamper {
this.pendingRequests = new Map();
}

onMessageHandler(event: MessageEvent): void {
console.log("full event data", event.data);
console.log("pending requests", this.pendingRequests);

const { type, value, requestId } = event.data || {};

// Handle messages without requestId (like PUBLIC_KEY_READY)
if (!requestId) {
if (type === IframeEventType.PublicKeyReady) {
this.iframePublicKey = value;
return;
}
return;
}

const pendingRequest = this.pendingRequests.get(requestId);
if (!pendingRequest) {
console.warn(`Received response for unknown request: ${requestId}`);
return;
}

// Remove from pending requests
this.pendingRequests.delete(requestId);

if (type === IframeEventType.Error) {
pendingRequest.reject(new Error(value));
return;
}

// Handle specific response types
switch (type) {
case IframeEventType.Stamp:
pendingRequest.resolve({
stampHeaderName,
stampHeaderValue: value,
});
break;
default:
pendingRequest.resolve(value);
}
}

/**
* Inserts the iframe on the page and returns a promise resolving to the iframe's public key
*/
async init(): Promise<string> {
this.container.appendChild(this.iframe);
/**
* Once the iframe is loaded, we send a message to the iframe to hand over the
* MessageChannel's second port, port2, and establish the secure communication channel.
* The iframe will use this port to send messages back to the parent page.
* See https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage#transfer
*/
this.iframe.addEventListener("load", () => {
if (
!this.iframe.contentWindow ||
!this.iframe.contentWindow.postMessage
) {
throw new Error(
"contentWindow or contentWindow.postMessage does not exist"
);
}
return new Promise((resolve, reject) => {
this.container.appendChild(this.iframe);

this.iframe.addEventListener("load", () => {
if (!this.iframe.contentWindow?.postMessage) {
reject(
new Error(
"contentWindow or contentWindow.postMessage does not exist"
)
);
return;
}

this.iframe.contentWindow.postMessage(
{ type: IframeEventType.TurnkeyInitMessageChannel },
this.iframeOrigin,
[this.messageChannel.port2]
);
});
this.iframe.contentWindow.postMessage(
{ type: IframeEventType.TurnkeyInitMessageChannel },
this.iframeOrigin,
[this.messageChannel.port2]
);
});

return new Promise((resolve, _reject) => {
/**
* The MessageChannel port1 property is the port that gets attached
* to the context that instantiated the MessageChannel. This class, the IframeStamper,
* instantied the MessageChannel and will use port1 to send messages to the iframe.
* See https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/port1
*/
this.messageChannel.port1.onmessage = (event) => {
// Handle initial PublicKeyReady event
if (event.data?.type === IframeEventType.PublicKeyReady) {
this.iframePublicKey = event.data["value"];
resolve(event.data["value"]);
this.iframePublicKey = event.data.value;
resolve(event.data.value);
}

// Handle all other messages
this.onMessageHandler(event);
};
});
}
Expand All @@ -241,54 +276,33 @@ export class IframeStamper {
}

/**
* Adds a message handler to the iframe's message channel
* Generic function to abstract away request creation
* @param type
* @param payload
* @returns
*/
addMessageHandler(): Promise<any> {
private createRequest<T>(
type: IframeEventType,
payload: any = {}
): Promise<T> {
console.log("creating request");
return new Promise((resolve, reject) => {
this.messageChannel.port1.onmessage = (event) => {
this.onMessageHandler(event, resolve, reject);
};
});
}
const requestId = generateUUID();

onMessageHandler(event: MessageEvent, resolve: any, reject: any): void {
const requestId = event.data?.requestId;

// If there's a requestId, try to find the pending request
if (requestId && this.pendingRequests.has(requestId)) {
const pendingRequest = this.pendingRequests.get(requestId)!;
this.pendingRequests.delete(requestId);

switch (event.data?.type) {
case IframeEventType.Stamp:
pendingRequest.resolve({
stampHeaderName: stampHeaderName,
stampHeaderValue: event.data["value"],
});
break;
case IframeEventType.Error:
pendingRequest.reject(event.data["value"]);
break;
default:
pendingRequest.resolve(event.data["value"]);
}
return;
}
console.log("new request id", requestId);

// Otherwise, if there's no requestId, handle per usual
switch (event.data?.type) {
case IframeEventType.Stamp:
resolve({
stampHeaderName: stampHeaderName,
stampHeaderValue: event.data["value"],
});
break;
case IframeEventType.Error:
reject(event.data["value"]);
break;
default:
resolve(event.data["value"]);
}
this.pendingRequests.set(requestId, {
resolve,
reject,
requestId,
});

this.messageChannel.port1.postMessage({
type,
requestId,
...payload,
});
});
}

/**
Expand All @@ -298,15 +312,8 @@ export class IframeStamper {
* This is used during recovery and auth flows.
*/
async injectCredentialBundle(bundle: string): Promise<boolean> {
return new Promise((resolve, reject) => {
this.messageChannel.port1.postMessage({
type: IframeEventType.InjectCredentialBundle,
value: bundle,
});

this.messageChannel.port1.onmessage = (event) => {
this.onMessageHandler(event, resolve, reject);
};
return this.createRequest<boolean>(IframeEventType.InjectCredentialBundle, {
value: bundle,
});
}

Expand All @@ -322,14 +329,11 @@ export class IframeStamper {
organizationId: string,
keyFormat?: KeyFormat
): Promise<boolean> {
this.messageChannel.port1.postMessage({
type: IframeEventType.InjectKeyExportBundle,
return this.createRequest<boolean>(IframeEventType.InjectKeyExportBundle, {
value: bundle,
keyFormat,
organizationId,
});

return this.addMessageHandler();
}

/**
Expand All @@ -342,13 +346,13 @@ export class IframeStamper {
bundle: string,
organizationId: string
): Promise<boolean> {
this.messageChannel.port1.postMessage({
type: IframeEventType.InjectWalletExportBundle,
value: bundle,
organizationId,
});

return this.addMessageHandler();
return this.createRequest<boolean>(
IframeEventType.InjectWalletExportBundle,
{
value: bundle,
organizationId,
}
);
}

/**
Expand All @@ -360,14 +364,11 @@ export class IframeStamper {
organizationId: string,
userId: string
): Promise<boolean> {
this.messageChannel.port1.postMessage({
type: IframeEventType.InjectImportBundle,
return this.createRequest<boolean>(IframeEventType.InjectImportBundle, {
value: bundle,
organizationId,
userId,
});

return this.addMessageHandler();
}

/**
Expand All @@ -377,11 +378,9 @@ export class IframeStamper {
* This is used during the wallet import flow.
*/
async extractWalletEncryptedBundle(): Promise<string> {
this.messageChannel.port1.postMessage({
type: IframeEventType.ExtractWalletEncryptedBundle,
});

return this.addMessageHandler();
return this.createRequest<string>(
IframeEventType.ExtractWalletEncryptedBundle
);
}

/**
Expand All @@ -392,53 +391,35 @@ export class IframeStamper {
* This is used during the private key import flow.
*/
async extractKeyEncryptedBundle(keyFormat?: KeyFormat): Promise<string> {
this.messageChannel.port1.postMessage({
type: IframeEventType.ExtractKeyEncryptedBundle,
keyFormat: keyFormat,
});

return this.addMessageHandler();
return this.createRequest<string>(
IframeEventType.ExtractKeyEncryptedBundle,
{ keyFormat }
);
}

/**
* Function to apply settings on allowed parameters in the iframe
* This is used to style the HTML element used for plaintext in wallet and private key import.
*/
async applySettings(settings: TIframeSettings): Promise<boolean> {
const settingsStr = JSON.stringify(settings);
this.messageChannel.port1.postMessage({
type: IframeEventType.ApplySettings,
value: settingsStr,
return this.createRequest<boolean>(IframeEventType.ApplySettings, {
value: JSON.stringify(settings),
});

return this.addMessageHandler();
}

/**
* Function to sign a payload with the underlying iframe
*/
async stamp(payload: string): Promise<TStamp> {
console.log("calling stamp");
if (this.iframePublicKey === null) {
throw new Error(
"null iframe public key. Have you called/awaited .init()?"
);
}

return new Promise((resolve, reject) => {
const requestId = generateUUID();

this.pendingRequests.set(requestId, {
resolve,
reject,
requestId,
});

this.messageChannel.port1.postMessage({
type: IframeEventType.StampRequest,
value: payload,
requestId
});
return this.createRequest<TStamp>(IframeEventType.StampRequest, {
value: payload,
});

}
}

0 comments on commit e9aeca7

Please sign in to comment.