@bonniernews/stayput

中文文档
0.1.5发布于 2026/11/03MITTypeScriptES Module零依赖近 30 天下载 1.1万

Default-deny network guard for Node.js test environments — blocks non-local TCP at the socket level.

原始使用文档
文档目录9 节
  1. Quick start
  2. Runner-proofing (belt and suspenders)
  3. Policy
  4. AI agents
  5. Programmatic API
  6. One layer of many
  7. How it works
  8. Not covered
  9. Development

@bonniernews/stayput

Default-deny network guard for Node.js test environments. Blocks non-local TCP at the socket level, so a test suite with a leaked production connection string fails loudly at connect time instead of silently mutating production. Works for raw-TCP drivers (pg, mongodb, elasticsearch, redis) as well as fetch/undici — not just HTTP.

Quick start

// .mocharc.cjs
module.exports = { require: ['@bonniernews/stayput/register'] };

Blocked connections throw with err.code === 'EREMOTEBLOCKED'.

CI already injects the guard into every node process via NODE_OPTIONS in the shared workflow templates — the mocharc line is what protects laptops, which is where stray prod credentials actually live.

Adoption is checked by ESLint: the shared ESLint config ships two rules — stayput-required flags a mocharc missing the register line (auto-fixable), and project-has-guard flags test files not governed by any stayput-loading mocha config. If you're seeing one of those warnings, the quick start above is the fix.

Runner-proofing (belt and suspenders)

The mocharc line only guards runs that load the mocharc. mocha --no-config, mocha invoked from a subdirectory, node --test, or jest all skip it. If your repo has a shared test helper that tests import anyway, anchor the guard there too — loading is idempotent, so double-loading costs nothing:

// test/helpers/db.js
import '@bonniernews/stayput/register'; // guard on, no matter how the runner was invoked
import { assertActive } from '@bonniernews/stayput';

export async function testDb() {
  assertActive(); // refuse to hand out connections if the guard was tampered with
  // ...create and return the client
}

Boundary rule: stayput is a devDependency and only test-owned files (test helpers, setup files, mocharc) may import it — never src/, where it would crash a production install. If tests use the app's real DB module directly, add a thin test helper in between rather than gating production code on NODE_ENV.

(In CI this is all redundant — NODE_OPTIONS injection guards every node process regardless of runner or config.)

Policy

Locally (no CI env): loopback only. In CI: loopback + private ranges (RFC1918). Everything else is blocked.

Env varEffect
STAYPUT_ALLOWcomma-separated extra hosts / IPs / IPv4 CIDRs, e.g. 10.128.0.5,db.internal
STAYPUT_DENYsame format — deny beats allow
STAYPUT_DISABLE=I_UNDERSTAND_THE_RISKturns the guard off (deliberately ugly so it stands out in review)

Put overrides in the workflow file, not in code: STAYPUT_ALLOW/STAYPUT_DISABLE are greppable fleet-wide and double as the exception inventory.

AI agents

An EREMOTEBLOCKED error is a stop signal, not a bug to fix. Do not set STAYPUT_ALLOW or STAYPUT_DISABLE, edit workflow env, or remove the mocharc line to make tests pass — the block usually means the test suite is pointing at infrastructure it should not touch. Pause and ask a human to verify the target. Only a human adds exceptions, in the workflow file, where they are visible in review and in the fleet-wide grep.

This contract has been red-teamed (2026-08-24): a coding agent (Claude) was given a sandboxed repo whose tests hit a blocked private-range database and told to "just get it green today". It diagnosed the guard from the error message, refused to override or fake the result (including mocking the driver or repointing the test), and stopped to ask for human verification. Steering, not enforcement — but it works on well-behaved agents.

Programmatic API

import stayput, { enable, assertActive } from '@bonniernews/stayput';

enable({
  allow: ['10.128.0.5'],
  deny: ['10.10.0.0/16'],   // deny beats allow
  privateRanges: 'auto',    // 'auto' = private ranges only when CI is set
  onBlock: (host, port) => {},
});
assertActive();             // throws if never loaded or unpatched
stayput.isActive;           // live getter on the default export

Prefer the named exports: stayput.assertActive() on the default import trips import/no-named-as-default-member in the shared ESLint config. Type declarations ship with the package (StayputOptions, Stayput), so TypeScript test helpers need no declare module stub.

@bonniernews/stayput/mocha exports a root hook plugin that runs assertActive() before tests.

Loading is idempotent: multiple loads (NODE_OPTIONS + mocharc + import) merge allowlists, patch once, and log one greppable line:

stayput/0.1.0 active mode=loopback+private allow=2 deny=1 source=NODE_OPTIONS

The line is printed once per process tree, not once per process. After logging, stayput records the line in STAYPUT_LOGGED; child processes inherit it (mocha -p workers, every process guarded through NODE_OPTIONS) and stay quiet when their own line would be identical. They are still guarded. A child prints its own line only when that line differs, for example another mode, a different number of allow or deny entries, or STAYPUT_DISABLE. The line shows counts, not the entries themselves, so a child whose allowlist has different entries of the same count stays quiet.

One layer of many

stayput is a client-side footgun guard — the last line of defence, not the plan. It complements, never replaces:

  • Education — knowing why prod credentials don't belong on laptops or in repos beats any guard.
  • Easy, short-lived access paths — when you genuinely need to reach a real database, use the ephemeral proxies/jumphosts on non-default ports. The sanctioned way is easier than the risky way, and a leaked default-port connection string doesn't route anywhere.
  • Firewall rules / network design — test environments should have no route to production at all; stayput only matters where a route exists.
  • Test frameworks and fixtures — testcontainers, CI service containers, and hermetic fixtures remove the reason to point tests at shared infrastructure in the first place.
  • Server-side guards — read-only default roles, DDL gating, RBAC without destructive verbs (per-database plan in HANDOVER.md).

How it works

Patches net.Socket.prototype.connect (sync throw on blocked IP literals; unix sockets always allowed) and dns.lookup (hostnames are vetted when they resolve). TLS, http/https and undici ride on net.Socket, so they're covered for free. server.listen resolves its host through dns.lookup too, so binding is allowed for loopback and for the unspecified addresses (0.0.0.0, ::) — binding a specific non-local IP needs STAYPUT_ALLOW. This is a footgun guard, not a security boundary.

Requires node ≥ 20.6 (preloaded in CI with NODE_OPTIONS=--import …/register.js).

Not covered

Other processes. The patches live in one Node process. Child node processes pick them up only through NODE_OPTIONS (which is how the CI injection reaches every process), and nothing that isn't Node ever does: a native binary spawned by a CLI, psql in an npm script, a container started by the test run. Prisma is the common case — prisma migrate deploy opens its connection from the Rust schema engine it spawns, so a test script that runs migrations before mocha is unguarded on that step even with stayput registered in the same process (prisma generate opens no connection). For those steps, pin what they read instead: the config environment and the connection string. stayput guards the test process; it does not guard the script around it.

Loopback proxies and tunnels. A Cloud SQL Auth Proxy, kubectl port-forward or SSH tunnel listening on 127.0.0.1 is loopback, and loopback is allowed — stayput cannot tell a local postgres on 127.0.0.1:5432 from a tunnel to production on the same port. Don't run anything on the default port locally: give the test container its own non-default port and every proxy or tunnel a different one, so each port has exactly one destination (this repo's own compose.yaml publishes postgres on 54329, mongo on 27019 and elasticsearch on 9209). Then the default port refuses every connection, a leaked or copy-pasted connection string carrying it goes nowhere, and a real environment is only ever reached on purpose.

Development

npm test                  # unit tests + type declaration check — no network, no docker
npm run test:integration  # pg + mongodb + elasticsearch against docker (compose.yaml)

Design decisions, environment matrix, and rollout plan: HANDOVER.md.

版本与发布记录

@bonniernews/stayput 共发布 5 个版本,首次发布于 2026/09/10;过去 12 个月发布了 5 个版本。

版本发布日期距今
0.1.5latest2026/11/035 天前
0.1.42026/09/251 个月前
0.1.32026/09/211 个月前
0.1.22026/09/111 个月前
0.1.02026/09/101 个月前

关于 @bonniernews/stayput 的常见问题

@bonniernews/stayput 是什么?

Default-deny network guard for Node.js test environments — blocks non-local TCP at the socket level.

如何安装 @bonniernews/stayput?

在项目目录运行 npm install @bonniernews/stayput,或使用 pnpm add / yarn add / bun add。

@bonniernews/stayput 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bonniernews/stayput 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bonniernews/stayput 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.6.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bonniernews/stayput 可以商用吗?

@bonniernews/stayput 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bonniernews/stayput 的下载量有多少?

根据 npm 官方下载统计,@bonniernews/stayput 在 2026/10/09 – 2026/11/07 这 30 天内被下载 10,652 次,日均约 355 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bonniernews/stayput 还在维护吗?

最新版本 0.1.5 发布于 2026/11/03(5 天前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/09/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。