Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement message channel #444

Merged
merged 17 commits into from
Dec 20, 2024
Merged

implement message channel #444

merged 17 commits into from
Dec 20, 2024

Conversation

turnekybc
Copy link
Contributor

Summary & Motivation

How I Tested These Changes

Did you add a changeset?

If updating one of our packages, you'll likely need to add a changeset to your PR. To do so, run pnpm changeset. pnpm changeset will generate a file where you should write a human friendly message about the changes. Note how this (example) includes the package name (should be auto added by the command) along with the type of semver change (major.minor.patch) (which you should set).

These changes will be used at release time to determine what packages to publish and how to bump their version. For more context see this comment.

Copy link

codesandbox-ci bot commented Dec 9, 2024

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Copy link
Contributor

@r-n-o r-n-o left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this MessageChannel-based solution, it's way better than the global listeners/handlers we had previously! 🚀

If we have channels in place between individual iframeStampers and their iframes we can completely disregard the notion "trusted origin" I believe! The channel is opened by a trusted origin, almost by definition.

The ambiguity which existed before ("is the iframe receiving a message from its parent or from another frame?" and "is the parent receiving from its child iframe or from another frame?") doesn't exist anymore because the MessageChannel is privately owned by the iframe stamper:

  • script on the parent page or other frames can't write to port1. This means the iframe knows any messages on port2 are from the right sender: the frame which instantiated it 🔒
  • other frames can't write to port2 because it's owned by the iframe. This means the iframe stamper is guaranteed that messages on port1 are from the iframe it instantiated, and nobody else 🔒

packages/sdk-react/src/contexts/TurnkeyContext.tsx Outdated Show resolved Hide resolved
packages/sdk-browser/src/__types__/base.ts Outdated Show resolved Hide resolved
packages/iframe-stamper/src/index.ts Outdated Show resolved Hide resolved
packages/iframe-stamper/src/index.ts Outdated Show resolved Hide resolved
packages/iframe-stamper/src/index.ts Outdated Show resolved Hide resolved
packages/iframe-stamper/src/index.ts Outdated Show resolved Hide resolved
packages/iframe-stamper/src/index.ts Outdated Show resolved Hide resolved
packages/iframe-stamper/src/index.ts Outdated Show resolved Hide resolved
@turnekybc turnekybc requested a review from r-n-o December 18, 2024 18:25
@turnekybc turnekybc marked this pull request as ready for review December 18, 2024 18:37
packages/iframe-stamper/src/index.ts Show resolved Hide resolved
Comment on lines 245 to +252
return new Promise((resolve, reject) => {
this.iframe.contentWindow?.postMessage(
{
type: IframeEventType.InjectCredentialBundle,
value: bundle,
},
"*"
);

window.addEventListener(
"message",
(event) => {
if (event.origin !== this.iframeOrigin) {
// There might be other things going on in the window, for example: react dev tools, other extensions, etc.
// Instead of erroring out we simply return. Not our event!
return;
}
if (event.data?.type === IframeEventType.BundleInjected) {
resolve(event.data["value"]);
}
if (event.data?.type === IframeEventType.Error) {
reject(event.data["value"]);
}
},
false
);
this.messageChannel.port1.postMessage({
type: IframeEventType.InjectCredentialBundle,
value: bundle,
});
this.messageChannel.port1.onmessage = (event) => {
this.onMessageHandler(event, resolve, reject);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the wrapping promise here? How about modifying this function to be:

this.messageChannel.port1.postMessage({
    type: IframeEventType.InjectCredentialBundle,
    value: bundle,
});

return addMessageHandler();

(looks like you've done that for the other functions, injectCredentialBundle is the odd one out!)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked through this on Slack: while odd indeed, we'd break the interface if we made this function synchronous, because it means callers would have to remove awaits. So, it'd be a cleaner interface, but we're choosing to keep it the way it is for compatibility reasons. Let's not ask people to change their code, there's no real benefit here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I'm missing something obvious — what is the difference in interface between the two? (injectCredentialBundle vs injectKeyExportBundle), i.e. why is injectCredentialBundle the odd one out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

injectCredentialBundle returns a promise which wraps both the postMessage and onmessage logic:

    return new Promise((resolve, reject) => {
      this.messageChannel.port1.postMessage({
        type: IframeEventType.InjectCredentialBundle,
        value: bundle,
      });
      this.messageChannel.port1.onmessage = (event) => {
        this.onMessageHandler(event, resolve, reject);
      };
    });

injectKeyExportBundle calls postMessage and then returns a promise which handles the onmessage logic:

    this.messageChannel.port1.postMessage({
      type: IframeEventType.InjectKeyExportBundle,
      value: bundle,
      keyFormat,
      organizationId,
    });

    return this.addMessageHandler();

@@ -233,35 +265,14 @@ export class IframeStamper {
organizationId: string,
keyFormat?: KeyFormat
): Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why TS isn't complaining about this, but I believe there's a type mismatch? The return type is Promise<boolean>, but addMessageHandler returns Promise<any>.

!this.iframe.contentWindow.postMessage
) {
throw new Error(
"contentWindow or contentWindow.postMessage does not exist"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a scenario where we'd want granularity over this? in other words, is it particularly weird if .postMessage specifically doesn't exist?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So TypeScript complains if you do this.iframe.contentWindow.postMessage(...) because the contentWindow is possibly null. I'm not really aware of a scenario where this would actually be the case in the wild unless there was an underlying bug in the browser itself. contentWindow is read-only so you wouldn't be able to null it out if you tried.

This check is more of a defensive coding pattern in case something is really broken.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% agree with being defensive here. was just curious. i'm guessing having this in the wild is a pretty rare occurrence

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think the internet would be broken

Copy link
Collaborator

@andrewkmin andrewkmin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

@turnekybc turnekybc merged commit 3e5e7b2 into main Dec 20, 2024
5 checks passed
@turnekybc turnekybc deleted the eng-1441 branch December 20, 2024 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants