-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathObserver.ts
More file actions
148 lines (129 loc) · 5.03 KB
/
Copy pathObserver.ts
File metadata and controls
148 lines (129 loc) · 5.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
* Observer — safe, client-side-only signal detectors (SPEC.md §6, §7).
*
* STRICT RULE: this file may ONLY implement the whitelisted detectors below.
* Never add canvas/WebGL fingerprinting, font enumeration, precise
* geolocation, timezone-to-city triangulation, or any signal combination
* that could build a unique identifier. Nothing here is ever persisted,
* logged, or sent over the network — every value is recomputed on demand
* and forgotten on refresh.
*/
import type { DetectorName } from '@/types/observation';
import { pickPhase } from '@/core/TimeOfDay';
interface NavigatorUAData {
platform?: string;
}
interface BatteryManager {
charging: boolean;
level: number;
}
interface NetworkInformation {
effectiveType?: string;
}
/** `navigator.userAgentData` / UA -> coarse OS bucket. */
function detectOs(): string | undefined {
const uaData = (navigator as Navigator & { userAgentData?: NavigatorUAData }).userAgentData;
// navigator.platform is deprecated but still the most accurate fallback on
// browsers without userAgentData (e.g. Safari/Firefox); userAgent is the last resort.
const platform = uaData?.platform ?? navigator.platform ?? navigator.userAgent;
if (!platform) return undefined;
if (/win/i.test(platform)) return 'Windows';
if (/mac/i.test(platform)) return 'macOS';
if (/linux/i.test(platform)) return 'Linux';
return 'other';
}
/** Local clock bucket, reusing TimeOfDay's phase logic plus a lateNight refinement. */
function detectTimeOfDay(): string {
const now = new Date();
const hour = now.getHours();
if (hour >= 0 && hour < 4) return 'lateNight';
return pickPhase(now);
}
/** Battery Status API -> 'low' | 'charging' | 'normal', or undefined if unsupported. */
async function detectBattery(): Promise<string | undefined> {
const getBattery = (navigator as Navigator & { getBattery?: () => Promise<BatteryManager> })
.getBattery;
if (!getBattery) return undefined;
try {
const battery = await getBattery();
if (battery.charging) return 'charging';
if (battery.level <= 0.2) return 'low';
return 'normal';
} catch {
return undefined;
}
}
/** `navigator.connection.effectiveType` -> 'slow' | 'fast', or undefined if unsupported. */
function detectConnection(): string | undefined {
const connection = (
navigator as Navigator & { connection?: NetworkInformation }
).connection;
const effectiveType = connection?.effectiveType;
if (!effectiveType) return undefined;
return effectiveType === '4g' ? 'fast' : 'slow';
}
/** `prefers-color-scheme` -> 'dark' | 'light'. */
function detectColorScheme(): string {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
/** `prefers-reduced-motion` -> 'reduce' | 'no-preference'. */
function detectReducedMotion(): string {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
? 'reduce'
: 'no-preference';
}
/** `navigator.language`, e.g. 'en-US', 'de'. */
function detectLocale(): string | undefined {
return navigator.language || undefined;
}
/** `navigator.hardwareConcurrency`, bucketed into 'few' (<=4) or 'many' (>4). */
function detectCores(): string | undefined {
const cores = navigator.hardwareConcurrency;
if (typeof cores !== 'number') return undefined;
return cores <= 4 ? 'few' : 'many';
}
/** `matchMedia('(pointer: coarse)')` -> 'touch' | 'mouse'. */
function detectPointer(): string {
return window.matchMedia('(pointer: coarse)').matches ? 'touch' : 'mouse';
}
/** Do Not Track / Global Privacy Control -> 'enabled' | 'disabled'. */
function detectPrivacy(): string {
const dnt = navigator.doNotTrack;
const gpc = (navigator as Navigator & { globalPrivacyControl?: boolean }).globalPrivacyControl;
const enabled = dnt === '1' || dnt === 'yes' || gpc === true;
return enabled ? 'enabled' : 'disabled';
}
/**
* Resolves every safe detector's current value. Detectors that are
* unsupported in this browser resolve to `undefined` and are simply
* omitted from the snapshot (never guessed or defaulted to a fake value).
*
* @param osOverride Optional debug-only override (`?os=` — see
* debugOverrides.ts) that replaces the real detected OS bucket. Purely a
* developer/demo convenience for exercising OS-gated observations
* without needing a different machine; never derived from anything the
* server sends or any additional browser signal.
*/
export async function resolveSignalSnapshot(
osOverride?: string,
): Promise<Partial<Record<DetectorName, string>>> {
const battery = await detectBattery();
const snapshot: Partial<Record<DetectorName, string>> = {
os: osOverride ?? detectOs(),
timeOfDay: detectTimeOfDay(),
battery,
connection: detectConnection(),
colorScheme: detectColorScheme(),
reducedMotion: detectReducedMotion(),
locale: detectLocale(),
cores: detectCores(),
pointer: detectPointer(),
privacy: detectPrivacy(),
};
for (const key of Object.keys(snapshot) as DetectorName[]) {
if (snapshot[key] === undefined) {
delete snapshot[key];
}
}
return snapshot;
}