comlink icon indicating copy to clipboard operation
comlink copied to clipboard

Namespaced APIs

Open Menecats opened this issue 4 years ago • 4 comments

This PR introduces the concept of namespaced proxies.

With these chenges it's now possible to expose multiple objects from a single worker.

E.g.

worker.js

const authentication = {
  login: function (username, password) {
    return Math.random() < .5;
  }
};
const logger = {
  log: function (message) {
    console.log(message);
  }
};
const obj = {
  counter: 0,
  inc() {
    this.counter++;
  }
};

Comlink.expose(obj);
Comlink.exposeNamespaced(authentication, 'auth-namespace');
Comlink.exposeNamespaced(logger, 'log-namespace');

main.js

const worker = new Worker("worker.js");

const obj = Comlink.wrap(worker);
const auth = Comlink.wrapNamespaced(worker, 'auth-namespace');
const logger = Comlink.wrapNamespaced(worker, 'log-namespace');

if (await auth.login('my-username', 's3cr3t pwd!')) {
  await obj.inc();
  await logger.log(await obj.counter));
} else {
  await logger.log('Invalid credentials');
}

Menecats avatar Sep 23 '21 20:09 Menecats

Out of curiosity, can't the same thing be accomplished by something like this (sorry haven't looked at Comlink in ages, but the idea is to expose a single root object which contains proxies for your namespaced objects)

// worker.js
Comlink.expose({
  obj: Comlink.proxy(obj),
  authentication: Comlink.proxy(authentication),
  logger: Comlink.proxy(logger)
});

//main.js
const root = Comlink.wrap(worker);
const [obj, auth, logger] = await Promise.all(['obj', 'authentication', 'logger'].map(async (prop) => root[prop]));

mhofman avatar Sep 23 '21 21:09 mhofman

Yes currently there's a little difference by using a root object and namespaces, but I've also started working on the issue #444, where you can specify an allowed origin when exposing something with Comlink.

Something like this: Note: the parameters are not yet final

worker.js

Comlink.expose(obj, /* Comlink.ExposeOptions */ { allowedOrigins: ['*'] /* This is the default */ });
Comlink.exposeNamespaced(authentication, /* Comlink.ExposeOptions */ { allowedOrigins: ['example.com'] });

By exposing a single root object is not currently possible to specify different origin restrictions for each of them.

Menecats avatar Sep 24 '21 05:09 Menecats

Hey @Menecats,

I appreciate your enthusiasm, but it would be good in the future to open an issue first before implementing a feature so we can talk about it. I don’t think I want to add this to Comlink. As @mhofman points out, you can already do this with normal objects in Comlink right now.

As for the per-namespace origins, can you tell me more about the use-cases? I haven’t encountered a need for that.

That being said, if you wanted to tackle #444 as you hinted at, that’d be awesome!

surma avatar Sep 24 '21 09:09 surma

Hey @surma,

Namespaced APIs

Sorry about the missing issue, I've implemented this feature in a fork of comlink that I'm using for a customer and I thought that someone else could have needed it.

My reasons for implementing this API are the following:

Our customer has a web application that orchestrates different web applications loaded inside iframes; the orchestrator (launcher from now on) and the different applications loaded by the launcher (dashboards from now on) communicate already by using Comlink, we need to implement some components that can be loaded dynamically into the dashboards and they also have to communicate with the launcher, the problem is that we can't use the already existing Comlink proxy since it's used by the primary communication protocol between dashboards and the launcher that has been developed by another third party vendor that created the dashboards and the launcher.

That's why I've implemented namespaces, this way the vendor can continue to use wrap and expose exclusively for itself, while I can use a custom namespace to allow my components to communicate between the launcher and the dashboards.

per-namespace origin

TLDR

nevermind, I've noticed that the MessageEvent.origin property is not set in webworkers

Long answer

The idea behind these changes was to restrict access to a particular object in a web worker that I want to make available only to a particular origin.

E.g. worker.js

var publicApi = {...}
var privateApi = {...}; // This API is should be availabe only if this worker is loaded from 'example.com'

Comlink.expose(publicApi);
Comlink.exposeNamespaced(privateApi, 'my-namespace' /* , Specify somehow the allowed origin*/);

A simple idea using the current API can be to check the page origin before exposing the value

if (isValidOrigin(self.location.origin)) {
  Comlink.expose({privateApi, publicApi});
} else {
  Comlink.expose({publicApi});
}

The problem with that is that both chrome and firefox allow you to redefine the location property and this will expose the privateApi object even if the worker is loaded from an unauthorized origin.

worker-loader.js

Object.defineProperty(self, 'location', {value: {origin: 'https://example.com'}})
importScripts('http://example.com/worker.js');

My idea was to look at the origin property in the event received by the listeners of each exposed object and if it didn't match the allowedOrigins the event would have been discarded.

But since MessageEvent.origin is set only in server-sent events and cross-document messaging as per spec this whole point is currently invalid.

Menecats avatar Sep 24 '21 13:09 Menecats

Origin filtering has been added to the expose method in https://github.com/GoogleChromeLabs/comlink/pull/605 (docs to follow in #606).

With regard to the namespaces proposed in this PR. Could the problem described not be solved by passing MessagePorts across the iframe postmessage channel, and thus you can expose separate interfaces for each origin by passing the appropriate port endpoint?

benjamind avatar Jan 24 '23 19:01 benjamind

Hi @benjamind, yeah that's something I didn't think about when i tackled this issue. I think that that's a much more straightforward solution than what I've come up with.

Menecats avatar Jan 30 '23 07:01 Menecats