Skip to content

Script sandbox ​

Ruleset scripts and calculator scripts share one sandbox. Both go through the same static safety check and run in the same kind of isolated runtime, so everything on this page applies to both.

How a script runs ​

  1. Static safety check. The source is parsed as a JavaScript module and every statement and expression is checked against the allowlists below. Any issue rejects the whole script and nothing runs. Rulesets are checked when they are saved, so an unsafe ruleset cannot be saved. Calculators are checked before every run.
  2. A fresh sandbox per run. Each run gets a new SES Compartment with no extra globals. The script sees only the frozen JavaScript built-ins and the arguments it is called with. Nothing carries over between runs, and one script cannot see another.
  3. Plain, frozen inputs. Arguments are rebuilt as plain objects and arrays (no prototypes, no functions) and deep-frozen. Writing to them throws.
  4. Checked output. What the script returns is normalized before anything uses it: ruleset results as described under return values, calculator dashboards by the calculator renderer's own limits.

Scripts run in two places:

  • In the browser, inside a Web Worker: the deck builder, card legality checks and calculators. If the sandbox worker cannot start on a device, the site offers (only after the failure) an opt-in to run deck rules outside the sandbox on that device. The opt-in expires after 30 days.
  • On the server, when a deck is submitted: a separate worker process re-runs the card, deck and deck set rules. A tournament's banned cards are rejected before any script runs.

Because a script is parsed as a module, it is always in strict mode.

Allowed globals ​

Globals a script may reference. Every other free identifier is rejected.

Globalnew allowed
Array
Boolean
DateYes
Infinity
JSON
MapYes
Math
NaN
Number
Object
RegExpYes
SetYes
String
undefined

The only constructors new may call (and only when not shadowed locally).

Everything else is rejected as unsafe_global, including some common names:

  • Error (new Error(…) fails as new_expression_unsupported): to fail a check, return false or { valid: false, issues } instead of throwing.
  • parseInt / parseFloat: use Number.parseInt / Number.parseFloat.
  • console, Promise, Symbol, WeakMap, globalThis, eval, Function, fetch and timers.
js
// Passes: only allowed globals, let/const and plain functions.
export default function validateDeck(deck) {
  const copies = new Map(Object.entries(deck));
  const most = Math.max(0, ...copies.values());

  return most <= Number.parseInt("3", 10);
}

Blocked properties ​

Property names a script may not read or write, as obj.name or obj["name"]. They reach prototypes, constructors or the call stack.

defineGetter, defineSetter, lookupGetter, lookupSetter, proto, arguments, callee, caller, constructor, prototype

The check only sees names written literally (card.constructor, card["constructor"]). A computed key such as card[key] is not checked statically; the frozen runtime is what contains it.

js
// Rejected: property_access_blocked.
export default function validateCard(card) {
  return card.constructor === Object;
}

Exports ​

A script has one default export. See entry points for how the runner finds the function.

The only named exports a script may carry beside its default: the optional deck-scope pool-filter hook export function resolvePoolFilters(input, context). Re-exports, export { … } lists, exported variables and any other exported function name stay rejected.

Allowed named exports: resolvePoolFilters.

Rejected syntax ​

The syntax rules are written into the checker itself (packages/game-adapters/src/tournament-create/script-safety.ts): the statement and expression visitors (visitStatement, visitExpression) reject anything not on their list, plus dedicated checks for var (visitVariableDeclaration), async and generators (visitFunctionLike) and getters and setters (visitObjectExpression). Rejected:

  • import declarations, dynamic import() and import.meta.
  • export * from …, export { … } lists, exported variables and any named export except resolvePoolFilters.
  • Classes (declarations and expressions), this and super.
  • var. Use let or const.
  • async functions, await, generator functions and yield.
  • delete.
  • Labeled statements and debugger.
  • Getters and setters in object literals ({ get total() { … } }).
  • Tagged templates. Plain template literals are fine.
  • Assigning to anything the script did not declare, such as a global.
  • new on anything but Date, Map, RegExp or Set.
  • Anything the parser rejects in strict mode, such as with or legacy octal literals (010). These fail as invalid_syntax.
js
// Rejected: variable_kind_unsupported, unsafe_global.
export default function validateDeck(deck) {
  var total = 0;
  fetch("https://example.com");
  return total === 0;
}
js
// Rejected: new_expression_unsupported (Error is not a constructor you may call).
export default function validateDeck(deck) {
  throw new Error("Not allowed");
}
js
// Rejected: async_unsupported, syntax_unsupported.
export default async function validateDeck(deck) {
  return this.check(deck);
}

Time limits ​

WhereLimit
Browser: one card, deck or deck set check2s
Browser: a card rule over the whole card pool15s
Browser: first catalog download before any check45s
Browser: one calculator run5s
Server: one deck submission check (all scopes)15s
Server: the queue job around that check30s

A script that runs past its limit (an endless loop, say) is stopped and the check reports an error instead of a pass. Keep scripts linear in the size of the deck or card pool: a card rule runs once for every card in the catalog when the builder filters the pool.

Safety issue codes ​

Each rejection carries one of these codes.

MemberValueDescription
AsyncUnsupported"async_unsupported"An async function or an await expression.
GeneratorUnsupported"generator_unsupported"A generator function (function*) or a yield expression.
InvalidSyntax"invalid_syntax"The source is empty or does not parse as an ES module.
NewExpressionUnsupported"new_expression_unsupported"new on anything other than an allowed constructor (Date, Map, RegExp, Set), or on a local that shadows one.
PropertyAccessBlocked"property_access_blocked"Reading or writing a blocked property name such as constructor, prototype or __proto__.
SyntaxUnsupported"syntax_unsupported"Disallowed syntax: imports, extra exports, classes, this, delete, labels, debugger, getters/setters, tagged templates, import() / import.meta, super.
UnsafeAssignment"unsafe_assignment"Assigning to a name the script did not declare (a global or an undeclared variable).
UnsafeGlobal"unsafe_global"Referencing a free identifier that is not on the allowed globals list (e.g. globalThis, eval, fetch).
VariableKindUnsupported"variable_kind_unsupported"A var declaration. Use let or const.

At most 12 issues are reported per script. The check stops at this many issues, so fixing them can reveal more.

Turny.gg documentation