Unsandboxed extensions
Unsandboxed extensions run as plain <script>
tags in the main window rather than in a sandbox. They have access to a lot of new powers and responsibilities that we will discuss below.
URL restrictions
To protect users from malicious extensions, extensions loaded from URLs will only run unsandboxed if their URL begins with one of these exactly:
https://extensions.turbowarp.org/
http://localhost:8000/
As you don't have control over extensions.turbowarp.org, you will have to use the latter option. For this, configure your local HTTP server to run on port 8000 instead of what you've been using so far.
When manually loading an extension from a file or JavaScript source code, there is an option to load the extension without the sandbox. This option to force an extension to run unsandboxed does not exist when using URLs due to security concerns.
Syntax
The syntax for unsandboxed extensions is very familiar but has some differences. Technically, if you just copy and paste your old sandboxed extensions as unsandboxed extensions, it will appear to just work. However, this is dangerous and is likely to cause bugs later.
If your sandboxed extension has code like like this:
// Old sandboxed extensions (worker or <iframe> sandbox):
class MyExtension {
getInfo () {
return { /* ... */ };
}
}
Scratch.extensions.register(new MyExtension());
Or if your extension uses an old "plugin" mechanism, such as this one: (if you don't recognize this code then don't worry about it)
class MyExtension {
getInfo () {
return { /* ... */ };
}
}
(function() {
var extensionInstance = new MyExtension(window.vm.extensionManager.runtime)
var serviceName = window.vm.extensionManager._registerInternalExtension(extensionInstance)
window.vm.extensionManager._loadedExtensions.set(extensionInstance.getInfo().id, serviceName)
})();
The unsandboxed version would have code like this:
(function(Scratch) {
'use strict';
class MyExtension {
getInfo () {
return { /* ... */ };
}
}
Scratch.extensions.register(new MyExtension());
})(Scratch);
Using this template prevents unsandboxed extensions from interfering with each other when they try to define variables, classes, or functions with the same name. By requiring everything to be defined in an immediately-invoked-function-expression (IIFE) and enabling strict mode, we prevent variables from accidentally leaking to the global scope.
All functions and variables defined by the extension must be defined within the IIFE. Additionally, each extension must make sure to use its own personal copy of the Scratch
API, which this template does automatically.
An interesting thing to note about this template is that it is backward compatible with sandboxed extensions. As long as the extension doesn't use any of the features given to unsandboxed extensions, it will continue to work the same as a sandboxed extension.
A more complete example
Here you can see a complete unsandboxed extension:
(function(Scratch) {
'use strict';
if (!Scratch.extensions.unsandboxed) {
throw new Error('This Hello World example must run unsandboxed');
}
class HelloWorld {
getInfo() {
return {
id: 'helloworldunsandboxed',
name: 'Unsandboxed Hello World',
blocks: [
{
opcode: 'hello',
blockType: Scratch.BlockType.REPORTER,
text: 'Hello!'
}
]
};
}
hello() {
return 'World!';
}
}
Scratch.extensions.register(new HelloWorld());
})(Scratch);
If you're using a local HTTP server, save this so you can access it through the server, then load the exact URL http://localhost:8000/hello-world-unsandboxed.js in TurboWarp. If nothing appears, see the developer console. If you see an error that the extension must be run unsandboxed, most likely you are using an old version of TurboWarp or you didn't load it from a URL that starts with http://localhost:8000/
exactly. 127.0.0.1 and 0.0.0.0 won't work! It must be localhost, port 8000 exactly.
If you're just using files, make sure to check the "Run extension without sandbox" box each time you load the extension.
Create a new empty project with a repeat (30) loop that adds the "hello" block to a list. Notice that it now runs instantly while the sandboxed version would've taken at least a second.
Observe that the majority of the code is still identical: You still create a class, then call Scratch.extensions.register(), then Scratch calls getInfo() which returns the same type of object. Just the surrounding template is different.
Increased power brings increased responsibility
Before we talk about the new APIs, we want to note some additional requirements for unsandboxed extensions:
- Blocks must not throw errors. While sandboxed extensions could, unsandboxed extensions that do this may break scripts.
- Input and boolean blocks must return a valid value. While sandboxed extensions are free to neglect this, unsandboxed extensions that don't return proper values (string, number, or boolean) can break scripts in unknown ways.
- Blocks must not get stuck in infinite loops. While sandboxed extensions will usually not be able to freeze the entire window if they get stuck in a loop, unsandboxed extensions will. This can result in data loss.
Accessing Scratch internals
The big thing that unsandboxed extensions can do is directly access Scratch internals.
const vm = Scratch.vm;
That's full access to the actual Scratch VM object. There is a lot you can do with this.
Remember -- every variable declaration must happen inside the IIFE.
// GOOD CODE
(function(Scratch) {
const vm = Scratch.vm;
// ...
}(Scratch));
// BAD CODE
const vm = Scratch.vm;
(function(Scratch) {
// ...
}(Scratch));
Dig around for a while to find what you're looking for. Your developer tools will be immensely useful as you can access Scratch
from there after an extension is loaded, or use the other debugging global variables that are available (but please don't use those in extensions). You may find the scratch-vm source code or @turbowarp/types to be useful resources.
Here is an example of an extension that uses Scratch.vm to toggle turbo mode, similar to the "runtime options" extension on extensions.turbowarp.org:
(function(Scratch) {
'use strict';
if (!Scratch.extensions.unsandboxed) {
throw new Error('This Turbo Mode example must run unsandboxed');
}
const vm = Scratch.vm;
class TurboMode {
getInfo() {
return {
id: 'turbomodeunsandboxed',
name: 'Turbo Mode',
blocks: [
{
opcode: 'set',
blockType: Scratch.BlockType.COMMAND,
text: 'set turbo mode to [ENABLED]',
arguments: {
ENABLED: {
type: Scratch.ArgumentType.STRING,
menu: 'ENABLED_MENU'
}
}
}
],
menus: {
ENABLED_MENU: {
acceptReporters: true,
items: ['on', 'off']
}
}
};
}
set(args) {
vm.setTurboMode(args.ENABLED === 'on');
}
}
Scratch.extensions.register(new TurboMode());
})(Scratch);
The block utility object
When a sandboxed custom extension is run, all it receives are the arguments that the scripts provided. It doesn't even know which sprite is executing it. We now introduce the second argument passed to block functions: BlockUtility.
The BlockUtility object, conventionally called util
, allows blocks in unsandboxed extensions to get direct access to the sprite that is running them using util.target
. Similar to the VM, this is the actual object used internally. You have full access to it.
Here is an example extension that demonstrates using util.target
to get the name of the current sprite or access variables.
(function(Scratch) {
'use strict';
if (!Scratch.extensions.unsandboxed) {
throw new Error('This Block Utility example must run unsandboxed');
}
class BlockUtilityExamples {
getInfo() {
return {
id: 'blockutilityexamples',
name: 'BlockUtility Examples',
blocks: [
{
opcode: 'getSpriteName',
text: 'sprite name',
blockType: Scratch.BlockType.REPORTER,
},
{
opcode: 'doesVariableExist',
text: 'is there a [TYPE] named [NAME]?',
blockType: Scratch.BlockType.BOOLEAN,
arguments: {
NAME: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'my variable'
},
TYPE: {
type: Scratch.ArgumentType.STRING,
menu: 'TYPE_MENU',
defaultValue: 'list'
}
}
}
],
menus: {
TYPE_MENU: {
acceptReporters: true,
items: [
// Value here corresponds to the internal types of the variables
// in scratch-vm. And yes, broadcasts are actually variables.
// https://github.com/TurboWarp/scratch-vm/blob/20c60193c1c567a65cca87b16d22c51963565a43/src/engine/variable.js#L43-L67
{
text: 'variable',
value: ''
},
'list',
{
text: 'broadcast',
value: 'broadcast_msg'
}
]
}
}
};
}
getSpriteName(args, util) {
return util.target.getName();
}
doesVariableExist(args, util) {
const variable = util.target.lookupVariableByNameAndType(args.NAME.toString(), args.TYPE);
// Remember: Boolean blocks need to explicitly return a boolean on their own
return !!variable;
}
}
Scratch.extensions.register(new BlockUtilityExamples());
})(Scratch);
Note that every sprite, script, and block shares the same block utility object. Instead of making a object each time your block runs, it just updates the properties of the shared object for performance. Thus, the only safe time to access util
is immediately when the block runs. Trying to access util
in a setTimeout, setInterval, Promise callback, or other non-syncronous callback will not work correctly. If you need to access properties from util
later, save them in a variable ahead of time.
// This is NOT reliable and may alert the wrong thing:
myBlock(args, util) {
setTimeout(() => {
alert(util.target.getName());
}, 1000);
}
// This will always work:
myBlock(args, util) {
const target = util.target;
setTimeout(() => {
alert(target.getName());
}, 1000);
}
Common templates
Here are some common copy-and-pasteable code snippets that can be used:
If the extension MUST be run unsandboxed, add this around the start:
if (!Scratch.extensions.unsandboxed) {
throw new Error('Extension Name must run unsandboxed');
}
If you're using the vm
, runtime
or Cast
APIs a lot, common practise is to define them around the start to save time:
const vm = Scratch.vm;
const runtime = vm.runtime;
const Cast = Scratch.Cast; // Discussed later.
Permissioned APIs
Whereas sandboxed extensions are free to use APIs such as fetch() as they please, unsandboxed extensions should instead ask for permission before making a request to any remote service. This gives the user control over their privacy. While there is no technical measures enforcing this at runtime, it is required for all extensions on extensions.turbowarp.org.
Requests to some popular services such as GitHub Pages or GitLab Pages may be automatically approved, while requests to other random websites may show a prompt to the user. You shouldn't make any assumptions about this, and your code needs to ensure that it can gracefully handle the user rejecting the prompt (the extension should behave the same as it does when there is no internet connection).
These permissioned APIs will also automatically prevent projects from running arbitrary JavaScript by attempting to, for example, redirect to a javascript:
URL.
Fetching APIs, WebSockets, images, audio files, etc.
Use Scratch.fetch(url)
instead of fetch(url)
. Check await Scratch.canFetch(url)
before using other APIs that connect to remote websites.
// Do not do this:
const response = await fetch(url);
// Do this instead:
const response = await Scratch.fetch(url);
// Do not do this:
const ws = new WebSocket(url);
// Do this instead:
if (await Scratch.canFetch(url)) {
const ws = new WebSocket(url);
}
// Do not do this:
const image = new Image();
image.src = src;
// Do this instead:
if (await Scratch.canFetch(src)) {
const image = new Image();
image.src = src;
}
// Do not do this:
const audio = new Audio(url);
// Do this instead:
if (await Scratch.canFetch(url)) {
const audio = new Audio(url);
}
Opening new tabs or windows
Use Scratch.openWindow(url)
instead of window.open(url)
. Scratch.openWindow
always sets the target to "_blank"
to open a new tab or window. If you can't use Scratch.openWindow(url)
for some reason, check await Scratch.canOpenWindow(url)
before calling window.open(url)
.
// Do not do this:
const win = window.open(url);
// Do this instead:
const win = await Scratch.openWindow(url);
// Do not do this:
const win = window.open(url, '_blank', 'width=400,height=400')
// Do this instead:
const win = await Scratch.openWindow(url, 'width=400,height=400');
Redirecting the current page
Use Scratch.redirect(url)
instead of location.href = url
. If you can't use Scratch.redirect(url)
, check await Scratch.canRedirect(url)
before running location.href = url
.
// Do not do this:
location.href = url;
// Do this instead:
await Scratch.redirect(url);