Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
2179a85
feat(fe): name the browser a session was created from
sea-snake Aug 22, 2026
e0b729f
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Aug 25, 2026
dbca3fb
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Aug 25, 2026
6c475b9
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Aug 25, 2026
5e44017
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Aug 31, 2026
83d5862
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Aug 31, 2026
b57732a
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Aug 31, 2026
807ae42
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 1, 2026
861a87f
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 1, 2026
5073b35
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 1, 2026
49f9472
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 1, 2026
4a13613
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 1, 2026
b1b9df8
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 2, 2026
681b554
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 4, 2026
43b85ef
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 4, 2026
ffcc72b
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 4, 2026
2ffcce9
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 4, 2026
6815d15
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 4, 2026
02fa4fd
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 4, 2026
b2fab42
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 4, 2026
d3d3cf1
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
b310dce
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
5070cb3
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
7c665f3
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
c34b091
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
a1f822e
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
afed366
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
483c552
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
024ee7c
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
0702e23
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
dfa15e1
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
3722872
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
e6bb005
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
152ab89
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
84d1d2f
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
91e085a
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
9a9a548
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
da4998c
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
e19fb37
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
a7f578f
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
3cdca40
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
7cca3fd
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
a419ada
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
e77ad43
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
d5c9535
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
6afa078
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
935aee7
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
7f110ca
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
67d8a49
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 5, 2026
ddcd375
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
d75c064
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
18bcdd7
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
a4bd52c
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
a9fa3d1
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
5a2f098
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
2825613
refactor(be): a browser, not a session device
sea-snake Sep 6, 2026
0784a43
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
0515304
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
30ffa14
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
a5531d6
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
5648ab5
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
17e4cf5
refactor(fe): the name-length bound says browser
sea-snake Sep 6, 2026
b101696
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
2dc03f8
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
d8f9690
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
56ff752
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
5d64ba4
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 6, 2026
8857792
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
99e5288
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
3edb915
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
7bef55c
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
bb12de2
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
e4dad50
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
bde8bc5
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
0633973
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
6be5b59
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
2da5f64
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
4ea8177
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
25c61ff
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 7, 2026
6e8c3f5
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 8, 2026
6b57e5b
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 8, 2026
e8622b3
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 8, 2026
04c01ff
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 8, 2026
e048ead
chore: merge fe/authorization-request-queue
sea-snake Sep 8, 2026
5044634
chore: merge fe/authorization-request-queue
sea-snake Sep 8, 2026
f71e0ca
chore: merge fe/authorization-request-queue
sea-snake Sep 8, 2026
3755e7f
chore: merge fe/authorization-request-queue
sea-snake Sep 8, 2026
262832c
chore: merge fe/authorization-request-queue
sea-snake Sep 8, 2026
5cf7db1
chore: merge fe/authorization-request-queue
sea-snake Sep 8, 2026
8478942
feat(frontend): describe the browser in tokens instead of composing a…
sea-snake Sep 8, 2026
52af608
chore: merge fe/authorization-request-queue
sea-snake Sep 8, 2026
786f1ba
chore: merge fe/authorization-request-queue
sea-snake Sep 9, 2026
6213822
feat(frontend): resolve the six named brands, and name the rest
sea-snake Sep 9, 2026
ab54c58
chore: merge fe/authorization-request-queue
sea-snake Sep 9, 2026
5680c83
fix(fe): handle the absent form-factor hint explicitly
sea-snake Sep 9, 2026
0dc6938
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 9, 2026
9b29e4c
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 9, 2026
6dc3b56
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 9, 2026
c496493
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 9, 2026
b547365
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 9, 2026
fe02515
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 9, 2026
8b42ad9
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 9, 2026
022f107
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 10, 2026
d192e97
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 10, 2026
0ffa028
refactor(browser): name a browser by the token it gives, wherever it …
sea-snake Sep 10, 2026
b6a514e
Merge branch 'fe/authorization-request-queue' into fe/describe-browser
sea-snake Sep 10, 2026
c46153f
fix(describe-browser): name only the form factors the canister has
sea-snake Sep 10, 2026
c83b3e0
style(describe-browser): format the test file
sea-snake Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions src/frontend/src/lib/stores/channelHandlers/describeBrowser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { afterEach, describe, expect, it } from "vitest";
import { browserLabel, describeBrowser } from "./describeBrowser";

const CHROME_ANDROID =
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36";
const FIREFOX_MAC =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0";
const IPAD_DESKTOP_MODE =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15";

const AGENTS: [string, string, number][] = [
[
"Chrome on iPhone",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1",
5,
],
[
"Firefox on iPhone",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/126.1 Mobile/15E148 Safari/605.1.15",
5,
],
[
"Edge on iPhone",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 EdgiOS/125.2535.60 Mobile/15E148 Safari/605.1.15",
5,
],
[
"Opera on iPhone",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPT/4.4.0 Mobile/15E148 Safari/604.1",
5,
],
[
"Safari on iPhone",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
5,
],
[
"Safari on iPad",
"Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
5,
],
["Safari on iPad", IPAD_DESKTOP_MODE, 5],
["Safari on Mac", IPAD_DESKTOP_MODE, 0],
["Firefox on Mac", FIREFOX_MAC, 0],
["Chrome on Android", CHROME_ANDROID, 5],
[
"Firefox on Android",
"Mozilla/5.0 (Android 14; Mobile; rv:126.0) Gecko/126.0 Firefox/126.0",
5,
],
[
"Samsung Internet on Android",
"Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36",
5,
],
[
"Edge on Android",
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36 EdgA/125.0.2535.51",
5,
],
[
"DuckDuckGo on Android",
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.0.0 Mobile DuckDuckGo/5 Safari/537.36",
5,
],
[
"Edge on Windows",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.2535.51",
0,
],
[
"Opera on Windows",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 OPR/110.0.0.0",
0,
],
[
"Chrome on Windows",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
0,
],
[
"Vivaldi on Linux",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Vivaldi/6.7.3329.41",
0,
],
[
"Chrome on Chromebook",
"Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
0,
],
["Browser on an unknown device", "curl/8.4.0", 0],
];

const stub = (props: Record<string, unknown>): void => {
for (const [name, value] of Object.entries(props)) {
Object.defineProperty(navigator, name, { value, configurable: true });
}
};

describe("browserLabel", () => {
it.each(AGENTS)("reads %s", (expected, agent, touchPoints) => {
expect(browserLabel({ agent, touchPoints })).toBe(expected);
});

it("names the device itself when the platform reports a model", () => {
expect(
browserLabel({
agent: CHROME_ANDROID,
touchPoints: 5,
model: "SM-S918B",
}),
).toBe("Chrome on SM-S918B");
});

it("leaves the label alone when the model is empty", () => {
expect(
browserLabel({ agent: CHROME_ANDROID, touchPoints: 5, model: "" }),
).toBe("Chrome on Android");
});

it("drops a model that would push the name past the canister's limit", () => {
expect(
browserLabel({
agent: CHROME_ANDROID,
touchPoints: 5,
model: "M".repeat(200),
}),
).toBe("Chrome on Android");
});
});

describe("describeBrowser", () => {
afterEach(() => {
stub({ userAgentData: undefined });
});

it("appends the model the platform reports", async () => {
stub({
userAgent: CHROME_ANDROID,
maxTouchPoints: 5,
userAgentData: {
getHighEntropyValues: () => Promise.resolve({ model: "Pixel 5" }),
},
});

await expect(describeBrowser()).resolves.toBe("Chrome on Pixel 5");
});

it("falls back to the platform when no model is available", async () => {
stub({
userAgent: CHROME_ANDROID,
maxTouchPoints: 5,
userAgentData: {
getHighEntropyValues: () => Promise.resolve({ model: "" }),
},
});

await expect(describeBrowser()).resolves.toBe("Chrome on Android");
});

it("falls back when the platform refuses the question", async () => {
stub({
userAgent: CHROME_ANDROID,
maxTouchPoints: 5,
userAgentData: {
getHighEntropyValues: () => Promise.reject(new Error("not allowed")),
},
});

await expect(describeBrowser()).resolves.toBe("Chrome on Android");
});

it("falls back on a browser without the API", async () => {
stub({ userAgent: FIREFOX_MAC, maxTouchPoints: 0 });

await expect(describeBrowser()).resolves.toBe("Firefox on Mac");
});
});
82 changes: 82 additions & 0 deletions src/frontend/src/lib/stores/channelHandlers/describeBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* The label a browser gives itself when it registers a session device.
*
* Self-reported, so it is something the user reads rather than evidence about where a
* session came from.
*/

/** Ordered most specific first: every later token also appears in the earlier ones' agents. */
const BROWSERS: [RegExp, string][] = [
[/CriOS\//, "Chrome"],
[/FxiOS\//, "Firefox"],
[/EdgiOS\//, "Edge"],
[/OPiOS\/|OPT\//, "Opera"],
[/Firefox\//, "Firefox"],
[/EdgA\/|Edg\//, "Edge"],
[/OPR\//, "Opera"],
[/SamsungBrowser\//, "Samsung Internet"],
[/Vivaldi\//, "Vivaldi"],
[/DuckDuckGo\//, "DuckDuckGo"],
[/Chrome\//, "Chrome"],
[/Safari\//, "Safari"],
];

const MAX_DEVICE_NAME_BYTES = 128;

const browserOf = (agent: string): string =>
BROWSERS.find(([token]) => token.test(agent))?.[1] ?? "Browser";

/** Names the device where a device word exists, since that is what its owner calls it. */
const platformOf = (agent: string, touchPoints: number): string => {
if (/CrOS/.test(agent)) return "Chromebook";
if (/Android/.test(agent)) return "Android";
if (/iPhone|iPod/.test(agent)) return "iPhone";
if (/iPad/.test(agent)) return "iPad";
// An iPad in desktop mode sends a Mac agent. A Mac reports no touch points.
if (/Macintosh|Mac OS X/.test(agent)) return touchPoints > 0 ? "iPad" : "Mac";
if (/Windows/.test(agent)) return "Windows";
if (/Linux|X11/.test(agent)) return "Linux";
return "an unknown device";
};

const withinLimit = (label: string): boolean =>
new TextEncoder().encode(label).length <= MAX_DEVICE_NAME_BYTES;

export const browserLabel = ({
agent,
touchPoints,
model,
}: {
agent: string;
touchPoints: number;
model?: string;
}): string => {
const browser = browserOf(agent);
const named = `${browser} on ${model}`;
return model !== undefined && model !== "" && withinLimit(named)
? named
: `${browser} on ${platformOf(agent, touchPoints)}`;
};

/** Populated on Android, and the only thing that names the device itself. */
const modelOf = async (): Promise<string | undefined> => {
const userAgentData = (
navigator as Navigator & {
userAgentData?: {
getHighEntropyValues?: (hints: string[]) => Promise<{ model?: string }>;
};
}
).userAgentData;
try {
return (await userAgentData?.getHighEntropyValues?.(["model"]))?.model;
} catch {
return undefined;
}
};

export const describeBrowser = async (): Promise<string> =>
browserLabel({
agent: navigator.userAgent,
touchPoints: navigator.maxTouchPoints,
model: await modelOf(),
});
Loading