is-incognito-mode 2.0.1 → 2.1.0
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 +65 -0
- package/README.md +82 -57
- package/dist/index.cjs +101 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -19
- package/dist/index.d.ts +61 -19
- package/dist/index.js +101 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Robust per-engine detection — fixes Chrome incognito false negatives for good.
|
|
8
|
+
|
|
9
|
+
**The problem.** Chrome now deliberately fakes `navigator.storage.estimate().quota`
|
|
10
|
+
at `usage + 10 GiB` in _every_ mode, specifically to defeat incognito detection.
|
|
11
|
+
Any threshold on that value — 120 MiB, 1 GiB, anything — is therefore unreliable
|
|
12
|
+
on modern Chrome.
|
|
13
|
+
|
|
14
|
+
**The fix.** Detection is now per-engine, using the signal that actually still
|
|
15
|
+
leaks for each:
|
|
16
|
+
- **Chromium** — reads the legacy `navigator.webkitTemporaryStorage` quota
|
|
17
|
+
(which is _not_ faked) and compares it to `performance.memory.jsHeapSizeLimit`.
|
|
18
|
+
An incognito quota is memory-bound and stays below ~2× the heap limit; a
|
|
19
|
+
normal quota is disk-bound and far above it. The test is device-relative, so
|
|
20
|
+
there is no brittle fixed byte count.
|
|
21
|
+
- **Firefox & Safari** — probes the Origin Private File System via
|
|
22
|
+
`navigator.storage.getDirectory()`, which is rejected in private mode
|
|
23
|
+
(Firefox: a security error; Safari: "unknown transient reason").
|
|
24
|
+
- **Legacy Edge / IE** — unchanged `PointerEvent` + `indexedDB` heuristic.
|
|
25
|
+
- Legacy `localStorage` / `indexedDB.open` probes are retained as fallbacks for
|
|
26
|
+
older Safari and Firefox without OPFS.
|
|
27
|
+
|
|
28
|
+
**API changes (all backward compatible at runtime):**
|
|
29
|
+
- `isIncognito()`, `detectIncognito()`, `DetectionResult`, and
|
|
30
|
+
`IncognitoDetectionError` are unchanged.
|
|
31
|
+
- `DetectionResult.strategy` values changed: new `chromium-quota` and
|
|
32
|
+
`opfs-probe`; `storage-quota` removed. The `strategy` field is informational
|
|
33
|
+
(debugging / analytics); switch on it accordingly.
|
|
34
|
+
- New exported types: `PerformanceLike`, `DeprecatedStorageQuota`.
|
|
35
|
+
- `NavigatorLike` / `WindowLike` / `StorageManagerLike` gained optional fields
|
|
36
|
+
(`webkitTemporaryStorage`, `performance`, `getDirectory`).
|
|
37
|
+
- `DEFAULT_PRIVATE_QUOTA_BYTES` (1 GiB) is retained as the fallback heap limit
|
|
38
|
+
used when `performance.memory` is unavailable.
|
|
39
|
+
- `privateQuotaThresholdBytes`, when set, still forces an absolute byte cutoff
|
|
40
|
+
for the Chromium strategy.
|
|
41
|
+
|
|
42
|
+
Reference: this mirrors the techniques in the actively-maintained
|
|
43
|
+
`detectIncognito` library (v1.6.2).
|
|
44
|
+
|
|
45
|
+
## 2.0.2
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- Fix: Chrome incognito false negative — raise default quota threshold from 120 MiB to 1 GiB.
|
|
50
|
+
|
|
51
|
+
Chrome 110+ raised the per-tab incognito storage ceiling: a private tab on a
|
|
52
|
+
desktop commonly reports `navigator.storage.estimate().quota` in the
|
|
53
|
+
500 MiB–1 GiB range. With the previous 120 MiB cutoff, every modern Chromium
|
|
54
|
+
incognito session was misclassified as normal.
|
|
55
|
+
|
|
56
|
+
`DEFAULT_PRIVATE_QUOTA_BYTES` is now `1024 × 1024 × 1024` (1 GiB), matching the
|
|
57
|
+
threshold used by the actively-maintained `detectIncognito` library and
|
|
58
|
+
covering Chromium, Firefox, and Safari private modes. Normal-mode quotas on
|
|
59
|
+
modern desktops are tens-to-hundreds of GiB so the safety margin is large.
|
|
60
|
+
|
|
61
|
+
A regression test pins the behaviour at a 800 MiB quota (the heart of the
|
|
62
|
+
modern Chromium incognito band).
|
|
63
|
+
|
|
64
|
+
If you previously depended on the 120 MiB cutoff, pass
|
|
65
|
+
`privateQuotaThresholdBytes: 120 * 1024 * 1024` to `detectIncognito()` to
|
|
66
|
+
restore the old behaviour.
|
|
67
|
+
|
|
3
68
|
## 2.0.1
|
|
4
69
|
|
|
5
70
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -54,9 +54,9 @@ a typed object with `browser`, `confidence`, `quota`, and `strategy` fields.
|
|
|
54
54
|
|
|
55
55
|
Browsers don't expose a "private mode" API on purpose — but private windows
|
|
56
56
|
still leak the fact through **resource limits** and **storage shape**.
|
|
57
|
-
`is-incognito-mode` packages the current state-of-the-art detection
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
`is-incognito-mode` packages the current state-of-the-art per-engine detection
|
|
58
|
+
as a tiny, typed, zero-dep module, so you can stop hand-rolling heuristics that
|
|
59
|
+
browsers patched out years ago.
|
|
60
60
|
|
|
61
61
|
A few real-world fits:
|
|
62
62
|
|
|
@@ -104,35 +104,51 @@ static file, no build step).
|
|
|
104
104
|
|
|
105
105
|
## How it decides (under the hood)
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
There is no single cross-browser signal — each engine leaks private mode in a
|
|
108
|
+
different place — so the library picks the right probe per engine.
|
|
109
109
|
|
|
110
110
|
<details>
|
|
111
111
|
<summary>Detection flow diagram</summary>
|
|
112
112
|
|
|
113
113
|
```mermaid
|
|
114
114
|
flowchart TD
|
|
115
|
-
A[detectIncognito] -->
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
D --
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
A[detectIncognito] --> D{which engine?}
|
|
116
|
+
D -- Chromium --> C1[webkitTemporaryStorage quota]
|
|
117
|
+
C1 --> C2{quota < 2 × jsHeapSizeLimit?}
|
|
118
|
+
C2 -- yes --> R1([private — high confidence])
|
|
119
|
+
C2 -- no --> R2([normal — high confidence])
|
|
120
|
+
D -- Firefox --> F1["navigator.storage.getDirectory() — OPFS"]
|
|
121
|
+
F1 --> F2{rejected with a security error?}
|
|
122
|
+
F2 -- yes --> R3([private — high confidence])
|
|
123
|
+
F2 -- no --> R4([normal — high confidence])
|
|
124
|
+
D -- Safari/WebKit --> S1["navigator.storage.getDirectory() — OPFS"]
|
|
125
|
+
S1 --> S2{rejected 'unknown transient reason'?}
|
|
126
|
+
S2 -- yes --> R5([private])
|
|
127
|
+
S2 -- no --> R6([normal])
|
|
128
|
+
D -- Edge legacy / IE --> G[PointerEvent + indexedDB heuristic]
|
|
123
129
|
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
130
|
```
|
|
128
131
|
|
|
129
132
|
</details>
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
**Chromium (Chrome, Edge, Brave, Opera, …).** Chrome deliberately fakes
|
|
135
|
+
`navigator.storage.estimate().quota` at `usage + 10 GiB` in _every_ mode
|
|
136
|
+
specifically to defeat detection — so that API is useless. The library instead
|
|
137
|
+
reads the **legacy `navigator.webkitTemporaryStorage`** quota, which still
|
|
138
|
+
reports the real per-origin limit: disk-bound (huge) in a normal tab,
|
|
139
|
+
memory-bound (small) in incognito. It compares that to
|
|
140
|
+
`performance.memory.jsHeapSizeLimit` — a per-renderer constant — so the test
|
|
141
|
+
adapts to the device instead of relying on a brittle fixed byte count.
|
|
142
|
+
|
|
143
|
+
**Firefox & Safari.** The **Origin Private File System**
|
|
144
|
+
(`navigator.storage.getDirectory()`) is rejected in private mode — Firefox
|
|
145
|
+
throws a security error, Safari throws "unknown transient reason". A clean
|
|
146
|
+
resolve means a normal window.
|
|
147
|
+
|
|
148
|
+
**Legacy Edge / IE.** No `indexedDB` while `PointerEvent` exists → private.
|
|
149
|
+
|
|
150
|
+
If you need to override the Chromium heuristic with an absolute byte cutoff,
|
|
151
|
+
pass `privateQuotaThresholdBytes` (see [Tuning](#tuning-the-detection)).
|
|
136
152
|
|
|
137
153
|
---
|
|
138
154
|
|
|
@@ -157,7 +173,7 @@ const { isPrivate, browser, confidence, quota, strategy } =
|
|
|
157
173
|
console.log(
|
|
158
174
|
`${browser} (${confidence}) — strategy: ${strategy}, quota: ${quota}`,
|
|
159
175
|
);
|
|
160
|
-
// → "chromium (high) — strategy:
|
|
176
|
+
// → "chromium (high) — strategy: chromium-quota, quota: 629145600"
|
|
161
177
|
```
|
|
162
178
|
|
|
163
179
|
Fields on `DetectionResult`:
|
|
@@ -166,25 +182,29 @@ Fields on `DetectionResult`:
|
|
|
166
182
|
| ------------ | ----------------------------- | ----------------------------------------------------------------------------------------- |
|
|
167
183
|
| `isPrivate` | `boolean` | Final verdict. |
|
|
168
184
|
| `browser` | `BrowserName` | Coarse engine: `chromium`, `firefox`, `safari`, `webkit`, `edge-legacy`, `ie`, `unknown`. |
|
|
169
|
-
| `confidence` | `'high' \| 'medium' \| 'low'` | `high` for
|
|
170
|
-
| `quota` | `number \| null` |
|
|
171
|
-
| `strategy` | `DetectionStrategyName` | Which probe produced the verdict.
|
|
185
|
+
| `confidence` | `'high' \| 'medium' \| 'low'` | `high` for the primary per-engine probe; `low` for legacy heuristics. |
|
|
186
|
+
| `quota` | `number \| null` | Temporary-storage quota in bytes (Chromium); `null` for the OPFS and legacy strategies. |
|
|
187
|
+
| `strategy` | `DetectionStrategyName` | Which probe produced the verdict — see [How it decides](#how-it-decides-under-the-hood). |
|
|
172
188
|
|
|
173
|
-
### Tuning the
|
|
189
|
+
### Tuning the detection
|
|
190
|
+
|
|
191
|
+
The Chromium strategy is **self-calibrating by default** — it compares the
|
|
192
|
+
temporary-storage quota to the device's JS heap limit, so you normally do not
|
|
193
|
+
need to configure anything. For an absolute byte cutoff instead, pass
|
|
194
|
+
`privateQuotaThresholdBytes`:
|
|
174
195
|
|
|
175
196
|
```ts
|
|
176
|
-
import {
|
|
177
|
-
detectIncognito,
|
|
178
|
-
DEFAULT_PRIVATE_QUOTA_BYTES,
|
|
179
|
-
} from 'is-incognito-mode';
|
|
197
|
+
import { detectIncognito } from 'is-incognito-mode';
|
|
180
198
|
|
|
199
|
+
// Classify a Chromium tab as private if its temporary-storage quota is
|
|
200
|
+
// below 2 GiB, instead of the default heap-relative heuristic.
|
|
181
201
|
const result = await detectIncognito({
|
|
182
|
-
privateQuotaThresholdBytes:
|
|
202
|
+
privateQuotaThresholdBytes: 2 * 1024 * 1024 * 1024,
|
|
183
203
|
});
|
|
184
204
|
```
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
|
|
206
|
+
`DEFAULT_PRIVATE_QUOTA_BYTES` (1 GiB) is exported as a reference value — it is
|
|
207
|
+
the fallback heap limit used when `performance.memory` is unavailable.
|
|
188
208
|
|
|
189
209
|
### Injecting globals (for testing)
|
|
190
210
|
|
|
@@ -198,14 +218,19 @@ const result = await detectIncognito({
|
|
|
198
218
|
globals: {
|
|
199
219
|
navigator: {
|
|
200
220
|
userAgent: 'Mozilla/5.0 ... Chrome/131.0',
|
|
201
|
-
|
|
202
|
-
|
|
221
|
+
// Legacy quota API: a small, memory-bound quota — i.e. an incognito tab.
|
|
222
|
+
webkitTemporaryStorage: {
|
|
223
|
+
queryUsageAndQuota: (onSuccess) => {
|
|
224
|
+
onSuccess(0, 600 * 1024 * 1024); // 600 MiB granted
|
|
225
|
+
},
|
|
203
226
|
},
|
|
204
227
|
},
|
|
205
|
-
window: {
|
|
228
|
+
window: {
|
|
229
|
+
performance: { memory: { jsHeapSizeLimit: 4 * 1024 * 1024 * 1024 } },
|
|
230
|
+
},
|
|
206
231
|
},
|
|
207
232
|
});
|
|
208
|
-
// result.isPrivate === true
|
|
233
|
+
// result.isPrivate === true (600 MiB < 2 × 4 GiB)
|
|
209
234
|
```
|
|
210
235
|
|
|
211
236
|
### Error handling
|
|
@@ -226,7 +251,7 @@ try {
|
|
|
226
251
|
// Probably a bot / curl / node-fetch
|
|
227
252
|
break;
|
|
228
253
|
case 'PROBE_FAILED':
|
|
229
|
-
//
|
|
254
|
+
// The engine's probe could not produce a verdict
|
|
230
255
|
break;
|
|
231
256
|
}
|
|
232
257
|
}
|
|
@@ -266,16 +291,16 @@ Full generated reference: **<https://yankouskia.github.io/is-incognito-mode/>**
|
|
|
266
291
|
|
|
267
292
|
### Browsers
|
|
268
293
|
|
|
269
|
-
| Engine | Detection strategy
|
|
270
|
-
| ---------------------- |
|
|
271
|
-
| Chromium ≥
|
|
272
|
-
| Firefox ≥
|
|
273
|
-
| Safari ≥
|
|
274
|
-
| Older Safari / WebKit | `localStorage` + `openDatabase` | medium-low |
|
|
275
|
-
| Older Firefox | `indexedDB.open` error path
|
|
276
|
-
| Edge (legacy) | `PointerEvent` heuristic
|
|
277
|
-
| IE 10–11 | `PointerEvent` heuristic
|
|
278
|
-
| All others (`unknown`) | throws `UNSUPPORTED_BROWSER`
|
|
294
|
+
| Engine | Detection strategy | Confidence |
|
|
295
|
+
| ---------------------- | -------------------------------------------- | ---------- |
|
|
296
|
+
| Chromium ≥ 76 | `webkitTemporaryStorage` quota vs heap limit | high |
|
|
297
|
+
| Firefox ≥ 111 | OPFS `navigator.storage.getDirectory()` | high |
|
|
298
|
+
| Safari ≥ 15.2 | OPFS `navigator.storage.getDirectory()` | high |
|
|
299
|
+
| Older Safari / WebKit | `localStorage` + `openDatabase` probes | medium-low |
|
|
300
|
+
| Older Firefox | `indexedDB.open` error path | low |
|
|
301
|
+
| Edge (legacy) | `PointerEvent` + `indexedDB` heuristic | low |
|
|
302
|
+
| IE 10–11 | `PointerEvent` + `indexedDB` heuristic | low |
|
|
303
|
+
| All others (`unknown`) | throws `UNSUPPORTED_BROWSER` | — |
|
|
279
304
|
|
|
280
305
|
### Node / runtimes
|
|
281
306
|
|
|
@@ -293,15 +318,15 @@ Rollup, esbuild, Bun, and Deno.
|
|
|
293
318
|
|
|
294
319
|
## What's new in v2
|
|
295
320
|
|
|
296
|
-
| | v1.x | v2.0
|
|
297
|
-
| ------------------- | -------------------------------------------------------- |
|
|
298
|
-
| Detection technique | FileSystem API + IndexedDB + localStorage + PointerEvent |
|
|
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`
|
|
321
|
+
| | v1.x | v2.0 |
|
|
322
|
+
| ------------------- | -------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
323
|
+
| Detection technique | FileSystem API + IndexedDB + localStorage + PointerEvent | per-engine probes: Chromium temp-storage quota, Firefox/Safari OPFS |
|
|
324
|
+
| TypeScript | shipped JS only | strict TypeScript source, full `.d.ts` |
|
|
325
|
+
| Module formats | UMD + CJS | ESM + CJS dual publish |
|
|
326
|
+
| Dependencies | `get-browser` | **zero** |
|
|
327
|
+
| Bundle size | ~3 kB min+gzip | **~1 kB min+gzip** |
|
|
328
|
+
| Engines | Node ≥ 8 | Node ≥ 20 |
|
|
329
|
+
| Error model | `throw 'string'` | `IncognitoDetectionError` with `code` |
|
|
305
330
|
|
|
306
331
|
See [`BREAKING_CHANGES.md`](./BREAKING_CHANGES.md) for migration recipes
|
|
307
332
|
and [`DECISIONS.md`](./DECISIONS.md) for the reasoning behind each big call.
|
package/dist/index.cjs
CHANGED
|
@@ -31,33 +31,53 @@ var IncognitoDetectionError = class extends Error {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
// src/strategies.ts
|
|
34
|
-
var DEFAULT_PRIVATE_QUOTA_BYTES =
|
|
34
|
+
var DEFAULT_PRIVATE_QUOTA_BYTES = 1024 * 1024 * 1024;
|
|
35
|
+
var HEAP_QUOTA_MULTIPLIER = 2;
|
|
35
36
|
var PROBE_KEY = "__is_incognito_mode_probe_cd1394e6__";
|
|
36
37
|
async function runStrategies(browser, globals, options) {
|
|
37
38
|
const errors = [];
|
|
38
|
-
if (canUseStorageEstimate(globals)) {
|
|
39
|
-
try {
|
|
40
|
-
const result = await detectViaStorageQuota(browser, globals, options);
|
|
41
|
-
if (result !== null) return result;
|
|
42
|
-
} catch (error) {
|
|
43
|
-
errors.push(error);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
39
|
switch (browser) {
|
|
47
|
-
case "
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
case "chromium": {
|
|
41
|
+
const result = await attempt(
|
|
42
|
+
() => detectViaChromiumQuota(browser, globals, options),
|
|
43
|
+
errors
|
|
44
|
+
);
|
|
50
45
|
if (result !== null) return result;
|
|
51
46
|
break;
|
|
52
47
|
}
|
|
53
48
|
case "firefox": {
|
|
54
|
-
const
|
|
55
|
-
|
|
49
|
+
const opfs = await attempt(
|
|
50
|
+
() => detectViaOpfsProbe(browser, globals, "Security error"),
|
|
51
|
+
errors
|
|
52
|
+
);
|
|
53
|
+
if (opfs !== null) return opfs;
|
|
54
|
+
const idb = await attempt(
|
|
55
|
+
() => detectViaFirefoxIndexedDB(browser, globals),
|
|
56
|
+
errors
|
|
57
|
+
);
|
|
58
|
+
if (idb !== null) return idb;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "safari":
|
|
62
|
+
case "webkit": {
|
|
63
|
+
const opfs = await attempt(
|
|
64
|
+
() => detectViaOpfsProbe(browser, globals, "unknown transient reason"),
|
|
65
|
+
errors
|
|
66
|
+
);
|
|
67
|
+
if (opfs !== null) return opfs;
|
|
68
|
+
const legacy = await attempt(
|
|
69
|
+
() => detectViaSafariStorage(browser, globals),
|
|
70
|
+
errors
|
|
71
|
+
);
|
|
72
|
+
if (legacy !== null) return legacy;
|
|
56
73
|
break;
|
|
57
74
|
}
|
|
58
75
|
case "edge-legacy":
|
|
59
76
|
case "ie": {
|
|
60
|
-
const result =
|
|
77
|
+
const result = await attempt(
|
|
78
|
+
() => detectViaLegacyEdge(browser, globals),
|
|
79
|
+
errors
|
|
80
|
+
);
|
|
61
81
|
if (result !== null) return result;
|
|
62
82
|
break;
|
|
63
83
|
}
|
|
@@ -68,24 +88,75 @@ async function runStrategies(browser, globals, options) {
|
|
|
68
88
|
errors.length > 0 ? { cause: errors[0] } : void 0
|
|
69
89
|
);
|
|
70
90
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
91
|
+
async function attempt(strategy, errors) {
|
|
92
|
+
try {
|
|
93
|
+
return await strategy();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
errors.push(error);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
73
98
|
}
|
|
74
|
-
async function
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
const
|
|
78
|
-
|
|
99
|
+
async function detectViaChromiumQuota(browser, globals, options) {
|
|
100
|
+
const tempStorage = globals.navigator?.webkitTemporaryStorage;
|
|
101
|
+
if (typeof tempStorage?.queryUsageAndQuota !== "function") return null;
|
|
102
|
+
const quota = await new Promise((resolve) => {
|
|
103
|
+
let settled = false;
|
|
104
|
+
const settle = (value) => {
|
|
105
|
+
if (!settled) {
|
|
106
|
+
settled = true;
|
|
107
|
+
resolve(value);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
tempStorage.queryUsageAndQuota(
|
|
112
|
+
(_used, grantedQuota) => {
|
|
113
|
+
settle(typeof grantedQuota === "number" ? grantedQuota : null);
|
|
114
|
+
},
|
|
115
|
+
() => {
|
|
116
|
+
settle(null);
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
} catch {
|
|
120
|
+
settle(null);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
79
123
|
if (quota === null) return null;
|
|
80
|
-
const isPrivate = quota < options.privateQuotaThresholdBytes;
|
|
124
|
+
const isPrivate = options.privateQuotaThresholdBytes === void 0 ? quota < chromiumHeapLimit(globals) * HEAP_QUOTA_MULTIPLIER : quota < options.privateQuotaThresholdBytes;
|
|
81
125
|
return {
|
|
82
126
|
isPrivate,
|
|
83
127
|
browser,
|
|
84
128
|
confidence: "high",
|
|
85
129
|
quota,
|
|
86
|
-
strategy: "
|
|
130
|
+
strategy: "chromium-quota"
|
|
87
131
|
};
|
|
88
132
|
}
|
|
133
|
+
function chromiumHeapLimit(globals) {
|
|
134
|
+
const limit = globals.window?.performance?.memory?.jsHeapSizeLimit;
|
|
135
|
+
return typeof limit === "number" && limit > 0 ? limit : DEFAULT_PRIVATE_QUOTA_BYTES;
|
|
136
|
+
}
|
|
137
|
+
async function detectViaOpfsProbe(browser, globals, privateErrorFragment) {
|
|
138
|
+
const storage = globals.navigator?.storage;
|
|
139
|
+
if (typeof storage?.getDirectory !== "function") return null;
|
|
140
|
+
try {
|
|
141
|
+
await storage.getDirectory();
|
|
142
|
+
return {
|
|
143
|
+
isPrivate: false,
|
|
144
|
+
browser,
|
|
145
|
+
confidence: "high",
|
|
146
|
+
quota: null,
|
|
147
|
+
strategy: "opfs-probe"
|
|
148
|
+
};
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
return {
|
|
152
|
+
isPrivate: message.includes(privateErrorFragment),
|
|
153
|
+
browser,
|
|
154
|
+
confidence: "high",
|
|
155
|
+
quota: null,
|
|
156
|
+
strategy: "opfs-probe"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
89
160
|
function detectViaSafariStorage(browser, globals) {
|
|
90
161
|
const storage = globals.window?.localStorage;
|
|
91
162
|
if (!storage) {
|
|
@@ -186,17 +257,8 @@ function detectViaLegacyEdge(browser, globals) {
|
|
|
186
257
|
if (!win) return null;
|
|
187
258
|
const hasIndexedDB = Boolean(win.indexedDB ?? globals.indexedDB);
|
|
188
259
|
const hasPointer = Boolean(win.PointerEvent ?? win.MSPointerEvent);
|
|
189
|
-
if (!hasIndexedDB && hasPointer) {
|
|
190
|
-
return {
|
|
191
|
-
isPrivate: true,
|
|
192
|
-
browser,
|
|
193
|
-
confidence: "low",
|
|
194
|
-
quota: null,
|
|
195
|
-
strategy: "legacy-edge"
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
260
|
return {
|
|
199
|
-
isPrivate:
|
|
261
|
+
isPrivate: !hasIndexedDB && hasPointer,
|
|
200
262
|
browser,
|
|
201
263
|
confidence: "low",
|
|
202
264
|
quota: null,
|
|
@@ -217,10 +279,11 @@ async function detectIncognito(options = {}) {
|
|
|
217
279
|
userAgent: globals.navigator.userAgent,
|
|
218
280
|
...globals.navigator.vendor === void 0 ? {} : { vendor: globals.navigator.vendor }
|
|
219
281
|
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
282
|
+
return runStrategies(
|
|
283
|
+
browser,
|
|
284
|
+
globals,
|
|
285
|
+
options.privateQuotaThresholdBytes === void 0 ? {} : { privateQuotaThresholdBytes: options.privateQuotaThresholdBytes }
|
|
286
|
+
);
|
|
224
287
|
}
|
|
225
288
|
async function isIncognito(options = {}) {
|
|
226
289
|
const result = await detectIncognito(options);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/browser.ts","../src/errors.ts","../src/strategies.ts","../src/detect.ts"],"names":["indexedDB"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Coarse browser-engine identifiers used to pick a detection strategy.\n *\n * We deliberately do not export brand names like \"Brave\" or \"Vivaldi\" — they\n * are all Chromium under the hood and the relevant private-mode behaviour\n * matches the engine, not the brand.\n */\nexport type BrowserName =\n | 'chromium'\n | 'firefox'\n | 'safari'\n | 'webkit'\n | 'edge-legacy'\n | 'ie'\n | 'unknown';\n\ninterface UserAgentSource {\n readonly userAgent: string;\n readonly vendor?: string;\n}\n\nconst CHROMIUM_HINTS = ['chrome/', 'crios/', 'chromium/', 'edg/', 'opr/'];\n\n/**\n * Classify the current navigator into a coarse browser engine.\n *\n * Pure function — accepts an optional `source` so it can be unit-tested\n * without polluting globals. In production we read from `navigator` directly.\n */\nexport function detectBrowser(source?: UserAgentSource): BrowserName {\n const ua = (source?.userAgent ?? '').toLowerCase();\n const vendor = (source?.vendor ?? '').toLowerCase();\n\n if (!ua) return 'unknown';\n\n if (ua.includes('msie ') || ua.includes('trident/')) return 'ie';\n if (ua.includes('edge/')) return 'edge-legacy';\n if (ua.includes('firefox/') || ua.includes('fxios/')) return 'firefox';\n\n const looksLikeChromium = CHROMIUM_HINTS.some((hint) => ua.includes(hint));\n if (looksLikeChromium) return 'chromium';\n\n // Safari sets `vendor === 'apple computer, inc.'`. iOS-Chrome/Firefox also\n // run on WebKit but advertise different UAs, so we reach this branch only\n // for \"real\" Safari and other WebKit shells.\n if (ua.includes('safari/') && (vendor.includes('apple') || !vendor)) {\n return 'safari';\n }\n if (ua.includes('applewebkit/')) return 'webkit';\n\n return 'unknown';\n}\n","/**\n * Stable identifiers for {@link IncognitoDetectionError} causes.\n *\n * Branching on `code` is safer than parsing `message` strings.\n */\nexport type IncognitoDetectionErrorCode =\n /** Invoked in a non-browser context (Node, worker without globals, etc.). */\n | 'NOT_A_BROWSER'\n /** Browser detected but no supported detection strategy applies. */\n | 'UNSUPPORTED_BROWSER'\n /** A probe threw before it could produce a definitive result. */\n | 'PROBE_FAILED';\n\n/**\n * Thrown when private-mode cannot be determined.\n *\n * @example\n * ```ts\n * import { isIncognito, IncognitoDetectionError } from 'is-incognito-mode';\n *\n * try {\n * await isIncognito();\n * } catch (error) {\n * if (error instanceof IncognitoDetectionError) {\n * console.warn(error.code, error.message);\n * }\n * }\n * ```\n */\nexport class IncognitoDetectionError extends Error {\n override readonly name = 'IncognitoDetectionError';\n readonly code: IncognitoDetectionErrorCode;\n\n constructor(\n code: IncognitoDetectionErrorCode,\n message: string,\n options?: { cause?: unknown },\n ) {\n super(message, options);\n this.code = code;\n }\n}\n","import type { BrowserName } from './browser.ts';\nimport { IncognitoDetectionError } from './errors.ts';\nimport type {\n DetectIncognitoOptions,\n DetectionGlobals,\n DetectionResult,\n} from './types.ts';\n\n/**\n * Default cutoff: 120 MB. Below this, every shipping browser we tested in 2024\n * is in a restricted (private) storage context. Above, it is normal.\n *\n * Sources: detectIncognito.js research notes; cross-checked against Chrome\n * 120+, Firefox 121+, Safari 17+.\n */\nexport const DEFAULT_PRIVATE_QUOTA_BYTES = 120 * 1024 * 1024;\n\n/**\n * Sentinel key used by the legacy localStorage probe. Random enough that\n * collisions with real user keys are impossible.\n */\nconst PROBE_KEY = '__is_incognito_mode_probe_cd1394e6__';\n\n/**\n * Try strategies in priority order, returning the first one that produces a\n * definitive answer. Throws {@link IncognitoDetectionError} if every strategy\n * declines.\n */\nexport async function runStrategies(\n browser: BrowserName,\n globals: DetectionGlobals,\n options: Required<Pick<DetectIncognitoOptions, 'privateQuotaThresholdBytes'>>,\n): Promise<DetectionResult> {\n const errors: unknown[] = [];\n\n // 1. navigator.storage.estimate — the modern, vendor-blessed-ish signal.\n if (canUseStorageEstimate(globals)) {\n try {\n const result = await detectViaStorageQuota(browser, globals, options);\n if (result !== null) return result;\n } catch (error) {\n errors.push(error);\n }\n }\n\n // 2. Per-engine fallbacks for older versions / restricted runtimes.\n switch (browser) {\n case 'safari':\n case 'webkit': {\n const result = detectViaSafariStorage(browser, globals);\n if (result !== null) return result;\n break;\n }\n case 'firefox': {\n const result = await detectViaFirefoxIndexedDB(browser, globals);\n if (result !== null) return result;\n break;\n }\n case 'edge-legacy':\n case 'ie': {\n const result = detectViaLegacyEdge(browser, globals);\n if (result !== null) return result;\n break;\n }\n case 'chromium':\n case 'unknown': {\n break;\n }\n }\n\n throw new IncognitoDetectionError(\n browser === 'unknown' ? 'UNSUPPORTED_BROWSER' : 'PROBE_FAILED',\n `No detection strategy produced a verdict for browser \"${browser}\".`,\n errors.length > 0 ? { cause: errors[0] } : undefined,\n );\n}\n\nfunction canUseStorageEstimate(globals: DetectionGlobals): boolean {\n return typeof globals.navigator?.storage?.estimate === 'function';\n}\n\nasync function detectViaStorageQuota(\n browser: BrowserName,\n globals: DetectionGlobals,\n options: Required<Pick<DetectIncognitoOptions, 'privateQuotaThresholdBytes'>>,\n): Promise<DetectionResult | null> {\n const storage = globals.navigator?.storage;\n if (!storage?.estimate) return null;\n\n const estimate = await storage.estimate();\n const quota = typeof estimate.quota === 'number' ? estimate.quota : null;\n if (quota === null) return null;\n\n const isPrivate = quota < options.privateQuotaThresholdBytes;\n return {\n isPrivate,\n browser,\n confidence: 'high',\n quota,\n strategy: 'storage-quota',\n };\n}\n\nfunction detectViaSafariStorage(\n browser: BrowserName,\n globals: DetectionGlobals,\n): DetectionResult | null {\n const storage = globals.window?.localStorage;\n if (!storage) {\n // No localStorage at all is itself a strong signal on Safari < 11 private.\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n\n try {\n storage.setItem(PROBE_KEY, '1');\n storage.removeItem(PROBE_KEY);\n } catch {\n return {\n isPrivate: true,\n browser,\n confidence: 'medium',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n\n const win = globals.window;\n if (typeof win.openDatabase === 'function') {\n try {\n win.openDatabase(null, null, null, null);\n } catch {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n }\n\n return {\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n}\n\nasync function detectViaFirefoxIndexedDB(\n browser: BrowserName,\n globals: DetectionGlobals,\n): Promise<DetectionResult | null> {\n const indexedDB = globals.indexedDB ?? globals.window?.indexedDB;\n if (!indexedDB) {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n };\n }\n\n return new Promise<DetectionResult>((resolve) => {\n let request: IDBOpenDBRequest;\n try {\n request = indexedDB.open(PROBE_KEY);\n } catch {\n resolve({\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n return;\n }\n\n request.addEventListener('error', () => {\n resolve({\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n });\n request.addEventListener('success', () => {\n try {\n request.result.close();\n indexedDB.deleteDatabase(PROBE_KEY);\n } catch {\n /* best-effort cleanup */\n }\n resolve({\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n });\n });\n}\n\nfunction detectViaLegacyEdge(\n browser: BrowserName,\n globals: DetectionGlobals,\n): DetectionResult | null {\n const win = globals.window;\n if (!win) return null;\n const hasIndexedDB = Boolean(win.indexedDB ?? globals.indexedDB);\n const hasPointer = Boolean(win.PointerEvent ?? win.MSPointerEvent);\n if (!hasIndexedDB && hasPointer) {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'legacy-edge',\n };\n }\n return {\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'legacy-edge',\n };\n}\n","import { detectBrowser } from './browser.ts';\nimport { IncognitoDetectionError } from './errors.ts';\nimport { DEFAULT_PRIVATE_QUOTA_BYTES, runStrategies } from './strategies.ts';\nimport type {\n DetectIncognitoOptions,\n DetectionGlobals,\n DetectionResult,\n} from './types.ts';\n\n/**\n * Detect whether the current browser is in private / incognito mode.\n *\n * Returns a rich {@link DetectionResult}. Most callers want the thinner\n * {@link isIncognito} convenience instead.\n *\n * @throws {IncognitoDetectionError}\n * - `NOT_A_BROWSER` when invoked outside a browser-like environment.\n * - `UNSUPPORTED_BROWSER` when the browser was classified as `unknown` and\n * no fallback strategy applied.\n * - `PROBE_FAILED` when every applicable strategy threw before producing a\n * definitive answer.\n *\n * @example\n * ```ts\n * import { detectIncognito } from 'is-incognito-mode';\n *\n * const { isPrivate, browser, confidence, quota } = await detectIncognito();\n * console.log(`${browser} (${confidence} confidence, quota: ${quota ?? '?'})`);\n * ```\n */\nexport async function detectIncognito(\n options: DetectIncognitoOptions = {},\n): Promise<DetectionResult> {\n const globals = resolveGlobals(options.globals);\n if (!isBrowserLike(globals)) {\n throw new IncognitoDetectionError(\n 'NOT_A_BROWSER',\n 'is-incognito-mode can only run in a browser-like environment.',\n );\n }\n\n const browser = detectBrowser({\n userAgent: globals.navigator.userAgent,\n ...(globals.navigator.vendor === undefined\n ? {}\n : { vendor: globals.navigator.vendor }),\n });\n\n const threshold =\n options.privateQuotaThresholdBytes ?? DEFAULT_PRIVATE_QUOTA_BYTES;\n\n return runStrategies(browser, globals, {\n privateQuotaThresholdBytes: threshold,\n });\n}\n\n/**\n * Convenience wrapper around {@link detectIncognito} that resolves to the\n * boolean verdict only. Drop-in compatible with v1's default export.\n *\n * @example\n * ```ts\n * import { isIncognito } from 'is-incognito-mode';\n *\n * if (await isIncognito()) {\n * showPaywall();\n * }\n * ```\n *\n * @throws {IncognitoDetectionError} See {@link detectIncognito}.\n */\nexport async function isIncognito(\n options: DetectIncognitoOptions = {},\n): Promise<boolean> {\n const result = await detectIncognito(options);\n return result.isPrivate;\n}\n\nfunction resolveGlobals(\n overrides: DetectionGlobals | undefined,\n): DetectionGlobals {\n if (overrides) return overrides;\n return {\n navigator: typeof navigator === 'undefined' ? undefined : navigator,\n window: typeof window === 'undefined' ? undefined : window,\n indexedDB: typeof indexedDB === 'undefined' ? undefined : indexedDB,\n };\n}\n\nfunction isBrowserLike(\n globals: DetectionGlobals,\n): globals is DetectionGlobals & {\n navigator: NonNullable<DetectionGlobals['navigator']>;\n} {\n return Boolean(globals.navigator?.userAgent);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/browser.ts","../src/errors.ts","../src/strategies.ts","../src/detect.ts"],"names":["indexedDB"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Coarse browser-engine identifiers used to pick a detection strategy.\n *\n * We deliberately do not export brand names like \"Brave\" or \"Vivaldi\" — they\n * are all Chromium under the hood and the relevant private-mode behaviour\n * matches the engine, not the brand.\n */\nexport type BrowserName =\n | 'chromium'\n | 'firefox'\n | 'safari'\n | 'webkit'\n | 'edge-legacy'\n | 'ie'\n | 'unknown';\n\ninterface UserAgentSource {\n readonly userAgent: string;\n readonly vendor?: string;\n}\n\nconst CHROMIUM_HINTS = ['chrome/', 'crios/', 'chromium/', 'edg/', 'opr/'];\n\n/**\n * Classify the current navigator into a coarse browser engine.\n *\n * Pure function — accepts an optional `source` so it can be unit-tested\n * without polluting globals. In production we read from `navigator` directly.\n */\nexport function detectBrowser(source?: UserAgentSource): BrowserName {\n const ua = (source?.userAgent ?? '').toLowerCase();\n const vendor = (source?.vendor ?? '').toLowerCase();\n\n if (!ua) return 'unknown';\n\n if (ua.includes('msie ') || ua.includes('trident/')) return 'ie';\n if (ua.includes('edge/')) return 'edge-legacy';\n if (ua.includes('firefox/') || ua.includes('fxios/')) return 'firefox';\n\n const looksLikeChromium = CHROMIUM_HINTS.some((hint) => ua.includes(hint));\n if (looksLikeChromium) return 'chromium';\n\n // Safari sets `vendor === 'apple computer, inc.'`. iOS-Chrome/Firefox also\n // run on WebKit but advertise different UAs, so we reach this branch only\n // for \"real\" Safari and other WebKit shells.\n if (ua.includes('safari/') && (vendor.includes('apple') || !vendor)) {\n return 'safari';\n }\n if (ua.includes('applewebkit/')) return 'webkit';\n\n return 'unknown';\n}\n","/**\n * Stable identifiers for {@link IncognitoDetectionError} causes.\n *\n * Branching on `code` is safer than parsing `message` strings.\n */\nexport type IncognitoDetectionErrorCode =\n /** Invoked in a non-browser context (Node, worker without globals, etc.). */\n | 'NOT_A_BROWSER'\n /** Browser detected but no supported detection strategy applies. */\n | 'UNSUPPORTED_BROWSER'\n /** A probe threw before it could produce a definitive result. */\n | 'PROBE_FAILED';\n\n/**\n * Thrown when private-mode cannot be determined.\n *\n * @example\n * ```ts\n * import { isIncognito, IncognitoDetectionError } from 'is-incognito-mode';\n *\n * try {\n * await isIncognito();\n * } catch (error) {\n * if (error instanceof IncognitoDetectionError) {\n * console.warn(error.code, error.message);\n * }\n * }\n * ```\n */\nexport class IncognitoDetectionError extends Error {\n override readonly name = 'IncognitoDetectionError';\n readonly code: IncognitoDetectionErrorCode;\n\n constructor(\n code: IncognitoDetectionErrorCode,\n message: string,\n options?: { cause?: unknown },\n ) {\n super(message, options);\n this.code = code;\n }\n}\n","import type { BrowserName } from './browser.ts';\nimport { IncognitoDetectionError } from './errors.ts';\nimport type {\n DetectIncognitoOptions,\n DetectionGlobals,\n DetectionResult,\n} from './types.ts';\n\n/**\n * Fallback JS-heap ceiling (1 GiB) used by the Chromium strategy when\n * `performance.memory.jsHeapSizeLimit` is unavailable. Also exported as the\n * reference value for the `privateQuotaThresholdBytes` advanced override.\n */\nexport const DEFAULT_PRIVATE_QUOTA_BYTES = 1024 * 1024 * 1024;\n\n/**\n * The Chromium temporary-storage quota in an incognito tab stays below roughly\n * twice the JS-heap ceiling (it is memory-bound, not disk-bound). Normal tabs\n * are disk-bound and far above it.\n */\nconst HEAP_QUOTA_MULTIPLIER = 2;\n\n/**\n * Sentinel key used by the legacy localStorage / indexedDB probes. Random\n * enough that collisions with real user keys are impossible.\n */\nconst PROBE_KEY = '__is_incognito_mode_probe_cd1394e6__';\n\n/**\n * Try strategies in priority order, returning the first one that produces a\n * definitive answer. Throws {@link IncognitoDetectionError} if every strategy\n * declines.\n */\nexport async function runStrategies(\n browser: BrowserName,\n globals: DetectionGlobals,\n options: Pick<DetectIncognitoOptions, 'privateQuotaThresholdBytes'>,\n): Promise<DetectionResult> {\n const errors: unknown[] = [];\n\n switch (browser) {\n case 'chromium': {\n const result = await attempt(\n () => detectViaChromiumQuota(browser, globals, options),\n errors,\n );\n if (result !== null) return result;\n break;\n }\n case 'firefox': {\n const opfs = await attempt(\n () => detectViaOpfsProbe(browser, globals, 'Security error'),\n errors,\n );\n if (opfs !== null) return opfs;\n const idb = await attempt(\n () => detectViaFirefoxIndexedDB(browser, globals),\n errors,\n );\n if (idb !== null) return idb;\n break;\n }\n case 'safari':\n case 'webkit': {\n const opfs = await attempt(\n () => detectViaOpfsProbe(browser, globals, 'unknown transient reason'),\n errors,\n );\n if (opfs !== null) return opfs;\n const legacy = await attempt(\n () => detectViaSafariStorage(browser, globals),\n errors,\n );\n if (legacy !== null) return legacy;\n break;\n }\n case 'edge-legacy':\n case 'ie': {\n const result = await attempt(\n () => detectViaLegacyEdge(browser, globals),\n errors,\n );\n if (result !== null) return result;\n break;\n }\n case 'unknown': {\n break;\n }\n }\n\n throw new IncognitoDetectionError(\n browser === 'unknown' ? 'UNSUPPORTED_BROWSER' : 'PROBE_FAILED',\n `No detection strategy produced a verdict for browser \"${browser}\".`,\n errors.length > 0 ? { cause: errors[0] } : undefined,\n );\n}\n\nasync function attempt(\n strategy: () => DetectionResult | null | Promise<DetectionResult | null>,\n errors: unknown[],\n): Promise<DetectionResult | null> {\n try {\n return await strategy();\n } catch (error) {\n errors.push(error);\n return null;\n }\n}\n\n/**\n * Chromium family (Chrome, Edge, Brave, Opera, …).\n *\n * Modern Chrome deliberately fakes `navigator.storage.estimate().quota` at\n * `usage + 10 GiB` in *every* mode to defeat detection — so that API is\n * useless here. The legacy `navigator.webkitTemporaryStorage` API was not\n * given the same treatment and still reports the real per-origin quota:\n * disk-bound (huge) in normal mode, memory-bound (small) in incognito.\n *\n * We compare it to `performance.memory.jsHeapSizeLimit` — a per-renderer\n * constant that does not change between modes — so the test adapts to the\n * device instead of relying on a brittle fixed byte count.\n */\nasync function detectViaChromiumQuota(\n browser: BrowserName,\n globals: DetectionGlobals,\n options: Pick<DetectIncognitoOptions, 'privateQuotaThresholdBytes'>,\n): Promise<DetectionResult | null> {\n const tempStorage = globals.navigator?.webkitTemporaryStorage;\n if (typeof tempStorage?.queryUsageAndQuota !== 'function') return null;\n\n const quota = await new Promise<number | null>((resolve) => {\n let settled = false;\n const settle = (value: number | null): void => {\n if (!settled) {\n settled = true;\n resolve(value);\n }\n };\n try {\n tempStorage.queryUsageAndQuota(\n (_used, grantedQuota) => {\n settle(typeof grantedQuota === 'number' ? grantedQuota : null);\n },\n () => {\n settle(null);\n },\n );\n } catch {\n settle(null);\n }\n });\n if (quota === null) return null;\n\n const isPrivate =\n options.privateQuotaThresholdBytes === undefined\n ? quota < chromiumHeapLimit(globals) * HEAP_QUOTA_MULTIPLIER\n : quota < options.privateQuotaThresholdBytes;\n\n return {\n isPrivate,\n browser,\n confidence: 'high',\n quota,\n strategy: 'chromium-quota',\n };\n}\n\nfunction chromiumHeapLimit(globals: DetectionGlobals): number {\n const limit = globals.window?.performance?.memory?.jsHeapSizeLimit;\n return typeof limit === 'number' && limit > 0\n ? limit\n : DEFAULT_PRIVATE_QUOTA_BYTES;\n}\n\n/**\n * Firefox and Safari: the Origin Private File System\n * (`navigator.storage.getDirectory()`) is rejected in private mode. The\n * rejection message differs per engine — Firefox says `Security error`,\n * Safari says `unknown transient reason` — so the caller passes the fragment\n * to match. A successful resolve means a normal window.\n */\nasync function detectViaOpfsProbe(\n browser: BrowserName,\n globals: DetectionGlobals,\n privateErrorFragment: string,\n): Promise<DetectionResult | null> {\n const storage = globals.navigator?.storage;\n if (typeof storage?.getDirectory !== 'function') return null;\n\n try {\n await storage.getDirectory();\n return {\n isPrivate: false,\n browser,\n confidence: 'high',\n quota: null,\n strategy: 'opfs-probe',\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n isPrivate: message.includes(privateErrorFragment),\n browser,\n confidence: 'high',\n quota: null,\n strategy: 'opfs-probe',\n };\n }\n}\n\n/** Older Safari / WebKit without OPFS: localStorage + openDatabase probes. */\nfunction detectViaSafariStorage(\n browser: BrowserName,\n globals: DetectionGlobals,\n): DetectionResult | null {\n const storage = globals.window?.localStorage;\n if (!storage) {\n // No localStorage at all is a strong signal on Safari < 11 private.\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n\n try {\n storage.setItem(PROBE_KEY, '1');\n storage.removeItem(PROBE_KEY);\n } catch {\n return {\n isPrivate: true,\n browser,\n confidence: 'medium',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n\n const win = globals.window;\n if (typeof win.openDatabase === 'function') {\n try {\n win.openDatabase(null, null, null, null);\n } catch {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n }\n\n return {\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n}\n\n/** Older Firefox without OPFS: `indexedDB.open` fails in private mode. */\nasync function detectViaFirefoxIndexedDB(\n browser: BrowserName,\n globals: DetectionGlobals,\n): Promise<DetectionResult | null> {\n const indexedDB = globals.indexedDB ?? globals.window?.indexedDB;\n if (!indexedDB) {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n };\n }\n\n return new Promise<DetectionResult>((resolve) => {\n let request: IDBOpenDBRequest;\n try {\n request = indexedDB.open(PROBE_KEY);\n } catch {\n resolve({\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n return;\n }\n\n request.addEventListener('error', () => {\n resolve({\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n });\n request.addEventListener('success', () => {\n try {\n request.result.close();\n indexedDB.deleteDatabase(PROBE_KEY);\n } catch {\n /* best-effort cleanup */\n }\n resolve({\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n });\n });\n}\n\n/** Legacy Edge / IE: no `indexedDB` while `PointerEvent` exists → private. */\nfunction detectViaLegacyEdge(\n browser: BrowserName,\n globals: DetectionGlobals,\n): DetectionResult | null {\n const win = globals.window;\n if (!win) return null;\n const hasIndexedDB = Boolean(win.indexedDB ?? globals.indexedDB);\n const hasPointer = Boolean(win.PointerEvent ?? win.MSPointerEvent);\n return {\n isPrivate: !hasIndexedDB && hasPointer,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'legacy-edge',\n };\n}\n","import { detectBrowser } from './browser.ts';\nimport { IncognitoDetectionError } from './errors.ts';\nimport { runStrategies } from './strategies.ts';\nimport type {\n DetectIncognitoOptions,\n DetectionGlobals,\n DetectionResult,\n WindowLike,\n} from './types.ts';\n\n/**\n * Detect whether the current browser is in private / incognito mode.\n *\n * Returns a rich {@link DetectionResult}. Most callers want the thinner\n * {@link isIncognito} convenience instead.\n *\n * @throws {IncognitoDetectionError}\n * - `NOT_A_BROWSER` when invoked outside a browser-like environment.\n * - `UNSUPPORTED_BROWSER` when the browser was classified as `unknown` and\n * no fallback strategy applied.\n * - `PROBE_FAILED` when every applicable strategy threw before producing a\n * definitive answer.\n *\n * @example\n * ```ts\n * import { detectIncognito } from 'is-incognito-mode';\n *\n * const { isPrivate, browser, confidence, quota } = await detectIncognito();\n * console.log(`${browser} (${confidence} confidence, quota: ${quota ?? '?'})`);\n * ```\n */\nexport async function detectIncognito(\n options: DetectIncognitoOptions = {},\n): Promise<DetectionResult> {\n const globals = resolveGlobals(options.globals);\n if (!isBrowserLike(globals)) {\n throw new IncognitoDetectionError(\n 'NOT_A_BROWSER',\n 'is-incognito-mode can only run in a browser-like environment.',\n );\n }\n\n const browser = detectBrowser({\n userAgent: globals.navigator.userAgent,\n ...(globals.navigator.vendor === undefined\n ? {}\n : { vendor: globals.navigator.vendor }),\n });\n\n return runStrategies(\n browser,\n globals,\n options.privateQuotaThresholdBytes === undefined\n ? {}\n : { privateQuotaThresholdBytes: options.privateQuotaThresholdBytes },\n );\n}\n\n/**\n * Convenience wrapper around {@link detectIncognito} that resolves to the\n * boolean verdict only. Drop-in compatible with v1's default export.\n *\n * @example\n * ```ts\n * import { isIncognito } from 'is-incognito-mode';\n *\n * if (await isIncognito()) {\n * showPaywall();\n * }\n * ```\n *\n * @throws {IncognitoDetectionError} See {@link detectIncognito}.\n */\nexport async function isIncognito(\n options: DetectIncognitoOptions = {},\n): Promise<boolean> {\n const result = await detectIncognito(options);\n return result.isPrivate;\n}\n\nfunction resolveGlobals(\n overrides: DetectionGlobals | undefined,\n): DetectionGlobals {\n if (overrides) return overrides;\n // Boundary cast on `window`: the live object carries non-standard properties\n // the detector relies on (notably `performance.memory`) that the standard\n // DOM lib types do not declare.\n return {\n navigator: typeof navigator === 'undefined' ? undefined : navigator,\n window:\n typeof window === 'undefined'\n ? undefined\n : (window as unknown as WindowLike),\n indexedDB: typeof indexedDB === 'undefined' ? undefined : indexedDB,\n };\n}\n\nfunction isBrowserLike(\n globals: DetectionGlobals,\n): globals is DetectionGlobals & {\n navigator: NonNullable<DetectionGlobals['navigator']>;\n} {\n return Boolean(globals.navigator?.userAgent);\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -10,12 +10,11 @@ type BrowserName = 'chromium' | 'firefox' | 'safari' | 'webkit' | 'edge-legacy'
|
|
|
10
10
|
/**
|
|
11
11
|
* Qualitative confidence in a detection outcome.
|
|
12
12
|
*
|
|
13
|
-
* - `high` — A direct, hard-to-fake signal (
|
|
14
|
-
*
|
|
13
|
+
* - `high` — A direct, hard-to-fake signal (the Chromium temporary-storage
|
|
14
|
+
* quota probe, or an OPFS access rejection in Firefox / Safari private mode).
|
|
15
15
|
* - `medium` — A heuristic that is right the overwhelming majority of the
|
|
16
|
-
* time but has known false-positive scenarios
|
|
17
|
-
*
|
|
18
|
-
* - `low` — A best-effort fallback (e.g. legacy-browser heuristics).
|
|
16
|
+
* time but has known false-positive scenarios.
|
|
17
|
+
* - `low` — A best-effort fallback (legacy-browser heuristics).
|
|
19
18
|
*/
|
|
20
19
|
type DetectionConfidence = 'high' | 'medium' | 'low';
|
|
21
20
|
/**
|
|
@@ -29,30 +28,48 @@ interface DetectionResult {
|
|
|
29
28
|
/** Detector's qualitative confidence in `isPrivate`. */
|
|
30
29
|
readonly confidence: DetectionConfidence;
|
|
31
30
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Storage quota in bytes that informed the verdict, when a quota-based
|
|
32
|
+
* strategy was used. `null` for strategies that do not read a quota
|
|
33
|
+
* (OPFS probe, legacy heuristics).
|
|
34
34
|
*/
|
|
35
35
|
readonly quota: number | null;
|
|
36
36
|
/** Identifier of the strategy that produced the verdict. */
|
|
37
37
|
readonly strategy: DetectionStrategyName;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Stable identifiers for the strategies. Useful for debugging and
|
|
40
|
+
* Stable identifiers for the detection strategies. Useful for debugging and
|
|
41
|
+
* analytics.
|
|
42
|
+
*
|
|
43
|
+
* - `chromium-quota` — Chromium-family: the legacy
|
|
44
|
+
* `navigator.webkitTemporaryStorage.queryUsageAndQuota` quota compared to
|
|
45
|
+
* `performance.memory.jsHeapSizeLimit`. This is the only Chromium signal
|
|
46
|
+
* that still works: modern Chrome fakes `navigator.storage.estimate()` at
|
|
47
|
+
* `usage + 10 GiB` in every mode specifically to defeat detection.
|
|
48
|
+
* - `opfs-probe` — Firefox / Safari: `navigator.storage.getDirectory()`
|
|
49
|
+
* (Origin Private File System) is rejected in private mode.
|
|
50
|
+
* - `firefox-indexeddb` — Older Firefox without OPFS: `indexedDB.open` error.
|
|
51
|
+
* - `safari-storage` — Older Safari / WebKit without OPFS: `localStorage` +
|
|
52
|
+
* `openDatabase` probes.
|
|
53
|
+
* - `legacy-edge` — Legacy Edge / IE: `PointerEvent` + `indexedDB` heuristic.
|
|
41
54
|
*/
|
|
42
|
-
type DetectionStrategyName = '
|
|
55
|
+
type DetectionStrategyName = 'chromium-quota' | 'opfs-probe' | 'firefox-indexeddb' | 'safari-storage' | 'legacy-edge';
|
|
43
56
|
/**
|
|
44
57
|
* Options for {@link detectIncognito}.
|
|
45
58
|
*/
|
|
46
59
|
interface DetectIncognitoOptions {
|
|
47
60
|
/**
|
|
48
|
-
* Override the global `navigator`
|
|
61
|
+
* Override the global `navigator` / `window` / `indexedDB` lookups. Used in
|
|
49
62
|
* tests; in production the live globals are used.
|
|
50
63
|
*/
|
|
51
64
|
readonly globals?: DetectionGlobals;
|
|
52
65
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
66
|
+
* **Advanced override.** When set, the Chromium strategy classifies the tab
|
|
67
|
+
* as private if the legacy temporary-storage quota is below this many bytes,
|
|
68
|
+
* instead of the default heap-relative heuristic.
|
|
69
|
+
*
|
|
70
|
+
* Leave this unset unless you have measured your audience: the default
|
|
71
|
+
* compares the quota to `performance.memory.jsHeapSizeLimit`, which adapts
|
|
72
|
+
* to the device automatically and is far more robust than any fixed number.
|
|
56
73
|
*/
|
|
57
74
|
readonly privateQuotaThresholdBytes?: number;
|
|
58
75
|
}
|
|
@@ -69,20 +86,47 @@ interface NavigatorLike {
|
|
|
69
86
|
readonly userAgent: string;
|
|
70
87
|
readonly vendor?: string;
|
|
71
88
|
readonly storage?: StorageManagerLike | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Legacy, non-standard quota API. Still present in Chromium and — unlike the
|
|
91
|
+
* modern Storage API — still reports the *real* per-origin quota, which is
|
|
92
|
+
* what makes incognito detection possible.
|
|
93
|
+
*/
|
|
94
|
+
readonly webkitTemporaryStorage?: DeprecatedStorageQuota | undefined;
|
|
72
95
|
}
|
|
73
96
|
interface StorageManagerLike {
|
|
74
97
|
estimate?(): Promise<{
|
|
75
98
|
quota?: number;
|
|
76
99
|
usage?: number;
|
|
77
100
|
}>;
|
|
101
|
+
/** Origin Private File System root. Rejected in Firefox / Safari private mode. */
|
|
102
|
+
getDirectory?(): Promise<unknown>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The legacy `navigator.webkitTemporaryStorage` shape. `queryUsageAndQuota`
|
|
106
|
+
* invokes `onSuccess(usedBytes, grantedQuotaBytes)`.
|
|
107
|
+
*/
|
|
108
|
+
interface DeprecatedStorageQuota {
|
|
109
|
+
queryUsageAndQuota(onSuccess: (usedBytes: number, grantedQuotaBytes: number) => void, onError?: (error: unknown) => void): void;
|
|
78
110
|
}
|
|
79
111
|
interface WindowLike {
|
|
80
112
|
readonly indexedDB?: IDBFactory | undefined;
|
|
81
113
|
readonly localStorage?: StorageLike | undefined;
|
|
82
114
|
readonly PointerEvent?: unknown;
|
|
83
115
|
readonly MSPointerEvent?: unknown;
|
|
116
|
+
readonly performance?: PerformanceLike | undefined;
|
|
84
117
|
openDatabase?(name: string | null, version: string | null, displayName: string | null, estimatedSize: number | null): unknown;
|
|
85
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Subset of `window.performance`. `memory` is a non-standard Chromium-only
|
|
121
|
+
* property; `jsHeapSizeLimit` is the per-renderer JS heap ceiling, which is
|
|
122
|
+
* stable across normal and incognito modes and therefore a reliable
|
|
123
|
+
* device-relative yardstick.
|
|
124
|
+
*/
|
|
125
|
+
interface PerformanceLike {
|
|
126
|
+
readonly memory?: {
|
|
127
|
+
readonly jsHeapSizeLimit?: number;
|
|
128
|
+
} | undefined;
|
|
129
|
+
}
|
|
86
130
|
interface StorageLike {
|
|
87
131
|
setItem(key: string, value: string): void;
|
|
88
132
|
removeItem(key: string): void;
|
|
@@ -164,12 +208,10 @@ declare class IncognitoDetectionError extends Error {
|
|
|
164
208
|
}
|
|
165
209
|
|
|
166
210
|
/**
|
|
167
|
-
*
|
|
168
|
-
* is
|
|
169
|
-
*
|
|
170
|
-
* Sources: detectIncognito.js research notes; cross-checked against Chrome
|
|
171
|
-
* 120+, Firefox 121+, Safari 17+.
|
|
211
|
+
* Fallback JS-heap ceiling (1 GiB) used by the Chromium strategy when
|
|
212
|
+
* `performance.memory.jsHeapSizeLimit` is unavailable. Also exported as the
|
|
213
|
+
* reference value for the `privateQuotaThresholdBytes` advanced override.
|
|
172
214
|
*/
|
|
173
215
|
declare const DEFAULT_PRIVATE_QUOTA_BYTES: number;
|
|
174
216
|
|
|
175
|
-
export { type BrowserName, DEFAULT_PRIVATE_QUOTA_BYTES, type DetectIncognitoOptions, type DetectionConfidence, type DetectionGlobals, type DetectionResult, type DetectionStrategyName, IncognitoDetectionError, type IncognitoDetectionErrorCode, type NavigatorLike, type StorageLike, type StorageManagerLike, type WindowLike, isIncognito as default, detectIncognito, isIncognito };
|
|
217
|
+
export { type BrowserName, DEFAULT_PRIVATE_QUOTA_BYTES, type DeprecatedStorageQuota, type DetectIncognitoOptions, type DetectionConfidence, type DetectionGlobals, type DetectionResult, type DetectionStrategyName, IncognitoDetectionError, type IncognitoDetectionErrorCode, type NavigatorLike, type PerformanceLike, type StorageLike, type StorageManagerLike, type WindowLike, isIncognito as default, detectIncognito, isIncognito };
|
package/dist/index.d.ts
CHANGED
|
@@ -10,12 +10,11 @@ type BrowserName = 'chromium' | 'firefox' | 'safari' | 'webkit' | 'edge-legacy'
|
|
|
10
10
|
/**
|
|
11
11
|
* Qualitative confidence in a detection outcome.
|
|
12
12
|
*
|
|
13
|
-
* - `high` — A direct, hard-to-fake signal (
|
|
14
|
-
*
|
|
13
|
+
* - `high` — A direct, hard-to-fake signal (the Chromium temporary-storage
|
|
14
|
+
* quota probe, or an OPFS access rejection in Firefox / Safari private mode).
|
|
15
15
|
* - `medium` — A heuristic that is right the overwhelming majority of the
|
|
16
|
-
* time but has known false-positive scenarios
|
|
17
|
-
*
|
|
18
|
-
* - `low` — A best-effort fallback (e.g. legacy-browser heuristics).
|
|
16
|
+
* time but has known false-positive scenarios.
|
|
17
|
+
* - `low` — A best-effort fallback (legacy-browser heuristics).
|
|
19
18
|
*/
|
|
20
19
|
type DetectionConfidence = 'high' | 'medium' | 'low';
|
|
21
20
|
/**
|
|
@@ -29,30 +28,48 @@ interface DetectionResult {
|
|
|
29
28
|
/** Detector's qualitative confidence in `isPrivate`. */
|
|
30
29
|
readonly confidence: DetectionConfidence;
|
|
31
30
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Storage quota in bytes that informed the verdict, when a quota-based
|
|
32
|
+
* strategy was used. `null` for strategies that do not read a quota
|
|
33
|
+
* (OPFS probe, legacy heuristics).
|
|
34
34
|
*/
|
|
35
35
|
readonly quota: number | null;
|
|
36
36
|
/** Identifier of the strategy that produced the verdict. */
|
|
37
37
|
readonly strategy: DetectionStrategyName;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Stable identifiers for the strategies. Useful for debugging and
|
|
40
|
+
* Stable identifiers for the detection strategies. Useful for debugging and
|
|
41
|
+
* analytics.
|
|
42
|
+
*
|
|
43
|
+
* - `chromium-quota` — Chromium-family: the legacy
|
|
44
|
+
* `navigator.webkitTemporaryStorage.queryUsageAndQuota` quota compared to
|
|
45
|
+
* `performance.memory.jsHeapSizeLimit`. This is the only Chromium signal
|
|
46
|
+
* that still works: modern Chrome fakes `navigator.storage.estimate()` at
|
|
47
|
+
* `usage + 10 GiB` in every mode specifically to defeat detection.
|
|
48
|
+
* - `opfs-probe` — Firefox / Safari: `navigator.storage.getDirectory()`
|
|
49
|
+
* (Origin Private File System) is rejected in private mode.
|
|
50
|
+
* - `firefox-indexeddb` — Older Firefox without OPFS: `indexedDB.open` error.
|
|
51
|
+
* - `safari-storage` — Older Safari / WebKit without OPFS: `localStorage` +
|
|
52
|
+
* `openDatabase` probes.
|
|
53
|
+
* - `legacy-edge` — Legacy Edge / IE: `PointerEvent` + `indexedDB` heuristic.
|
|
41
54
|
*/
|
|
42
|
-
type DetectionStrategyName = '
|
|
55
|
+
type DetectionStrategyName = 'chromium-quota' | 'opfs-probe' | 'firefox-indexeddb' | 'safari-storage' | 'legacy-edge';
|
|
43
56
|
/**
|
|
44
57
|
* Options for {@link detectIncognito}.
|
|
45
58
|
*/
|
|
46
59
|
interface DetectIncognitoOptions {
|
|
47
60
|
/**
|
|
48
|
-
* Override the global `navigator`
|
|
61
|
+
* Override the global `navigator` / `window` / `indexedDB` lookups. Used in
|
|
49
62
|
* tests; in production the live globals are used.
|
|
50
63
|
*/
|
|
51
64
|
readonly globals?: DetectionGlobals;
|
|
52
65
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
66
|
+
* **Advanced override.** When set, the Chromium strategy classifies the tab
|
|
67
|
+
* as private if the legacy temporary-storage quota is below this many bytes,
|
|
68
|
+
* instead of the default heap-relative heuristic.
|
|
69
|
+
*
|
|
70
|
+
* Leave this unset unless you have measured your audience: the default
|
|
71
|
+
* compares the quota to `performance.memory.jsHeapSizeLimit`, which adapts
|
|
72
|
+
* to the device automatically and is far more robust than any fixed number.
|
|
56
73
|
*/
|
|
57
74
|
readonly privateQuotaThresholdBytes?: number;
|
|
58
75
|
}
|
|
@@ -69,20 +86,47 @@ interface NavigatorLike {
|
|
|
69
86
|
readonly userAgent: string;
|
|
70
87
|
readonly vendor?: string;
|
|
71
88
|
readonly storage?: StorageManagerLike | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Legacy, non-standard quota API. Still present in Chromium and — unlike the
|
|
91
|
+
* modern Storage API — still reports the *real* per-origin quota, which is
|
|
92
|
+
* what makes incognito detection possible.
|
|
93
|
+
*/
|
|
94
|
+
readonly webkitTemporaryStorage?: DeprecatedStorageQuota | undefined;
|
|
72
95
|
}
|
|
73
96
|
interface StorageManagerLike {
|
|
74
97
|
estimate?(): Promise<{
|
|
75
98
|
quota?: number;
|
|
76
99
|
usage?: number;
|
|
77
100
|
}>;
|
|
101
|
+
/** Origin Private File System root. Rejected in Firefox / Safari private mode. */
|
|
102
|
+
getDirectory?(): Promise<unknown>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The legacy `navigator.webkitTemporaryStorage` shape. `queryUsageAndQuota`
|
|
106
|
+
* invokes `onSuccess(usedBytes, grantedQuotaBytes)`.
|
|
107
|
+
*/
|
|
108
|
+
interface DeprecatedStorageQuota {
|
|
109
|
+
queryUsageAndQuota(onSuccess: (usedBytes: number, grantedQuotaBytes: number) => void, onError?: (error: unknown) => void): void;
|
|
78
110
|
}
|
|
79
111
|
interface WindowLike {
|
|
80
112
|
readonly indexedDB?: IDBFactory | undefined;
|
|
81
113
|
readonly localStorage?: StorageLike | undefined;
|
|
82
114
|
readonly PointerEvent?: unknown;
|
|
83
115
|
readonly MSPointerEvent?: unknown;
|
|
116
|
+
readonly performance?: PerformanceLike | undefined;
|
|
84
117
|
openDatabase?(name: string | null, version: string | null, displayName: string | null, estimatedSize: number | null): unknown;
|
|
85
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Subset of `window.performance`. `memory` is a non-standard Chromium-only
|
|
121
|
+
* property; `jsHeapSizeLimit` is the per-renderer JS heap ceiling, which is
|
|
122
|
+
* stable across normal and incognito modes and therefore a reliable
|
|
123
|
+
* device-relative yardstick.
|
|
124
|
+
*/
|
|
125
|
+
interface PerformanceLike {
|
|
126
|
+
readonly memory?: {
|
|
127
|
+
readonly jsHeapSizeLimit?: number;
|
|
128
|
+
} | undefined;
|
|
129
|
+
}
|
|
86
130
|
interface StorageLike {
|
|
87
131
|
setItem(key: string, value: string): void;
|
|
88
132
|
removeItem(key: string): void;
|
|
@@ -164,12 +208,10 @@ declare class IncognitoDetectionError extends Error {
|
|
|
164
208
|
}
|
|
165
209
|
|
|
166
210
|
/**
|
|
167
|
-
*
|
|
168
|
-
* is
|
|
169
|
-
*
|
|
170
|
-
* Sources: detectIncognito.js research notes; cross-checked against Chrome
|
|
171
|
-
* 120+, Firefox 121+, Safari 17+.
|
|
211
|
+
* Fallback JS-heap ceiling (1 GiB) used by the Chromium strategy when
|
|
212
|
+
* `performance.memory.jsHeapSizeLimit` is unavailable. Also exported as the
|
|
213
|
+
* reference value for the `privateQuotaThresholdBytes` advanced override.
|
|
172
214
|
*/
|
|
173
215
|
declare const DEFAULT_PRIVATE_QUOTA_BYTES: number;
|
|
174
216
|
|
|
175
|
-
export { type BrowserName, DEFAULT_PRIVATE_QUOTA_BYTES, type DetectIncognitoOptions, type DetectionConfidence, type DetectionGlobals, type DetectionResult, type DetectionStrategyName, IncognitoDetectionError, type IncognitoDetectionErrorCode, type NavigatorLike, type StorageLike, type StorageManagerLike, type WindowLike, isIncognito as default, detectIncognito, isIncognito };
|
|
217
|
+
export { type BrowserName, DEFAULT_PRIVATE_QUOTA_BYTES, type DeprecatedStorageQuota, type DetectIncognitoOptions, type DetectionConfidence, type DetectionGlobals, type DetectionResult, type DetectionStrategyName, IncognitoDetectionError, type IncognitoDetectionErrorCode, type NavigatorLike, type PerformanceLike, type StorageLike, type StorageManagerLike, type WindowLike, isIncognito as default, detectIncognito, isIncognito };
|
package/dist/index.js
CHANGED
|
@@ -27,33 +27,53 @@ var IncognitoDetectionError = class extends Error {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
// src/strategies.ts
|
|
30
|
-
var DEFAULT_PRIVATE_QUOTA_BYTES =
|
|
30
|
+
var DEFAULT_PRIVATE_QUOTA_BYTES = 1024 * 1024 * 1024;
|
|
31
|
+
var HEAP_QUOTA_MULTIPLIER = 2;
|
|
31
32
|
var PROBE_KEY = "__is_incognito_mode_probe_cd1394e6__";
|
|
32
33
|
async function runStrategies(browser, globals, options) {
|
|
33
34
|
const errors = [];
|
|
34
|
-
if (canUseStorageEstimate(globals)) {
|
|
35
|
-
try {
|
|
36
|
-
const result = await detectViaStorageQuota(browser, globals, options);
|
|
37
|
-
if (result !== null) return result;
|
|
38
|
-
} catch (error) {
|
|
39
|
-
errors.push(error);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
35
|
switch (browser) {
|
|
43
|
-
case "
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
case "chromium": {
|
|
37
|
+
const result = await attempt(
|
|
38
|
+
() => detectViaChromiumQuota(browser, globals, options),
|
|
39
|
+
errors
|
|
40
|
+
);
|
|
46
41
|
if (result !== null) return result;
|
|
47
42
|
break;
|
|
48
43
|
}
|
|
49
44
|
case "firefox": {
|
|
50
|
-
const
|
|
51
|
-
|
|
45
|
+
const opfs = await attempt(
|
|
46
|
+
() => detectViaOpfsProbe(browser, globals, "Security error"),
|
|
47
|
+
errors
|
|
48
|
+
);
|
|
49
|
+
if (opfs !== null) return opfs;
|
|
50
|
+
const idb = await attempt(
|
|
51
|
+
() => detectViaFirefoxIndexedDB(browser, globals),
|
|
52
|
+
errors
|
|
53
|
+
);
|
|
54
|
+
if (idb !== null) return idb;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case "safari":
|
|
58
|
+
case "webkit": {
|
|
59
|
+
const opfs = await attempt(
|
|
60
|
+
() => detectViaOpfsProbe(browser, globals, "unknown transient reason"),
|
|
61
|
+
errors
|
|
62
|
+
);
|
|
63
|
+
if (opfs !== null) return opfs;
|
|
64
|
+
const legacy = await attempt(
|
|
65
|
+
() => detectViaSafariStorage(browser, globals),
|
|
66
|
+
errors
|
|
67
|
+
);
|
|
68
|
+
if (legacy !== null) return legacy;
|
|
52
69
|
break;
|
|
53
70
|
}
|
|
54
71
|
case "edge-legacy":
|
|
55
72
|
case "ie": {
|
|
56
|
-
const result =
|
|
73
|
+
const result = await attempt(
|
|
74
|
+
() => detectViaLegacyEdge(browser, globals),
|
|
75
|
+
errors
|
|
76
|
+
);
|
|
57
77
|
if (result !== null) return result;
|
|
58
78
|
break;
|
|
59
79
|
}
|
|
@@ -64,24 +84,75 @@ async function runStrategies(browser, globals, options) {
|
|
|
64
84
|
errors.length > 0 ? { cause: errors[0] } : void 0
|
|
65
85
|
);
|
|
66
86
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
87
|
+
async function attempt(strategy, errors) {
|
|
88
|
+
try {
|
|
89
|
+
return await strategy();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
errors.push(error);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
69
94
|
}
|
|
70
|
-
async function
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
const
|
|
74
|
-
|
|
95
|
+
async function detectViaChromiumQuota(browser, globals, options) {
|
|
96
|
+
const tempStorage = globals.navigator?.webkitTemporaryStorage;
|
|
97
|
+
if (typeof tempStorage?.queryUsageAndQuota !== "function") return null;
|
|
98
|
+
const quota = await new Promise((resolve) => {
|
|
99
|
+
let settled = false;
|
|
100
|
+
const settle = (value) => {
|
|
101
|
+
if (!settled) {
|
|
102
|
+
settled = true;
|
|
103
|
+
resolve(value);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
tempStorage.queryUsageAndQuota(
|
|
108
|
+
(_used, grantedQuota) => {
|
|
109
|
+
settle(typeof grantedQuota === "number" ? grantedQuota : null);
|
|
110
|
+
},
|
|
111
|
+
() => {
|
|
112
|
+
settle(null);
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
} catch {
|
|
116
|
+
settle(null);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
75
119
|
if (quota === null) return null;
|
|
76
|
-
const isPrivate = quota < options.privateQuotaThresholdBytes;
|
|
120
|
+
const isPrivate = options.privateQuotaThresholdBytes === void 0 ? quota < chromiumHeapLimit(globals) * HEAP_QUOTA_MULTIPLIER : quota < options.privateQuotaThresholdBytes;
|
|
77
121
|
return {
|
|
78
122
|
isPrivate,
|
|
79
123
|
browser,
|
|
80
124
|
confidence: "high",
|
|
81
125
|
quota,
|
|
82
|
-
strategy: "
|
|
126
|
+
strategy: "chromium-quota"
|
|
83
127
|
};
|
|
84
128
|
}
|
|
129
|
+
function chromiumHeapLimit(globals) {
|
|
130
|
+
const limit = globals.window?.performance?.memory?.jsHeapSizeLimit;
|
|
131
|
+
return typeof limit === "number" && limit > 0 ? limit : DEFAULT_PRIVATE_QUOTA_BYTES;
|
|
132
|
+
}
|
|
133
|
+
async function detectViaOpfsProbe(browser, globals, privateErrorFragment) {
|
|
134
|
+
const storage = globals.navigator?.storage;
|
|
135
|
+
if (typeof storage?.getDirectory !== "function") return null;
|
|
136
|
+
try {
|
|
137
|
+
await storage.getDirectory();
|
|
138
|
+
return {
|
|
139
|
+
isPrivate: false,
|
|
140
|
+
browser,
|
|
141
|
+
confidence: "high",
|
|
142
|
+
quota: null,
|
|
143
|
+
strategy: "opfs-probe"
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
return {
|
|
148
|
+
isPrivate: message.includes(privateErrorFragment),
|
|
149
|
+
browser,
|
|
150
|
+
confidence: "high",
|
|
151
|
+
quota: null,
|
|
152
|
+
strategy: "opfs-probe"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
85
156
|
function detectViaSafariStorage(browser, globals) {
|
|
86
157
|
const storage = globals.window?.localStorage;
|
|
87
158
|
if (!storage) {
|
|
@@ -182,17 +253,8 @@ function detectViaLegacyEdge(browser, globals) {
|
|
|
182
253
|
if (!win) return null;
|
|
183
254
|
const hasIndexedDB = Boolean(win.indexedDB ?? globals.indexedDB);
|
|
184
255
|
const hasPointer = Boolean(win.PointerEvent ?? win.MSPointerEvent);
|
|
185
|
-
if (!hasIndexedDB && hasPointer) {
|
|
186
|
-
return {
|
|
187
|
-
isPrivate: true,
|
|
188
|
-
browser,
|
|
189
|
-
confidence: "low",
|
|
190
|
-
quota: null,
|
|
191
|
-
strategy: "legacy-edge"
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
256
|
return {
|
|
195
|
-
isPrivate:
|
|
257
|
+
isPrivate: !hasIndexedDB && hasPointer,
|
|
196
258
|
browser,
|
|
197
259
|
confidence: "low",
|
|
198
260
|
quota: null,
|
|
@@ -213,10 +275,11 @@ async function detectIncognito(options = {}) {
|
|
|
213
275
|
userAgent: globals.navigator.userAgent,
|
|
214
276
|
...globals.navigator.vendor === void 0 ? {} : { vendor: globals.navigator.vendor }
|
|
215
277
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
278
|
+
return runStrategies(
|
|
279
|
+
browser,
|
|
280
|
+
globals,
|
|
281
|
+
options.privateQuotaThresholdBytes === void 0 ? {} : { privateQuotaThresholdBytes: options.privateQuotaThresholdBytes }
|
|
282
|
+
);
|
|
220
283
|
}
|
|
221
284
|
async function isIncognito(options = {}) {
|
|
222
285
|
const result = await detectIncognito(options);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/browser.ts","../src/errors.ts","../src/strategies.ts","../src/detect.ts"],"names":["indexedDB"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Coarse browser-engine identifiers used to pick a detection strategy.\n *\n * We deliberately do not export brand names like \"Brave\" or \"Vivaldi\" — they\n * are all Chromium under the hood and the relevant private-mode behaviour\n * matches the engine, not the brand.\n */\nexport type BrowserName =\n | 'chromium'\n | 'firefox'\n | 'safari'\n | 'webkit'\n | 'edge-legacy'\n | 'ie'\n | 'unknown';\n\ninterface UserAgentSource {\n readonly userAgent: string;\n readonly vendor?: string;\n}\n\nconst CHROMIUM_HINTS = ['chrome/', 'crios/', 'chromium/', 'edg/', 'opr/'];\n\n/**\n * Classify the current navigator into a coarse browser engine.\n *\n * Pure function — accepts an optional `source` so it can be unit-tested\n * without polluting globals. In production we read from `navigator` directly.\n */\nexport function detectBrowser(source?: UserAgentSource): BrowserName {\n const ua = (source?.userAgent ?? '').toLowerCase();\n const vendor = (source?.vendor ?? '').toLowerCase();\n\n if (!ua) return 'unknown';\n\n if (ua.includes('msie ') || ua.includes('trident/')) return 'ie';\n if (ua.includes('edge/')) return 'edge-legacy';\n if (ua.includes('firefox/') || ua.includes('fxios/')) return 'firefox';\n\n const looksLikeChromium = CHROMIUM_HINTS.some((hint) => ua.includes(hint));\n if (looksLikeChromium) return 'chromium';\n\n // Safari sets `vendor === 'apple computer, inc.'`. iOS-Chrome/Firefox also\n // run on WebKit but advertise different UAs, so we reach this branch only\n // for \"real\" Safari and other WebKit shells.\n if (ua.includes('safari/') && (vendor.includes('apple') || !vendor)) {\n return 'safari';\n }\n if (ua.includes('applewebkit/')) return 'webkit';\n\n return 'unknown';\n}\n","/**\n * Stable identifiers for {@link IncognitoDetectionError} causes.\n *\n * Branching on `code` is safer than parsing `message` strings.\n */\nexport type IncognitoDetectionErrorCode =\n /** Invoked in a non-browser context (Node, worker without globals, etc.). */\n | 'NOT_A_BROWSER'\n /** Browser detected but no supported detection strategy applies. */\n | 'UNSUPPORTED_BROWSER'\n /** A probe threw before it could produce a definitive result. */\n | 'PROBE_FAILED';\n\n/**\n * Thrown when private-mode cannot be determined.\n *\n * @example\n * ```ts\n * import { isIncognito, IncognitoDetectionError } from 'is-incognito-mode';\n *\n * try {\n * await isIncognito();\n * } catch (error) {\n * if (error instanceof IncognitoDetectionError) {\n * console.warn(error.code, error.message);\n * }\n * }\n * ```\n */\nexport class IncognitoDetectionError extends Error {\n override readonly name = 'IncognitoDetectionError';\n readonly code: IncognitoDetectionErrorCode;\n\n constructor(\n code: IncognitoDetectionErrorCode,\n message: string,\n options?: { cause?: unknown },\n ) {\n super(message, options);\n this.code = code;\n }\n}\n","import type { BrowserName } from './browser.ts';\nimport { IncognitoDetectionError } from './errors.ts';\nimport type {\n DetectIncognitoOptions,\n DetectionGlobals,\n DetectionResult,\n} from './types.ts';\n\n/**\n * Default cutoff: 120 MB. Below this, every shipping browser we tested in 2024\n * is in a restricted (private) storage context. Above, it is normal.\n *\n * Sources: detectIncognito.js research notes; cross-checked against Chrome\n * 120+, Firefox 121+, Safari 17+.\n */\nexport const DEFAULT_PRIVATE_QUOTA_BYTES = 120 * 1024 * 1024;\n\n/**\n * Sentinel key used by the legacy localStorage probe. Random enough that\n * collisions with real user keys are impossible.\n */\nconst PROBE_KEY = '__is_incognito_mode_probe_cd1394e6__';\n\n/**\n * Try strategies in priority order, returning the first one that produces a\n * definitive answer. Throws {@link IncognitoDetectionError} if every strategy\n * declines.\n */\nexport async function runStrategies(\n browser: BrowserName,\n globals: DetectionGlobals,\n options: Required<Pick<DetectIncognitoOptions, 'privateQuotaThresholdBytes'>>,\n): Promise<DetectionResult> {\n const errors: unknown[] = [];\n\n // 1. navigator.storage.estimate — the modern, vendor-blessed-ish signal.\n if (canUseStorageEstimate(globals)) {\n try {\n const result = await detectViaStorageQuota(browser, globals, options);\n if (result !== null) return result;\n } catch (error) {\n errors.push(error);\n }\n }\n\n // 2. Per-engine fallbacks for older versions / restricted runtimes.\n switch (browser) {\n case 'safari':\n case 'webkit': {\n const result = detectViaSafariStorage(browser, globals);\n if (result !== null) return result;\n break;\n }\n case 'firefox': {\n const result = await detectViaFirefoxIndexedDB(browser, globals);\n if (result !== null) return result;\n break;\n }\n case 'edge-legacy':\n case 'ie': {\n const result = detectViaLegacyEdge(browser, globals);\n if (result !== null) return result;\n break;\n }\n case 'chromium':\n case 'unknown': {\n break;\n }\n }\n\n throw new IncognitoDetectionError(\n browser === 'unknown' ? 'UNSUPPORTED_BROWSER' : 'PROBE_FAILED',\n `No detection strategy produced a verdict for browser \"${browser}\".`,\n errors.length > 0 ? { cause: errors[0] } : undefined,\n );\n}\n\nfunction canUseStorageEstimate(globals: DetectionGlobals): boolean {\n return typeof globals.navigator?.storage?.estimate === 'function';\n}\n\nasync function detectViaStorageQuota(\n browser: BrowserName,\n globals: DetectionGlobals,\n options: Required<Pick<DetectIncognitoOptions, 'privateQuotaThresholdBytes'>>,\n): Promise<DetectionResult | null> {\n const storage = globals.navigator?.storage;\n if (!storage?.estimate) return null;\n\n const estimate = await storage.estimate();\n const quota = typeof estimate.quota === 'number' ? estimate.quota : null;\n if (quota === null) return null;\n\n const isPrivate = quota < options.privateQuotaThresholdBytes;\n return {\n isPrivate,\n browser,\n confidence: 'high',\n quota,\n strategy: 'storage-quota',\n };\n}\n\nfunction detectViaSafariStorage(\n browser: BrowserName,\n globals: DetectionGlobals,\n): DetectionResult | null {\n const storage = globals.window?.localStorage;\n if (!storage) {\n // No localStorage at all is itself a strong signal on Safari < 11 private.\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n\n try {\n storage.setItem(PROBE_KEY, '1');\n storage.removeItem(PROBE_KEY);\n } catch {\n return {\n isPrivate: true,\n browser,\n confidence: 'medium',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n\n const win = globals.window;\n if (typeof win.openDatabase === 'function') {\n try {\n win.openDatabase(null, null, null, null);\n } catch {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n }\n\n return {\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n}\n\nasync function detectViaFirefoxIndexedDB(\n browser: BrowserName,\n globals: DetectionGlobals,\n): Promise<DetectionResult | null> {\n const indexedDB = globals.indexedDB ?? globals.window?.indexedDB;\n if (!indexedDB) {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n };\n }\n\n return new Promise<DetectionResult>((resolve) => {\n let request: IDBOpenDBRequest;\n try {\n request = indexedDB.open(PROBE_KEY);\n } catch {\n resolve({\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n return;\n }\n\n request.addEventListener('error', () => {\n resolve({\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n });\n request.addEventListener('success', () => {\n try {\n request.result.close();\n indexedDB.deleteDatabase(PROBE_KEY);\n } catch {\n /* best-effort cleanup */\n }\n resolve({\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n });\n });\n}\n\nfunction detectViaLegacyEdge(\n browser: BrowserName,\n globals: DetectionGlobals,\n): DetectionResult | null {\n const win = globals.window;\n if (!win) return null;\n const hasIndexedDB = Boolean(win.indexedDB ?? globals.indexedDB);\n const hasPointer = Boolean(win.PointerEvent ?? win.MSPointerEvent);\n if (!hasIndexedDB && hasPointer) {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'legacy-edge',\n };\n }\n return {\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'legacy-edge',\n };\n}\n","import { detectBrowser } from './browser.ts';\nimport { IncognitoDetectionError } from './errors.ts';\nimport { DEFAULT_PRIVATE_QUOTA_BYTES, runStrategies } from './strategies.ts';\nimport type {\n DetectIncognitoOptions,\n DetectionGlobals,\n DetectionResult,\n} from './types.ts';\n\n/**\n * Detect whether the current browser is in private / incognito mode.\n *\n * Returns a rich {@link DetectionResult}. Most callers want the thinner\n * {@link isIncognito} convenience instead.\n *\n * @throws {IncognitoDetectionError}\n * - `NOT_A_BROWSER` when invoked outside a browser-like environment.\n * - `UNSUPPORTED_BROWSER` when the browser was classified as `unknown` and\n * no fallback strategy applied.\n * - `PROBE_FAILED` when every applicable strategy threw before producing a\n * definitive answer.\n *\n * @example\n * ```ts\n * import { detectIncognito } from 'is-incognito-mode';\n *\n * const { isPrivate, browser, confidence, quota } = await detectIncognito();\n * console.log(`${browser} (${confidence} confidence, quota: ${quota ?? '?'})`);\n * ```\n */\nexport async function detectIncognito(\n options: DetectIncognitoOptions = {},\n): Promise<DetectionResult> {\n const globals = resolveGlobals(options.globals);\n if (!isBrowserLike(globals)) {\n throw new IncognitoDetectionError(\n 'NOT_A_BROWSER',\n 'is-incognito-mode can only run in a browser-like environment.',\n );\n }\n\n const browser = detectBrowser({\n userAgent: globals.navigator.userAgent,\n ...(globals.navigator.vendor === undefined\n ? {}\n : { vendor: globals.navigator.vendor }),\n });\n\n const threshold =\n options.privateQuotaThresholdBytes ?? DEFAULT_PRIVATE_QUOTA_BYTES;\n\n return runStrategies(browser, globals, {\n privateQuotaThresholdBytes: threshold,\n });\n}\n\n/**\n * Convenience wrapper around {@link detectIncognito} that resolves to the\n * boolean verdict only. Drop-in compatible with v1's default export.\n *\n * @example\n * ```ts\n * import { isIncognito } from 'is-incognito-mode';\n *\n * if (await isIncognito()) {\n * showPaywall();\n * }\n * ```\n *\n * @throws {IncognitoDetectionError} See {@link detectIncognito}.\n */\nexport async function isIncognito(\n options: DetectIncognitoOptions = {},\n): Promise<boolean> {\n const result = await detectIncognito(options);\n return result.isPrivate;\n}\n\nfunction resolveGlobals(\n overrides: DetectionGlobals | undefined,\n): DetectionGlobals {\n if (overrides) return overrides;\n return {\n navigator: typeof navigator === 'undefined' ? undefined : navigator,\n window: typeof window === 'undefined' ? undefined : window,\n indexedDB: typeof indexedDB === 'undefined' ? undefined : indexedDB,\n };\n}\n\nfunction isBrowserLike(\n globals: DetectionGlobals,\n): globals is DetectionGlobals & {\n navigator: NonNullable<DetectionGlobals['navigator']>;\n} {\n return Boolean(globals.navigator?.userAgent);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/browser.ts","../src/errors.ts","../src/strategies.ts","../src/detect.ts"],"names":["indexedDB"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Coarse browser-engine identifiers used to pick a detection strategy.\n *\n * We deliberately do not export brand names like \"Brave\" or \"Vivaldi\" — they\n * are all Chromium under the hood and the relevant private-mode behaviour\n * matches the engine, not the brand.\n */\nexport type BrowserName =\n | 'chromium'\n | 'firefox'\n | 'safari'\n | 'webkit'\n | 'edge-legacy'\n | 'ie'\n | 'unknown';\n\ninterface UserAgentSource {\n readonly userAgent: string;\n readonly vendor?: string;\n}\n\nconst CHROMIUM_HINTS = ['chrome/', 'crios/', 'chromium/', 'edg/', 'opr/'];\n\n/**\n * Classify the current navigator into a coarse browser engine.\n *\n * Pure function — accepts an optional `source` so it can be unit-tested\n * without polluting globals. In production we read from `navigator` directly.\n */\nexport function detectBrowser(source?: UserAgentSource): BrowserName {\n const ua = (source?.userAgent ?? '').toLowerCase();\n const vendor = (source?.vendor ?? '').toLowerCase();\n\n if (!ua) return 'unknown';\n\n if (ua.includes('msie ') || ua.includes('trident/')) return 'ie';\n if (ua.includes('edge/')) return 'edge-legacy';\n if (ua.includes('firefox/') || ua.includes('fxios/')) return 'firefox';\n\n const looksLikeChromium = CHROMIUM_HINTS.some((hint) => ua.includes(hint));\n if (looksLikeChromium) return 'chromium';\n\n // Safari sets `vendor === 'apple computer, inc.'`. iOS-Chrome/Firefox also\n // run on WebKit but advertise different UAs, so we reach this branch only\n // for \"real\" Safari and other WebKit shells.\n if (ua.includes('safari/') && (vendor.includes('apple') || !vendor)) {\n return 'safari';\n }\n if (ua.includes('applewebkit/')) return 'webkit';\n\n return 'unknown';\n}\n","/**\n * Stable identifiers for {@link IncognitoDetectionError} causes.\n *\n * Branching on `code` is safer than parsing `message` strings.\n */\nexport type IncognitoDetectionErrorCode =\n /** Invoked in a non-browser context (Node, worker without globals, etc.). */\n | 'NOT_A_BROWSER'\n /** Browser detected but no supported detection strategy applies. */\n | 'UNSUPPORTED_BROWSER'\n /** A probe threw before it could produce a definitive result. */\n | 'PROBE_FAILED';\n\n/**\n * Thrown when private-mode cannot be determined.\n *\n * @example\n * ```ts\n * import { isIncognito, IncognitoDetectionError } from 'is-incognito-mode';\n *\n * try {\n * await isIncognito();\n * } catch (error) {\n * if (error instanceof IncognitoDetectionError) {\n * console.warn(error.code, error.message);\n * }\n * }\n * ```\n */\nexport class IncognitoDetectionError extends Error {\n override readonly name = 'IncognitoDetectionError';\n readonly code: IncognitoDetectionErrorCode;\n\n constructor(\n code: IncognitoDetectionErrorCode,\n message: string,\n options?: { cause?: unknown },\n ) {\n super(message, options);\n this.code = code;\n }\n}\n","import type { BrowserName } from './browser.ts';\nimport { IncognitoDetectionError } from './errors.ts';\nimport type {\n DetectIncognitoOptions,\n DetectionGlobals,\n DetectionResult,\n} from './types.ts';\n\n/**\n * Fallback JS-heap ceiling (1 GiB) used by the Chromium strategy when\n * `performance.memory.jsHeapSizeLimit` is unavailable. Also exported as the\n * reference value for the `privateQuotaThresholdBytes` advanced override.\n */\nexport const DEFAULT_PRIVATE_QUOTA_BYTES = 1024 * 1024 * 1024;\n\n/**\n * The Chromium temporary-storage quota in an incognito tab stays below roughly\n * twice the JS-heap ceiling (it is memory-bound, not disk-bound). Normal tabs\n * are disk-bound and far above it.\n */\nconst HEAP_QUOTA_MULTIPLIER = 2;\n\n/**\n * Sentinel key used by the legacy localStorage / indexedDB probes. Random\n * enough that collisions with real user keys are impossible.\n */\nconst PROBE_KEY = '__is_incognito_mode_probe_cd1394e6__';\n\n/**\n * Try strategies in priority order, returning the first one that produces a\n * definitive answer. Throws {@link IncognitoDetectionError} if every strategy\n * declines.\n */\nexport async function runStrategies(\n browser: BrowserName,\n globals: DetectionGlobals,\n options: Pick<DetectIncognitoOptions, 'privateQuotaThresholdBytes'>,\n): Promise<DetectionResult> {\n const errors: unknown[] = [];\n\n switch (browser) {\n case 'chromium': {\n const result = await attempt(\n () => detectViaChromiumQuota(browser, globals, options),\n errors,\n );\n if (result !== null) return result;\n break;\n }\n case 'firefox': {\n const opfs = await attempt(\n () => detectViaOpfsProbe(browser, globals, 'Security error'),\n errors,\n );\n if (opfs !== null) return opfs;\n const idb = await attempt(\n () => detectViaFirefoxIndexedDB(browser, globals),\n errors,\n );\n if (idb !== null) return idb;\n break;\n }\n case 'safari':\n case 'webkit': {\n const opfs = await attempt(\n () => detectViaOpfsProbe(browser, globals, 'unknown transient reason'),\n errors,\n );\n if (opfs !== null) return opfs;\n const legacy = await attempt(\n () => detectViaSafariStorage(browser, globals),\n errors,\n );\n if (legacy !== null) return legacy;\n break;\n }\n case 'edge-legacy':\n case 'ie': {\n const result = await attempt(\n () => detectViaLegacyEdge(browser, globals),\n errors,\n );\n if (result !== null) return result;\n break;\n }\n case 'unknown': {\n break;\n }\n }\n\n throw new IncognitoDetectionError(\n browser === 'unknown' ? 'UNSUPPORTED_BROWSER' : 'PROBE_FAILED',\n `No detection strategy produced a verdict for browser \"${browser}\".`,\n errors.length > 0 ? { cause: errors[0] } : undefined,\n );\n}\n\nasync function attempt(\n strategy: () => DetectionResult | null | Promise<DetectionResult | null>,\n errors: unknown[],\n): Promise<DetectionResult | null> {\n try {\n return await strategy();\n } catch (error) {\n errors.push(error);\n return null;\n }\n}\n\n/**\n * Chromium family (Chrome, Edge, Brave, Opera, …).\n *\n * Modern Chrome deliberately fakes `navigator.storage.estimate().quota` at\n * `usage + 10 GiB` in *every* mode to defeat detection — so that API is\n * useless here. The legacy `navigator.webkitTemporaryStorage` API was not\n * given the same treatment and still reports the real per-origin quota:\n * disk-bound (huge) in normal mode, memory-bound (small) in incognito.\n *\n * We compare it to `performance.memory.jsHeapSizeLimit` — a per-renderer\n * constant that does not change between modes — so the test adapts to the\n * device instead of relying on a brittle fixed byte count.\n */\nasync function detectViaChromiumQuota(\n browser: BrowserName,\n globals: DetectionGlobals,\n options: Pick<DetectIncognitoOptions, 'privateQuotaThresholdBytes'>,\n): Promise<DetectionResult | null> {\n const tempStorage = globals.navigator?.webkitTemporaryStorage;\n if (typeof tempStorage?.queryUsageAndQuota !== 'function') return null;\n\n const quota = await new Promise<number | null>((resolve) => {\n let settled = false;\n const settle = (value: number | null): void => {\n if (!settled) {\n settled = true;\n resolve(value);\n }\n };\n try {\n tempStorage.queryUsageAndQuota(\n (_used, grantedQuota) => {\n settle(typeof grantedQuota === 'number' ? grantedQuota : null);\n },\n () => {\n settle(null);\n },\n );\n } catch {\n settle(null);\n }\n });\n if (quota === null) return null;\n\n const isPrivate =\n options.privateQuotaThresholdBytes === undefined\n ? quota < chromiumHeapLimit(globals) * HEAP_QUOTA_MULTIPLIER\n : quota < options.privateQuotaThresholdBytes;\n\n return {\n isPrivate,\n browser,\n confidence: 'high',\n quota,\n strategy: 'chromium-quota',\n };\n}\n\nfunction chromiumHeapLimit(globals: DetectionGlobals): number {\n const limit = globals.window?.performance?.memory?.jsHeapSizeLimit;\n return typeof limit === 'number' && limit > 0\n ? limit\n : DEFAULT_PRIVATE_QUOTA_BYTES;\n}\n\n/**\n * Firefox and Safari: the Origin Private File System\n * (`navigator.storage.getDirectory()`) is rejected in private mode. The\n * rejection message differs per engine — Firefox says `Security error`,\n * Safari says `unknown transient reason` — so the caller passes the fragment\n * to match. A successful resolve means a normal window.\n */\nasync function detectViaOpfsProbe(\n browser: BrowserName,\n globals: DetectionGlobals,\n privateErrorFragment: string,\n): Promise<DetectionResult | null> {\n const storage = globals.navigator?.storage;\n if (typeof storage?.getDirectory !== 'function') return null;\n\n try {\n await storage.getDirectory();\n return {\n isPrivate: false,\n browser,\n confidence: 'high',\n quota: null,\n strategy: 'opfs-probe',\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n isPrivate: message.includes(privateErrorFragment),\n browser,\n confidence: 'high',\n quota: null,\n strategy: 'opfs-probe',\n };\n }\n}\n\n/** Older Safari / WebKit without OPFS: localStorage + openDatabase probes. */\nfunction detectViaSafariStorage(\n browser: BrowserName,\n globals: DetectionGlobals,\n): DetectionResult | null {\n const storage = globals.window?.localStorage;\n if (!storage) {\n // No localStorage at all is a strong signal on Safari < 11 private.\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n\n try {\n storage.setItem(PROBE_KEY, '1');\n storage.removeItem(PROBE_KEY);\n } catch {\n return {\n isPrivate: true,\n browser,\n confidence: 'medium',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n\n const win = globals.window;\n if (typeof win.openDatabase === 'function') {\n try {\n win.openDatabase(null, null, null, null);\n } catch {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n }\n }\n\n return {\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'safari-storage',\n };\n}\n\n/** Older Firefox without OPFS: `indexedDB.open` fails in private mode. */\nasync function detectViaFirefoxIndexedDB(\n browser: BrowserName,\n globals: DetectionGlobals,\n): Promise<DetectionResult | null> {\n const indexedDB = globals.indexedDB ?? globals.window?.indexedDB;\n if (!indexedDB) {\n return {\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n };\n }\n\n return new Promise<DetectionResult>((resolve) => {\n let request: IDBOpenDBRequest;\n try {\n request = indexedDB.open(PROBE_KEY);\n } catch {\n resolve({\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n return;\n }\n\n request.addEventListener('error', () => {\n resolve({\n isPrivate: true,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n });\n request.addEventListener('success', () => {\n try {\n request.result.close();\n indexedDB.deleteDatabase(PROBE_KEY);\n } catch {\n /* best-effort cleanup */\n }\n resolve({\n isPrivate: false,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'firefox-indexeddb',\n });\n });\n });\n}\n\n/** Legacy Edge / IE: no `indexedDB` while `PointerEvent` exists → private. */\nfunction detectViaLegacyEdge(\n browser: BrowserName,\n globals: DetectionGlobals,\n): DetectionResult | null {\n const win = globals.window;\n if (!win) return null;\n const hasIndexedDB = Boolean(win.indexedDB ?? globals.indexedDB);\n const hasPointer = Boolean(win.PointerEvent ?? win.MSPointerEvent);\n return {\n isPrivate: !hasIndexedDB && hasPointer,\n browser,\n confidence: 'low',\n quota: null,\n strategy: 'legacy-edge',\n };\n}\n","import { detectBrowser } from './browser.ts';\nimport { IncognitoDetectionError } from './errors.ts';\nimport { runStrategies } from './strategies.ts';\nimport type {\n DetectIncognitoOptions,\n DetectionGlobals,\n DetectionResult,\n WindowLike,\n} from './types.ts';\n\n/**\n * Detect whether the current browser is in private / incognito mode.\n *\n * Returns a rich {@link DetectionResult}. Most callers want the thinner\n * {@link isIncognito} convenience instead.\n *\n * @throws {IncognitoDetectionError}\n * - `NOT_A_BROWSER` when invoked outside a browser-like environment.\n * - `UNSUPPORTED_BROWSER` when the browser was classified as `unknown` and\n * no fallback strategy applied.\n * - `PROBE_FAILED` when every applicable strategy threw before producing a\n * definitive answer.\n *\n * @example\n * ```ts\n * import { detectIncognito } from 'is-incognito-mode';\n *\n * const { isPrivate, browser, confidence, quota } = await detectIncognito();\n * console.log(`${browser} (${confidence} confidence, quota: ${quota ?? '?'})`);\n * ```\n */\nexport async function detectIncognito(\n options: DetectIncognitoOptions = {},\n): Promise<DetectionResult> {\n const globals = resolveGlobals(options.globals);\n if (!isBrowserLike(globals)) {\n throw new IncognitoDetectionError(\n 'NOT_A_BROWSER',\n 'is-incognito-mode can only run in a browser-like environment.',\n );\n }\n\n const browser = detectBrowser({\n userAgent: globals.navigator.userAgent,\n ...(globals.navigator.vendor === undefined\n ? {}\n : { vendor: globals.navigator.vendor }),\n });\n\n return runStrategies(\n browser,\n globals,\n options.privateQuotaThresholdBytes === undefined\n ? {}\n : { privateQuotaThresholdBytes: options.privateQuotaThresholdBytes },\n );\n}\n\n/**\n * Convenience wrapper around {@link detectIncognito} that resolves to the\n * boolean verdict only. Drop-in compatible with v1's default export.\n *\n * @example\n * ```ts\n * import { isIncognito } from 'is-incognito-mode';\n *\n * if (await isIncognito()) {\n * showPaywall();\n * }\n * ```\n *\n * @throws {IncognitoDetectionError} See {@link detectIncognito}.\n */\nexport async function isIncognito(\n options: DetectIncognitoOptions = {},\n): Promise<boolean> {\n const result = await detectIncognito(options);\n return result.isPrivate;\n}\n\nfunction resolveGlobals(\n overrides: DetectionGlobals | undefined,\n): DetectionGlobals {\n if (overrides) return overrides;\n // Boundary cast on `window`: the live object carries non-standard properties\n // the detector relies on (notably `performance.memory`) that the standard\n // DOM lib types do not declare.\n return {\n navigator: typeof navigator === 'undefined' ? undefined : navigator,\n window:\n typeof window === 'undefined'\n ? undefined\n : (window as unknown as WindowLike),\n indexedDB: typeof indexedDB === 'undefined' ? undefined : indexedDB,\n };\n}\n\nfunction isBrowserLike(\n globals: DetectionGlobals,\n): globals is DetectionGlobals & {\n navigator: NonNullable<DetectionGlobals['navigator']>;\n} {\n return Boolean(globals.navigator?.userAgent);\n}\n"]}
|
package/package.json
CHANGED