Burn After Reading: Sandboxed drop-in JS Runtime Burn After Reading: Sandboxed drop-in JS Runtime

Burn After Reading: Sandboxed drop-in JS Runtime

EXECUTIVE SUMMARY (FOR REAL EXECUTIVES)

Afterburner is a sandboxed JavaScript runtime built in Rust; burn is its CLI and package manager. It exists for a few reasons: to run code you did not write and did not read, with guarantees that come from the runtime, not from trust; to be a multi-threaded JS and TS runtime; and to run data operations efficiently.


  • The execution gap at AI scale. Code generation became machine-rate; review stayed human-rate. Every script here runs behind a capability manifold: sealed by default, with every filesystem root, network host, and env key an explicit grant. “Can we run this?” stops being a meeting.
    • AI agents and copilots: model-generated transforms run the moment they land, inside a chamber that grants them nothing by default. You ship fast and you still sleep.
  • Isolation without the infrastructure tax. Sandboxing per function call instead of per container or kernel: instruction budgets, memory caps, wall-clock timeouts. A billion rows through a sandboxed JavaScript UDF in about a minute, on one machine.
    • Secure customer code execution: your customers will write code you cannot predict, and with Afterburner you do not have to predict it. Their script runs sealed behind its own manifold; what it can reach is your decision, not theirs.
  • One function, every data system. A UDF packaged once runs in a warehouse, a dataframe library, a stream processor, or a queue worker. Sealed and deterministic, so results are cacheable and retries are free.
    • Data enrichment, everywhere at once: one matching or scoring function, written once, runs in the warehouse, the dataframe pipeline, and the stream job with identical results in all three.
  • Supply chain by construction. The .afb package format has no install scripts at all, content-addressed dependencies, byte-reproducible builds, and capabilities visible on the registry before you install.
  • Embedding as a feature, not a feat. The full runtime drops into your own system in five lines of Rust. Library and CLI are the same engine, both first-class.
    • Plugin systems: let users extend your product without inviting them into your process. An editor, a gateway, a game, anything with an extension point.
  • Nothing to relearn. The compatibility bar is the Node 26 API surface: express, fastify, and hono run inside the sandbox unchanged.
  • Sovereign by design. The registry is one self-contained binary you can run inside your own perimeter: your packages, your namespace, no third party in the trust chain.

Somewhere, right now, a model is writing JavaScript for somebody’s production pipeline. The code is often genuinely good, that is the part people still underestimate tbh. Nowadays that code arrives at a volume no review pipeline was ever sized for, carrying the same privileges as code a senior engineer walked through three review rounds. Multiply that by every copilot, every agent loop, every “just have the model write the transform” feature that shipped this year. Not mentioning every company try to throw some “AI” around.

To be clear where I stand, this is not an anti-AI post. In Afterburner, I also got help from AI. That’s not the stance. It is designs or ideas that will last in this age. The generation side is the real thing, I use it daily, it writes code I am glad to have, and it earned its spotlight. What never kept pace is the execution side: model-written code still lands in infrastructure that was designed around hand-written trust (and also being aware that what it does). And the workarounds I keep running into look like this:

  • In-process eval() wearing a nicer function name. Check your codebase. I will wait. (I saw this in other codebases).
  • A container per request. An entire OS userland, summoned to run forty lines of string formatting. DBs do that… and then, big database companies explicitly mark UDFs in their docs as “don’t run UDFs as much as possible”.
  • A microVM per snippet. A kernel boots so that two numbers may be added. Warming up per request, cold starts and writing some hacks like surgical warm ups so there is always a warmed up machine somewhere (good idea, but in long run maybe not so good). The isolation is real; so is the kernel.
  • V8 isolates. Seriously capable, but now your threat model includes a browser engine, and embedding that browser engine is a serious job.
  • “The generated code gets reviewed.” Some of it, by disciplined teams, for real. But review happens at human speed and generation no longer does; the math does not close.

I have been building data engines long enough to recognize this pattern: when every team is solving the same problem badly, a layer is missing underneath all of them. The missing layer is a runtime that treats untrusted code as the normal case, the thing it was born to do, instead of an exception bolted onto a runtime that assumes trust.

That is the layer I went and built.

Afterburner, and You Drive It with burn

Afterburner1 is a sandboxed JavaScript VM for Rust. Untrusted scripts run with memory limits, instruction budgets, wall-clock timeouts, and capability-gated I/O, and the whole thing ships with its own package format, its own registry, and its own package manager. Afterburner is the engine; burn is the CLI in your hands.

If you are going to think xkcd 927, feel free. I am not doing this for competing with other package registries at all. They work in coherence with burn.

Terminal window
curl -fsSL https://afterburner.sh | sh
burn -e 'module.exports = () => 42'

One decision shaped everything else, so it goes first: the programmatic library and the CLI are both front doors. Same engine behind both. The library exists because the interesting hosts are programs, your database, your dataframe library, your stream processor. The CLI exists because humans and CI pipelines deserve better than embedding ceremony. Neither is a thin shell over the other, and we keep them honest by treating both as the project and product.

Inside, every subsystem is named like a turbine part, and the names are not decoration (I am always mesmerized by jet engines though), they tell you what the part does. The script is fuel. It burns inside the combustion chamber, the WASM sandbox. The manifold is the intake that decides what is allowed to reach the chamber at all. The fuel gauge caps how much it may burn. Thrust is the call that hands you the result. Even the Node-compatibility layer is called the plenum, which is the chamber that feeds an intake manifold.

Für meine deutschen Leserinnen,
Turbinen-Luftstrahl-Triebwerk, Seite 184, Das Technikbuch, Marshall Brain, 2015.

Untrusted JS

Plenum: Node 26 surface

Manifold: capability intake

Combustion Chamber: WASM sandbox

FuelGauge: fuel | memory | timeout

Thrust: your result

Why JavaScript, of all things? It is what the models emit when nobody specifies otherwise (python is other option I know - at the end of the day you can make python ~> js or ts conversion and it will work on Afterburner), it is what your team already reads, and it is where the largest package ecosystem in existence lives. You meet the world where it is.

And why Rust? Ask Bun. The runtime that made Zig part of its very identity started moving to Rust this year, in 20262, and think about how crazy that is for a moment: a team that fast, that deep into a language, concluding the move is worth it anyway. I take my point! Rust community already built the entire toolchain a secure runtime stands on. A WASM engine with fuel metering and instance pooling, memory-safe TLS, vendored storage, a package manager worth imitating. We did not have to invent any of that. Afterburner inherits a decade of hardened work, and a sandbox should stand on exactly that kind of decade.

Some people say WASM is slow

This claim has survived some time on the strength of measurements that were never examined twice. Somebody timed a cold instantiation. Someone tried to pass large chunks of bytes to a WASM module one blob at a time. Somebody serialized a JSON object across the host boundary once per element and published the carnage. Experimentations published. I take things with a grain of salt.

Here is what the same sandbox does on an 18-core Xeon, running JavaScript UDFs under full isolation, no dataset tricks, no exotic hardware:

PathThroughputOne billion rows takes
Columnar, typed batches16.8M rows/sec~1 minute
Columnar, JSON rows600K rows/sec~28 minutes
Batched JSON197K rows/sec~1.4 hours
Per-row calls, single thread793 rows/sec~350 hours

Same engine in every row. Same sandbox, same script semantics. Top to bottom is a factor of roughly 2,400×, and the only variable is how often execution crosses the border between host and guest. That bottom row, the 350-hour one, is the configuration most “WASM is slow” verdicts were rendered on. People profiled their own integration and sentenced the runtime for it.

The runtime was never the slow part. The border was.

What closed the gap is couple of small optimizations the WASM ecosystem shipped while public attention was elsewhere: modules pre-initialized so startup stops being an event, bytecode caches so a repeated script never compiles twice, pooled instances so isolation stops costing an allocation per call, fuel metering cheap enough that enforcing an instruction budget is a rounding error. Stack them and you get a billion rows through a sandboxed JavaScript function in about a minute. That WASM slide decks in companies are due for an update now, haha!

More Than JS Glue

WebAssembly had the misfortune of debuting as a browser demo. “Compile your C++ and call it from JavaScript” was the pitch, an entire generation of engineers filed it under frontend glue, and the filing stuck.

Strip all things away and look at what the WASM format actually is3: portable, specified, linear-memory isolated, and, this is the part that matters, born with no ambient authority. A WASM guest cannot open a file, reach a network, or read a clock unless the host explicitly hands it that capability. Operating systems have spent fifty years retrofitting that property onto processes with mixed success. WASM actually solved the 20 years old jail problem in a sense (maybe unintentionally).

There is a second freedom hiding here, and it is the one I care about most: embedding stops being an act of heroism. A capable sandbox that costs a dependency line changes who gets to have one. The places that need safe execution were never “the server” in the abstract. It is the database that wants user logic next to the data instead of shipping data to the logic. The dataframe library that wants a user function over a column without a Python subprocess in the loop. The stream processor, the queue consumer, the editor plugin host, the agent harness. Each of these is an embedding problem before it is a runtime problem, and until now the answer to each was: tough, bring a browser engine or bring a JVM.

This is also where Afterburner stopped being a curiosity project for me. My entire trail, the morsel work, the bytecode work4, the engines built to chew through billions of rows, kept arriving at the same locked door: users want their logic to run inside the system, and the system has nowhere safe to put it. I ignored it intentionally for years. Then models started writing the logic as well, which is frankly the best thing that ever happened to my corner of the field, and suddenly that door stood between me and everything I wanted to build next. I believed in WASM’s ceiling early; what changed is that the ceiling acquired a deadline.

I did the homework at a volume I will not fully confess to. Engine sources, WASI design threads, component-model debates, sandbox escape disclosures, even cargo limitations I had for this Rust project, and a decade of package-manager post-mortems, Node changelogs by the release. If reading research papers burned calories, this would also be a fitness blog. (I won’t be a good PT if I was going to be with my chubbiness but anyway). The point of admitting this: the decisions below were chosen, against alternatives I can name, not inherited as defaults.

The Manifold

Every capability a script could want passes through one structure. Filesystem roots, network hosts, environment keys, crypto, child processes, inbound ports, even whether process.exit() is permitted to mean anything. Sealed is the zero value: nothing in, nothing out.

{
"fs": "None",
"net": { "OutboundHttp": ["api.anthropic.com"] },
"env": { "AllowList": ["ANTHROPIC_API_KEY"] },
"crypto": false,
"child_process": false,
"allow_exit": false
}

Read that as:

This script may speak HTTPS to one named host, may read one named environment variable, and beyond that the world does not exist for it.

Not “is blocked from the world”, the distinction matters. Inside the chamber there is nothing to find, because the manifold never admitted it. And when a script reaches for something it was not granted, the denial is a loud, typed error.

Now the honest part about defaults. The burn CLI starts open, the way node does, your laptop, your code, your dev loop. --sandbox flips it sealed and the allow-flags re-open exactly what you name:

Terminal window
burn --sandbox --allow-net=api.anthropic.com --allow-env=ANTHROPIC_API_KEY agent.js

The embedding library starts sealed, because embedded code is by definition somebody else’s. And a package carries its manifold inside the package: when you run one, the profile it declared is the profile it gets, and the registry renders those capabilities on the package page before you install. You inspect what a package can touch the way you inspect an ingredients label. That this sounds futuristic says more about npm than about us.

Open afterburner.sh and look at the top navigation. There is a button labeled Node.js Docs, and it points at nodejs.org, latest v26 API reference5.

One of the former colleagues was amazed by other runtime jsut links to Node 20/24 (I don’t recall which) docs in their page directly.

He said:

“Super nice, they directly linked the Node version X docs!”

I want to see him happy when he(and many other people) reads this post! He is partially the reason that I started this project.

We did not write documentation for the runtime surface. We linked to Node’s. That is the compatibility bar stated as a UI element: full Node 26, and if a thing is described in those docs, it is supposed to work here, in the sandbox, behind the manifold, with no real process underneath to leak into. Polyfill modules cover the builtin surface, fs through worker_threads through http through crypto, alongside the globals: fetch, Buffer, URL, structuredClone, AbortSignal.timeout. require('express') resolves and serves. So do packages on npm.

The detail I am proudest of is what we call L3 shadows. Certain npm packages are really native-addon delivery vehicles, sharp, bcrypt, argon2, sqlite3, and a native addon inside a WASM sandbox is a contradiction in terms. So those names resolve to pure-Rust implementations behind the identical require. Your code does not change by a character. What changes is that node-gyp exits your life, which I classify as a quality-of-life feature with health benefits.

Is the entire Node 26 surface finished today? No. The surface is enormous (we are near there - aka what is not there is running wasi inside wasi, which i will never going to design it like that) and the remaining corners are being closed in public, where the checklist is visible. But notice what that navbar button does to us: the target is pinned in our own UI, one click from the landing page, where we cannot quietly redefine success. We took the bar everyone hedges on and made it a permanent link.

Vision of Afterburner is:

  • Performance
  • Security (as much as possible - you can’t be fully secure)
  • Compatibility (third gear)

A Sovereign Package Ecosystem

Showed this before I write this blog post to some of my friends. They keep asking why I built my own package format instead of riding npm. The short answer: a sandbox that installs dependencies through npm’s trust model is a vault with the back door propped open. The walls are irrelevant if the supply route runs arbitrary code on the host before your program even starts.

You know this folklore by heart, and lately it does not even stay folklore long enough to age.

The industry holds a memorial service for its supply chain roughly every eighteen months, publishes lessons learned, and changes very little.

So packages here are .afb files, and the format is a list of refusals:

  • No install scripts. Not sandboxed, not warned-about. The format has no field where code-at-install-time could live. The entire attack class is unrepresentable.
  • Deterministic bytes. burn package emits an identical archive for identical input, every time, on every machine.
  • Content addressing. Dependencies pin to a SHA-256 digest. A name can be argued over; a digest resolves to exactly one artifact or fails.
  • The manifold ships inside. Capabilities are part of the package, declared in its manifest, visible on the registry before installation.
  • npm interop on our terms. Existing npm packages can be pulled through a pure-Rust installer that verifies integrity, executes nothing, and turns native addons away at the gate.

The workflow will feel familiar if you have used Cargo, which is deliberate, imitation being the sincerest form of taste(also admiration):

Terminal window
burn new acme/greeter
cd greeter
burn run # entry point from afb.toml
burn test # in the sandbox
burn package # deterministic .afb
burn publish

And sovereign is a design constraint, not marketing perfume. The registry is one self-contained binary, SQLite and a filesystem by default, Postgres and object storage when you outgrow that. The full ecosystem, packages, resolution, publishing, the registry itself, can live entirely inside your own perimeter, under your namespace, your retention rules, your trust roots, with no third party anywhere in the chain. Your dependencies stop being hostage to a stranger’s account security or a platform’s mood. After watching this movie on repeat for a decade, I wanted to be that person who projects it.

UDFs: One Function, Every System That Holds Data

This is where everything above converges with everything I have ever written here, so let me say it plainly: this use case is a reason the runtime exists. A requirement we built toward from the first commit. The sandbox, the determinism, the columnar lane, the package contract, all of it points at letting a function travel to wherever the data lives.

Every system that holds data eventually reinvents the same feature: let the user run a function over the records. Warehouses call it a UDF. Dataframe libraries call it apply. Stream processors call it map. Queue consumers are the feature with extra infrastructure around it. Each reinvention is incompatible with the others, packages differently, and gets wounded by user code in its own special way.

Afterburner’s answer is to make the UDF a package with a small machine-readable contract: a udf(record) function, a vectorized batch(records), and a schema descriptor stating inputs, outputs, determinism, and required capabilities. The valuable ones declare capabilities: []. It can be sealed, pure, deterministic. A host can cache results by package version and input, retry freely, and schedule them anywhere, because a function that can touch nothing can hurt nothing.

const lev = require('burn/levenshtein');
lev.udf({ a: 'kitten', b: 'sitting' });
// => { distance: 3, ratio: 0.5714… }
// vectorized form, for hosts that think in batches and columns
lev.batch([
{ a: 'kitten', b: 'sitting' },
{ a: 'flaw', b: 'lawn' },
]);
// => [ { distance: 3, … }, { distance: 2, … } ]

And burn/levenshtein package is in the registry. It is live on the registry right now8, version, digest, capability chips and all, one burn add burn/levenshtein away from your manifest. See for yourself:

This is an entire UDF package body, gross margin per record, ready for burn package:

function udf(r) {
const margin = (r.revenue - r.cost) / r.revenue;
return { sku: r.sku, margin: Number(margin.toFixed(4)) };
}
module.exports = udf;
module.exports.udf = udf;
module.exports.batch = (rows) => rows.map(udf);
module.exports.schema = {
name: 'acme/margin',
version: '0.1.0',
kind: 'udf',
summary: 'Gross margin per record',
input: { sku: 'string', revenue: 'number', cost: 'number' },
output: { sku: 'string', margin: 'number' },
deterministic: true,
capabilities: [] // sealed: no fs, no net, no env
};

That schema block is the part that turns a function into infrastructure. A host never needs to know a package personally; it reads the contract and decides at runtime:

function applyUdf(pkg, rows) {
const fn = require(pkg);
if (!fn.schema || fn.schema.kind !== 'udf') throw new Error(pkg + ' is not a UDF');
return fn.batch ? fn.batch(rows) : rows.map(fn.udf);
}
applyUdf('burn/phonetic', [{ value: 'Ashcraft' }]);
// => [ { code: 'A261', algorithm: 'soundex' } ]

Ten lines, and a host supports every UDF published so far and every one not written yet.

A first-party catalog is already on the registry: string distances, phonetic encodings, statistics, geo math, hashing, JSON flattening, CSV, image operations, plus LLM provider clients whose manifolds pin them to exactly their own API host and nothing else. They compose the way you would hope: block candidate pairs with phonetic, score the survivors with levenshtein, and you have assembled entity resolution out of two sealed packages and an afternoon. And the CLI collapses the whole story into a shell pipeline, JSON in, JSON out:

Terminal window
echo '{"a":"kitten","b":"sitting"}' | burn thrust score.js
# {"distance":3,"ratio":0.5714285714285714}

The payoff is portability of behavior. The same .afb runs as a scalar UDF inside a SQL engine, over whole columns inside a dataframe library (the 16.8M rows/sec lane from the table exists precisely for this), as the map stage of a Flink or Kafka Streams job, between staging and marts in an ELT pipeline, inside a queue worker. One package, one declared capability set, one behavior, every host. The function stays still and the runtime does the traveling.

Close the loop with the opening: when a model generates a transform for your pipeline now, it does not land as a string fed to eval inside your process. It lands as a sealed package, with a declared schema, zero capabilities, and a deterministic contract a host can verify and cache. Model-written code stops being the nervous exception and becomes a unit you operate like any other software: versioned, inspectable, cacheable, deployable anywhere the runtime goes. Sandbox is there so you can say yes to all of it, at full volume, and mean it. That is the whole point of this post in one sentence.

Wiring Your Assistant Is One Command

Go back to the first paragraph: a model is writing JavaScript for somebody’s pipeline. When that model is your coding assistant on your machine, today it just shells out and runs node whatever.js with your full privileges, because raw execution is the only path it knows.

burn agent install gives it a better one. It wires a pre-tool hook and a short instruction block into the assistants you already run, and from then on, whenever one of them reaches for node, npm, or npx, the run goes through the sandbox, sealed by default.

$ burn agent install
Route every JavaScript run through the sealed sandbox. Space toggles, Enter confirms.
🔥 Wire which assistants? ›
[🔥] Claude Code (detected)
[ ] OpenAI Codex (not detected - config will be created)
[🔥] Gemini CLI (detected)
[ ] Cursor (not detected - config will be created)
[🔥] GitHub Copilot (detected)
[🔥] Antigravity (agy) (detected)

It detects what you have, pre-checks those, and lets you toggle the rest. Confirm, and it tells you exactly what it touched, nothing hidden:

# after you confirm...
Route every JavaScript run through the sealed sandbox. Space toggles, Enter confirms.
✓ + wired hook -> /home/vclq/.claude/settings.json
✓ + added instructions -> /home/vclq/.claude/CLAUDE.md
✓ + wired hook -> /home/vclq/.gemini/settings.json
✓ + added instructions -> /home/vclq/.gemini/GEMINI.md
✓ + wired hook -> /home/vclq/.copilot/hooks/burn.json
✓ + wired hook -> /home/vclq/.gemini/config/hooks.json
✓ + refreshed instructions -> /home/vclq/.gemini/GEMINI.md
✓ Claude Code, Gemini CLI, GitHub Copilot, Antigravity (agy) now run JavaScript inside burn.
Undo anytime: burn agent uninstall · one-off bypass: BURN_AGENT_HOOK=0

The hook itself is small. When the assistant is about to run JavaScript outside the sandbox, the hook catches the tool call and answers with the corrected version: re-run it as burn --sandbox node app.js, sealed, and open only the capability the code genuinely needs, narrowly, never all of them at once. The assistant reads that back, rewrites its own command, and the next time through the classifier sees the burn prefix and lets it pass. No fine-tuning, no new API for the model to learn, it rides the permission flow these tools already have.

AI integration works like:

  • It fails open. If the hook cannot read a tool call, it stays quiet and lets your assistant work. The redirect is there to catch the JavaScript path.
  • It is fully reversible. burn agent uninstall removes exactly what install added and touches nothing else. burn agent status shows what is wired, burn agent disable pauses the redirect without unwiring, and BURN_AGENT_HOOK=0 skips it for a single run.
  • Six assistants today. The ones you have installed get wired; the ones you do not get a config written, ready for when you do.

That is the practical close on the whole post. The model writes the code, you did not read it, and it still runs sealed, without you having to remember to make that happen.

Embedding Afterburner in Rust

I claimed embedding should cost a dependency line. Here it is:

use afterburner::Afterburner;
use serde_json::json;
let ab = Afterburner::new()?;
let id = ab.register("module.exports = (d) => d.n + 1")?;
let out = ab.run(&id, &json!({ "n": 41 }))?; // => 42

When the requirements grow up, the builder is where the gauges live, an instruction budget, a memory ceiling, a wall-clock cap, a manifold, a worker count. Past that line the engine schedules scripts across threads with bounded queues, work stealing when idle, and memory sharing awareness:

let ab = Afterburner::builder()
.fuel(1_000_000_000)
.memory_bytes(64 << 20)
.timeout_ms(30_000)
.manifold(Manifold {
fs: FsAccess::ReadOnly(vec!["/var/data".into()]),
..Manifold::sealed()
})
.threaded(8)
.build()?;

For code you actually trust there is a native QuickJS path with sub-microsecond startup, and an adaptive mode, Flying Start9, that launches native and tier-switches to the WASM chamber in the background.

Btw, project license is BSL 1.1 with each release converting to Apache-2.0 after four years; personal projects, students, non-commercial open source, and internal evaluation are free, which rounds to everyone currently reading.

What This Adds Up To (I want to try that thing)

So where do you fit? Follow the arrows, there is a door for everyone:

Yes

Yes

No

No

Yes

No

Yes

Yes

Yes

Do you know
JS or TS?

Install burn locally,
use it locally

Do you
have AI around?

Drive it with
the burn agent

PROFIT! Your code
in a real sandbox

Do you
have AI around?

Hand the burn agent
to your AI

Pull ready-made packages
from the registry

Want the
runtime itself?

Use the afterburner crate,
it is the same engine

Want to add it to an
existing project of yours?

Do you
know Rust?

It lives on crates.io as afterburner10, installs from afterburner.sh, and develops in the open on GitHub11.

And since you read this far: internals and more intricate details of how Afterburner works will be for another day. And much thanks for actually reading it.

Terminal window
curl -fsSL https://afterburner.sh | sh

Light it up. If you come up with issues, please open an issue on the GitHub repository. Let’s build it together (also looking for people to take care of and operate the registry), and not let it to go:

The bottom is loaded with nice people, Albert. Only cream and b******s rise.

References

  1. Site & install: afterburner.sh
  2. Bun’s move to Rust, as one merged PR: oven-sh/bun#30412
  3. What the WASM format actually is: Understanding WebAssembly text format, MDN
  4. Earlier groundwork: (AKA shameless plug) Morsels, Compilation, and People · Fitting a Query Engine in Three Cache Lines
  5. The Node 26 surface, as documented by Node: nodejs.org/docs/latest-v26.x/api
  6. The 2025 npm wave: Shai-Hulud, the self-replicating npm worm · the chalk and debug account takeover · GitHub’s hardening plan in response
  7. The npm supply chain, this year alone: North Korea-nexus actor compromises Axios · Red Hat’s npm packages compromised (RHSB-2026-006) · mass attack across TanStack and Mistral packages
  8. Registry: registry.afterburner.sh
  9. The principle behind Flying Start: Adaptive Execution of Compiled Queries, Kohn, Leis, Neumann, ICDE 2018
  10. Crate: crates.io/crates/afterburner · docs.rs/afterburner
  11. Source: github.com/afterburner-sh/afterburner

← Back to blog