Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899/** * Where a run happened, as the device says it: the browser and operating system versions and the device's own clock and time zone, written into every archive * (`device.json`) so a result says which machine made it. There is no address and no location in it: an extension cannot see its own address and has no permission to ask * where it is (the time zone is the coarse answer). The central system records the address it saw the device connect from, apart from this. Runs in the extension and in Node. */
export interface DeviceInfo { /** The device's clock when the report was made, as an ISO time in UTC. */ collectedAt: string; /** The same moment on the device's own wall clock (`YYYY-MM-DD HH:MM:SS`). */ localTime: string; timeZone: string; /** Minutes east of UTC (negative to the west). */ utcOffsetMinutes: number; language: string; browser: { name: string; version: string }; os: { name: string; version: string; architecture?: string; bitness?: string }; extensionVersion: string; online: boolean;}
/** What the browser can be asked, each one injected so Node can test the logic and a browser that lacks one still answers. */export interface DeviceInfoSources { now(): number; timeZone(): string; /** `Date.getTimezoneOffset()`: minutes the device's clock is BEHIND UTC (300 for UTC-05:00). */ timezoneOffsetMinutes(): number; language(): string; userAgent(): string; /** `navigator.userAgentData.getHighEntropyValues([...])`, or nothing when the browser has none. */ highEntropy(): Promise<{ platform?: string; platformVersion?: string; architecture?: string; bitness?: string; fullVersionList?: Array<{ brand: string; version: string }> } | undefined>; /** `chrome.runtime.getPlatformInfo()`. */ platformInfo(): Promise<{ os?: string; arch?: string } | undefined>; online(): boolean; extensionVersion(): string;}
const text = (v: unknown, max = 80): string => (typeof v === "string" ? v.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").slice(0, max) : "");const attempt = async <T>(f: () => Promise<T>): Promise<T | undefined> => { try { return await f(); } catch { return undefined; }};
export async function collectDeviceInfo(s: DeviceInfoSources): Promise<DeviceInfo> { const high = await attempt(() => s.highEntropy()); const platform = await attempt(() => s.platformInfo()); const now = s.now(); const utcOffsetMinutes = -s.timezoneOffsetMinutes() || 0; const real = (high?.fullVersionList ?? []).filter((b) => typeof b?.brand === "string" && !/^not/i.test(b.brand)); const chosen = real.find((b) => /google chrome/i.test(b.brand)) ?? real.find((b) => /chromium/i.test(b.brand)) ?? real[0]; const fromAgent = /Chrome\/(\d+(?:\.\d+)*)/.exec(s.userAgent()); const browser = chosen !== undefined ? { name: text(chosen.brand), version: text(chosen.version) } : { name: fromAgent !== null ? "Chrome" : "", version: fromAgent?.[1] ?? "" }; const osName = text(high?.platform) || text(platform?.os); const architecture = text(high?.architecture) || text(platform?.arch); const bitness = text(high?.bitness); return { collectedAt: new Date(now).toISOString(), localTime: new Date(now + utcOffsetMinutes * 60_000).toISOString().slice(0, 19).replace("T", " "), timeZone: text(s.timeZone()), utcOffsetMinutes, language: text(s.language()), browser, os: { name: osName, version: text(high?.platformVersion), ...(architecture !== "" ? { architecture } : {}), ...(bitness !== "" ? { bitness } : {}) }, extensionVersion: text(s.extensionVersion()), online: s.online(), };}
/** What the central system keeps of a report from a device: known fields only, as bounded text without control characters. Anything else is dropped. */export function parseDeviceInfo(raw: unknown): { ok: true; info: DeviceInfo } | { ok: false; message: string } { const no = (message: string) => ({ ok: false as const, message }); if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return no("the device report is not an object"); const r = raw as Record<string, unknown>; if (typeof r.collectedAt !== "string" || Number.isNaN(Date.parse(r.collectedAt)) || r.collectedAt.length > 40) return no("collectedAt is not a time"); if (typeof r.timeZone !== "string") return no("timeZone is not text"); if (!Number.isInteger(r.utcOffsetMinutes) || Math.abs(r.utcOffsetMinutes as number) > 1440) return no("utcOffsetMinutes is not a number of minutes"); const b = (r.browser ?? {}) as Record<string, unknown>; const o = (r.os ?? {}) as Record<string, unknown>; const arch = text(o.architecture); const bits = text(o.bitness); return { ok: true, info: { collectedAt: r.collectedAt, localTime: text(r.localTime, 40), timeZone: text(r.timeZone), utcOffsetMinutes: r.utcOffsetMinutes as number, language: text(r.language), browser: { name: text(b.name), version: text(b.version) }, os: { name: text(o.name), version: text(o.version), ...(arch !== "" ? { architecture: arch } : {}), ...(bits !== "" ? { bitness: bits } : {}) }, extensionVersion: text(r.extensionVersion), online: r.online === true, }, };}
/** One line for a person: the browser, the system and the device's clock. */export function describeDevice(d: DeviceInfo): string { const sign = d.utcOffsetMinutes < 0 ? "-" : "+"; const abs = Math.abs(d.utcOffsetMinutes); const offset = `UTC${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`; const browser = d.browser.name !== "" || d.browser.version !== "" ? `${d.browser.name} ${d.browser.version}`.trim() : "unknown browser"; const system = d.os.name !== "" ? `${d.os.name}${d.os.version !== "" ? ` ${d.os.version}` : ""}${d.os.architecture !== undefined ? ` (${d.os.architecture})` : ""}` : "unknown system"; return `${browser} on ${system}, local time ${d.localTime}${d.timeZone !== "" ? ` ${d.timeZone}` : ""} (${offset})`;}