import { JSONRPCResponse, JSONRPCServer } from 'json-rpc-2.0'; import { EflynSdk, isValidOrigin, WrappedJSONRPCRequest, WrappedJSONRPCResponse } from './index'; import { clientBridge } from './client-bridge'; import { constructEventOnServer, deserializeEvent, deserializeRequest, isEvent, isEventSource, isRequest, SDK_EVENT_KEY, serializeEvent, serializeResponse, WrappedJSONRPCEvent } from './constants'; import EventEmitter from 'eventemitter3'; interface EflynSdkServerOptions { sdk: Partial; postMessageToProxies?: (options: { message: string }) => void; allowedOrigins?: string[]; debug?: boolean; } /** * Make the SDK available to child windows via window.postMessage * @param options */ export class EflynSdkServer { private allowedOrigins: string[] = []; private debug = false; private readonly sdkEventEmitter = new EventEmitter(); private readonly server = new JSONRPCServer(); private readonly postMessageToProxies?: (options: { message: string }) => void; get sdk(): EflynSdk { return this.sdkEventEmitter as EflynSdk; } listen(): void { window.addEventListener('message', this.serverWindowMessageHandler); } destroy(): void { window.removeEventListener('message', this.serverWindowMessageHandler); } constructor(options: EflynSdkServerOptions) { const { sdk: sdkBase, postMessageToProxies } = options; Object.assign(this.sdkEventEmitter, sdkBase); /** * Server origin + any allowed origins declared here */ this.setOrigins(options.allowedOrigins ?? []); this.debug = options.debug ?? false; this.postMessageToProxies = postMessageToProxies; this.log("Starting server with provided SDK", this.allowedOrigins); /** * Build the server object based on all namespaces and methods available on the provided SDK object */ for (const namespace of Object.keys(this.sdk)) { for (const method of Object.keys(this.sdk[namespace])) { this.server.addMethod(`${namespace}.${method}`, (args = []) => { this.log(`Invoking method '${namespace}.${method}' with arguments`, args); return this.sdk[namespace][method](...args); }); } } const oldEmit = this.sdk.emit; this.sdk.emit = ((name: string, data) => { oldEmit.apply(this.sdk, [name, data]); this.broadcastEvent({ name, data, allowedOrigins: this.allowedOrigins, }) }) as any } private log(message: string, ...optionalParams: any[]) { if (this.debug) { console.log(`server: ${message}`, ...optionalParams); } }; setOrigins(origins: string[]) { this.allowedOrigins = Array.from(new Set([window.location.origin, ...origins])); } private serverWindowMessageHandler = (event: MessageEvent) => { this.log("Received message event", event); if (!isValidOrigin(event.origin, this.allowedOrigins)) { this.log('Message from invalid origin', event); return; } /** * Save reference for later broadcasting */ clientBridge.connect(event.origin, event.source as Window); this.log("Connected iframe service with event source"); // Handle requests emitted on server window if (isRequest(event.data)) { const sdkRequest: WrappedJSONRPCRequest = deserializeRequest(event.data); /** * SECONDARY CHECK FOR ANY REQUESTS COMING VIA PROXIES */ if (isValidOrigin(sdkRequest.origin, this.allowedOrigins)) { this.log("Processing SDK request", sdkRequest); this.server.receive(sdkRequest).then((jsonRPCResponse: JSONRPCResponse) => { const wrappedResponse: WrappedJSONRPCResponse = { ...jsonRPCResponse, allowedOrigins: this.allowedOrigins, } this.log("Sending JSONRPC response", wrappedResponse); this.sendResponse(wrappedResponse, event.source as Window); }); } else { this.log('Request from invalid origin', event); } } // Eflyn event was broadcast on server window if (isEventSource(event.data)) { const wrappedEvent: WrappedJSONRPCEvent = { ...constructEventOnServer(event.data), allowedOrigins: this.allowedOrigins, } this.log("Broadcasting Eflyn event", event.data, wrappedEvent); this.broadcastEvent(wrappedEvent); this.sdkEventEmitter.emit(wrappedEvent.name, wrappedEvent.data); } } private broadcastEvent(event: WrappedJSONRPCEvent) { const message = serializeEvent(event); clientBridge.postMessageToAll(message, this.allowedOrigins); this.postMessageToProxies?.({ message }); } /** * Output events and responses */ private sendResponse(resp: WrappedJSONRPCResponse, targetWindow: Window) { this.log("Broadcasting message", resp); const message = serializeResponse(resp); if (targetWindow !== window) { targetWindow.postMessage(message); } else { this.log("Broadcasting response (or event) message to all clients", message); this.postMessageToProxies?.({ message }); } } }