whatwg / loader

Loader Standard
https://whatwg.github.io/loader/
Creative Commons Zero v1.0 Universal
609 stars 45 forks source link

Safely hooking the loader of an iframe #155

Open andyearnshaw opened 7 years ago

andyearnshaw commented 7 years ago

I'd like to put this idea forward to get some opinions about it. I work in the ad-serving industry, where I manage scripts that deliver ads to pages (as cross-origin iframes) and libraries for creative developers. My involvement in both these areas drives me to try and improve how the 2 things work together.

Being able to hook parts of the loader for one of these iframes would allow me to provide a lot of convenience for creative developers. This is an off-shoot of a similar problem to the one I talk about in https://github.com/whatwg/html/issues/2161.

Probably oversimplified example:

Parent script

const API_BASE = 'http://location.of/widgetapi';

class Widget {
    constructor(manifest) {
        this.iframe = document.createElement('iframe');
        this.iframe.src = manifest.url;

        // Hook the iframe loader to provide "built-in" modules to the widget
        this.iframe.loader.hook('@@widget/api', {
            resolve: () => `${ API_BASE }/${ manifest.apiVersion }/api.js`
        });
        this.iframe.loader.hook('@@widget/manifest', {
            fetch: () => `export default ${ JSON.stringify(manifest) }`
        });
        this.iframe.loader.hook('@@widget/foo-component', {
            resolve: () => `${ API_BASE }/${ manifest.apiVersion }/components/foo.js`
        });
    }
    appendTo(el) {
        el.appendChild(this.iframe);
    }
}

let widget = new Widget({
        name: 'widgetA',
        url: 'http://some.other.com/widgets/widgetA.html',
        apiVersion: '^1.0.0'
    });

Iframe snippet

<script type="module">
    import api from '@@widget/api';
    import Foo from '@@widget/foo-component';

    let foo = new Foo();
    api.getLoggedInUser()
        .then((user) => foo.bar = user);
</script>

This provides a nice application environment for the widget developer without them needing to worry about implementation details. The app environment can give arbitrary data—that it, potentially, already has—to the widget without needing to write clunky message send/receive callbacks or force the widget to make a separate HTTP request for the data.

Other, potentially real-world examples:

Providing an API to advertising creative developers (my main use case):

import { expand } from '@@creative-api';
cta.onclick = () => expand().then(showPage2);

eBay could provide an API to sellers for their item descriptions (eBay forbid external scripts in descriptions to prevent user tracking/abuse)

import eBay from '@@eBay';
eBay.getMyOtherItems().then(renderCarousel);

Hosted apps/games on facebook no longer require special tokens for interacting with the API:

import FB from '@@facebook';
FB.getUser().then(...);

Security

Naturally, security is going to be a concern. Being able to hook and map a window's module loader is potentially dangerous. Concerns can, hopefully, be alleviated by the following restrictions:

caridy commented 7 years ago

@andyearnshaw IMO, this use-case that you have described is probably something that we will allow via Realms (https://github.com/tc39/proposal-realms/issues/46) rather than having to use the loader API. It will give you more control on the isolation pieces, and even proxying modules that are shared from the parent script if needed.

andyearnshaw commented 7 years ago

@caridy, Realms look interesting (and I'm super-stoked for them!), but looks like an entire "pseudo" DOM implementation (or some kind of reinvented wheel) would be needed for a Realm in order for it to render a widget. That's a big cost for something you already get for free with iframes. For instance:

I think that replicating all that functionality with a Realm, while possibly doable, seems tricky to do. This strawman "supplies" modules to a ready-made environment (the iframe) which seems like a much simpler approach for this kind of use case.