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 +90 -0
- package/LICENSE +1 -1
- package/README.md +307 -47
- package/dist/index.cjs +247 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +239 -0
- package/dist/index.js.map +1 -0
- package/package.json +137 -14
- package/dist/isIncognito.js +0 -1
- package/index.js +0 -55
- package/webpack.config.js +0 -32
- package/yarn.lock +0 -3709
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
package/README.md
CHANGED
|
@@ -1,75 +1,335 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# `is-incognito-mode`
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### **Detect private / incognito browsing in 4 lines of code.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
_Zero dependencies — fully typed — dual ESM + CJS — ~1 kB gzipped._
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/is-incognito-mode)
|
|
10
|
+
[](https://www.npmjs.com/package/is-incognito-mode)
|
|
11
|
+
[](https://github.com/yankouskia/is-incognito-mode/actions/workflows/ci.yml)
|
|
12
|
+
[](./coverage)
|
|
13
|
+
[](https://bundlephobia.com/package/is-incognito-mode)
|
|
14
|
+
[](./src/index.ts)
|
|
15
|
+
[](./LICENSE)
|
|
10
16
|
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
32
|
+
npm install is-incognito-mode
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { isIncognito } from 'is-incognito-mode';
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
if (await isIncognito()) {
|
|
39
|
+
showPaywall();
|
|
40
|
+
} else {
|
|
41
|
+
trackVisit();
|
|
42
|
+
}
|
|
19
43
|
```
|
|
20
44
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 < 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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
261
|
+
Full generated reference: **<https://yankouskia.github.io/is-incognito-mode/>**
|
|
49
262
|
|
|
50
|
-
|
|
263
|
+
---
|
|
51
264
|
|
|
265
|
+
## Compatibility
|
|
52
266
|
|
|
53
|
-
|
|
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
|
-
|
|
280
|
+
### Node / runtimes
|
|
59
281
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
+
## License
|
|
74
334
|
|
|
75
|
-
|
|
335
|
+
[MIT](./LICENSE) © Aliaksandr Yankouski
|