lna-readiness
v0.1.0
Published
Static CI scanner that finds requests Chrome 142 Local Network Access will silently break — before they ship. CLI + ESLint plugin + GitHub Action.
Maintainers
Readme
lna-readiness
Find the requests Chrome 142 will silently break — before your users do.
$ npx lna-readiness src
src/print.ts
12:3 error fetch() to a local address (http://localhost:9100/print) will be gated by
Chrome 142 LNA and fail silently if the user denies the prompt.
lna/fetch-private-target
fix: fetch("http://localhost:9100/print", { targetAddressSpace: "local" })
1 error, 0 warnings across 214 filesWhat changed
Chrome 142 shipped the Local Network Access permission to the stable channel in late 2025. Any request from a web page to a more-private address space now goes behind a one-time prompt:
localhost,127.0.0.0/8,::1,0.0.0.0- private IPv4:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 - carrier-grade NAT
100.64.0.0/10(Tailscale and friends) - link-local
169.254.0.0/16, IPv6fe80::/10 - IPv6 ULA
fc00::/7 *.localand*.localhost(mDNS)
If the user denies or dismisses the prompt, the request fails silently — no console error you can catch reliably, no rejected promise you'd expect. The LocalNetworkAccessRestrictionsTemporaryOptOut enterprise escape hatch is removed after M152, so the gate hardens through 2026.
This breaks a specific, real set of apps: POS and label-printer bridges (the QZ Tray pattern), Office/Figma/Photoshop add-ins talking to a desktop companion over localhost, IoT and router config UIs served from 192.168.x.x, camera snapshot <img> tags, and iframe embeds that reach a private-IP backend.
The fix exists in docs — a targetAddressSpace hint on fetch, an allow="local-network-access" attribute on iframes, a delegated permission from the top frame. What's been missing is a way to find which requests in a codebase need it without shipping to production and waiting for the bug reports. That's this tool.
Install
npm i -D lna-readiness
# or run it once, no install:
npx lna-readinessCLI
lna-readiness # scan the source tree from the current dir
lna-readiness src app pages # scan specific dirs/globs
lna-readiness --json # machine-readable, for tooling
lna-readiness --markdown # a table for PR comments / CI summaries
lna-readiness --fail-on-warning # warnings fail the build too
lna-readiness --max-warnings 5 # allow up to 5 warningsExit code is 1 when there are errors (or warnings over your threshold), 0 otherwise — drop it into any CI step.
What it flags
| Rule | What it catches |
| --- | --- |
| lna/fetch-private-target | fetch() to a private/local host with no targetAddressSpace hint |
| lna/xhr-private-target | XMLHttpRequest.open() to a private/local host |
| lna/axios-private-target | axios.*() to a private/local host |
| lna/websocket-private-target | new WebSocket() to a private/local host |
| lna/eventsource-private-target | new EventSource() to a private/local host |
| lna/iframe-missing-allow | <iframe> at a private/local URL missing allow="local-network-access" |
| lna/{img,script,source,...}-private-src | sub-resource loaded from a private/local host |
It scans .js/.jsx/.ts/.tsx/.mjs/.cjs, .vue, .svelte, and .html. Requests with a URL it can't resolve statically (a variable, a template with an interpolated host) are left alone — no guessing, so no noise.
ESLint plugin
Same rules, live in your editor and existing lint run (flat config):
// eslint.config.js
import lna from 'lna-readiness/eslint-plugin';
export default [
{
plugins: { lna },
rules: { 'lna/no-unguarded-local-network-request': 'error' },
},
];GitHub Action
# .github/workflows/lna.yml
name: LNA Readiness
on: [pull_request]
jobs:
lna:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: fernforge/lna-readiness@v1
with:
# paths: 'src' # optional, defaults to the whole repo
# fail-on-warning: true # optional
# max-warnings: 0 # optionalThe action writes a Markdown table to the job summary and exposes errors / warnings outputs.
Programmatic
import { scan, isPrivateNetworkUrl, targetSpaceOf } from 'lna-readiness';
const { findings } = scan(process.cwd());
isPrivateNetworkUrl('http://192.168.1.20/label'); // true
targetSpaceOf('ws://printer.local:8181/'); // 'local'
targetSpaceOf('https://api.stripe.com'); // 'public'What this does not do
It's static analysis. It catches requests whose target is a literal string in your source. It can't see a host that's only known at runtime (from config, an env var, or user input) — for those, test with the prompt denied and handle the failure explicitly. It also doesn't check that you actually hold the granted permission; a passing scan means "these calls carry the right hints/attributes," not "these calls will succeed."
Background
- Chrome: Local Network Access and the Private Network Access → LNA transition
- WICG spec: Local Network Access
License
MIT
