惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
AI
AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
U
Unit 42
Vercel News
Vercel News
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
Microsoft Security Blog
Microsoft Security Blog
The GitHub Blog
The GitHub Blog
WordPress大学
WordPress大学
Martin Fowler
Martin Fowler
博客园 - 【当耐特】
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
F
Full Disclosure
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Help Net Security
Recorded Future
Recorded Future
N
News and Events Feed by Topic
雷峰网
雷峰网
V
Vulnerabilities – Threatpost
Schneier on Security
Schneier on Security
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
O
OpenAI News
Project Zero
Project Zero
罗磊的独立博客
G
GRAHAM CLULEY
腾讯CDC
P
Privacy International News Feed
V
V2EX
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
H
Heimdal Security Blog
L
LINUX DO - 热门话题
Forbes - Security
Forbes - Security
美团技术团队
MongoDB | Blog
MongoDB | Blog
Security Latest
Security Latest
M
MIT News - Artificial intelligence
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog

Echo JS

Desktop apps Introducing mermaid-lint: Stop Shipping Broken Diagrams How We Cut Slow Responses by 80% Migrating to Next.js App Router The quiet problem with unnecessary async - Matt Smith GitHub - ant-design/ant-design-cli: Ant Design on your command line. Query component knowledge, analyze project usage, and guide migrations — fully offline. Uncovering the Magic Behind Playwright React Performance Isn’t About useMemo — It’s About Render Boundaries T2 No Escape Hatches · Prickles GitHub - auto-agent-protocol/auto-agent-protocol: Automotive Agent Protocol GitHub - coactionjs/coaction: Zustand-style state management with built-in render tracking and cached computed state GitHub - williamtroup/Rattribute.js: ❓ A lightweight JavaScript library for automatically changing HTML element attributes based on responsive screen sizes. GitHub - williamtroup/Rink.js: 🔗 A JavaScript library for generating responsive HTML link targets. SVAR Kanban: Interactive Task Board Component for Svelte, React, and Vue Performance Cost of Popular 3rd Party Scripts date-light - Tiny date utilities for JavaScript When React Hooks Stop Scaling: Moving Complex State to Zustand - Oren Farhi GitHub - jskits/loggerjs: A faster, more powerful isomorphic logger Out Loud — Free AI Text to Speech Pocket DB — Embedded single-file NoSQL for Node.js billboard.js 4.0 release: Canvas rendering mode, 94.3% faster! GitHub - thegruber/linkpeek: Secure TypeScript link preview and URL metadata extractor for Open Graph, Twitter Cards, JSON-LD, Node/Bun/Deno/edge. GitHub - evoluteur/healing-frequencies: Play the healing frequencies of various sets of tuning forks: Solfeggio, Organs, Mineral nutrients, Ohm, Chakras, Cosmic octave, Otto, DNA nucleotides... or custom. Animated sine waves - 27 lines of pure JavaScript Framework | Neutralinojs GitHub - AllThingsSmitty/typescript-tips-everyone-should-know: ✅ A curated collection of practical TypeScript patterns that improve safety, readability, maintainability, and developer experience. 🧠 Heat.js : JavaScript Heat Map GitHub - iDev-Games/State-JS: State.js is a CSS‑reactive framework that makes UI state and updates flow through CSS instead of JavaScript logic. GitHub - yankouskia/gameplate: :video_game: Boilerplate for creating game with WebGL & Redux :game_die: GitHub - yankouskia/get-browser: 💻 Lightweight tool to identify the browser (mobile+desktop detection)📱
GitHub - yankouskia/is-incognito-mode: Identify whether browser is in incognito mode 👀
2026-07-09 · via Echo JS

Detect private / incognito browsing in 4 lines of code.

Zero dependencies — fully typed — dual ESM + CJS — timeout-safe & cancelable — ~2 kB min+gzip.

npm version npm downloads CI Coverage Bundle size Types Docs License

Try the live demo →

Open it in a normal window. Then re-open it in private/incognito mode. Watch the verdict flip.



30-second tour

npm install is-incognito-mode
import { isIncognito } from 'is-incognito-mode';

if (await isIncognito()) {
  showPaywall();
} else {
  trackVisit();
}

That's it. One async call, one boolean. Works on Chrome, Firefox, Safari, Edge, and (best-effort) the long tail of older WebKit shells.

Need more than a yes/no? Use detectIncognito() for a typed object with browser, confidence, quota, and strategy fields.


Why you'd use this

Browsers don't expose a "private mode" API on purpose — but private windows still leak the fact through resource limits and storage shape. is-incognito-mode packages the current state-of-the-art per-engine detection as a tiny, typed, zero-dep module, so you can stop hand-rolling heuristics that browsers patched out years ago.

A few real-world fits:

Scenario What you do
Soft paywall Discourage incognito bypass without hard-blocking the user.
Respectful analytics Skip beacon calls in private sessions to honor the signal.
Long forms / surveys Warn before storing state that will vanish on close.
Fraud / abuse signals One input among many — never the sole decider.
E2E test conditioning Branch tests based on whether you're driving a private tab.

Install

pnpm add is-incognito-mode      # or  npm i is-incognito-mode
                                # or  yarn add is-incognito-mode
                                # or  bun add is-incognito-mode

No-install — straight from a CDN:

<script type="module">
  import { isIncognito } from 'https://esm.sh/is-incognito-mode@2';
  console.log(await isIncognito());
</script>

See it run

A ready-to-run demo page is hosted alongside the docs:

https://yankouskia.github.io/is-incognito-mode/demo/

Open it once in a regular window, then once in incognito/private — the verdict, browser, confidence, quota, and strategy update live. Source: examples/browser/index.html (single static file, no build step).


How it decides (under the hood)

There is no single cross-browser signal — each engine leaks private mode in a different place — so the library picks the right probe per engine.

Detection flow diagram
flowchart TD
    A[detectIncognito] --> D{which engine?}
    D -- Chromium --> C1["navigator.storage.estimate()"]
    C1 --> C2{"headroom (quota − usage) &lt; 9.5 GiB?"}
    C2 -- yes --> R1([private — high confidence])
    C2 -- no --> R2([normal — high confidence])
    D -- Firefox --> F1["navigator.storage.getDirectory() — OPFS"]
    F1 --> F2{rejected with a security error?}
    F2 -- yes --> R3([private — high confidence])
    F2 -- no --> R4([normal — high confidence])
    D -- Safari/WebKit --> S1["navigator.storage.getDirectory() — OPFS"]
    S1 --> S2{rejected 'unknown transient reason'?}
    S2 -- yes --> R5([private])
    S2 -- no --> R6([normal])
    D -- Edge legacy / IE --> G[PointerEvent + indexedDB heuristic]
    D -- unknown --> X([throw UNSUPPORTED_BROWSER])
Loading

Chromium (Chrome, Edge, Brave, Opera, …). Chrome's predictable-reported-quota mitigation (default since Chromium 147) was meant to mask incognito by reporting a fixed storage quota — but it didn't fully equalize the two modes. Empirically, navigator.storage.estimate() reports quota = usage + 10 GiB in a normal tab and usage + 9 GiB in an incognito tab. The library looks at the headroom (quota − usage): subtracting usage cancels real consumption and leaves just that offset — a stable 10 GiB vs 9 GiB. Below 9.5 GiB → incognito. (This also catches pre-147 Chromium, whose incognito quota was a small dynamic value.)

Firefox & Safari. The Origin Private File System (navigator.storage.getDirectory()) is rejected in private mode — Firefox throws a security error, Safari throws "unknown transient reason". A clean resolve means a normal window.

Legacy Edge / IE. No indexedDB while PointerEvent exists → private.

If you need to override the 9.5 GiB Chromium cutoff, pass privateQuotaThresholdBytes (see Tuning).


Usage

Boolean verdict

import { isIncognito } from 'is-incognito-mode';

const inPrivate = await isIncognito();

Rich detection result

import { detectIncognito } from 'is-incognito-mode';

const { isPrivate, browser, confidence, quota, strategy } =
  await detectIncognito();

console.log(
  `${browser} (${confidence}) — strategy: ${strategy}, quota: ${quota}`,
);
// → "chromium (high) — strategy: chromium-quota, quota: 9663676416"

Fields on DetectionResult:

field type notes
isPrivate boolean Final verdict.
browser BrowserName Coarse engine: chromium, firefox, safari, webkit, edge-legacy, ie, unknown.
confidence 'high' | 'medium' | 'low' high for the primary per-engine probe; low for legacy heuristics.
quota number | null estimate().quota in bytes (Chromium); null for the OPFS and legacy strategies.
strategy DetectionStrategyName Which probe produced the verdict — see How it decides.

Timeouts & cancellation

Detection is a Promise, and in rare browser states a storage probe can stall — for example a Firefox indexedDB.open request that never fires success or error. On a critical render path (paywall, analytics gate) that risks freezing your code, so you can put a deadline on the call:

import { detectIncognito } from 'is-incognito-mode';

// Rejects with an IncognitoDetectionError of code 'TIMEOUT' if no verdict
// arrives within 5 seconds. Recommended on any hot path.
const result = await detectIncognito({ timeoutMs: 5000 });

Pass an AbortSignal to cancel detection — e.g. tied to a component lifecycle so a verdict that arrives after the user has navigated away is discarded:

import { detectIncognito, IncognitoDetectionError } from 'is-incognito-mode';

const controller = new AbortController();
// React: useEffect(() => () => controller.abort(), []);

try {
  await detectIncognito({ signal: controller.signal });
} catch (error) {
  if (error instanceof IncognitoDetectionError && error.code === 'ABORTED') {
    // expected — the caller cancelled. Ignore.
  }
}

Both options compose; whichever fires first wins (TIMEOUT vs ABORTED). When a bound trips, the in-flight probe is abandoned and its listeners detached. timeoutMs defaults to undefined (no deadline), so existing callers are unaffected — but enabling it is the safe default for production.

Caching the verdict

Private / incognito state can't change within a page load, so re-running the storage probes on every call is wasted work. Pass cache: true to memoize the first successful verdict and return it instantly thereafter:

import { detectIncognito } from 'is-incognito-mode';

await detectIncognito({ cache: true }); // probes once
await detectIncognito({ cache: true }); // instant — same cached result
  • Scope. The cache is keyed by the live navigator object, so it lives exactly as long as the page (per-document, per-origin). Nothing is stored globally — injecting a fresh globals (as tests and per-request SSR do) gives a fresh, isolated cache with no teardown to remember.
  • Failures aren't cached. A TIMEOUT, ABORTED, or PROBE_FAILED rejection is never stored, so the next call retries cleanly.
  • It caches the verdict, not the inputs. A cache hit ignores a later call's differing privateQuotaThresholdBytes. If you vary the threshold per call, leave cache off.

Off by default — existing behaviour is unchanged.

Tuning the detection

You normally do not need to configure anything. The Chromium strategy compares the storage headroom (estimate().quota − estimate().usage) to a 9.5 GiB cutoff — the midpoint between the 10 GiB Chrome reports for a normal tab and the 9 GiB it reports for an incognito tab. To override that cutoff:

import { detectIncognito } from 'is-incognito-mode';

const result = await detectIncognito({
  privateQuotaThresholdBytes: 9.5 * 1024 * 1024 * 1024,
});

DEFAULT_PRIVATE_QUOTA_BYTES (9.5 GiB) is exported as the reference value.

Injecting globals (for testing)

detectIncognito accepts a globals override so unit tests don't have to monkey-patch navigator or window:

import { detectIncognito } from 'is-incognito-mode';

const result = await detectIncognito({
  globals: {
    navigator: {
      userAgent: 'Mozilla/5.0 ... Chrome/148.0',
      // Chrome reports quota = usage + 9 GiB for an incognito tab.
      storage: {
        estimate: () => Promise.resolve({ quota: 9 * 1024 ** 3, usage: 0 }),
      },
    },
    window: {},
  },
});
// result.isPrivate === true  (9 GiB headroom < 9.5 GiB)

Error handling

import { isIncognito, IncognitoDetectionError } from 'is-incognito-mode';

try {
  const incognito = await isIncognito();
  // ...
} catch (error) {
  if (error instanceof IncognitoDetectionError) {
    switch (error.code) {
      case 'NOT_A_BROWSER':
        // Server-side render path
        break;
      case 'UNSUPPORTED_BROWSER':
        // Probably a bot / curl / node-fetch
        break;
      case 'PROBE_FAILED':
        // The engine's probe could not produce a verdict
        break;
      case 'TIMEOUT':
        // Detection exceeded the `timeoutMs` deadline
        break;
      case 'ABORTED':
        // Detection was cancelled via the `signal` option
        break;
    }
  }
}

CommonJS

const { isIncognito } = require('is-incognito-mode');

// Default-import-style:
const detect = require('is-incognito-mode').default;

API at a glance

Export Kind Description
isIncognito(options?) function Resolves to boolean.
detectIncognito(options?) function Resolves to a rich DetectionResult.
IncognitoDetectionError class Typed error with code: 'NOT_A_BROWSER' | 'UNSUPPORTED_BROWSER' | 'PROBE_FAILED' | 'TIMEOUT' | 'ABORTED'.
DEFAULT_PRIVATE_QUOTA_BYTES const Chromium headroom cutoff (9.5 GiB).
BrowserName (type) type Coarse engine name.
DetectionResult (type) type Rich result shape — see "Usage".
DetectionConfidence (type) type 'high' | 'medium' | 'low'.
DetectionStrategyName (type) type Strategy identifier.
DetectIncognitoOptions (type) type Options bag.

Full generated reference: https://yankouskia.github.io/is-incognito-mode/


Compatibility

Browsers

Engine Detection strategy Confidence
Chromium (incl. 147+) storage.estimate() headroom (quota−usage) high
Firefox ≥ 111 OPFS navigator.storage.getDirectory() high
Safari ≥ 15.2 OPFS navigator.storage.getDirectory() high
Older Safari / WebKit localStorage + openDatabase probes medium-low
Older Firefox indexedDB.open error path low
Edge (legacy) PointerEvent + indexedDB heuristic low
IE 10–11 PointerEvent + indexedDB heuristic low
All others (unknown) throws UNSUPPORTED_BROWSER

Node / runtimes

Not supported at runtime — this is a browser-only package and will throw NOT_A_BROWSER if invoked without a navigator. The package builds on Node ≥ 20.

Bundlers & frameworks

Ships ESM and CJS with proper exports map and .d.ts / .d.cts. Works out-of-the-box in Vite, Next.js (client components), Remix, Astro, Webpack, Rollup, esbuild, Bun, and Deno.


What's new in v2

v1.x v2.0
Detection technique FileSystem API + IndexedDB + localStorage + PointerEvent per-engine probes: Chromium quota headroom, Firefox/Safari OPFS
TypeScript shipped JS only strict TypeScript source, full .d.ts
Module formats UMD + CJS ESM + CJS dual publish
Dependencies get-browser zero
Bundle size ~3 kB min+gzip ~2 kB min+gzip (≈1.2 kB brotli)
Engines Node ≥ 8 Node ≥ 20
Error model throw 'string' IncognitoDetectionError with code

See BREAKING_CHANGES.md for migration recipes and DECISIONS.md for the reasoning behind each big call.


Comparison with alternatives

  • detectincognitojs — excellent, similar in spirit. Pick that if you want a UMD bundle or a richer per-browser breakdown.
  • Inline UA sniff + try/catch around localStorage — broken in every modern browser. Don't.
  • Just check window.webkitRequestFileSystem — patched out of Chrome 76. Don't.

Contributing

Pull requests welcome. See CONTRIBUTING.md for the dev loop, conventional commits, and the changeset workflow. Be excellent — the Contributor Covenant 2.1 applies.

Security

Report vulnerabilities privately per SECURITY.md.

License

MIT © Aliaksandr Yankouski