The setup/constraints/etc. you're describing sound more like a permission-based, ACL-like approach.
Capability security is generally based on the following:
(1) A "capability" to perform some action is (by definition) something which is necessary and sufficient to perform that action. If that action is calling a particular function (like 'eval'), then we can treat the function's name/reference as a capability. This fits the definition since (a) we can only call functions which we have a name/reference for, and (b) anything with the name/reference to a function can call that function. For example:
function somethingWhichCanUseEval(eval) {
return eval("foo");
}
function somethingWhichCannotUseEval() {
throw "Not given a reference to 'eval', so we don't have anything we can call";
}
(Note the contrast to your statement that "runtimes can't tell which dependency a particular API call is coming from". Such functionality would actually violate (b) above, making it much
harder to implement capability security!)
(2) Capabilities are kept secret to begin with, and we pass them around to whatever needs them. In particular, this requires that (i) we can't ask for a list of capabilities (e.g. a list of references to every function), and (ii) we can't guess a capability (e.g. names should be cryptographically-random strings). For example:
// All guessable references to the eval function need to be shadowed, e.g.
function eval() {
throw "Error: Something attempted to call 'eval' without using the package";
}
window.eval = eval;
// etc.
// The package manager can give each package a cryptographically-random "import-name"
// When a package is imported, it's given the import-names of those packages it directly
// depends on
function myPackage(deps) {
// The 'deps' argument is a map from package names to (randomly generated) import-names
// We can import a package if we know its import-name, hence import-names are capabilities
// for importing packages
// We can import the 'eval' package by getting its import-name from the 'deps' map
const evalPkg = require(deps['eval']);
// The 'eval' package provides a reference to the 'eval' function
const eval = evalPkg.eval;
// At this point, we have the capability to call 'eval' (or pass it around to other functions, etc.)
}
(3) If we find ourselves "checking permissions" then it's already too late (those without permission shouldn't have even been capable of asking in the first place). Likewise if we try to "validate the caller's identity", since (i) that's a case of ambient authority (the "caller" may be a Confused Deputy), and (ii) it would prevent us delegating capabilities to others to act on our behalf, which in turn requires granting sweeping access to a broad range of systems 'just in case' anyone might want to use them.
(4) We can wrap capabilities in a more restricted interface, which acts as a capability for some more restricted action. For example, the following code provides the capability to evaluate code of the form 'foo = bar;':
function namerPkg(deps) {
const eval = require(deps['eval']).eval;
// Regex for valid Javascript names
const jsNameRegex = /[_a-zA-Z][_a-zA-Z0-9]*/;
return {
// A function which evals '<oldName> = <newName>;'
'namer': function namer(oldName, newName) {
// Parse/sanitise the input
const oldJSNames = oldName.match(jsNameRegex);
const newJSNames = newName.match(jsNameRegex);
if (oldJSNames == null ||
newJSNames == null ||
oldJSNames.length == 0 ||
newJSNames.length == 0) {
throw "Error: namer needs to be given Javascript-compatible names";
}
const oldJSName = oldJSNames[0];
const newJSName = newJSNames[0];
// Now it's safe to call eval
eval(newJSName + " = " + oldJSName + ";");
};
};
}