Skip to content

Commit

Permalink
Update / document cross-origin message protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
rvanasa committed Jan 27, 2022
1 parent 8f37308 commit 2a9af8a
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ target/

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
Expand Down
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,60 @@ dfx deploy --argument '(null)'
- Clone the package-set repo: https://github.com/dfinity/vessel-package-set
- Make sure [`dhall` and `dhall-to-json` are installed](https://docs.dhall-lang.org/tutorials/Getting-started_Generate-JSON-or-YAML.html#os-x) with `apt` or `brew`
- `dhall-to-json --file vessel-package-set/src/packages.dhall > motoko-playground/src/config/package-set.json`

## Editor Integrations

Motoko Playground supports
limited [cross-origin communication](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). If you are
building a custom smart contract editor or similar application, you can use the following code to start deploying a project using Motoko Playground:

```js
const PLAYGROUND_ORIGIN = 'https://m7sm4-2iaaa-aaaab-qabra-cai.raw.ic0.app'
const APP_ID = 'MyEditor'

const userFiles = {
'Main.mo': 'actor { public func hello() : async Text { "Hello World" } }',
}

const playground = window.open(`${PLAYGROUND_ORIGIN}?post=${APP_ID}`, 'playground')

// Call repeatedly until loaded (interval ID used for acknowledgement)
const ack = setInterval(() => {
const request = {
type: 'workplace',
acknowledge: ack,
deploy: true,
actions: [{
type: 'loadProject',
payload: {
files: userFiles,
}
}]
}
const data = `${APP_ID}${JSON.stringify(request)}`
console.log('Request data:', data)
playground.postMessage(data, PLAYGROUND_ORIGIN)
}, 1000)

// Listen for acknowledgement
const responseListener = ({source, origin, data}) => {
if(
typeof data === 'string' &&
data.startsWith(APP_ID) &&
source === playground &&
origin === PLAYGROUND_ORIGIN
) {
console.log('Response data:', data)
// Parse JSON part of message
const response = JSON.parse(data.substring(APP_ID.length))
if(response.acknowledge === ack) {
clearInterval(ack)
window.removeEventListener('message', responseListener)
}
}
}
window.addEventListener('message', responseListener)
```

Note: this works for `localhost`out of the box. If you would like to use this feature in production, please submit a PR
adding your application's public URL to [`src/integrations/allowedOrigins.js`](src/integrations/allowedOrigins.js).
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,16 @@ const urlParams = new URLSearchParams(window.location.search);
const hasUrlParams = !!(
urlParams.get("git") ||
urlParams.get("tag") ||
urlParams.get("editor")
urlParams.get("post")
);
async function fetchFromUrlParams(
dispatch: (WorkplaceReducerAction) => void
): Promise<Record<string, string> | undefined> {
const git = urlParams.get("git");
const tag = urlParams.get("tag");
const editor = urlParams.get("editor");
if (editor) {
return setupEditorIntegration(editor, dispatch);
const editorKey = urlParams.get("post");
if (editorKey) {
return setupEditorIntegration(editorKey, dispatch);
}
if (git) {
const repo = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// please submit a PR including the URL prefix for your application.
// Read more: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns

const ALLOWED_ORIGIN_PREFIXES = [
"http://localhost", // Local machine
const ALLOWED_ORIGINS = [
/^https?:\/\/(localhost|127.0.0.1)(:[0-9]+)?$/, // Localhost
"https://blocks-editor.github.io", // Blocks (visual Motoko smart contract editor)
];

export default ALLOWED_ORIGIN_PREFIXES;
export default ALLOWED_ORIGINS;
30 changes: 20 additions & 10 deletions src/integrations/editorIntegration.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { WorkplaceReducerAction } from "../contexts/WorkplaceState";
import ALLOWED_ORIGIN_PREFIXES from "./allowedOriginPrefixes";
import ALLOWED_ORIGINS from "./allowedOrigins";

type EditorIntegrationHooks = {
deploy: () => Promise<void>;
};

type EditorIntegrationMessage = {
type EditorIntegrationRequest = {
type: "workplace";
acknowledge: number;
actions: [WorkplaceReducerAction];
deploy?: boolean;
};

type EditorIntegrationResponse = {
acknowledge: number;
};

export const INTEGRATION_HOOKS: Partial<EditorIntegrationHooks> = {};

// Cached return value to ensure at most one initialization
Expand All @@ -31,10 +35,9 @@ export async function setupEditorIntegration(
if (previousResult) {
return previousResult;
}
const messagePrefix = `editor_${editorKey}:`;

// Handle JSON messages from the external editor
const handleMessage = async (message: EditorIntegrationMessage) => {
const handleMessage = async (message: EditorIntegrationRequest) => {
if (message.type == "workplace") {
message.actions.forEach((action) => {
dispatch(action);
Expand All @@ -55,22 +58,29 @@ export async function setupEditorIntegration(
try {
// Ensure the message is from an allowed origin
if (
!ALLOWED_ORIGIN_PREFIXES.some((prefix) => origin.startsWith(prefix))
!ALLOWED_ORIGINS.some((allowed) =>
allowed instanceof RegExp
? allowed.test(origin)
: allowed === origin
)
) {
return;
}

// Validate and parse integration message
if (typeof data === "string" && data.startsWith(messagePrefix)) {
const message = JSON.parse(data.substring(messagePrefix.length));
// Validate and parse integration message (example: `CustomEditor{"type":"workplace","actions":[...]}`)
if (typeof data === "string" && data.startsWith(editorKey)) {
const message = JSON.parse(data.substring(editorKey.length));
if (process.env.NODE_ENV === "development") {
console.log("Received integration message:", message);
}
await handleMessage(message);
if (!(source instanceof MessagePort)) {
// Send acknowledgement
// Send response (example: `CustomEditor{"acknowledge":123}`)
const response: EditorIntegrationResponse = {
acknowledge: message.acknowledge,
};
source?.postMessage(
`${messagePrefix}acknowledge:${message.acknowledge}`,
`${editorKey}${JSON.stringify(response)}`,
origin
);
}
Expand Down

0 comments on commit 2a9af8a

Please sign in to comment.