is-incognito-mode 2.0.2 → 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 +42 -0
- package/README.md +82 -61
- package/dist/index.cjs +100 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -31
- package/dist/index.d.ts +61 -31
- package/dist/index.js +100 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
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
|
+
|
|
3
45
|
## 2.0.2
|
|
4
46
|
|
|
5
47
|
### 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,37 +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
|
-
quota
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
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)).
|
|
138
152
|
|
|
139
153
|
---
|
|
140
154
|
|
|
@@ -159,7 +173,7 @@ const { isPrivate, browser, confidence, quota, strategy } =
|
|
|
159
173
|
console.log(
|
|
160
174
|
`${browser} (${confidence}) — strategy: ${strategy}, quota: ${quota}`,
|
|
161
175
|
);
|
|
162
|
-
// → "chromium (high) — strategy:
|
|
176
|
+
// → "chromium (high) — strategy: chromium-quota, quota: 629145600"
|
|
163
177
|
```
|
|
164
178
|
|
|
165
179
|
Fields on `DetectionResult`:
|
|
@@ -168,27 +182,29 @@ Fields on `DetectionResult`:
|
|
|
168
182
|
| ------------ | ----------------------------- | ----------------------------------------------------------------------------------------- |
|
|
169
183
|
| `isPrivate` | `boolean` | Final verdict. |
|
|
170
184
|
| `browser` | `BrowserName` | Coarse engine: `chromium`, `firefox`, `safari`, `webkit`, `edge-legacy`, `ie`, `unknown`. |
|
|
171
|
-
| `confidence` | `'high' \| 'medium' \| 'low'` | `high` for
|
|
172
|
-
| `quota` | `number \| null` |
|
|
173
|
-
| `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). |
|
|
174
188
|
|
|
175
|
-
### 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`:
|
|
176
195
|
|
|
177
196
|
```ts
|
|
178
|
-
import {
|
|
179
|
-
detectIncognito,
|
|
180
|
-
DEFAULT_PRIVATE_QUOTA_BYTES,
|
|
181
|
-
} from 'is-incognito-mode';
|
|
197
|
+
import { detectIncognito } from 'is-incognito-mode';
|
|
182
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.
|
|
183
201
|
const result = await detectIncognito({
|
|
184
|
-
privateQuotaThresholdBytes:
|
|
202
|
+
privateQuotaThresholdBytes: 2 * 1024 * 1024 * 1024,
|
|
185
203
|
});
|
|
186
204
|
```
|
|
187
205
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
threshold for your audience, or raise it on devices with very little physical
|
|
191
|
-
storage where normal mode itself may report less than 1 GiB.
|
|
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.
|
|
192
208
|
|
|
193
209
|
### Injecting globals (for testing)
|
|
194
210
|
|
|
@@ -202,14 +218,19 @@ const result = await detectIncognito({
|
|
|
202
218
|
globals: {
|
|
203
219
|
navigator: {
|
|
204
220
|
userAgent: 'Mozilla/5.0 ... Chrome/131.0',
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
},
|
|
207
226
|
},
|
|
208
227
|
},
|
|
209
|
-
window: {
|
|
228
|
+
window: {
|
|
229
|
+
performance: { memory: { jsHeapSizeLimit: 4 * 1024 * 1024 * 1024 } },
|
|
230
|
+
},
|
|
210
231
|
},
|
|
211
232
|
});
|
|
212
|
-
// result.isPrivate === true
|
|
233
|
+
// result.isPrivate === true (600 MiB < 2 × 4 GiB)
|
|
213
234
|
```
|
|
214
235
|
|
|
215
236
|
### Error handling
|
|
@@ -230,7 +251,7 @@ try {
|
|
|
230
251
|
// Probably a bot / curl / node-fetch
|
|
231
252
|
break;
|
|
232
253
|
case 'PROBE_FAILED':
|
|
233
|
-
//
|
|
254
|
+
// The engine's probe could not produce a verdict
|
|
234
255
|
break;
|
|
235
256
|
}
|
|
236
257
|
}
|
|
@@ -270,16 +291,16 @@ Full generated reference: **<https://yankouskia.github.io/is-incognito-mode/>**
|
|
|
270
291
|
|
|
271
292
|
### Browsers
|
|
272
293
|
|
|
273
|
-
| Engine | Detection strategy
|
|
274
|
-
| ---------------------- |
|
|
275
|
-
| Chromium ≥
|
|
276
|
-
| Firefox ≥
|
|
277
|
-
| Safari ≥
|
|
278
|
-
| Older Safari / WebKit | `localStorage` + `openDatabase` | medium-low |
|
|
279
|
-
| Older Firefox | `indexedDB.open` error path
|
|
280
|
-
| Edge (legacy) | `PointerEvent` heuristic
|
|
281
|
-
| IE 10–11 | `PointerEvent` heuristic
|
|
282
|
-
| 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` | — |
|
|
283
304
|
|
|
284
305
|
### Node / runtimes
|
|
285
306
|
|
|
@@ -297,15 +318,15 @@ Rollup, esbuild, Bun, and Deno.
|
|
|
297
318
|
|
|
298
319
|
## What's new in v2
|
|
299
320
|
|
|
300
|
-
| | v1.x | v2.0
|
|
301
|
-
| ------------------- | -------------------------------------------------------- |
|
|
302
|
-
| Detection technique | FileSystem API + IndexedDB + localStorage + PointerEvent |
|
|
303
|
-
| TypeScript | shipped JS only | strict TypeScript source, full `.d.ts`
|
|
304
|
-
| Module formats | UMD + CJS | ESM + CJS dual publish
|
|
305
|
-
| Dependencies | `get-browser` | **zero**
|
|
306
|
-
| Bundle size | ~3 kB min+gzip | **~1 kB min+gzip**
|
|
307
|
-
| Engines | Node ≥ 8 | Node ≥ 20
|
|
308
|
-
| 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` |
|
|
309
330
|
|
|
310
331
|
See [`BREAKING_CHANGES.md`](./BREAKING_CHANGES.md) for migration recipes
|
|
311
332
|
and [`DECISIONS.md`](./DECISIONS.md) for the reasoning behind each big call.
|
package/dist/index.cjs
CHANGED
|
@@ -32,32 +32,52 @@ var IncognitoDetectionError = class extends Error {
|
|
|
32
32
|
|
|
33
33
|
// src/strategies.ts
|
|
34
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: 1 GiB. Below this, every shipping browser we tested is in a\n * restricted (private) storage context; above, it is normal.\n *\n * History: pre-2022 Chrome capped incognito at ~120 MiB and that was the\n * canonical threshold. Chrome 110+ raised the incognito ceiling and modern\n * versions can report 500 MiB–1 GiB for an incognito tab on a desktop, so the\n * old 120 MiB threshold misses Chromium-family private mode. 1 GiB\n * (1024 × 1024 × 1024) is the value the actively maintained `detectIncognito`\n * library settled on; we match it.\n *\n * On normal-mode desktops the quota is typically 10–80 % of free disk —\n * tens or hundreds of GiB — so this threshold has substantial margin.\n *\n * Devices with very small total storage (low-end mobiles, restricted ChromeOS\n * profiles) may produce false positives. Override with\n * `privateQuotaThresholdBytes` if that matters to you.\n */\nexport const DEFAULT_PRIVATE_QUOTA_BYTES = 1024 * 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,31 +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
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
57
73
|
*/
|
|
58
74
|
readonly privateQuotaThresholdBytes?: number;
|
|
59
75
|
}
|
|
@@ -70,20 +86,47 @@ interface NavigatorLike {
|
|
|
70
86
|
readonly userAgent: string;
|
|
71
87
|
readonly vendor?: string;
|
|
72
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;
|
|
73
95
|
}
|
|
74
96
|
interface StorageManagerLike {
|
|
75
97
|
estimate?(): Promise<{
|
|
76
98
|
quota?: number;
|
|
77
99
|
usage?: number;
|
|
78
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;
|
|
79
110
|
}
|
|
80
111
|
interface WindowLike {
|
|
81
112
|
readonly indexedDB?: IDBFactory | undefined;
|
|
82
113
|
readonly localStorage?: StorageLike | undefined;
|
|
83
114
|
readonly PointerEvent?: unknown;
|
|
84
115
|
readonly MSPointerEvent?: unknown;
|
|
116
|
+
readonly performance?: PerformanceLike | undefined;
|
|
85
117
|
openDatabase?(name: string | null, version: string | null, displayName: string | null, estimatedSize: number | null): unknown;
|
|
86
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
|
+
}
|
|
87
130
|
interface StorageLike {
|
|
88
131
|
setItem(key: string, value: string): void;
|
|
89
132
|
removeItem(key: string): void;
|
|
@@ -165,23 +208,10 @@ declare class IncognitoDetectionError extends Error {
|
|
|
165
208
|
}
|
|
166
209
|
|
|
167
210
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* History: pre-2022 Chrome capped incognito at ~120 MiB and that was the
|
|
172
|
-
* canonical threshold. Chrome 110+ raised the incognito ceiling and modern
|
|
173
|
-
* versions can report 500 MiB–1 GiB for an incognito tab on a desktop, so the
|
|
174
|
-
* old 120 MiB threshold misses Chromium-family private mode. 1 GiB
|
|
175
|
-
* (1024 × 1024 × 1024) is the value the actively maintained `detectIncognito`
|
|
176
|
-
* library settled on; we match it.
|
|
177
|
-
*
|
|
178
|
-
* On normal-mode desktops the quota is typically 10–80 % of free disk —
|
|
179
|
-
* tens or hundreds of GiB — so this threshold has substantial margin.
|
|
180
|
-
*
|
|
181
|
-
* Devices with very small total storage (low-end mobiles, restricted ChromeOS
|
|
182
|
-
* profiles) may produce false positives. Override with
|
|
183
|
-
* `privateQuotaThresholdBytes` if that matters to you.
|
|
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.
|
|
184
214
|
*/
|
|
185
215
|
declare const DEFAULT_PRIVATE_QUOTA_BYTES: number;
|
|
186
216
|
|
|
187
|
-
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,31 +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
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
57
73
|
*/
|
|
58
74
|
readonly privateQuotaThresholdBytes?: number;
|
|
59
75
|
}
|
|
@@ -70,20 +86,47 @@ interface NavigatorLike {
|
|
|
70
86
|
readonly userAgent: string;
|
|
71
87
|
readonly vendor?: string;
|
|
72
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;
|
|
73
95
|
}
|
|
74
96
|
interface StorageManagerLike {
|
|
75
97
|
estimate?(): Promise<{
|
|
76
98
|
quota?: number;
|
|
77
99
|
usage?: number;
|
|
78
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;
|
|
79
110
|
}
|
|
80
111
|
interface WindowLike {
|
|
81
112
|
readonly indexedDB?: IDBFactory | undefined;
|
|
82
113
|
readonly localStorage?: StorageLike | undefined;
|
|
83
114
|
readonly PointerEvent?: unknown;
|
|
84
115
|
readonly MSPointerEvent?: unknown;
|
|
116
|
+
readonly performance?: PerformanceLike | undefined;
|
|
85
117
|
openDatabase?(name: string | null, version: string | null, displayName: string | null, estimatedSize: number | null): unknown;
|
|
86
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
|
+
}
|
|
87
130
|
interface StorageLike {
|
|
88
131
|
setItem(key: string, value: string): void;
|
|
89
132
|
removeItem(key: string): void;
|
|
@@ -165,23 +208,10 @@ declare class IncognitoDetectionError extends Error {
|
|
|
165
208
|
}
|
|
166
209
|
|
|
167
210
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* History: pre-2022 Chrome capped incognito at ~120 MiB and that was the
|
|
172
|
-
* canonical threshold. Chrome 110+ raised the incognito ceiling and modern
|
|
173
|
-
* versions can report 500 MiB–1 GiB for an incognito tab on a desktop, so the
|
|
174
|
-
* old 120 MiB threshold misses Chromium-family private mode. 1 GiB
|
|
175
|
-
* (1024 × 1024 × 1024) is the value the actively maintained `detectIncognito`
|
|
176
|
-
* library settled on; we match it.
|
|
177
|
-
*
|
|
178
|
-
* On normal-mode desktops the quota is typically 10–80 % of free disk —
|
|
179
|
-
* tens or hundreds of GiB — so this threshold has substantial margin.
|
|
180
|
-
*
|
|
181
|
-
* Devices with very small total storage (low-end mobiles, restricted ChromeOS
|
|
182
|
-
* profiles) may produce false positives. Override with
|
|
183
|
-
* `privateQuotaThresholdBytes` if that matters to you.
|
|
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.
|
|
184
214
|
*/
|
|
185
215
|
declare const DEFAULT_PRIVATE_QUOTA_BYTES: number;
|
|
186
216
|
|
|
187
|
-
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
|
@@ -28,32 +28,52 @@ var IncognitoDetectionError = class extends Error {
|
|
|
28
28
|
|
|
29
29
|
// src/strategies.ts
|
|
30
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: 1 GiB. Below this, every shipping browser we tested is in a\n * restricted (private) storage context; above, it is normal.\n *\n * History: pre-2022 Chrome capped incognito at ~120 MiB and that was the\n * canonical threshold. Chrome 110+ raised the incognito ceiling and modern\n * versions can report 500 MiB–1 GiB for an incognito tab on a desktop, so the\n * old 120 MiB threshold misses Chromium-family private mode. 1 GiB\n * (1024 × 1024 × 1024) is the value the actively maintained `detectIncognito`\n * library settled on; we match it.\n *\n * On normal-mode desktops the quota is typically 10–80 % of free disk —\n * tens or hundreds of GiB — so this threshold has substantial margin.\n *\n * Devices with very small total storage (low-end mobiles, restricted ChromeOS\n * profiles) may produce false positives. Override with\n * `privateQuotaThresholdBytes` if that matters to you.\n */\nexport const DEFAULT_PRIVATE_QUOTA_BYTES = 1024 * 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