is-incognito-mode 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,90 @@
1
+ # Changelog
2
+
3
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Docs and housekeeping pass following the v2.0.0 release.
8
+ - README leads with a 30-second tour before the "why" section so readers see
9
+ action first; the detection-flow Mermaid diagram is collapsible; use-cases
10
+ promoted to a scenario table; fixed a stale code-sample strategy name
11
+ (`storage-quota`, not `storage-estimate`).
12
+ - Removed unused `detectCurrentBrowser()` export (no production caller) and
13
+ the corresponding test. Only `detectBrowser()` taking explicit args is used.
14
+ - Removed the never-returned `'unknown'` member from `DetectionStrategyName`.
15
+ - `.gitignore` adds `.claude/`.
16
+
17
+ ## 2.0.0
18
+
19
+ ### Major Changes
20
+
21
+ - 11770a6: ## v2.0.0 — complete modernization
22
+
23
+ The detection vectors used by v1 (FileSystem API, IndexedDB error, localStorage
24
+ exception, PointerEvent heuristics) have all been patched out of mainline
25
+ browsers since 2019. v2 replaces them with `navigator.storage.estimate()` quota
26
+ thresholding, which is the current state-of-the-art technique and works across
27
+ Chromium, Firefox, and WebKit.
28
+
29
+ Other changes:
30
+ - Source rewritten in strict TypeScript. Full `.d.ts` ship.
31
+ - Dual ESM + CJS publish with a proper `exports` map. UMD bundle removed.
32
+ - New named exports: `detectIncognito()` (rich result), `IncognitoDetectionError`
33
+ (typed errors with `code`), `DEFAULT_PRIVATE_QUOTA_BYTES`.
34
+ - The default export is preserved for v1 drop-in compatibility.
35
+ - Zero runtime dependencies (was: `get-browser`).
36
+ - `engines.node >= 20`.
37
+
38
+ See `BREAKING_CHANGES.md` for migration recipes.
39
+
40
+ All notable changes to this project are documented in this file. From v2.0.0
41
+ onward, entries are generated by [Changesets](https://github.com/changesets/changesets);
42
+ historical pre-v2 entries are preserved as-is.
43
+
44
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
45
+ and this project adheres to [Semantic Versioning](https://semver.org/).
46
+
47
+ ---
48
+
49
+ ## [2.0.0] — Unreleased
50
+
51
+ > Generated from the pending changeset at release time. See
52
+ > `BREAKING_CHANGES.md` for migration recipes.
53
+
54
+ ### Added
55
+
56
+ - `detectIncognito()` returning a rich `DetectionResult` (browser, confidence,
57
+ quota, strategy).
58
+ - `IncognitoDetectionError` with stable `code` literal-union (`NOT_A_BROWSER`
59
+ / `UNSUPPORTED_BROWSER` / `PROBE_FAILED`).
60
+ - `DEFAULT_PRIVATE_QUOTA_BYTES` and a `privateQuotaThresholdBytes` option for
61
+ tuning the detector.
62
+ - TypeScript types ship with the package.
63
+ - Dual ESM + CJS publish; `exports` map; npm provenance.
64
+
65
+ ### Changed
66
+
67
+ - **Detection technique** rewritten around `navigator.storage.estimate()` quota
68
+ probing. The v1 vectors (FileSystem API, IndexedDB error path, localStorage
69
+ exception, PointerEvent heuristics) were patched by browser vendors between
70
+ 2019 and 2023 and were returning incorrect results in current browsers.
71
+ - Build chain replaced (Webpack 4 / Babel 7 → tsup / esbuild).
72
+ - Package manager pinned to pnpm via `packageManager`.
73
+ - `engines.node` bumped to `>= 20`.
74
+
75
+ ### Removed
76
+
77
+ - UMD bundle (`dist/isIncognito.js`). Use the ESM build via a CDN for direct
78
+ `<script>` usage.
79
+ - The `get-browser` runtime dependency (zero deps now).
80
+
81
+ ---
82
+
83
+ ## [1.1.0] — 2019
84
+
85
+ - Update Firefox detection to fire callback correctly.
86
+ - Update `get-browser` to correctly identify Chromium.
87
+
88
+ ## [1.0.0] — 2019
89
+
90
+ - Initial public release.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019 Aliaksandr Yankouski
3
+ Copyright (c) 2019-2026 Aliaksandr Yankouski
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,75 +1,335 @@
1
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/yankouskia/is-incognito-mode/pulls) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/yankouskia/is-incognito-mode/blob/master/LICENSE)
1
+ <div align="center">
2
2
 
3
- [![NPM](https://nodei.co/npm/is-incognito-mode.png?downloads=true)](https://www.npmjs.com/package/is-incognito-mode)
3
+ # `is-incognito-mode`
4
4
 
5
- # is-incognito-mode
5
+ ### **Detect private / incognito browsing in 4 lines of code.**
6
6
 
7
- 👤Function to identify whether browser is in incognito mode 👀
7
+ _Zero dependencies fully typed dual ESM + CJS — ~1 kB gzipped._
8
8
 
9
- ## How to use
9
+ [![npm version](https://img.shields.io/npm/v/is-incognito-mode.svg?logo=npm&color=cb3837)](https://www.npmjs.com/package/is-incognito-mode)
10
+ [![npm downloads](https://img.shields.io/npm/dm/is-incognito-mode.svg?color=cb3837)](https://www.npmjs.com/package/is-incognito-mode)
11
+ [![CI](https://github.com/yankouskia/is-incognito-mode/actions/workflows/ci.yml/badge.svg)](https://github.com/yankouskia/is-incognito-mode/actions/workflows/ci.yml)
12
+ [![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen?logo=vitest)](./coverage)
13
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/is-incognito-mode?label=min%2Bgzip&color=44cc11)](https://bundlephobia.com/package/is-incognito-mode)
14
+ [![Types](https://img.shields.io/npm/types/is-incognito-mode.svg?logo=typescript&color=3178c6)](./src/index.ts)
15
+ [![License](https://img.shields.io/npm/l/is-incognito-mode.svg?color=blue)](./LICENSE)
10
16
 
11
- To install library:
17
+ <br />
18
+
19
+ ### **[Try the live demo →](https://yankouskia.github.io/is-incognito-mode/demo/)**
20
+
21
+ _Open it in a normal window. Then re-open it in private/incognito mode. Watch the verdict flip._
22
+
23
+ <br />
24
+
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## 30-second tour
12
30
 
13
31
  ```sh
14
- # yarn
15
- yarn add is-incognito-mode
32
+ npm install is-incognito-mode
33
+ ```
34
+
35
+ ```ts
36
+ import { isIncognito } from 'is-incognito-mode';
16
37
 
17
- # npm
18
- npm install is-incognito-mode --save
38
+ if (await isIncognito()) {
39
+ showPaywall();
40
+ } else {
41
+ trackVisit();
42
+ }
19
43
  ```
20
44
 
21
- ```js
22
- // ES6 modules
23
- import isIncognito from 'is-incognito-mode';
24
-
25
- // CommonJS modules
26
- const isIncognito = require('is-incognito-mode').default;
27
-
28
- /*
29
- Function returns Promise, which could be:
30
- - resolved with true, if Incognito mode is opened
31
- - resolved with false, if regular window is opened
32
- - rejected if no possibility to identify
33
- */
34
- isIncognito()
35
- .then(isPrivate => {
36
- if (isPrivate) {
37
- alert('There is no porn! Why are you using Incognito mode?');
38
- } else {
39
- console.log('Incognito mode is NOT activated')
45
+ That's it. **One async call, one boolean.** Works on Chrome, Firefox, Safari,
46
+ Edge, and (best-effort) the long tail of older WebKit shells.
47
+
48
+ Need more than a yes/no? Use [`detectIncognito()`](#rich-detection-result) for
49
+ a typed object with `browser`, `confidence`, `quota`, and `strategy` fields.
50
+
51
+ ---
52
+
53
+ ## Why you'd use this
54
+
55
+ Browsers don't expose a "private mode" API on purpose — but private windows
56
+ still leak the fact through **resource limits** and **storage shape**.
57
+ `is-incognito-mode` packages the current state-of-the-art detection (quota
58
+ probing via `navigator.storage.estimate()`) as a tiny, typed, zero-dep module,
59
+ so you can stop hand-rolling heuristics that browsers patched out in 2019.
60
+
61
+ A few real-world fits:
62
+
63
+ | Scenario | What you do |
64
+ | ------------------------- | ----------------------------------------------------------- |
65
+ | **Soft paywall** | Discourage incognito bypass without hard-blocking the user. |
66
+ | **Respectful analytics** | Skip beacon calls in private sessions to honor the signal. |
67
+ | **Long forms / surveys** | Warn before storing state that will vanish on close. |
68
+ | **Fraud / abuse signals** | One input among many — never the sole decider. |
69
+ | **E2E test conditioning** | Branch tests based on whether you're driving a private tab. |
70
+
71
+ ---
72
+
73
+ ## Install
74
+
75
+ ```sh
76
+ pnpm add is-incognito-mode # or npm i is-incognito-mode
77
+ # or yarn add is-incognito-mode
78
+ # or bun add is-incognito-mode
79
+ ```
80
+
81
+ **No-install — straight from a CDN**:
82
+
83
+ ```html
84
+ <script type="module">
85
+ import { isIncognito } from 'https://esm.sh/is-incognito-mode@2';
86
+ console.log(await isIncognito());
87
+ </script>
88
+ ```
89
+
90
+ ---
91
+
92
+ ## See it run
93
+
94
+ A ready-to-run demo page is hosted alongside the docs:
95
+
96
+ > **https://yankouskia.github.io/is-incognito-mode/demo/**
97
+
98
+ Open it once in a regular window, then once in incognito/private — the
99
+ verdict, browser, confidence, quota, and strategy update live.
100
+ Source: [`examples/browser/index.html`](./examples/browser/index.html) (single
101
+ static file, no build step).
102
+
103
+ ---
104
+
105
+ ## How it decides (under the hood)
106
+
107
+ The library tries the cleanest signal first and falls back to engine-specific
108
+ probes for older browsers. Click to expand:
109
+
110
+ <details>
111
+ <summary>Detection flow diagram</summary>
112
+
113
+ ```mermaid
114
+ flowchart TD
115
+ A[detectIncognito] --> B{navigator.storage<br/>.estimate available?}
116
+ B -- yes --> C{quota &lt; 120 MiB?}
117
+ C -- yes --> R1([private — high confidence])
118
+ C -- no --> R2([normal — high confidence])
119
+ B -- no --> D{which browser?}
120
+ D -- Safari/WebKit --> E[localStorage probe<br/>+ openDatabase probe]
121
+ D -- Firefox --> F[indexedDB.open error path]
122
+ D -- Edge legacy / IE --> G[PointerEvent + window.indexedDB heuristic]
123
+ D -- unknown --> X([throw UNSUPPORTED_BROWSER])
124
+ E --> R3([private/normal — medium])
125
+ F --> R4([private/normal — low])
126
+ G --> R5([private/normal — low])
127
+ ```
128
+
129
+ </details>
130
+
131
+ The default threshold is **120 MiB** — Chromium gives incognito tabs ~10 % of
132
+ disk capped at 120 MiB, and Firefox / Safari private modes cap similarly.
133
+ Devices with very small total storage can hit false positives; raise the
134
+ threshold or lower-bound against `navigator.deviceMemory * 1 GiB` if that
135
+ matters to you.
136
+
137
+ ---
138
+
139
+ ## Usage
140
+
141
+ ### Boolean verdict
142
+
143
+ ```ts
144
+ import { isIncognito } from 'is-incognito-mode';
145
+
146
+ const inPrivate = await isIncognito();
147
+ ```
148
+
149
+ ### Rich detection result
150
+
151
+ ```ts
152
+ import { detectIncognito } from 'is-incognito-mode';
153
+
154
+ const { isPrivate, browser, confidence, quota, strategy } =
155
+ await detectIncognito();
156
+
157
+ console.log(
158
+ `${browser} (${confidence}) — strategy: ${strategy}, quota: ${quota}`,
159
+ );
160
+ // → "chromium (high) — strategy: storage-quota, quota: 33554432"
161
+ ```
162
+
163
+ Fields on `DetectionResult`:
164
+
165
+ | field | type | notes |
166
+ | ------------ | ----------------------------- | ----------------------------------------------------------------------------------------- |
167
+ | `isPrivate` | `boolean` | Final verdict. |
168
+ | `browser` | `BrowserName` | Coarse engine: `chromium`, `firefox`, `safari`, `webkit`, `edge-legacy`, `ie`, `unknown`. |
169
+ | `confidence` | `'high' \| 'medium' \| 'low'` | `high` for direct quota signal; `low` for legacy heuristics. |
170
+ | `quota` | `number \| null` | Total storage quota in bytes, when `storage.estimate()` was available. |
171
+ | `strategy` | `DetectionStrategyName` | Which probe produced the verdict. |
172
+
173
+ ### Tuning the quota threshold
174
+
175
+ ```ts
176
+ import {
177
+ detectIncognito,
178
+ DEFAULT_PRIVATE_QUOTA_BYTES,
179
+ } from 'is-incognito-mode';
180
+
181
+ const result = await detectIncognito({
182
+ privateQuotaThresholdBytes: DEFAULT_PRIVATE_QUOTA_BYTES * 2,
183
+ });
184
+ ```
185
+
186
+ Default is **120 MiB**. Raise it if you see false positives on small-disk
187
+ devices.
188
+
189
+ ### Injecting globals (for testing)
190
+
191
+ `detectIncognito` accepts a `globals` override so unit tests don't have to
192
+ monkey-patch `navigator` or `window`:
193
+
194
+ ```ts
195
+ import { detectIncognito } from 'is-incognito-mode';
196
+
197
+ const result = await detectIncognito({
198
+ globals: {
199
+ navigator: {
200
+ userAgent: 'Mozilla/5.0 ... Chrome/131.0',
201
+ storage: {
202
+ estimate: () => Promise.resolve({ quota: 32 * 1024 * 1024 }),
203
+ },
204
+ },
205
+ window: {},
206
+ },
207
+ });
208
+ // result.isPrivate === true
209
+ ```
210
+
211
+ ### Error handling
212
+
213
+ ```ts
214
+ import { isIncognito, IncognitoDetectionError } from 'is-incognito-mode';
215
+
216
+ try {
217
+ const incognito = await isIncognito();
218
+ // ...
219
+ } catch (error) {
220
+ if (error instanceof IncognitoDetectionError) {
221
+ switch (error.code) {
222
+ case 'NOT_A_BROWSER':
223
+ // Server-side render path
224
+ break;
225
+ case 'UNSUPPORTED_BROWSER':
226
+ // Probably a bot / curl / node-fetch
227
+ break;
228
+ case 'PROBE_FAILED':
229
+ // Storage API rejected, no fallback applied
230
+ break;
40
231
  }
41
- })
42
- .catch(e => {
43
- console.log(e.message);
44
- })
232
+ }
233
+ }
234
+ ```
235
+
236
+ ### CommonJS
237
+
238
+ ```js
239
+ const { isIncognito } = require('is-incognito-mode');
240
+
241
+ // Default-import-style:
242
+ const detect = require('is-incognito-mode').default;
45
243
  ```
46
244
 
245
+ ---
246
+
247
+ ## API at a glance
248
+
249
+ | Export | Kind | Description |
250
+ | ------------------------------- | -------- | ------------------------------------------------------------------------------------ |
251
+ | `isIncognito(options?)` | function | Resolves to `boolean`. |
252
+ | `detectIncognito(options?)` | function | Resolves to a rich `DetectionResult`. |
253
+ | `IncognitoDetectionError` | class | Typed error with `code: 'NOT_A_BROWSER' \| 'UNSUPPORTED_BROWSER' \| 'PROBE_FAILED'`. |
254
+ | `DEFAULT_PRIVATE_QUOTA_BYTES` | const | Default threshold (`120 × 1024 × 1024`). |
255
+ | `BrowserName` (type) | type | Coarse engine name. |
256
+ | `DetectionResult` (type) | type | Rich result shape — see "Usage". |
257
+ | `DetectionConfidence` (type) | type | `'high' \| 'medium' \| 'low'`. |
258
+ | `DetectionStrategyName` (type) | type | Strategy identifier. |
259
+ | `DetectIncognitoOptions` (type) | type | Options bag. |
47
260
 
48
- ## Demo
261
+ Full generated reference: **<https://yankouskia.github.io/is-incognito-mode/>**
49
262
 
50
- [DEMO can be found here](https://yankouskia.github.io/is-incognito-mode/example/index.html)
263
+ ---
51
264
 
265
+ ## Compatibility
52
266
 
53
- Incognito Window | Regular Window
54
- :-------------------------:|:-------------------------:
55
- <img src="./resources/private.png" data-canonical-src="./resources/private.png" width="300" /> | <img src="./resources/public.png" data-canonical-src="./resources/public.png" width="300" />
267
+ ### Browsers
56
268
 
269
+ | Engine | Detection strategy | Confidence |
270
+ | ---------------------- | ------------------------------- | ---------- |
271
+ | Chromium ≥ 80 | `navigator.storage.estimate` | high |
272
+ | Firefox ≥ 75 | `navigator.storage.estimate` | high |
273
+ | Safari ≥ 13 | `navigator.storage.estimate` | high |
274
+ | Older Safari / WebKit | `localStorage` + `openDatabase` | medium-low |
275
+ | Older Firefox | `indexedDB.open` error path | low |
276
+ | Edge (legacy) | `PointerEvent` heuristic | low |
277
+ | IE 10–11 | `PointerEvent` heuristic | low |
278
+ | All others (`unknown`) | throws `UNSUPPORTED_BROWSER` | — |
57
279
 
58
- ## API
280
+ ### Node / runtimes
59
281
 
60
- `isIncognito: Promise<boolean>`
282
+ Not supported at runtime — this is a **browser-only** package and will throw
283
+ `NOT_A_BROWSER` if invoked without a `navigator`. The package _builds_ on
284
+ Node ≥ 20.
61
285
 
62
- Result `Promise` is
63
- - resolved with `true`, if Incognito mode is opened.
64
- - resolved with `false`, if regular window is opened
65
- - rejected if no possibility to identify
286
+ ### Bundlers & frameworks
66
287
 
288
+ Ships ESM and CJS with proper `exports` map and `.d.ts` / `.d.cts`. Works
289
+ out-of-the-box in Vite, Next.js (client components), Remix, Astro, Webpack,
290
+ Rollup, esbuild, Bun, and Deno.
291
+
292
+ ---
293
+
294
+ ## What's new in v2
295
+
296
+ | | v1.x | v2.0 |
297
+ | ------------------- | -------------------------------------------------------- | ------------------------------------------------------------ |
298
+ | Detection technique | FileSystem API + IndexedDB + localStorage + PointerEvent | `navigator.storage.estimate()` quota (with legacy fallbacks) |
299
+ | TypeScript | shipped JS only | strict TypeScript source, full `.d.ts` |
300
+ | Module formats | UMD + CJS | ESM + CJS dual publish |
301
+ | Dependencies | `get-browser` | **zero** |
302
+ | Bundle size | ~3 kB min+gzip | **~1 kB min+gzip** |
303
+ | Engines | Node ≥ 8 | Node ≥ 20 |
304
+ | Error model | `throw 'string'` | `IncognitoDetectionError` with `code` |
305
+
306
+ See [`BREAKING_CHANGES.md`](./BREAKING_CHANGES.md) for migration recipes
307
+ and [`DECISIONS.md`](./DECISIONS.md) for the reasoning behind each big call.
308
+
309
+ ---
310
+
311
+ ## Comparison with alternatives
312
+
313
+ - **[`detectincognitojs`](https://github.com/Joe12387/detectIncognito)** —
314
+ excellent, similar in spirit. Pick that if you want a UMD bundle or a richer
315
+ per-browser breakdown.
316
+ - **Inline UA sniff + `try/catch` around `localStorage`** — broken in every
317
+ modern browser. Don't.
318
+ - **Just check `window.webkitRequestFileSystem`** — patched out of Chrome 76.
319
+ Don't.
320
+
321
+ ---
67
322
 
68
323
  ## Contributing
69
324
 
70
- `is-incognito-mode` is open-source library, opened for contributions
325
+ Pull requests welcome. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the dev
326
+ loop, conventional commits, and the changeset workflow. Be excellent — the
327
+ [Contributor Covenant 2.1](./CODE_OF_CONDUCT.md) applies.
328
+
329
+ ## Security
71
330
 
331
+ Report vulnerabilities privately per [`SECURITY.md`](./SECURITY.md).
72
332
 
73
- ### License
333
+ ## License
74
334
 
75
- `is-incognito-mode` is [MIT licensed](https://github.com/yankouskia/is-incognito-mode/blob/master/LICENSE)
335
+ [MIT](./LICENSE) © Aliaksandr Yankouski