@uekichinos/browser-gate 0.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 ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@uekichinos/browser-gate` are documented here.
4
+
5
+ ## [0.1.0] - 2026-04-11
6
+ ### Added
7
+ - Initial release
8
+ - `browserGate()` — detect outdated browsers and redirect or callback
9
+ - Feature detection mode (default) — checks globalThis, fetch, Promise.allSettled, IntersectionObserver, CSS.supports
10
+ - Minimum version mode — configurable per-browser version thresholds
11
+ - Latest version mode — live check via endoflife.date API with optional tolerance
12
+ - All three modes combinable — any failure triggers the outdated handler
13
+ - ESM, CJS, and IIFE builds
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # @uekichinos/browser-gate
2
+
3
+ Detect outdated browsers and redirect or block access. Supports three detection modes — use one, two, or all three together.
4
+
5
+ - **Feature detection** (default) — checks for modern browser APIs
6
+ - **Minimum version** — fails if browser version is below your threshold
7
+ - **Latest version** — live check via [endoflife.date](https://endoflife.date) API
8
+
9
+ Zero dependencies. Works via ESM, CommonJS, or `<script>` tag.
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @uekichinos/browser-gate
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Quick start
22
+
23
+ Place in `<head>` — before your app loads — so outdated browsers are caught early.
24
+
25
+ ```html
26
+ <script type="module">
27
+ import { browserGate } from '@uekichinos/browser-gate'
28
+ await browserGate({ redirect: '/outdated' })
29
+ </script>
30
+ ```
31
+
32
+ ---
33
+
34
+ ## API
35
+
36
+ ### `browserGate(options)`
37
+
38
+ Returns a `Promise<void>`. Resolves silently if the browser passes all checks. Redirects or calls `onOutdated` if it fails.
39
+
40
+ ```ts
41
+ await browserGate(options: BrowserGateOptions): Promise<void>
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Options
47
+
48
+ | Option | Type | Default | Description |
49
+ |---|---|---|---|
50
+ | `redirect` | `string` | — | URL to redirect to when outdated |
51
+ | `onOutdated` | `(info: OutdatedInfo) => void` | — | Callback instead of redirect |
52
+ | `features` | `FeatureKey[] \| true \| false` | `true` | Feature detection (see below) |
53
+ | `minVersions` | `{ chrome?: number, firefox?: number, safari?: number, edge?: number, opera?: number }` | — | Minimum version per browser |
54
+ | `checkLatest` | `boolean \| { tolerance?: number }` | — | Live latest-version check |
55
+
56
+ Either `redirect` or `onOutdated` must be provided.
57
+
58
+ ---
59
+
60
+ ## Detection modes
61
+
62
+ ### Mode 1 — Feature detection (default)
63
+
64
+ Runs automatically. Checks whether the browser supports five modern APIs:
65
+
66
+ | Feature | Absent in |
67
+ |---|---|
68
+ | `globalThis` | IE11, very old browsers |
69
+ | `fetch` | IE11 |
70
+ | `Promise.allSettled` | Chrome < 76, Firefox < 71, Safari < 13 |
71
+ | `IntersectionObserver` | IE11, old Safari |
72
+ | `CSS.supports` | IE11 |
73
+
74
+ ```js
75
+ // Default — checks all five features
76
+ await browserGate({ redirect: '/outdated' })
77
+
78
+ // Custom feature list
79
+ await browserGate({
80
+ redirect: '/outdated',
81
+ features: ['fetch', 'IntersectionObserver'],
82
+ })
83
+
84
+ // Disable feature detection
85
+ await browserGate({
86
+ redirect: '/outdated',
87
+ features: false,
88
+ minVersions: { chrome: 100 },
89
+ })
90
+ ```
91
+
92
+ ### Mode 2 — Minimum version
93
+
94
+ Checks the detected browser version against your thresholds. Only browsers you list are checked — others pass through.
95
+
96
+ Uses User-Agent parsing. Note: UA strings can be spoofed, so this is best combined with feature detection.
97
+
98
+ ```js
99
+ await browserGate({
100
+ redirect: '/outdated',
101
+ minVersions: {
102
+ chrome: 100,
103
+ firefox: 100,
104
+ safari: 15,
105
+ edge: 100,
106
+ },
107
+ })
108
+ ```
109
+
110
+ ### Mode 3 — Latest version (async)
111
+
112
+ Fetches live version data from [endoflife.date](https://endoflife.date) and checks whether the browser is up to date.
113
+
114
+ Use `tolerance` to allow a few versions behind (useful since Chrome releases every 4 weeks).
115
+
116
+ Fails open on network error, timeout (5s), or unrecognised browser — your users are never blocked due to an API outage.
117
+
118
+ ```js
119
+ // Must be on latest
120
+ await browserGate({ redirect: '/outdated', checkLatest: true })
121
+
122
+ // Allow up to 2 versions behind
123
+ await browserGate({ redirect: '/outdated', checkLatest: { tolerance: 2 } })
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Combining all three modes
129
+
130
+ Any failing check triggers the outdated handler.
131
+
132
+ ```js
133
+ await browserGate({
134
+ redirect: '/outdated',
135
+ features: ['fetch', 'IntersectionObserver', 'CSS.supports'],
136
+ minVersions: { chrome: 100, safari: 15 },
137
+ checkLatest: { tolerance: 2 },
138
+ })
139
+ ```
140
+
141
+ ---
142
+
143
+ ## `onOutdated` callback
144
+
145
+ Use `onOutdated` instead of `redirect` to handle the outdated case yourself.
146
+
147
+ ```js
148
+ await browserGate({
149
+ onOutdated: (info) => {
150
+ console.log(info.browser) // 'chrome'
151
+ console.log(info.version) // '80'
152
+ console.log(info.reasons)
153
+ // [
154
+ // 'Missing feature: IntersectionObserver',
155
+ // 'Below minimum version: chrome >= 100 (detected: 80)',
156
+ // ]
157
+
158
+ document.body.innerHTML = `<p>Please update your browser.</p>`
159
+ },
160
+ })
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Via `<script>` tag (no bundler)
166
+
167
+ ```html
168
+ <script src="https://unpkg.com/@uekichinos/browser-gate/dist/index.global.js"></script>
169
+ <script>
170
+ BrowserGate.browserGate({ redirect: '/outdated' })
171
+ </script>
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Supported browsers detected
177
+
178
+ | Browser | Detected via |
179
+ |---|---|
180
+ | Chrome | `Chrome/XX` in UA |
181
+ | Firefox | `Firefox/XX` in UA |
182
+ | Safari | `Version/XX Safari` in UA |
183
+ | Edge (Chromium) | `Edg/XX` in UA |
184
+ | Edge (legacy) | `Edge/XX` in UA |
185
+ | Opera | `OPR/XX` in UA |
186
+
187
+ Unrecognised browsers always pass through.
188
+
189
+ ---
190
+
191
+ ## License
192
+
193
+ MIT © [uekichinos](https://www.npmjs.com/~uekichinos)
package/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|-----------|
7
+ | 0.1.x | Yes |
8
+
9
+ ## Reporting a Vulnerability
10
+
11
+ If you discover a security vulnerability, please **do not** open a public GitHub issue.
12
+
13
+ Instead, report it privately via GitHub:
14
+ [https://github.com/uekichinos/browser-gate/security/advisories/new](https://github.com/uekichinos/browser-gate/security/advisories/new)
15
+
16
+ Please include:
17
+ - A description of the vulnerability
18
+ - Steps to reproduce
19
+ - Potential impact
20
+
21
+ You can expect a response within **72 hours**. If the vulnerability is confirmed, a fix will be released as soon as possible and credited to you (unless you prefer to remain anonymous).
package/dist/index.cjs ADDED
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ browserGate: () => browserGate
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/detect-features.ts
28
+ var DEFAULT_FEATURES = [
29
+ "globalThis",
30
+ "fetch",
31
+ "Promise.allSettled",
32
+ "IntersectionObserver",
33
+ "CSS.supports"
34
+ ];
35
+ function detectMissingFeatures(features) {
36
+ return features.filter((f) => !hasFeature(f));
37
+ }
38
+ function hasFeature(key) {
39
+ switch (key) {
40
+ case "globalThis":
41
+ return typeof globalThis !== "undefined";
42
+ case "fetch":
43
+ return typeof fetch !== "undefined";
44
+ case "Promise.allSettled":
45
+ return typeof Promise !== "undefined" && typeof Promise.allSettled === "function";
46
+ case "IntersectionObserver":
47
+ return typeof IntersectionObserver !== "undefined";
48
+ case "CSS.supports":
49
+ return typeof CSS !== "undefined" && typeof CSS.supports === "function";
50
+ }
51
+ }
52
+
53
+ // src/detect-latest.ts
54
+ var ENDOFLIFE_URLS = {
55
+ chrome: "https://endoflife.date/api/chrome.json",
56
+ firefox: "https://endoflife.date/api/firefox.json",
57
+ safari: "https://endoflife.date/api/safari.json",
58
+ edge: "https://endoflife.date/api/edge.json"
59
+ };
60
+ var FETCH_TIMEOUT_MS = 5e3;
61
+ async function checkLatestVersion(browser, tolerance = 0) {
62
+ if (browser.name === "unknown" || browser.version === null) {
63
+ return { passed: true };
64
+ }
65
+ const url = ENDOFLIFE_URLS[browser.name];
66
+ if (!url) return { passed: true };
67
+ try {
68
+ const controller = new AbortController();
69
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
70
+ const res = await fetch(url, { signal: controller.signal });
71
+ clearTimeout(timer);
72
+ if (!res.ok) return { passed: true };
73
+ const data = await res.json();
74
+ if (!Array.isArray(data) || data.length === 0) return { passed: true };
75
+ const latestCycle = parseInt(data[0].cycle, 10);
76
+ if (isNaN(latestCycle)) return { passed: true };
77
+ const diff = latestCycle - browser.version;
78
+ if (diff > tolerance) {
79
+ return {
80
+ passed: false,
81
+ reason: `Outdated: ${browser.name} ${browser.version} is ${diff} version(s) behind latest (${latestCycle})`
82
+ };
83
+ }
84
+ return { passed: true };
85
+ } catch {
86
+ return { passed: true };
87
+ }
88
+ }
89
+
90
+ // src/ua-parser.ts
91
+ function parseUA(ua) {
92
+ const edgeMatch = ua.match(/Edg\/(\d+)/);
93
+ if (edgeMatch) return { name: "edge", version: parseInt(edgeMatch[1], 10) };
94
+ const edgeLegacyMatch = ua.match(/Edge\/(\d+)/);
95
+ if (edgeLegacyMatch) return { name: "edge", version: parseInt(edgeLegacyMatch[1], 10) };
96
+ const operaMatch = ua.match(/OPR\/(\d+)/);
97
+ if (operaMatch) return { name: "opera", version: parseInt(operaMatch[1], 10) };
98
+ const chromeMatch = ua.match(/Chrome\/(\d+)/);
99
+ if (chromeMatch) return { name: "chrome", version: parseInt(chromeMatch[1], 10) };
100
+ const firefoxMatch = ua.match(/Firefox\/(\d+)/);
101
+ if (firefoxMatch) return { name: "firefox", version: parseInt(firefoxMatch[1], 10) };
102
+ const safariMatch = ua.match(/Version\/(\d+).*Safari/);
103
+ if (safariMatch) return { name: "safari", version: parseInt(safariMatch[1], 10) };
104
+ return { name: "unknown", version: null };
105
+ }
106
+
107
+ // src/detect-version.ts
108
+ function checkMinVersions(minVersions, ua) {
109
+ const browser = parseUA(ua);
110
+ if (browser.name === "unknown" || browser.version === null) {
111
+ return { passed: true };
112
+ }
113
+ const min = minVersions[browser.name];
114
+ if (min === void 0) return { passed: true };
115
+ if (browser.version < min) {
116
+ return {
117
+ passed: false,
118
+ reason: `Below minimum version: ${browser.name} >= ${min} (detected: ${browser.version})`
119
+ };
120
+ }
121
+ return { passed: true };
122
+ }
123
+
124
+ // src/index.ts
125
+ async function browserGate(options) {
126
+ const ua = navigator.userAgent;
127
+ const browser = parseUA(ua);
128
+ const reasons = [];
129
+ if (options.features !== false) {
130
+ const featuresToCheck = options.features === true || options.features === void 0 ? DEFAULT_FEATURES : options.features;
131
+ const missing = detectMissingFeatures(featuresToCheck);
132
+ for (const f of missing) {
133
+ reasons.push(`Missing feature: ${f}`);
134
+ }
135
+ }
136
+ if (options.minVersions) {
137
+ const result = checkMinVersions(options.minVersions, ua);
138
+ if (!result.passed && result.reason) reasons.push(result.reason);
139
+ }
140
+ if (options.checkLatest) {
141
+ const tolerance = typeof options.checkLatest === "object" ? options.checkLatest.tolerance ?? 0 : 0;
142
+ const result = await checkLatestVersion(browser, tolerance);
143
+ if (!result.passed && result.reason) reasons.push(result.reason);
144
+ }
145
+ if (reasons.length === 0) return;
146
+ const info = {
147
+ browser: browser.name,
148
+ version: browser.version !== null ? String(browser.version) : null,
149
+ reasons
150
+ };
151
+ if (options.onOutdated) {
152
+ options.onOutdated(info);
153
+ } else if (options.redirect) {
154
+ window.location.href = options.redirect;
155
+ }
156
+ }
157
+ // Annotate the CommonJS export names for ESM import in node:
158
+ 0 && (module.exports = {
159
+ browserGate
160
+ });
161
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,71 @@
1
+ type BrowserName = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
2
+ type FeatureKey = 'globalThis' | 'fetch' | 'Promise.allSettled' | 'IntersectionObserver' | 'CSS.supports';
3
+ interface OutdatedInfo {
4
+ /** Detected browser name e.g. `'chrome'`, or `'unknown'` if undetected */
5
+ browser: string;
6
+ /** Detected major version e.g. `'98'`, or `null` if undetected */
7
+ version: string | null;
8
+ /** List of reasons the browser was considered outdated */
9
+ reasons: string[];
10
+ }
11
+ interface BrowserGateOptions {
12
+ /**
13
+ * URL to redirect to when the browser is outdated.
14
+ * Either `redirect` or `onOutdated` must be provided.
15
+ */
16
+ redirect?: string;
17
+ /**
18
+ * Called with outdated browser info instead of redirecting.
19
+ * Either `redirect` or `onOutdated` must be provided.
20
+ */
21
+ onOutdated?: (info: OutdatedInfo) => void;
22
+ /**
23
+ * Feature detection mode (default — always runs unless set to `false`).
24
+ * - `true` or omitted — checks the default feature set
25
+ * - `FeatureKey[]` — checks only the specified features
26
+ * - `false` — disables feature detection entirely
27
+ */
28
+ features?: FeatureKey[] | boolean;
29
+ /**
30
+ * Minimum version mode — fail if the detected browser version is below the threshold.
31
+ * Only browsers listed here are checked; others are allowed through.
32
+ *
33
+ * @example
34
+ * { chrome: 100, firefox: 100, safari: 15, edge: 100 }
35
+ */
36
+ minVersions?: Partial<Record<BrowserName, number>>;
37
+ /**
38
+ * Latest version mode — fetch live data from endoflife.date and fail if
39
+ * the browser is behind the latest release (within optional tolerance).
40
+ * - `true` — zero tolerance (must be on latest)
41
+ * - `{ tolerance: N }` — allow up to N versions behind latest
42
+ */
43
+ checkLatest?: boolean | {
44
+ tolerance?: number;
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Checks whether the current browser is outdated and redirects or calls
50
+ * `onOutdated` if it is. Resolves without doing anything if the browser passes.
51
+ *
52
+ * Three detection modes — use one, two, or all three together:
53
+ * - **Feature detection** (default) — checks for modern browser APIs
54
+ * - **Minimum version** — fails if browser version is below your threshold
55
+ * - **Latest version** — fetches live data from endoflife.date (async)
56
+ *
57
+ * @example
58
+ * // Default — feature detection only
59
+ * await browserGate({ redirect: '/outdated' })
60
+ *
61
+ * @example
62
+ * // All three combined
63
+ * await browserGate({
64
+ * redirect: '/outdated',
65
+ * minVersions: { chrome: 100, safari: 15 },
66
+ * checkLatest: { tolerance: 2 },
67
+ * })
68
+ */
69
+ declare function browserGate(options: BrowserGateOptions): Promise<void>;
70
+
71
+ export { type BrowserGateOptions, type BrowserName, type FeatureKey, type OutdatedInfo, browserGate };
@@ -0,0 +1,71 @@
1
+ type BrowserName = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
2
+ type FeatureKey = 'globalThis' | 'fetch' | 'Promise.allSettled' | 'IntersectionObserver' | 'CSS.supports';
3
+ interface OutdatedInfo {
4
+ /** Detected browser name e.g. `'chrome'`, or `'unknown'` if undetected */
5
+ browser: string;
6
+ /** Detected major version e.g. `'98'`, or `null` if undetected */
7
+ version: string | null;
8
+ /** List of reasons the browser was considered outdated */
9
+ reasons: string[];
10
+ }
11
+ interface BrowserGateOptions {
12
+ /**
13
+ * URL to redirect to when the browser is outdated.
14
+ * Either `redirect` or `onOutdated` must be provided.
15
+ */
16
+ redirect?: string;
17
+ /**
18
+ * Called with outdated browser info instead of redirecting.
19
+ * Either `redirect` or `onOutdated` must be provided.
20
+ */
21
+ onOutdated?: (info: OutdatedInfo) => void;
22
+ /**
23
+ * Feature detection mode (default — always runs unless set to `false`).
24
+ * - `true` or omitted — checks the default feature set
25
+ * - `FeatureKey[]` — checks only the specified features
26
+ * - `false` — disables feature detection entirely
27
+ */
28
+ features?: FeatureKey[] | boolean;
29
+ /**
30
+ * Minimum version mode — fail if the detected browser version is below the threshold.
31
+ * Only browsers listed here are checked; others are allowed through.
32
+ *
33
+ * @example
34
+ * { chrome: 100, firefox: 100, safari: 15, edge: 100 }
35
+ */
36
+ minVersions?: Partial<Record<BrowserName, number>>;
37
+ /**
38
+ * Latest version mode — fetch live data from endoflife.date and fail if
39
+ * the browser is behind the latest release (within optional tolerance).
40
+ * - `true` — zero tolerance (must be on latest)
41
+ * - `{ tolerance: N }` — allow up to N versions behind latest
42
+ */
43
+ checkLatest?: boolean | {
44
+ tolerance?: number;
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Checks whether the current browser is outdated and redirects or calls
50
+ * `onOutdated` if it is. Resolves without doing anything if the browser passes.
51
+ *
52
+ * Three detection modes — use one, two, or all three together:
53
+ * - **Feature detection** (default) — checks for modern browser APIs
54
+ * - **Minimum version** — fails if browser version is below your threshold
55
+ * - **Latest version** — fetches live data from endoflife.date (async)
56
+ *
57
+ * @example
58
+ * // Default — feature detection only
59
+ * await browserGate({ redirect: '/outdated' })
60
+ *
61
+ * @example
62
+ * // All three combined
63
+ * await browserGate({
64
+ * redirect: '/outdated',
65
+ * minVersions: { chrome: 100, safari: 15 },
66
+ * checkLatest: { tolerance: 2 },
67
+ * })
68
+ */
69
+ declare function browserGate(options: BrowserGateOptions): Promise<void>;
70
+
71
+ export { type BrowserGateOptions, type BrowserName, type FeatureKey, type OutdatedInfo, browserGate };
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ var BrowserGate = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ browserGate: () => browserGate
25
+ });
26
+
27
+ // src/detect-features.ts
28
+ var DEFAULT_FEATURES = [
29
+ "globalThis",
30
+ "fetch",
31
+ "Promise.allSettled",
32
+ "IntersectionObserver",
33
+ "CSS.supports"
34
+ ];
35
+ function detectMissingFeatures(features) {
36
+ return features.filter((f) => !hasFeature(f));
37
+ }
38
+ function hasFeature(key) {
39
+ switch (key) {
40
+ case "globalThis":
41
+ return typeof globalThis !== "undefined";
42
+ case "fetch":
43
+ return typeof fetch !== "undefined";
44
+ case "Promise.allSettled":
45
+ return typeof Promise !== "undefined" && typeof Promise.allSettled === "function";
46
+ case "IntersectionObserver":
47
+ return typeof IntersectionObserver !== "undefined";
48
+ case "CSS.supports":
49
+ return typeof CSS !== "undefined" && typeof CSS.supports === "function";
50
+ }
51
+ }
52
+
53
+ // src/detect-latest.ts
54
+ var ENDOFLIFE_URLS = {
55
+ chrome: "https://endoflife.date/api/chrome.json",
56
+ firefox: "https://endoflife.date/api/firefox.json",
57
+ safari: "https://endoflife.date/api/safari.json",
58
+ edge: "https://endoflife.date/api/edge.json"
59
+ };
60
+ var FETCH_TIMEOUT_MS = 5e3;
61
+ async function checkLatestVersion(browser, tolerance = 0) {
62
+ if (browser.name === "unknown" || browser.version === null) {
63
+ return { passed: true };
64
+ }
65
+ const url = ENDOFLIFE_URLS[browser.name];
66
+ if (!url) return { passed: true };
67
+ try {
68
+ const controller = new AbortController();
69
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
70
+ const res = await fetch(url, { signal: controller.signal });
71
+ clearTimeout(timer);
72
+ if (!res.ok) return { passed: true };
73
+ const data = await res.json();
74
+ if (!Array.isArray(data) || data.length === 0) return { passed: true };
75
+ const latestCycle = parseInt(data[0].cycle, 10);
76
+ if (isNaN(latestCycle)) return { passed: true };
77
+ const diff = latestCycle - browser.version;
78
+ if (diff > tolerance) {
79
+ return {
80
+ passed: false,
81
+ reason: `Outdated: ${browser.name} ${browser.version} is ${diff} version(s) behind latest (${latestCycle})`
82
+ };
83
+ }
84
+ return { passed: true };
85
+ } catch {
86
+ return { passed: true };
87
+ }
88
+ }
89
+
90
+ // src/ua-parser.ts
91
+ function parseUA(ua) {
92
+ const edgeMatch = ua.match(/Edg\/(\d+)/);
93
+ if (edgeMatch) return { name: "edge", version: parseInt(edgeMatch[1], 10) };
94
+ const edgeLegacyMatch = ua.match(/Edge\/(\d+)/);
95
+ if (edgeLegacyMatch) return { name: "edge", version: parseInt(edgeLegacyMatch[1], 10) };
96
+ const operaMatch = ua.match(/OPR\/(\d+)/);
97
+ if (operaMatch) return { name: "opera", version: parseInt(operaMatch[1], 10) };
98
+ const chromeMatch = ua.match(/Chrome\/(\d+)/);
99
+ if (chromeMatch) return { name: "chrome", version: parseInt(chromeMatch[1], 10) };
100
+ const firefoxMatch = ua.match(/Firefox\/(\d+)/);
101
+ if (firefoxMatch) return { name: "firefox", version: parseInt(firefoxMatch[1], 10) };
102
+ const safariMatch = ua.match(/Version\/(\d+).*Safari/);
103
+ if (safariMatch) return { name: "safari", version: parseInt(safariMatch[1], 10) };
104
+ return { name: "unknown", version: null };
105
+ }
106
+
107
+ // src/detect-version.ts
108
+ function checkMinVersions(minVersions, ua) {
109
+ const browser = parseUA(ua);
110
+ if (browser.name === "unknown" || browser.version === null) {
111
+ return { passed: true };
112
+ }
113
+ const min = minVersions[browser.name];
114
+ if (min === void 0) return { passed: true };
115
+ if (browser.version < min) {
116
+ return {
117
+ passed: false,
118
+ reason: `Below minimum version: ${browser.name} >= ${min} (detected: ${browser.version})`
119
+ };
120
+ }
121
+ return { passed: true };
122
+ }
123
+
124
+ // src/index.ts
125
+ async function browserGate(options) {
126
+ const ua = navigator.userAgent;
127
+ const browser = parseUA(ua);
128
+ const reasons = [];
129
+ if (options.features !== false) {
130
+ const featuresToCheck = options.features === true || options.features === void 0 ? DEFAULT_FEATURES : options.features;
131
+ const missing = detectMissingFeatures(featuresToCheck);
132
+ for (const f of missing) {
133
+ reasons.push(`Missing feature: ${f}`);
134
+ }
135
+ }
136
+ if (options.minVersions) {
137
+ const result = checkMinVersions(options.minVersions, ua);
138
+ if (!result.passed && result.reason) reasons.push(result.reason);
139
+ }
140
+ if (options.checkLatest) {
141
+ const tolerance = typeof options.checkLatest === "object" ? options.checkLatest.tolerance ?? 0 : 0;
142
+ const result = await checkLatestVersion(browser, tolerance);
143
+ if (!result.passed && result.reason) reasons.push(result.reason);
144
+ }
145
+ if (reasons.length === 0) return;
146
+ const info = {
147
+ browser: browser.name,
148
+ version: browser.version !== null ? String(browser.version) : null,
149
+ reasons
150
+ };
151
+ if (options.onOutdated) {
152
+ options.onOutdated(info);
153
+ } else if (options.redirect) {
154
+ window.location.href = options.redirect;
155
+ }
156
+ }
157
+ return __toCommonJS(index_exports);
158
+ })();
159
+ //# sourceMappingURL=index.global.js.map
package/dist/index.js ADDED
@@ -0,0 +1,134 @@
1
+ // src/detect-features.ts
2
+ var DEFAULT_FEATURES = [
3
+ "globalThis",
4
+ "fetch",
5
+ "Promise.allSettled",
6
+ "IntersectionObserver",
7
+ "CSS.supports"
8
+ ];
9
+ function detectMissingFeatures(features) {
10
+ return features.filter((f) => !hasFeature(f));
11
+ }
12
+ function hasFeature(key) {
13
+ switch (key) {
14
+ case "globalThis":
15
+ return typeof globalThis !== "undefined";
16
+ case "fetch":
17
+ return typeof fetch !== "undefined";
18
+ case "Promise.allSettled":
19
+ return typeof Promise !== "undefined" && typeof Promise.allSettled === "function";
20
+ case "IntersectionObserver":
21
+ return typeof IntersectionObserver !== "undefined";
22
+ case "CSS.supports":
23
+ return typeof CSS !== "undefined" && typeof CSS.supports === "function";
24
+ }
25
+ }
26
+
27
+ // src/detect-latest.ts
28
+ var ENDOFLIFE_URLS = {
29
+ chrome: "https://endoflife.date/api/chrome.json",
30
+ firefox: "https://endoflife.date/api/firefox.json",
31
+ safari: "https://endoflife.date/api/safari.json",
32
+ edge: "https://endoflife.date/api/edge.json"
33
+ };
34
+ var FETCH_TIMEOUT_MS = 5e3;
35
+ async function checkLatestVersion(browser, tolerance = 0) {
36
+ if (browser.name === "unknown" || browser.version === null) {
37
+ return { passed: true };
38
+ }
39
+ const url = ENDOFLIFE_URLS[browser.name];
40
+ if (!url) return { passed: true };
41
+ try {
42
+ const controller = new AbortController();
43
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
44
+ const res = await fetch(url, { signal: controller.signal });
45
+ clearTimeout(timer);
46
+ if (!res.ok) return { passed: true };
47
+ const data = await res.json();
48
+ if (!Array.isArray(data) || data.length === 0) return { passed: true };
49
+ const latestCycle = parseInt(data[0].cycle, 10);
50
+ if (isNaN(latestCycle)) return { passed: true };
51
+ const diff = latestCycle - browser.version;
52
+ if (diff > tolerance) {
53
+ return {
54
+ passed: false,
55
+ reason: `Outdated: ${browser.name} ${browser.version} is ${diff} version(s) behind latest (${latestCycle})`
56
+ };
57
+ }
58
+ return { passed: true };
59
+ } catch {
60
+ return { passed: true };
61
+ }
62
+ }
63
+
64
+ // src/ua-parser.ts
65
+ function parseUA(ua) {
66
+ const edgeMatch = ua.match(/Edg\/(\d+)/);
67
+ if (edgeMatch) return { name: "edge", version: parseInt(edgeMatch[1], 10) };
68
+ const edgeLegacyMatch = ua.match(/Edge\/(\d+)/);
69
+ if (edgeLegacyMatch) return { name: "edge", version: parseInt(edgeLegacyMatch[1], 10) };
70
+ const operaMatch = ua.match(/OPR\/(\d+)/);
71
+ if (operaMatch) return { name: "opera", version: parseInt(operaMatch[1], 10) };
72
+ const chromeMatch = ua.match(/Chrome\/(\d+)/);
73
+ if (chromeMatch) return { name: "chrome", version: parseInt(chromeMatch[1], 10) };
74
+ const firefoxMatch = ua.match(/Firefox\/(\d+)/);
75
+ if (firefoxMatch) return { name: "firefox", version: parseInt(firefoxMatch[1], 10) };
76
+ const safariMatch = ua.match(/Version\/(\d+).*Safari/);
77
+ if (safariMatch) return { name: "safari", version: parseInt(safariMatch[1], 10) };
78
+ return { name: "unknown", version: null };
79
+ }
80
+
81
+ // src/detect-version.ts
82
+ function checkMinVersions(minVersions, ua) {
83
+ const browser = parseUA(ua);
84
+ if (browser.name === "unknown" || browser.version === null) {
85
+ return { passed: true };
86
+ }
87
+ const min = minVersions[browser.name];
88
+ if (min === void 0) return { passed: true };
89
+ if (browser.version < min) {
90
+ return {
91
+ passed: false,
92
+ reason: `Below minimum version: ${browser.name} >= ${min} (detected: ${browser.version})`
93
+ };
94
+ }
95
+ return { passed: true };
96
+ }
97
+
98
+ // src/index.ts
99
+ async function browserGate(options) {
100
+ const ua = navigator.userAgent;
101
+ const browser = parseUA(ua);
102
+ const reasons = [];
103
+ if (options.features !== false) {
104
+ const featuresToCheck = options.features === true || options.features === void 0 ? DEFAULT_FEATURES : options.features;
105
+ const missing = detectMissingFeatures(featuresToCheck);
106
+ for (const f of missing) {
107
+ reasons.push(`Missing feature: ${f}`);
108
+ }
109
+ }
110
+ if (options.minVersions) {
111
+ const result = checkMinVersions(options.minVersions, ua);
112
+ if (!result.passed && result.reason) reasons.push(result.reason);
113
+ }
114
+ if (options.checkLatest) {
115
+ const tolerance = typeof options.checkLatest === "object" ? options.checkLatest.tolerance ?? 0 : 0;
116
+ const result = await checkLatestVersion(browser, tolerance);
117
+ if (!result.passed && result.reason) reasons.push(result.reason);
118
+ }
119
+ if (reasons.length === 0) return;
120
+ const info = {
121
+ browser: browser.name,
122
+ version: browser.version !== null ? String(browser.version) : null,
123
+ reasons
124
+ };
125
+ if (options.onOutdated) {
126
+ options.onOutdated(info);
127
+ } else if (options.redirect) {
128
+ window.location.href = options.redirect;
129
+ }
130
+ }
131
+ export {
132
+ browserGate
133
+ };
134
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@uekichinos/browser-gate",
3
+ "version": "0.1.0",
4
+ "description": "Detect outdated browsers and redirect or block access. Supports feature detection (default), minimum version checks, and live latest-version checks via endoflife.date — use one, two, or all three together.",
5
+ "keywords": [
6
+ "uekichinos",
7
+ "browser",
8
+ "outdated",
9
+ "browser-detect",
10
+ "browser-redirect",
11
+ "browser-gate",
12
+ "legacy-browser",
13
+ "browser-support",
14
+ "feature-detection"
15
+ ],
16
+ "author": {
17
+ "name": "uekichinos",
18
+ "url": "https://www.npmjs.com/~uekichinos"
19
+ },
20
+ "homepage": "https://github.com/uekichinos/browser-gate#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/uekichinos/browser-gate.git"
24
+ },
25
+ "license": "MIT",
26
+ "funding": {
27
+ "type": "github",
28
+ "url": "https://github.com/sponsors/uekichinos"
29
+ },
30
+ "type": "module",
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js",
38
+ "require": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist/*.js",
43
+ "dist/*.cjs",
44
+ "dist/*.d.ts",
45
+ "dist/*.d.cts",
46
+ "dist/*.global.js",
47
+ "CHANGELOG.md",
48
+ "SECURITY.md"
49
+ ],
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "prepublishOnly": "pnpm build && pnpm test",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "lint": "tsc --noEmit"
59
+ },
60
+ "devDependencies": {
61
+ "happy-dom": "^20.8.9",
62
+ "tsup": "^8.2.4",
63
+ "typescript": "^5.5.4",
64
+ "vitest": "^3.2.4"
65
+ }
66
+ }