Skip to content

Commit 0f78320

Browse files
committed
fix(settings): avoid unsolicited local font permission prompt
1 parent 438969d commit 0f78320

4 files changed

Lines changed: 162 additions & 4 deletions

File tree

crates/agent-gateway/test/webui/font-family.test.mjs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ import { createWebModuleLoader } from "../helpers/load-web-module.mjs";
55
const loader = createWebModuleLoader();
66
const fontFamily = loader.loadModule("src/lib/shared/fontFamily.ts");
77

8+
async function withNavigator(value, task) {
9+
const previous = Object.getOwnPropertyDescriptor(globalThis, "navigator");
10+
Object.defineProperty(globalThis, "navigator", {
11+
configurable: true,
12+
enumerable: true,
13+
value,
14+
});
15+
try {
16+
return await task();
17+
} finally {
18+
if (previous) {
19+
Object.defineProperty(globalThis, "navigator", previous);
20+
} else {
21+
delete globalThis.navigator;
22+
}
23+
}
24+
}
25+
826
test("font family normalizer keeps freeform stacks and rejects unsafe values", () => {
927
assert.equal(fontFamily.normalizeFontFamily(""), "");
1028
assert.equal(fontFamily.normalizeFontFamily("system"), "system");
@@ -125,7 +143,38 @@ test("applying font families updates CSS variables and only emits code changes",
125143
}
126144
});
127145

128-
test("listLocalFontFamilies uses queryLocalFonts when available", async () => {
146+
test("listLocalFontFamilies does not trigger a local-fonts permission prompt", async () => {
147+
const previous = globalThis.queryLocalFonts;
148+
let queryCount = 0;
149+
globalThis.queryLocalFonts = async () => {
150+
queryCount += 1;
151+
return [{ family: "Inter" }];
152+
};
153+
try {
154+
await withNavigator(
155+
{
156+
permissions: {
157+
query: async (descriptor) => {
158+
assert.deepEqual(descriptor, { name: "local-fonts" });
159+
return { state: "prompt" };
160+
},
161+
},
162+
},
163+
async () => {
164+
assert.deepEqual(await fontFamily.listLocalFontFamilies(), []);
165+
},
166+
);
167+
assert.equal(queryCount, 0);
168+
} finally {
169+
if (previous === undefined) {
170+
delete globalThis.queryLocalFonts;
171+
} else {
172+
globalThis.queryLocalFonts = previous;
173+
}
174+
}
175+
});
176+
177+
test("listLocalFontFamilies uses queryLocalFonts when permission is already granted", async () => {
129178
const previous = globalThis.queryLocalFonts;
130179
globalThis.queryLocalFonts = async () => [
131180
{ family: "Inter" },
@@ -134,7 +183,16 @@ test("listLocalFontFamilies uses queryLocalFonts when available", async () => {
134183
{ family: " " },
135184
];
136185
try {
137-
assert.deepEqual(await fontFamily.listLocalFontFamilies(), ["Inter", "PingFang SC"]);
186+
await withNavigator(
187+
{
188+
permissions: {
189+
query: async () => ({ state: "granted" }),
190+
},
191+
},
192+
async () => {
193+
assert.deepEqual(await fontFamily.listLocalFontFamilies(), ["Inter", "PingFang SC"]);
194+
},
195+
);
138196
} finally {
139197
if (previous === undefined) {
140198
delete globalThis.queryLocalFonts;

crates/agent-gateway/web/src/lib/shared/fontFamily.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ type LocalFontData = {
6161

6262
type QueryLocalFonts = () => Promise<LocalFontData[]>;
6363

64+
type LocalFontPermissions = {
65+
query: (descriptor: { name: "local-fonts" }) => Promise<{ state?: string }>;
66+
};
67+
68+
async function hasGrantedLocalFontPermission(): Promise<boolean> {
69+
const permissions = (
70+
globalThis as typeof globalThis & {
71+
navigator?: { permissions?: LocalFontPermissions };
72+
}
73+
).navigator?.permissions;
74+
if (!permissions || typeof permissions.query !== "function") return false;
75+
76+
try {
77+
const status = await permissions.query({ name: "local-fonts" });
78+
return status.state === "granted";
79+
} catch {
80+
return false;
81+
}
82+
}
83+
6484
export function normalizeFontFamily(value: unknown): string {
6585
if (typeof value !== "string") return "";
6686
const trimmed = value.trim().replace(/\s+/g, " ");
@@ -186,6 +206,7 @@ export async function listLocalFontFamilies(): Promise<string[]> {
186206
}
187207
).queryLocalFonts;
188208
if (typeof queryLocalFonts !== "function") return [];
209+
if (!(await hasGrantedLocalFontPermission())) return [];
189210

190211
try {
191212
const fonts = await queryLocalFonts();

crates/agent-gui/src/lib/shared/fontFamily.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ type LocalFontData = {
6161

6262
type QueryLocalFonts = () => Promise<LocalFontData[]>;
6363

64+
type LocalFontPermissions = {
65+
query: (descriptor: { name: "local-fonts" }) => Promise<{ state?: string }>;
66+
};
67+
68+
async function hasGrantedLocalFontPermission(): Promise<boolean> {
69+
const permissions = (
70+
globalThis as typeof globalThis & {
71+
navigator?: { permissions?: LocalFontPermissions };
72+
}
73+
).navigator?.permissions;
74+
if (!permissions || typeof permissions.query !== "function") return false;
75+
76+
try {
77+
const status = await permissions.query({ name: "local-fonts" });
78+
return status.state === "granted";
79+
} catch {
80+
return false;
81+
}
82+
}
83+
6484
export function normalizeFontFamily(value: unknown): string {
6585
if (typeof value !== "string") return "";
6686
const trimmed = value.trim().replace(/\s+/g, " ");
@@ -186,6 +206,7 @@ export async function listLocalFontFamilies(): Promise<string[]> {
186206
}
187207
).queryLocalFonts;
188208
if (typeof queryLocalFonts !== "function") return [];
209+
if (!(await hasGrantedLocalFontPermission())) return [];
189210

190211
try {
191212
const fonts = await queryLocalFonts();

crates/agent-gui/test/system/font-family.test.mjs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ import { createTsModuleLoader } from "../helpers/load-ts-module.mjs";
55
const loader = createTsModuleLoader();
66
const fontFamily = loader.loadModule("src/lib/shared/fontFamily.ts");
77

8+
async function withNavigator(value, task) {
9+
const previous = Object.getOwnPropertyDescriptor(globalThis, "navigator");
10+
Object.defineProperty(globalThis, "navigator", {
11+
configurable: true,
12+
enumerable: true,
13+
value,
14+
});
15+
try {
16+
return await task();
17+
} finally {
18+
if (previous) {
19+
Object.defineProperty(globalThis, "navigator", previous);
20+
} else {
21+
delete globalThis.navigator;
22+
}
23+
}
24+
}
25+
826
test("font family normalizer keeps freeform stacks and rejects unsafe values", () => {
927
assert.equal(fontFamily.normalizeFontFamily(""), "");
1028
assert.equal(fontFamily.normalizeFontFamily("system"), "system");
@@ -125,7 +143,38 @@ test("applying font families updates CSS variables and only emits code changes",
125143
}
126144
});
127145

128-
test("listLocalFontFamilies uses queryLocalFonts when available", async () => {
146+
test("listLocalFontFamilies does not trigger a local-fonts permission prompt", async () => {
147+
const previous = globalThis.queryLocalFonts;
148+
let queryCount = 0;
149+
globalThis.queryLocalFonts = async () => {
150+
queryCount += 1;
151+
return [{ family: "Inter" }];
152+
};
153+
try {
154+
await withNavigator(
155+
{
156+
permissions: {
157+
query: async (descriptor) => {
158+
assert.deepEqual(descriptor, { name: "local-fonts" });
159+
return { state: "prompt" };
160+
},
161+
},
162+
},
163+
async () => {
164+
assert.deepEqual(await fontFamily.listLocalFontFamilies(), []);
165+
},
166+
);
167+
assert.equal(queryCount, 0);
168+
} finally {
169+
if (previous === undefined) {
170+
delete globalThis.queryLocalFonts;
171+
} else {
172+
globalThis.queryLocalFonts = previous;
173+
}
174+
}
175+
});
176+
177+
test("listLocalFontFamilies uses queryLocalFonts when permission is already granted", async () => {
129178
const previous = globalThis.queryLocalFonts;
130179
globalThis.queryLocalFonts = async () => [
131180
{ family: "Inter" },
@@ -134,7 +183,16 @@ test("listLocalFontFamilies uses queryLocalFonts when available", async () => {
134183
{ family: " " },
135184
];
136185
try {
137-
assert.deepEqual(await fontFamily.listLocalFontFamilies(), ["Inter", "PingFang SC"]);
186+
await withNavigator(
187+
{
188+
permissions: {
189+
query: async () => ({ state: "granted" }),
190+
},
191+
},
192+
async () => {
193+
assert.deepEqual(await fontFamily.listLocalFontFamilies(), ["Inter", "PingFang SC"]);
194+
},
195+
);
138196
} finally {
139197
if (previous === undefined) {
140198
delete globalThis.queryLocalFonts;

0 commit comments

Comments
 (0)