Philipp Dunkel · @pipobscure.com · pip@pip.fyi
"I'm going to spend the first third of this talk on jobs I've had, because the design of this thing is downstream of a specific sequence of things going wrong."
One correction to make early, because people assume otherwise: the runtime's job is to run archives. It does not decide which archives are allowed to run. Signing and verification are optional tools living entirely outside Node. Everything after that builds on the capability, not on the checking.
The tense is settled. "Node can now run a program directly out of a zip archive" is true of a stock Node: the last piece, --vfs-load, shipped in v26.10 on 21 Sep 2026. It is still behind --experimental-vfs β say that much β and slide 0x14 has the detail.
Housekeeping: questions at the end, there'll be time for one or two.
Press T to start the pace timer. Red = behind plan.
Something you don't run, in the path of every deploy.
Knowing what you asked for is not knowing what you got.
Routine now. Not exotic, not rare, not somebody else's problem.
Shipping a tool means shipping an install procedure, and then supporting it.
A library only reaches people who already knew they had the problem.
Picked up over the years, in a few different jobs. The next few slides are where some of them came from.
"These are the things that kept coming up, in different jobs, over quite a lot of years."
35 seconds. Read the five names and move β the detail is in the examples that follow. Don't dwell on how many there are or where each one came from; the slide just puts the problems on the table.
It also does structural work: it tells the audience Act I has a shape and isn't a career retrospective.
The pipeline ran npm install
against the public registry at deploy time. So shipping was gated on a service we didn't run,
with an SLA that wasn't ours.
"The code we were deploying hadn't changed. The build changed β because the build reached out over the internet, and the internet said no."
The real lesson isn't downtime, it's non-determinism. Even with the registry up, resolving at deploy time can hand you a different tree than an hour ago. Downtime is just the loud version.
The chart is npm's own data, pulled from status.npmjs.org/api/v2/incidents.json β the 50 most recent incidents they publish, Oct 2023 to Jul 2026. That is one every 20 days, and 11 of the 50 were flagged major or critical. Say it's their number, not yours β it's much stronger that way, and it's re-runnable if someone doubts it. Note the honest limit: the API returns the 50 most recent, so this is a window, not the whole history.
Pre-empt the obvious objection: "that's what lockfiles are for." Concede it β lockfiles fixed which versions. They didn't remove the network from the deploy path, and they don't tell you the bytes behind a version are the bytes you reviewed. That's chapter 2.
This is chapter one of four, and each chapter teaches something different. Don't merge them β the point is that different contexts produced different requirements, and only together do they specify the design.
malicious npm package records in OpenSSF's public database. Mostly typosquats and spam — but that is the background rate the five above had to hide in.
"Every one of these shipped a correctly published package. The version number you asked for is the version number you got. And you still got owned."
The trajectory is the argument: accident β targeted theft β account takeover β patient infiltration β a worm that spreads by itself. Walk the arc; don't read the list.
Verify counts before presenting. Shai-Hulud reporting varies by source and wave. Safe framing: "hundreds of packages across two waves in late 2025, and CISA issued an advisory." Don't quote a precise number from the stage.
The 30,930 figure is a count of npm records in OpenSSF's public malicious-packages database (github.com/ossf/malicious-packages). Be precise about what it is: overwhelmingly typosquats and SEO spam, not 30,000 event-streams. The honest use is exactly how the slide frames it β that is the background noise a real attack gets to hide inside. If you'd rather not defend the number, drop the tile and let the timeline carry the slide.
Note what this does and doesn't motivate. It motivates being able to know and check what you shipped. It does not motivate the runtime policing signatures β that comes up in chapter 4 and I argue against it. Don't let the fear slide oversell.
Bring your own. One file doesn't care where it sits.
Anywhere. chmod +x. Done.
One file has one hash. Compare it.
"The single best usability improvement I ever made to an internal tool was deleting its installation instructions."
Self-execution is the underrated half. A file you still invoke as node thing.js is a file plus a convention. A file with a shebang is a program β it works in a Makefile, in CI, in someone's dotfiles, in a container ENTRYPOINT.
Plant the seed: the shebang trick in the demo is the direct answer to takeaway 06, and it's the moment people react to.
Deliberately not in this talk: SEA. If someone asks about shipping to a machine with no Node at all β yes, there's a path where the same archive rides on a self-contained binary. One sentence, then move on. It's a different talk.
Bundlers collapse JavaScript into one file. An application is an
entry point, sibling modules, a package.json, templates, static
assets, sometimes a native addon.
Node's module resolution — and every single
fs call — assumes that tree is on a real disk. A bundler rewrites its slice of module
loading; everything else keeps pointing at directories that aren't there.
Don't teach the bundler about more file types.
Teach the runtime that a file can be a directory.
Then assets work. Addons work. __dirname works.
Dynamic require works. Not because anyone special-cased them — because they all resolve
against a tree, and the tree is real.
"Every bundler bug you've ever filed is downstream of this. It rewrote the imports it could see, then some library did a dynamic require, and it fell over at runtime, in production, on a Friday."
This slide converts skeptics β everyone has been burned by exactly this. Let the code block land. Pause after "does not."
This also answers "why did you have to touch Node?" β because the alternative is patching resolution from userland, forever, incompletely.
Temporal put instants, durations, calendars and the arithmetic between them into the language. It did not put your business rules there.
Nobody standardised what counts as a working day, or which timezone your invoices settle in. Those change. Those are yours.
Ten years also taught me the other half: a standard is slow, and then it is permanent. That's correct for a primitive. It is fatal for anything that has to keep up with an adversary.
Primitive: an application can be loaded and run out of an archive.
Policy: whether you should trust this particular archive.
The first belongs in Node. The second must not.
"I spent about a decade getting Temporal into JavaScript. The thing that took the longest wasn't the API β it was working out where the line was. What belongs in the language, and what has to stay outside it."
This chapter sets up the mechanism/policy split that Act II uses to sort what went into Node from what didn't. Give it its time even though the slide looks light.
The "slow, then permanent" point does real work: it's why you'd want the runtime to stay out of trust decisions. Getting a signature format into Node takes two years; getting it out takes ten.
Say the primitive/policy split out loud and slowly. It's the sentence the next six slides depend on, and it's the sentence you want people repeating afterwards.
A zip plus a manifest of per-entry checksums, and a signature over those. Not over the archive.
Inherited jar signing wholesale, and then had to replace it. Three times.
A zip with a shebang glued on the front. Runs directly. No integrity story at all.
"None of this is a new idea. The jar file is older than most of the packages on npm. What's new is that Node can do the loading part natively, instead of a userland bootstrap doing it badly."
Deliberately don't unpack the failures here β the Android story pays off later, when the signing scheme comes up. Just plant "and then had to replace it, three times" and let it sit unexplained.
zipapp is the one to be warm about: it nails the ergonomics you want, and it makes no claims it can't keep. That's a defensible position, not a flaw.
Credibility: naming prior art accurately buys you the right to critique it in twenty slides' time.
Mechanism. Stable for decades. Nothing to keep up with.
Policy. Adversarial. Must be replaceable.
The left column is what merged. The right column is a library I wrote, and you could write a different one.
"So here's the split. Left column: things that have been true since 1996 and will be true in 2050. Right column: things that were wrong last year and will be wrong again next year."
Timing checkpoint: you should be at about 7 minutes.
Read the right column slowly β it's the list people assume must be built in, and every item on it is a thing some platform baked in and regretted.
The closing line sorts the rest of the deck: everything after this is either the merged capability or the optional layer. Land it, then move.
baseOffset — a zip thatZip records offsets internally. Seed them with a base offset and the archive stays valid even when it is not at the start of its file.
No custom container format. No injection tool. Concatenation.
"This is the least glamorous slide in the deck, and it's the one that makes everything else possible."
Hammer "still a valid zip." It is not a bespoke container that happens to resemble one. That's what makes unzip app.nzip work in the demo β and inspectability is a genuine property, not a party trick. It's also the thing jar got right and everybody since keeps re-deriving.
If asked why this had to be in core rather than userland: because the loader needs to read it during bootstrap, before userland exists.
--vfs-load=src mounts src. The provider comes from the source, not
the name: a directory gets RealFSProvider, zip bytes get ZipProvider.require /
import resolves against it. The archive's own
package.json "main" decides what runs.
--vfs-load takes the entry point from the mount, never from
a positional. So a trailing --vfs-load with no value takes whatever the kernel appends —
— which is the script's own path. The file mounts itself and runs its own embedded main.
argv[1] is the
source, not the mount point, so the file can still read its own bytes — which is how a launcher
verifies itself. Worker threads inherit the mounts.
Get the credit right here, out loud. node:vfs β the subsystem itself, MemoryProvider, RealFSProvider β is Matteo Collina's, and so is the VFS module loading that makes a mount resolvable. Yours is the zip support in node:zlib, the Zip VFS provider, and these two flags. The status slide breaks it down; don't blur it here.
"The shebang thing is my favourite accident. The kernel appends the script's path to the interpreter line. If the last flag is a --vfs-load still waiting for a value⦠it gets the script itself."
One honest footnote, if anyone is taking notes: this exact form is not what the tool actually ships as a prefix. The kernel appends the path and then the user's arguments, which leaves nowhere to write the -- that stops node claiming them β so app.nzip --help prints node's help. The shipped prefix is two lines of sh that exec node with "$0" and a --, and slide 0x0F shows it. The trick on this slide is real and is a property of the flag; it is just not sufficient on its own.
Two details worth having ready. The mount point is node's to choose, not yours β mounts live in a reserved namespace, so one can never shadow a real path. And because the provider is picked from the source's bytes rather than its name, a signed archive can be called app.nzip, bundle.zip or nothing in particular and still mount the same way β which matters in Act III, where renaming a file must not change what checks it.
The environment cannot redirect what runs. --vfs-load is refused in NODE_OPTIONS outright β which entry point runs is the command line's decision.
"What about more than one mount?" Expect it. The command line mounts exactly one source, the program; a program that wants more makes them through node:vfs, where it also holds the instance. v26.10 did ship a second flag, --vfs-mount, that mounted without running β the next patch removes it (66162) and reserves layer 0 for the --vfs-load source, so it sits at the same mount point in every thread, with a program's own mounts numbered from 1. Mounts are not nameable either: that proposal (66119) was closed.
If you gave this talk in August, this flag looked different. It selected a mount by 0-based index β --vfs-load=1 β which meant counting your --vfs-mounts and an optional-valued flag that the options parser could only express as an alias onto a hidden index flag. Naming the source deleted the counting, the hidden flag and the range check; dropping --vfs-mount deletes the list it counted into. Worth a sentence if anyone has read the older PR.
If someone asks about native addons β and at a Node conference someone will β they shipped in v26.9 as their own pull request (65680), because dlopen opens a shared object by path and a VFS path has no inode to open. It reads the addon out of the mount and loads it from a private, self-cleaning image: an anonymous memfd on Linux, so the bytes never touch the disk at all, an unlinked temp file elsewhere. Before it, a bundle containing a .node file mounted and then failed at require β it was the last thing a bundle could not carry. The status slide has it as its own row.
Note what's absent from this slide: any mention of trust. Mounting an archive is just mounting an archive. Node will happily run an unsigned one, and that's correct behaviour.
registerProvider() — youRealFSProvider and
ZipProvider are just the last two entries.-r module can install a provider
for the source that is about to be mounted.Node ships a hole, not a policy. It says: here is where a decision could go, and it is none of my business what you put there.
the rest of this talk
build a manifest by observation
or fetch, or cache, or audit
which is rather the point
"Node ships a hole rather than a policy. That sounds like a cop-out β the next slide is one reason it isn't."
Be exact about the ordering β preloads run during bootstrap; provider selection happens after. That gap is the extension point.
Careful with your verbs from here on. Node does not refuse anything. Node does not enforce anything. A provider you chose to install can decline to return a filesystem. If you don't install it, your archive runs. That's not a weakness in the design β it's the design.
The four little cards matter: they show the extension point isn't secretly a signing hook with extra steps.
Bake a signature format into the runtime and you have promised to be right about cryptography forever, on the release cadence of a runtime.
Leave the hole, and upgrading your signature scheme is a dependency bump — not an OS release, not a Node major, not a five-year deprecation.
One design decision, argued properly β not the thesis of the talk. It earns its ~85 seconds because the evidence is good, but don't oversell it: it is one of several choices, and the talk does not stand or fall on it.
"Android is the natural experiment. They took jar signing β a 1996 design β put it in the platform, and then spent fifteen years discovering what was wrong with it. Every fix was an OS release. Every device that couldn't update, didn't get it."
Get the facts right, they're checkable: v1 is jar signing, which signs per-entry digests listed in a manifest rather than the archive itself. Janus (CVE-2017-13156, 2017) exploited exactly that gap. v2 arrived in Android 7 with a whole-file hash; v3 in Android 9 added key rotation; v4 in Android 11 added streaming install. The jar scheme has its own version of this β entries added after signing leave a jar "partially signed" rather than invalid.
The move to make explicitly: "I am building a signing scheme in this talk. I think it's a decent one. I would not want it in Node β because in three years I'll have learned why it's wrong, and I'd like to be able to fix it."
That self-implicating turn is what keeps this from sounding like an excuse for unfinished work.
The central directory is inside the hash, so the member list is signed. You can't add an entry.
Whole-file, not per-entry. There is no gap between structures to hide in.
unsigned invalid untrusted valid
"Everything from here down is optional. None of it is in Node. If you delete all of it, your archive still runs β you just don't know who made it."
Where the signature lives is the neat bit: it has to be inside the file, but can't be covered by its own hash. Zip hands you that β the EOCD comment is the last structure, so you hash everything up to it.
The three cards are the payoff for slide 0x05. This scheme is prior art plus its post-mortems: jar's mistake was signing a list of entries rather than the archive; apk v2's fix was whole-file. Say that you copied their homework and their corrections.
The four states, briefly β the interesting one is "valid but untrusted": genuinely signed, bytes intact, and you have no idea who this person is. That's a different problem from tampering, and conflating them is how you train people to click through warnings.
Discover the file list by observation, then ./app.nzip.
Sigstore, and all four states including a flipped byte.
Install the provider and watch the mount decline.
Extract it and read what you were actually sent.
Verify → extract → review the tree for malicious code.
Timing checkpoint: aim to be at ~13 minutes here. If you're past 16, cut demo 4 to a single sentence.
Terminal hygiene: font size up, scrollback cleared, short prompt, already cd'd in, and the built artifacts present as a fallback so a failed build doesn't end the demo.
One sentence of setup, then get into the terminal. Don't read the cards.
Not static analysis. The app runs behind a recording provider — another use of that same extension point — which writes down every file it opens.
Two kilobytes for this one. No install, no
node_modules, no wrapper — the file mounts itself and runs its own main.
Where `bundle` came from, if anyone wonders: the release's bundle.nzip, run once with node --vfs-load ./bundle.nzip install β which verifies a signature and leaves the CLI on your PATH. That is demo 6 and the last slide of this act; here it is just the tool being used.
Beat 1 β the manifest (25s). "Static dependency analysis is perennially wrong β dynamic require, data files, conditional imports. So don't analyse. Run it and write down what it touched." Note it records open() too, so streamed files get caught. Also: this is a second provider, which quietly proves the extension point isn't signing-shaped.
The line to point at is the comment: lib/never-used.js is in the source tree and is not in the manifest, because the run never opened it. That is the whole argument for observation over analysis in one line of output.
Beat 2 β build (20s). Fast. --launcher is worth half a sentence: it puts the shell prefix the package ships in front, at create time, so the audit sees it and signing keeps it. --prefix <file> is there for your own launcher, or for a node binary.
Beat 3 β head -c (25s). This is the beat people react to. The front of the file is a shebang and nothing else. Let it sit.
Beat 4 β run it (25s). "Twenty-one kilobytes, and it is a program."
Fallback: artifacts are committed β just ./app.nzip help and move on. Do not debug on stage.
Say the honest thing here: running ./app.nzip directly does not verify anything. It's a zip with a shebang; Node mounts it and runs it. Verification is the next demo, and it's opt-in. Getting ahead of that keeps you consistent with slide 0x0B.
That last one is not a bug. Node was asked to run a zip, so it ran a zip. Caring is something you switch on.
Refused before it becomes a filesystem — the entry point is never resolved, so nothing inside it runs. Not because Node objects, but because you loaded something that does.
The running-the-broken-one beat matters. It looks like a failure; it's the design working. Say clearly: "Node ran the tampered archive, and that is correct. Node's job is loading, not judging."
Then the turn: mount it through the verifying provider and the same file is refused β by a library, which you chose, and could replace tomorrow with one that checks something else entirely.
On the command: bundle run is the short way to say node -r @pipobscure/bundle/register --experimental-vfs --vfs-load=app.nzip, which is what the comment above it spells out β worth reading aloud once, because the point of the slide is that the policy arrives as a preload you chose rather than as something the runtime does. Anything after the archive is the program's own argv.
Prep: cp app.nzip /tmp/app.nzip.bak before the dd, restore straight after, or every later demo breaks.
Order: valid β tampered/invalid β runs anyway β opt in β refused. Don't reorder; the punchline needs the setup.
Sigstore: if the live signing path isn't ready, verify a pre-signed artifact and say the signing ran in CI. Keep the identity on screen β "signed by this repo, by this workflow" is the point, and it beats CN=Test Signer as a demo. Don't get into certificate lifetimes on stage β that's an implementation detail of the tool, not of the argument. Prepared answer at the end if anyone asks.
A signed container you cannot inspect asks you to trust the signature and the vendor.
One you can open with
unzip lets you trust the code.
Which is why the chain is a real, extractable filename and not a magic footer — a plain zip utility on a machine with none of this installed can pull it out.
"Signing tells you the bytes are the bytes someone meant to send. It tells you nothing about whether those bytes are good. Different questions β and you need to be able to ask the second one."
Fast: 40 seconds. It sets up demo 5, which is where it pays.
Cut this first if you're long β one sentence carries it: "and because it's a real zip, you can just unzip it, which matters for the last demo."
A bundle is a closed set. Nothing resolves later; nothing is fetched at install. That is the shape a review can actually be complete over.
So the review is a step, not an afterthought — and it sits before the signature, because a signature is a claim about bytes you stand behind.
The verdict names the archive's sha256, so rebuilding invalidates it. Otherwise the gate approves any later build on the strength of one earlier approval — which is every “review completed” checkbox ever.
The skill exists (skills/audit-bundle/) and the gate is a shipped command (bundle audit --check), so this is a live demo rather than a promise. Plant something findable first β see below. If you are running long, show the refusal and skip the review itself; the refusal is the point.
"Every attack on the timeline earlier was legitimately signed by a legitimately compromised account. Provenance would have told you it came from the real maintainer β and been completely useless. You need both halves."
The move to make here is that this used to be the last demo β a thing you could do to an archive somebody sent you. It is now step three of four, because the same argument applies pointing inward: if you would not run someone else's bundle without reading it, don't sign your own without reading it either. Same skill, same checklist, run at both ends.
The strong framing: this is why the trust layer being optional and pluggable matters. Signature checking is one policy. "Extract it and read it" is another. "Diff it against the last release" is a third. Same hole, different things plugged into it.
On the diff: in the release pipeline the baseline is the currently published bundle, verified before use β because a diff makes everything the baseline already contained read as "unchanged" and therefore go unread. Worth one sentence if someone asks why not just review the source tree: what ships is the archive, and whether the set is right isn't visible until the bundle exists.
If demoing live: plant something obvious in the tree (an env read plus an outbound POST) so there's something to find.
No wrapper script. 904 members, sigstore included — the dependency tree stops being something you fetch and becomes a fixed set /audit-bundle can read in full.
If the model doesn't hold for this tool, it doesn't hold for anything. So it doesn't get an exemption.
Pinned to the release workflow, not to me. Running it doesn't check it — a #! carries no preload. Checking is a separate act, with a copy you already trust.
This is the dog-food slide, and as of this build it is true rather than aspirational. It used to be a prepared Q&A answer that ended "it isn't built yet". It is built: npm run manifest:cli β pack:cli β audit β sign:cli, and the published package carries the result.
"Everything I've asked you to do to somebody else's artifact, this tool does to itself. It arrives as one signed file. The command you run is a launcher that verifies that file, mounts it, and re-hashes every member as it reads it. There is no install step to compromise, because there is no install step."
If someone asks about the lock file β and they will β the answer is on the kicker. A lock file pins the bytes of a tree that is still fetched and executed at install time. A signed bundle removes the step rather than pinning it. "The same bytes" stops being a claim about versions and becomes literally the same bytes. The lock file stays useful for contributors reproducing a build; that's a different question.
There is no launcher, and that is the interesting bit. An earlier version put a small JS shim in front that verified the archive before mounting it. It got deleted: the shim was unsigned code installed beside the thing it vouched for, so anyone who could swap the archive could swap the shim β it detected corruption, not an adversary β and it put an opaque step in front of the one file you want people reading. bin now points straight at the artifact.
Say the honest half out loud: running it does not verify it. Checking is a separate act with a copy you already trust β bundle verify bundle.nzip, or bundle run bundle.nzip to execute it through the verifying mount. Same opt-in story as slide 0x10, applied to the tool itself.
Cut this slide first if you are running long. It is the most satisfying beat in Act III and the least load-bearing β the argument survives without it.
Nothing runs to install it. Nothing lands on disk that did not verify. The signer is recorded, and an update that changes identity is refused.
JavaScript in the archive is architecture-neutral. Native code is not — so carry all of them, and bind the one this machine needs at startup. Still one file, one signature, one audit.
The extension is load-bearing on Windows only: install
registers .nzip so the name alone runs it. Unix uses the
#! prefix and ignores the extension. Same bytes
everywhere.
"This is the part I only got to build because 26.10 shipped. A CLI is now one signed file you can put on a release page β one file for every operating system and every architecture β and getting it is one command that checks a signature before anything touches your disk."
Everything on this slide is released Node. No open pull requests: the mount and the loader are v26.10, the zip support is v26.8, the provider is v26.9. The only userland part is the signing and the fetching, which is the package.
The comparison to make out loud is curl | sh. Same one-line convenience, minus the two properties that make it dangerous: nothing is executed to install, and nothing is written until the signature verifies. Then say the honest half β the first install is trust-on-first-use unless you pass --identity, and after that the recorded identity is required on every update.
On multi-arch, the answer is better than people expect. Pure JavaScript is one artifact for every OS and architecture. Native code does not have to break that: an archive is a file *tree*, so it can carry a library per platform and pick at startup β dlopen(`lib/${process.platform}-${process.arch}/core.so`) β because node loads a shared object straight out of a mount, and node:ffi or a .node addon both work from there. One file, one signature, one audit, every machine. What it costs is bytes: you ship every architecture to everybody, which is a size trade rather than a portability one, and publishing per-platform archives remains available if that trade is wrong for you. examples/native-cli in the repo is a Rust CLI called through node:ffi if anyone asks.
If someone asks about Windows: the file association is what replaces #! there, bundle install registers it for the current user with no administrator, and the tests for it run on windows-latest in CI. One artifact still serves both, because unix reads the prefix and ignores the name while Windows reads the name and ignores the prefix.
Cut this slide before the limitations one if you are short; it is the most recent and the least load-bearing for the argument.
| Work package | Where | Who |
|---|---|---|
node:vfs, and module loadingthe subsystem, and resolving a program out of a mount |
v26.4 | Matteo Collina |
Zip support in node:zlibZipEntry / ZipFile / ZipBuffer, baseOffset |
v26.8 | Philipp Dunkel |
| Zip VFS provider mount an archive as a file tree |
v26.9 | Philipp Dunkel |
--vfs-load, registerProvidermount it, run the program out of it, choose what backs it |
v26.10 | Philipp Dunkel |
Native addons from a mountdlopen wants an inode; a VFS path has none |
v26.9 | Philipp Dunkel |
| Signing, verifying, recording the optional layer — and deliberately so |
userland | @pipobscure/bundle |
| SEA assets behind a VFS mount what makes a self-verifying executable tidy |
v26.9 | Matteo Collina |
Still behind --experimental-vfs. All of it is released: a stock
Node 26.10 runs every demo in this talk.
β Check every row the week of the talk. As of 22 Sep 2026 every row is released: node:vfs and the module loading in v26.4, the zip support in node:zlib in v26.8, in v26.9 (16 Sep) the Zip VFS provider, native addons from a mount, and SEA assets behind a VFS mount, and in v26.10 (21 Sep) --vfs-load and registerProvider (65748). If anything else moved, change the row rather than glossing it; the credibility you built in Act I is worth more than a tidy table.
Be exact about the version. Say "26.10 or later" β it is the floor this package's CI tests on. If anyone has 26.10 installed and finds --vfs-mount in its help, it is on its way out in the next patch; nothing in the talk uses it.
Attribution matters on this slide and people do read it. The top row is Matteo Collina's β the VFS subsystem itself and the module loading β and so is the SEA-assets one. Say so out loud; don't let a table of your own name imply otherwise.
The line to land: "the mechanism is in Node. The policy is a package you can fork, replace, or ignore. That separation is the deliverable."
It redirects fs calls whose resolved path falls under a mount. It does not
confine untrusted code. Node's own docs say so.
A checked mount proves the code is what someone you trust signed. Once it runs it has the full authority of the process. Provenance, not privilege.
Drop the -r and the archive mounts unverified. That is not a hole to close;
it is the consequence of Node not owning your trust policy.
So will yours. That's the whole argument for keeping it out of core, and I'd be a hypocrite to pretend otherwise.
And it is experimental: a flag, an API that can still move, and a userland package at version zero.
Do not skip this even if you're over. Cut demo 4 instead. Naming your own limits before the audience does is the highest-return thing in the last three minutes.
The third card is the one to dwell on, because it's the question a security person will ask, and the answer is the design rather than an apology: yes, someone can skip the check. If that's unacceptable in your environment, that's your build pipeline's job, or your OS's β not the language runtime's.
"If you take one caveat away: this is provenance, not privilege. It tells you the code is what someone you trust signed. It does nothing about what that code does next."
Node supplies the loading. The archive format, the signing, the verifying and the inspecting are ordinary code on top — which means you can replace any of it, including mine.
Take it and build something. A different signature scheme, an audit step, a provider that does something I haven't thought of.
@pipobscure/bundle
github.com/pipobscure/bundles
@pipobscure.com · pip@pip.fyi
One ask: build something on it. The interesting outcome isn't the signing library β it's someone writing a provider that does something you and I haven't thought of.
"I've been annoyed by this since 2009. The annoying part was never packaging β it was never being able to answer 'what exactly is running, and who put it there' without a paragraph of hedging. Now the runtime gives me a file I can point at, and lets me decide for myself what checking it means."
Then stop. Don't trail off. Land it, advance, take questions.
Prepared answers:
The timer turns red when you are behind the planned pace for the current slide and green when you are ahead. Planned runtime is about 28 minutes; see the deck README for the cut order.