const handlers = {
get: (target, key) => key in target ? target[key] : undefined,
set: (target, key, value) => {
if (key in target) {
target[key] = value;
}
return value;
}
};
const { revoke, proxy } = Proxy.revocable(ctx, handlers);
// elsewhere
try {
proxy.canvas.width = 500;
} catch (e) { console.log("Access has been revoked", e); }
Something like that should do what you're expecting.
A revocable proxy, with handlers for get and set traps, for the context.
Just keep in mind that when an instance of Proxy.revocable()
is revoked, any subsequent access of that proxy will throw, and thus everything now needs to use try/catch, in the case that it has, indeed, been revoked.
Just for fun, here's how you can do the exact same thing without fear of throwing (in terms of simply using the accessor; no guarantee for doing something wrong while you still have access):
const RevocableAccess = (item, revoked = false) => ({
access: f => revoked ? undefined : f(item),
revoke: () => { revoked = true; }
});
const { revoke, access: useContext } = RevocableAccess(ctx);
useContext(ctx => ctx.canvas.width = 500);
revoke();
useContext(ctx => ctx.canvas.width = 200); // never fires
Edit
As pointed out in the comments below, I completely neglected to test for the method calls on the host object, which, it turns out, are all protected. This comes down to weirdness in the host objects, which get to play by their own rules.
With a proxy as above, proxy.drawImage.apply(ctx, args)
would work just fine.
This, however, is counter-intuitive.
Cases that I'm assuming fail here, are Canvas, Image, Audio, Video, Promise (for instance based methods) and the like. I haven't conferred with the spec on this part of Proxies, and whether this is a property-descriptor thing, or a host-bindings thing, but I'm going to assume that it's the latter, if not both.
That said, you should be able to override it with the following change:
const { proxy, revoke } = Proxy.revocable(ctx, {
get(object, key) {
if (!(key in object)) {
return undefined;
}
const value = object[key];
return typeof value === "function"
? (...args) => value.apply(object, args)
: value;
}
});
Here, I am still "getting" the method off of the original object, to call it.
It just so happens that in the case of the value being a function, I call bind to return a function that maintains the this
relationship to the original context. Proxies usually handle this common JS issue.
...this causes its own security concern; someone could cache the value out, now, and have permanent access to, say, drawImage
, by saying
const draw = proxy.drawImage;
...
Then again, they already had the ability to save the real render context, just by saying
const ctx = proxy.canvas.getContext("2d");
...so I'm assuming some level of good-faith, here.
For a more secure solution, there are other fixes, though with canvas, unless it's in-memory only, the context is ultimately going to be available to anyone who can read the DOM.