is-incognito-mode 2.0.1 → 2.1.0

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