Skip to content
Back to skills

Napi Boundary

ASecurity

Rules for the napi-rs adapter layer in packages/core/src (vorn_core.node) and the TypeScript that loads it. Use when adding or changing a native export, or wiring a native path into the Node server behind a switch.

  • 26 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 6, 2026
ai-agentstypescriptrustgonodegitapi

Works with

  • terminal
  • api

Security analysis

A100/100

Scanned October 6, 2026

npx -y skills add vorn-run/vorn --skill napi-boundary --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Napi Boundary?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Napi Boundary
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vorn-run-napi-boundary/badge)](https://www.skillsdirectory.com/skills/vorn-run-napi-boundary)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: napi-boundary
description: Rules for the napi-rs adapter layer in packages/core/src (vorn_core.node) and the TypeScript that loads it. Use when adding or changing a native export, or wiring a native path into the Node server behind a switch.
---

# The napi boundary

`vorn-core` (`packages/core/src`) is a thin adapter over plain crates. It is
loaded into the Node server as `vorn_core.node` by
`packages/server/src/native-core.ts`. Everything here exists to keep one
property: **a native path can only make the server faster, never take it
down.**

## What lives where

| Layer                           | Contains                                                                |
| ------------------------------- | ----------------------------------------------------------------------- |
| `crates/<name>` (`vorn-<name>`) | All logic, its error enum, unit/property tests, Criterion benches       |
| `src/<name>.rs`                 | `#[napi]` types and functions: convert, bound, map errors. Nothing else |
| `packages/server/src/...`       | The switch, the JS fallback, logging why the native path is off         |

If an adapter function grows a loop or a branch on domain data, that logic
belongs in the crate.

## Rules

1. **No panic reaches Node.** Every sync export is
   `#[napi(catch_unwind)]`. An `async` export runs its work in
   `tokio::task::spawn_blocking` and maps the `JoinError`, as `src/git.rs`
   does. An uncaught panic unwinding into Node aborts the server and every
   terminal it hosts.
2. **Never block the event loop.** Sync exports must be cheap and bounded
   (feed one flush, read a field). Anything that can take milliseconds (git,
   history search, bulk analysis, disk) is `#[napi] pub async fn` returning a
   promise, with concurrency bounded by a `Semaphore` in a `LazyLock`.
3. **Cross the boundary per flush, not per chunk.** Batch bytes on the JS side
   and pass one `Buffer` (or one string) per flush. Return what the server
   needs from that flush in one object.
4. **Bytes as bytes.** Prefer `Buffer` / `&[u8]` for terminal data; take a
   `String` only where the JS side already has one, and offer both when both
   callers exist (`feed` and `feed_bytes` in `src/screen.rs`).
5. **Errors become messages.** Map each crate error with a `to_napi` helper to
   `napi::Error::from_reason(err.to_string())`. When parity matters, the
   message matches what the TypeScript it replaces threw.
6. **Free native memory explicitly.** V8 cannot see native allocations, so
   stateful objects hold `Option<Inner>` and expose a `free()` the server calls
   when the session ends; every method treats `None` as freed.
7. **Numbers.** JS numbers become `u32`/`i64`/`f64` at the boundary; convert
   to `usize` and newtypes with `TryFrom` inside, never with `as` on
   untrusted values.
8. **`#[napi(object)]` structs are wire types.** Keep them flat and stable,
   document each field, and do not leak crate-internal types through them.
9. **Feature gates.** Anything depending on libghostty-vt sits behind
   `#[cfg(feature = "ghostty")]` so `build.mjs --no-ghostty` still builds.

## The server side

- `native-core.ts` loads the binary once and never throws. A server whose
  binary is missing or fails to load keeps running without what the core
  provides, logs the reason, and reports it on Settings › Experimental.
- New native work sits behind one Experimental switch for the whole batch
  being tried, not one per feature. With the switch on and the binary missing,
  the server stays on the TypeScript, logs why, and says so on the settings
  page.
- When the batch becomes the default, its switch and the TypeScript it
  replaced go in the same change, after the TypeScript's outputs are recorded
  as fixtures the native path is tested against.
- Tests that need the binary check for `packages/core/vorn_core.node` and skip
  with a reason when it is absent.

## Checklist for a new export

- [ ] Logic and tests are in the crate; the adapter only converts.
- [ ] `catch_unwind` on sync exports, or async with `spawn_blocking`.
- [ ] Bounded work per call on Node's thread; heavy work is async.
- [ ] Errors mapped to readable messages; no `unwrap` on JS input.
- [ ] Native memory released by an explicit `free()` where state is held.
- [ ] Switch, fallback and its log line exist on the server side.
- [ ] Parity test feeds the same input to both paths.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…