@uekichinos/sentinel 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,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@uekichinos/sentinel` are documented here.
4
+
5
+ ## [0.1.0] - 2026-04-12
6
+ ### Added
7
+ - Initial release
8
+ - `createSentinel(options)` — create an idle detector instance
9
+ - `sentinel.start()` — begin listening for activity
10
+ - `sentinel.stop()` — remove all listeners and cancel the countdown
11
+ - `sentinel.reset()` — restart the countdown, transitions idle → active if needed
12
+ - `sentinel.isIdle()` — returns current idle state
13
+ - TTL timeout strings: `'30s'`, `'5m'`, `'15m'`, `'1h'` and ms numbers
14
+ - `onIdle` / `onActive` callbacks
15
+ - `notify` option — fires a `fetch` request to a backend endpoint when idle
16
+ - Dynamic `headers` function — evaluated at idle time for fresh tokens
17
+ - `watchVisibility` — pauses countdown when the tab is hidden (default: true)
18
+ - `throttle` — minimum ms between activity handler calls (default: 500)
19
+ - `events` — configurable list of DOM events that count as activity
20
+ - Fails silently on notify network errors — never blocks callbacks
21
+ - ESM, CJS, and IIFE builds
22
+ - Zero dependencies
23
+ - 28 tests
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # @uekichinos/sentinel
2
+
3
+ [![Socket Badge](https://badge.socket.dev/npm/package/@uekichinos/sentinel/0.1.0)](https://socket.dev/npm/package/@uekichinos/sentinel/overview/0.1.0)
4
+
5
+ Lightweight idle detection for the browser. Fires callbacks and optionally notifies a backend when the user goes inactive. Zero dependencies.
6
+
7
+ ```js
8
+ const sentinel = createSentinel({
9
+ timeout: '15m',
10
+ onIdle: () => showLogoutWarning(),
11
+ onActive: () => hideLogoutWarning(),
12
+ })
13
+
14
+ sentinel.start()
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @uekichinos/sentinel
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Quick start
28
+
29
+ ```js
30
+ import { createSentinel } from '@uekichinos/sentinel'
31
+
32
+ const sentinel = createSentinel({
33
+ timeout: '15m',
34
+ onIdle: () => console.log('User is idle'),
35
+ onActive: () => console.log('User is back'),
36
+ })
37
+
38
+ sentinel.start()
39
+ ```
40
+
41
+ ---
42
+
43
+ ## API
44
+
45
+ ### `createSentinel(options)`
46
+
47
+ Returns a `SentinelInstance`.
48
+
49
+ ```ts
50
+ createSentinel(options: SentinelOptions): SentinelInstance
51
+ ```
52
+
53
+ ### `sentinel.start()`
54
+
55
+ Begins listening for user activity and starts the idle countdown. Safe to call multiple times — idempotent.
56
+
57
+ ### `sentinel.stop()`
58
+
59
+ Removes all event listeners and cancels the countdown. Resets internal state so `start()` can be called again.
60
+
61
+ ### `sentinel.reset()`
62
+
63
+ Restarts the idle countdown from zero. If currently idle, transitions back to active and fires `onActive`.
64
+
65
+ ### `sentinel.isIdle()`
66
+
67
+ Returns `true` if the user is currently idle.
68
+
69
+ ---
70
+
71
+ ## Options
72
+
73
+ | Option | Type | Default | Description |
74
+ |--------|------|---------|-------------|
75
+ | `timeout` | `TtlInput` | — | How long before the user is considered idle |
76
+ | `onIdle` | `() => void` | — | Called when the user transitions active → idle |
77
+ | `onActive` | `() => void` | — | Called when the user transitions idle → active |
78
+ | `notify` | `NotifyOptions` | — | Fetch a backend endpoint when idle (see below) |
79
+ | `events` | `string[]` | see below | DOM events that count as activity |
80
+ | `throttle` | `number` | `500` | Min ms between activity handler calls |
81
+ | `watchVisibility` | `boolean` | `true` | Pause countdown when the tab is hidden |
82
+
83
+ **Default events:** `mousemove`, `keydown`, `scroll`, `click`, `touchstart`
84
+
85
+ ---
86
+
87
+ ## TTL formats
88
+
89
+ | Format | Duration |
90
+ |--------|----------|
91
+ | `'30s'` | 30 seconds |
92
+ | `'5m'` | 5 minutes |
93
+ | `'15m'` | 15 minutes |
94
+ | `'1h'` | 1 hour |
95
+ | `5000` | 5000 milliseconds |
96
+
97
+ ---
98
+
99
+ ## `notify` — backend notification
100
+
101
+ Fire a fetch request automatically when the user goes idle. Useful for invalidating server-side sessions or logging inactivity.
102
+
103
+ ```js
104
+ const sentinel = createSentinel({
105
+ timeout: '15m',
106
+ notify: {
107
+ url: '/api/session/idle',
108
+ method: 'POST', // default
109
+ headers: () => ({ // function — evaluated at idle time
110
+ Authorization: `Bearer ${getToken()}`,
111
+ 'Content-Type': 'application/json',
112
+ }),
113
+ body: { reason: 'idle' },
114
+ },
115
+ })
116
+ ```
117
+
118
+ | Option | Type | Default | Description |
119
+ |--------|------|---------|-------------|
120
+ | `url` | `string` | — | Endpoint to call |
121
+ | `method` | `string` | `'POST'` | HTTP method |
122
+ | `headers` | `Record<string, string> \| () => Record<string, string>` | — | Static or dynamic headers |
123
+ | `body` | `unknown` | — | Request body — serialised to JSON |
124
+
125
+ **Headers as a function** — the function is called at the moment idle fires, not at init. This ensures you always send a fresh token rather than one captured when the page loaded.
126
+
127
+ ```js
128
+ // Token captured at init — may be stale after a refresh
129
+ headers: { Authorization: `Bearer ${getToken()}` }
130
+
131
+ // Token evaluated at idle time — always fresh
132
+ headers: () => ({ Authorization: `Bearer ${getToken()}` })
133
+ ```
134
+
135
+ The notify request fails silently on network error — `onIdle` always fires regardless.
136
+
137
+ ---
138
+
139
+ ## Examples
140
+
141
+ ### Auto-logout with session warning
142
+
143
+ ```js
144
+ let warningTimer
145
+
146
+ const sentinel = createSentinel({
147
+ timeout: '14m',
148
+ onIdle: () => {
149
+ showWarning('You will be logged out in 1 minute')
150
+ warningTimer = setTimeout(() => logout(), 60_000)
151
+ },
152
+ onActive: () => {
153
+ hideWarning()
154
+ clearTimeout(warningTimer)
155
+ },
156
+ notify: {
157
+ url: '/api/session/extend',
158
+ headers: () => ({ Authorization: `Bearer ${getToken()}` }),
159
+ },
160
+ })
161
+
162
+ sentinel.start()
163
+ ```
164
+
165
+ ### Pause API polling when idle
166
+
167
+ ```js
168
+ let pollInterval
169
+
170
+ const sentinel = createSentinel({
171
+ timeout: '5m',
172
+ onIdle: () => {
173
+ clearInterval(pollInterval)
174
+ },
175
+ onActive: () => {
176
+ pollInterval = setInterval(fetchData, 5000)
177
+ },
178
+ })
179
+
180
+ pollInterval = setInterval(fetchData, 5000)
181
+ sentinel.start()
182
+ ```
183
+
184
+ ### Stop on page unload
185
+
186
+ ```js
187
+ sentinel.start()
188
+ window.addEventListener('beforeunload', () => sentinel.stop())
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Via `<script>` tag (no bundler)
194
+
195
+ ```html
196
+ <script src="https://unpkg.com/@uekichinos/sentinel/dist/index.global.js"></script>
197
+ <script>
198
+ const sentinel = Sentinel.createSentinel({
199
+ timeout: '15m',
200
+ onIdle: () => console.log('idle'),
201
+ })
202
+ sentinel.start()
203
+ </script>
204
+ ```
205
+
206
+ ---
207
+
208
+ ## License
209
+
210
+ 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/sentinel/security/advisories/new](https://github.com/uekichinos/sentinel/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,153 @@
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
+ createSentinel: () => createSentinel
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/parse-ttl.ts
28
+ var UNITS = {
29
+ s: 1e3,
30
+ m: 6e4,
31
+ h: 36e5,
32
+ d: 864e5
33
+ };
34
+ function parseTtl(ttl) {
35
+ if (typeof ttl === "number") {
36
+ return ttl > 0 ? ttl : null;
37
+ }
38
+ const match = ttl.match(/^(\d+(?:\.\d+)?)(s|m|h|d)$/);
39
+ if (!match) return null;
40
+ const value = parseFloat(match[1]);
41
+ const unit = match[2];
42
+ return value > 0 ? value * UNITS[unit] : null;
43
+ }
44
+
45
+ // src/index.ts
46
+ var DEFAULT_EVENTS = ["mousemove", "keydown", "scroll", "click", "touchstart"];
47
+ var DEFAULT_THROTTLE = 500;
48
+ function resolveHeaders(headers) {
49
+ if (!headers) return {};
50
+ return typeof headers === "function" ? headers() : headers;
51
+ }
52
+ async function fireNotify(notify) {
53
+ const headers = resolveHeaders(notify.headers);
54
+ const hasBody = notify.body !== void 0;
55
+ try {
56
+ await fetch(notify.url, {
57
+ method: notify.method ?? "POST",
58
+ headers: {
59
+ ...hasBody ? { "Content-Type": "application/json" } : {},
60
+ ...headers
61
+ },
62
+ ...hasBody ? { body: JSON.stringify(notify.body) } : {}
63
+ });
64
+ } catch {
65
+ }
66
+ }
67
+ function createSentinel(options) {
68
+ const parsedTimeout = parseTtl(options.timeout);
69
+ if (!parsedTimeout) throw new Error(`@uekichinos/sentinel: invalid timeout "${options.timeout}"`);
70
+ const timeoutMs = parsedTimeout;
71
+ const events = options.events ?? DEFAULT_EVENTS;
72
+ const throttleMs = options.throttle ?? DEFAULT_THROTTLE;
73
+ const watchVisibility = options.watchVisibility ?? true;
74
+ let timer = null;
75
+ let idle = false;
76
+ let started = false;
77
+ let lastActivity = 0;
78
+ function scheduleIdle() {
79
+ if (timer !== null) clearTimeout(timer);
80
+ timer = setTimeout(() => {
81
+ timer = null;
82
+ idle = true;
83
+ options.onIdle?.();
84
+ if (options.notify) fireNotify(options.notify);
85
+ }, timeoutMs);
86
+ }
87
+ function handleActivity() {
88
+ const now = Date.now();
89
+ if (now - lastActivity < throttleMs) return;
90
+ lastActivity = now;
91
+ if (idle) {
92
+ idle = false;
93
+ options.onActive?.();
94
+ }
95
+ scheduleIdle();
96
+ }
97
+ function handleVisibility() {
98
+ if (document.hidden) {
99
+ if (timer !== null) {
100
+ clearTimeout(timer);
101
+ timer = null;
102
+ }
103
+ } else {
104
+ handleActivity();
105
+ }
106
+ }
107
+ return {
108
+ start() {
109
+ if (started) return;
110
+ started = true;
111
+ idle = false;
112
+ lastActivity = Date.now();
113
+ for (const event of events) {
114
+ document.addEventListener(event, handleActivity, { passive: true });
115
+ }
116
+ if (watchVisibility) {
117
+ document.addEventListener("visibilitychange", handleVisibility);
118
+ }
119
+ scheduleIdle();
120
+ },
121
+ stop() {
122
+ if (!started) return;
123
+ started = false;
124
+ if (timer !== null) {
125
+ clearTimeout(timer);
126
+ timer = null;
127
+ }
128
+ for (const event of events) {
129
+ document.removeEventListener(event, handleActivity);
130
+ }
131
+ if (watchVisibility) {
132
+ document.removeEventListener("visibilitychange", handleVisibility);
133
+ }
134
+ },
135
+ reset() {
136
+ if (!started) return;
137
+ if (idle) {
138
+ idle = false;
139
+ options.onActive?.();
140
+ }
141
+ lastActivity = Date.now();
142
+ scheduleIdle();
143
+ },
144
+ isIdle() {
145
+ return idle;
146
+ }
147
+ };
148
+ }
149
+ // Annotate the CommonJS export names for ESM import in node:
150
+ 0 && (module.exports = {
151
+ createSentinel
152
+ });
153
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,61 @@
1
+ type TtlInput = number | `${number}s` | `${number}m` | `${number}h` | `${number}d`;
2
+ type NotifyOptions = {
3
+ /** URL to POST to when the user goes idle */
4
+ url: string;
5
+ /** HTTP method — defaults to 'POST' */
6
+ method?: string;
7
+ /**
8
+ * Headers to include in the request.
9
+ * Pass a function to evaluate headers at call time — useful for dynamic tokens.
10
+ */
11
+ headers?: Record<string, string> | (() => Record<string, string>);
12
+ /** Optional request body — serialised to JSON */
13
+ body?: unknown;
14
+ };
15
+ type SentinelOptions = {
16
+ /** How long before the user is considered idle */
17
+ timeout: TtlInput;
18
+ /** DOM events that count as user activity (default: mousemove, keydown, scroll, click, touchstart) */
19
+ events?: string[];
20
+ /** Minimum ms between activity handler calls — prevents hammering on mousemove (default: 500) */
21
+ throttle?: number;
22
+ /** Pause the idle countdown while the tab is hidden (default: true) */
23
+ watchVisibility?: boolean;
24
+ /** Fire a fetch request when the user goes idle */
25
+ notify?: NotifyOptions;
26
+ /** Called when the user transitions from active → idle */
27
+ onIdle?: () => void;
28
+ /** Called when the user transitions from idle → active */
29
+ onActive?: () => void;
30
+ };
31
+ type SentinelInstance = {
32
+ /** Start listening for activity and begin the idle countdown */
33
+ start(): void;
34
+ /** Stop all listeners and cancel the countdown */
35
+ stop(): void;
36
+ /** Reset the idle countdown (and transition idle → active if currently idle) */
37
+ reset(): void;
38
+ /** Returns true if the user is currently idle */
39
+ isIdle(): boolean;
40
+ };
41
+
42
+ /**
43
+ * Creates an idle detector that fires callbacks and optionally notifies a
44
+ * backend endpoint when the user stops interacting with the page.
45
+ *
46
+ * @example
47
+ * const sentinel = createSentinel({
48
+ * timeout: '15m',
49
+ * notify: {
50
+ * url: '/api/session/idle',
51
+ * headers: () => ({ Authorization: `Bearer ${getToken()}` }),
52
+ * },
53
+ * onIdle: () => showLogoutWarning(),
54
+ * onActive: () => hideLogoutWarning(),
55
+ * })
56
+ *
57
+ * sentinel.start()
58
+ */
59
+ declare function createSentinel(options: SentinelOptions): SentinelInstance;
60
+
61
+ export { type NotifyOptions, type SentinelInstance, type SentinelOptions, type TtlInput, createSentinel };
@@ -0,0 +1,61 @@
1
+ type TtlInput = number | `${number}s` | `${number}m` | `${number}h` | `${number}d`;
2
+ type NotifyOptions = {
3
+ /** URL to POST to when the user goes idle */
4
+ url: string;
5
+ /** HTTP method — defaults to 'POST' */
6
+ method?: string;
7
+ /**
8
+ * Headers to include in the request.
9
+ * Pass a function to evaluate headers at call time — useful for dynamic tokens.
10
+ */
11
+ headers?: Record<string, string> | (() => Record<string, string>);
12
+ /** Optional request body — serialised to JSON */
13
+ body?: unknown;
14
+ };
15
+ type SentinelOptions = {
16
+ /** How long before the user is considered idle */
17
+ timeout: TtlInput;
18
+ /** DOM events that count as user activity (default: mousemove, keydown, scroll, click, touchstart) */
19
+ events?: string[];
20
+ /** Minimum ms between activity handler calls — prevents hammering on mousemove (default: 500) */
21
+ throttle?: number;
22
+ /** Pause the idle countdown while the tab is hidden (default: true) */
23
+ watchVisibility?: boolean;
24
+ /** Fire a fetch request when the user goes idle */
25
+ notify?: NotifyOptions;
26
+ /** Called when the user transitions from active → idle */
27
+ onIdle?: () => void;
28
+ /** Called when the user transitions from idle → active */
29
+ onActive?: () => void;
30
+ };
31
+ type SentinelInstance = {
32
+ /** Start listening for activity and begin the idle countdown */
33
+ start(): void;
34
+ /** Stop all listeners and cancel the countdown */
35
+ stop(): void;
36
+ /** Reset the idle countdown (and transition idle → active if currently idle) */
37
+ reset(): void;
38
+ /** Returns true if the user is currently idle */
39
+ isIdle(): boolean;
40
+ };
41
+
42
+ /**
43
+ * Creates an idle detector that fires callbacks and optionally notifies a
44
+ * backend endpoint when the user stops interacting with the page.
45
+ *
46
+ * @example
47
+ * const sentinel = createSentinel({
48
+ * timeout: '15m',
49
+ * notify: {
50
+ * url: '/api/session/idle',
51
+ * headers: () => ({ Authorization: `Bearer ${getToken()}` }),
52
+ * },
53
+ * onIdle: () => showLogoutWarning(),
54
+ * onActive: () => hideLogoutWarning(),
55
+ * })
56
+ *
57
+ * sentinel.start()
58
+ */
59
+ declare function createSentinel(options: SentinelOptions): SentinelInstance;
60
+
61
+ export { type NotifyOptions, type SentinelInstance, type SentinelOptions, type TtlInput, createSentinel };
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var Sentinel = (() => {
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
+ createSentinel: () => createSentinel
25
+ });
26
+
27
+ // src/parse-ttl.ts
28
+ var UNITS = {
29
+ s: 1e3,
30
+ m: 6e4,
31
+ h: 36e5,
32
+ d: 864e5
33
+ };
34
+ function parseTtl(ttl) {
35
+ if (typeof ttl === "number") {
36
+ return ttl > 0 ? ttl : null;
37
+ }
38
+ const match = ttl.match(/^(\d+(?:\.\d+)?)(s|m|h|d)$/);
39
+ if (!match) return null;
40
+ const value = parseFloat(match[1]);
41
+ const unit = match[2];
42
+ return value > 0 ? value * UNITS[unit] : null;
43
+ }
44
+
45
+ // src/index.ts
46
+ var DEFAULT_EVENTS = ["mousemove", "keydown", "scroll", "click", "touchstart"];
47
+ var DEFAULT_THROTTLE = 500;
48
+ function resolveHeaders(headers) {
49
+ if (!headers) return {};
50
+ return typeof headers === "function" ? headers() : headers;
51
+ }
52
+ async function fireNotify(notify) {
53
+ const headers = resolveHeaders(notify.headers);
54
+ const hasBody = notify.body !== void 0;
55
+ try {
56
+ await fetch(notify.url, {
57
+ method: notify.method ?? "POST",
58
+ headers: {
59
+ ...hasBody ? { "Content-Type": "application/json" } : {},
60
+ ...headers
61
+ },
62
+ ...hasBody ? { body: JSON.stringify(notify.body) } : {}
63
+ });
64
+ } catch {
65
+ }
66
+ }
67
+ function createSentinel(options) {
68
+ const parsedTimeout = parseTtl(options.timeout);
69
+ if (!parsedTimeout) throw new Error(`@uekichinos/sentinel: invalid timeout "${options.timeout}"`);
70
+ const timeoutMs = parsedTimeout;
71
+ const events = options.events ?? DEFAULT_EVENTS;
72
+ const throttleMs = options.throttle ?? DEFAULT_THROTTLE;
73
+ const watchVisibility = options.watchVisibility ?? true;
74
+ let timer = null;
75
+ let idle = false;
76
+ let started = false;
77
+ let lastActivity = 0;
78
+ function scheduleIdle() {
79
+ if (timer !== null) clearTimeout(timer);
80
+ timer = setTimeout(() => {
81
+ timer = null;
82
+ idle = true;
83
+ options.onIdle?.();
84
+ if (options.notify) fireNotify(options.notify);
85
+ }, timeoutMs);
86
+ }
87
+ function handleActivity() {
88
+ const now = Date.now();
89
+ if (now - lastActivity < throttleMs) return;
90
+ lastActivity = now;
91
+ if (idle) {
92
+ idle = false;
93
+ options.onActive?.();
94
+ }
95
+ scheduleIdle();
96
+ }
97
+ function handleVisibility() {
98
+ if (document.hidden) {
99
+ if (timer !== null) {
100
+ clearTimeout(timer);
101
+ timer = null;
102
+ }
103
+ } else {
104
+ handleActivity();
105
+ }
106
+ }
107
+ return {
108
+ start() {
109
+ if (started) return;
110
+ started = true;
111
+ idle = false;
112
+ lastActivity = Date.now();
113
+ for (const event of events) {
114
+ document.addEventListener(event, handleActivity, { passive: true });
115
+ }
116
+ if (watchVisibility) {
117
+ document.addEventListener("visibilitychange", handleVisibility);
118
+ }
119
+ scheduleIdle();
120
+ },
121
+ stop() {
122
+ if (!started) return;
123
+ started = false;
124
+ if (timer !== null) {
125
+ clearTimeout(timer);
126
+ timer = null;
127
+ }
128
+ for (const event of events) {
129
+ document.removeEventListener(event, handleActivity);
130
+ }
131
+ if (watchVisibility) {
132
+ document.removeEventListener("visibilitychange", handleVisibility);
133
+ }
134
+ },
135
+ reset() {
136
+ if (!started) return;
137
+ if (idle) {
138
+ idle = false;
139
+ options.onActive?.();
140
+ }
141
+ lastActivity = Date.now();
142
+ scheduleIdle();
143
+ },
144
+ isIdle() {
145
+ return idle;
146
+ }
147
+ };
148
+ }
149
+ return __toCommonJS(index_exports);
150
+ })();
151
+ //# sourceMappingURL=index.global.js.map
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ // src/parse-ttl.ts
2
+ var UNITS = {
3
+ s: 1e3,
4
+ m: 6e4,
5
+ h: 36e5,
6
+ d: 864e5
7
+ };
8
+ function parseTtl(ttl) {
9
+ if (typeof ttl === "number") {
10
+ return ttl > 0 ? ttl : null;
11
+ }
12
+ const match = ttl.match(/^(\d+(?:\.\d+)?)(s|m|h|d)$/);
13
+ if (!match) return null;
14
+ const value = parseFloat(match[1]);
15
+ const unit = match[2];
16
+ return value > 0 ? value * UNITS[unit] : null;
17
+ }
18
+
19
+ // src/index.ts
20
+ var DEFAULT_EVENTS = ["mousemove", "keydown", "scroll", "click", "touchstart"];
21
+ var DEFAULT_THROTTLE = 500;
22
+ function resolveHeaders(headers) {
23
+ if (!headers) return {};
24
+ return typeof headers === "function" ? headers() : headers;
25
+ }
26
+ async function fireNotify(notify) {
27
+ const headers = resolveHeaders(notify.headers);
28
+ const hasBody = notify.body !== void 0;
29
+ try {
30
+ await fetch(notify.url, {
31
+ method: notify.method ?? "POST",
32
+ headers: {
33
+ ...hasBody ? { "Content-Type": "application/json" } : {},
34
+ ...headers
35
+ },
36
+ ...hasBody ? { body: JSON.stringify(notify.body) } : {}
37
+ });
38
+ } catch {
39
+ }
40
+ }
41
+ function createSentinel(options) {
42
+ const parsedTimeout = parseTtl(options.timeout);
43
+ if (!parsedTimeout) throw new Error(`@uekichinos/sentinel: invalid timeout "${options.timeout}"`);
44
+ const timeoutMs = parsedTimeout;
45
+ const events = options.events ?? DEFAULT_EVENTS;
46
+ const throttleMs = options.throttle ?? DEFAULT_THROTTLE;
47
+ const watchVisibility = options.watchVisibility ?? true;
48
+ let timer = null;
49
+ let idle = false;
50
+ let started = false;
51
+ let lastActivity = 0;
52
+ function scheduleIdle() {
53
+ if (timer !== null) clearTimeout(timer);
54
+ timer = setTimeout(() => {
55
+ timer = null;
56
+ idle = true;
57
+ options.onIdle?.();
58
+ if (options.notify) fireNotify(options.notify);
59
+ }, timeoutMs);
60
+ }
61
+ function handleActivity() {
62
+ const now = Date.now();
63
+ if (now - lastActivity < throttleMs) return;
64
+ lastActivity = now;
65
+ if (idle) {
66
+ idle = false;
67
+ options.onActive?.();
68
+ }
69
+ scheduleIdle();
70
+ }
71
+ function handleVisibility() {
72
+ if (document.hidden) {
73
+ if (timer !== null) {
74
+ clearTimeout(timer);
75
+ timer = null;
76
+ }
77
+ } else {
78
+ handleActivity();
79
+ }
80
+ }
81
+ return {
82
+ start() {
83
+ if (started) return;
84
+ started = true;
85
+ idle = false;
86
+ lastActivity = Date.now();
87
+ for (const event of events) {
88
+ document.addEventListener(event, handleActivity, { passive: true });
89
+ }
90
+ if (watchVisibility) {
91
+ document.addEventListener("visibilitychange", handleVisibility);
92
+ }
93
+ scheduleIdle();
94
+ },
95
+ stop() {
96
+ if (!started) return;
97
+ started = false;
98
+ if (timer !== null) {
99
+ clearTimeout(timer);
100
+ timer = null;
101
+ }
102
+ for (const event of events) {
103
+ document.removeEventListener(event, handleActivity);
104
+ }
105
+ if (watchVisibility) {
106
+ document.removeEventListener("visibilitychange", handleVisibility);
107
+ }
108
+ },
109
+ reset() {
110
+ if (!started) return;
111
+ if (idle) {
112
+ idle = false;
113
+ options.onActive?.();
114
+ }
115
+ lastActivity = Date.now();
116
+ scheduleIdle();
117
+ },
118
+ isIdle() {
119
+ return idle;
120
+ }
121
+ };
122
+ }
123
+ export {
124
+ createSentinel
125
+ };
126
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@uekichinos/sentinel",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight idle detection for the browser. Fires callbacks and optionally notifies a backend when the user goes inactive. Supports TTL timeouts, tab visibility pausing, activity throttling, and dynamic auth headers. Zero dependencies.",
5
+ "keywords": [
6
+ "uekichinos",
7
+ "sentinel",
8
+ "idle",
9
+ "idle-detection",
10
+ "inactivity",
11
+ "session-timeout",
12
+ "auto-logout",
13
+ "user-activity",
14
+ "visibility"
15
+ ],
16
+ "author": {
17
+ "name": "uekichinos",
18
+ "url": "https://www.npmjs.com/~uekichinos"
19
+ },
20
+ "homepage": "https://github.com/uekichinos/sentinel#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/uekichinos/sentinel.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/uekichinos/sentinel/issues"
27
+ },
28
+ "license": "MIT",
29
+ "funding": {
30
+ "type": "github",
31
+ "url": "https://github.com/sponsors/uekichinos"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "type": "module",
37
+ "main": "./dist/index.cjs",
38
+ "module": "./dist/index.js",
39
+ "types": "./dist/index.d.ts",
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.ts",
43
+ "import": "./dist/index.js",
44
+ "require": "./dist/index.cjs"
45
+ }
46
+ },
47
+ "files": [
48
+ "dist/*.js",
49
+ "dist/*.cjs",
50
+ "dist/*.d.ts",
51
+ "dist/*.d.cts",
52
+ "dist/*.global.js",
53
+ "CHANGELOG.md",
54
+ "SECURITY.md"
55
+ ],
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "prepublishOnly": "pnpm build && pnpm test",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest",
64
+ "lint": "tsc --noEmit"
65
+ },
66
+ "devDependencies": {
67
+ "happy-dom": "^20.8.9",
68
+ "tsup": "^8.2.4",
69
+ "typescript": "^5.5.4",
70
+ "vitest": "^3.2.4"
71
+ }
72
+ }