authorgravatar for jztmanyl@gmail.comjztmanyl <jztmanyl@gmail.com> 2026-10-08 20:48:56+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-08 13:48:56-05:00
loga0066d5cb7d18abd786c1c23bf835f898490fd37
tree9bd88c369e1cda8597af0b5f380c7f6c64843c2d
parente8f5850b824572bab76bf810696f875eb648eff9
signature Signed by PGP key B5690EEEBB952194

fix(desktop): generate valid User-Agent that follows RFC 9110 guidelines (#17264)


4 files changed, 79 insertions(+), 10 deletions(-)

apps/desktop/src/app/DesktopAppIdentity.test.ts+34-1
......@@ -217,7 +217,7 @@ describe("DesktopAppIdentity", () => {
217217 const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
218218 yield* identity.configure;
219219
220 assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]);
220 assert.deepEqual(calls.setName, ["T3 Code Alpha"]);
221221 assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)");
222222 assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3");
223223 assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab");
......@@ -237,6 +237,39 @@ describe("DesktopAppIdentity", () => {
237237 );
238238 });
239239
240 it.effect.each([
241 { stage: "Alpha", environment: {} },
242 {
243 stage: "Nightly",
244 environment: { appVersion: "0.0.43-nightly.20260929.2428" },
245 },
246 {
247 stage: "Dev",
248 environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } },
249 },
250 ])("uses a valid native User-Agent product name for $stage", ({ stage, environment }) => {
251 const calls: ElectronAppCalls = {
252 setAboutPanelOptions: [],
253 setDockIcon: [],
254 setName: [],
255 };
256
257 return withIdentity(
258 Effect.gen(function* () {
259 const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
260 yield* identity.configure;
261
262 const runtimeName = calls.setName[0];
263 assert.isDefined(runtimeName);
264 assert.equal(runtimeName, `T3 Code ${stage}`);
265 // RFC 9110's token grammar, after Electron removes ASCII spaces.
266 assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/);
267 assert.equal(calls.setAboutPanelOptions[0]?.applicationName, `T3 Code (${stage})`);
268 }),
269 { calls, environment },
270 );
271 });
272
240273 it.effect("sets the dock icon only when running unpackaged", () => {
241274 const calls: ElectronAppCalls = {
242275 setAboutPanelOptions: [],
apps/desktop/src/app/DesktopAppIdentity.ts+6-1
......@@ -91,7 +91,12 @@ export const make = Effect.gen(function* () {
9191
9292 const configure = Effect.gen(function* () {
9393 const commitHash = yield* resolveAboutCommitHash;
94 yield* electronApp.setName(environment.displayName);
94 // Electron removes spaces from this name to build the native User-Agent
95 // product token, but leaves parentheses intact. Keep the runtime name valid
96 // without rewriting preview sessions (which breaks Turnstile, #7110).
97 yield* electronApp.setName(
98 `${environment.branding.baseName} ${environment.branding.stageLabel}`,
99 );
95100 yield* electronApp.setAboutPanelOptions({
96101 applicationName: environment.displayName,
97102 applicationVersion: environment.appVersion,
apps/desktop/src/window/DesktopApplicationMenu.test.ts+35-1
......@@ -103,6 +103,7 @@ const layerElectronMenu = (
103103const configureMenu = (
104104 selectedAction: Deferred.Deferred<string>,
105105 applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,
106 environment: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {},
106107) =>
107108 Effect.gen(function* () {
108109 const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
......@@ -116,7 +117,7 @@ const configureMenu = (
116117 Layer.provideMerge(layerElectronDialog),
117118 Layer.provideMerge(layerElectronApp),
118119 Layer.provideMerge(
119 DesktopEnvironment.layer(environmentInput).pipe(
120 DesktopEnvironment.layer({ ...environmentInput, ...environment }).pipe(
120121 Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),
121122 ),
122123 ),
......@@ -125,6 +126,39 @@ const configureMenu = (
125126 );
126127
127128describe("DesktopApplicationMenu", () => {
129 it.effect("keeps display branding in the macOS application menu", () =>
130 Effect.gen(function* () {
131 const selectedAction = yield* Deferred.make<string>();
132 const applicationMenuTemplate =
133 yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();
134
135 yield* configureMenu(selectedAction, applicationMenuTemplate, {
136 platform: "darwin",
137 appVersion: "0.0.43-nightly.20260929.2428",
138 });
139
140 const template = yield* Deferred.await(applicationMenuTemplate);
141 const applicationMenu = template[0];
142 assert.isDefined(applicationMenu);
143 assert.equal(applicationMenu.label, "T3 Code (Nightly)");
144 if (!Array.isArray(applicationMenu.submenu)) {
145 throw new Error("Expected application menu submenu to be an array.");
146 }
147 assert.equal(
148 applicationMenu.submenu.find((item) => item.role === "about")?.label,
149 "About T3 Code (Nightly)",
150 );
151 assert.equal(
152 applicationMenu.submenu.find((item) => item.role === "hide")?.label,
153 "Hide T3 Code (Nightly)",
154 );
155 assert.equal(
156 applicationMenu.submenu.find((item) => item.role === "quit")?.label,
157 "Quit T3 Code (Nightly)",
158 );
159 }),
160 );
161
128162 it.effect("installs the native menu and routes Settings through DesktopWindow", () =>
129163 Effect.gen(function* () {
130164 const selectedAction = yield* Deferred.make<string>();
apps/desktop/src/window/DesktopApplicationMenu.ts+4-7
......@@ -7,7 +7,6 @@ import * as Schema from "effect/Schema";
77import type * as Electron from "electron";
88
99import { makeComponentLogger } from "../app/DesktopObservability.ts";
10import * as ElectronApp from "../electron/ElectronApp.ts";
1110import * as ElectronDialog from "../electron/ElectronDialog.ts";
1211import * as ElectronMenu from "../electron/ElectronMenu.ts";
1312import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
......@@ -107,10 +106,8 @@ const handleCheckForUpdatesMenuClick = Effect.gen(function* () {
107106
108107/** @public Service construction is part of the canonical Effect module API. */
109108export const make = Effect.gen(function* () {
110 const electronApp = yield* ElectronApp.ElectronApp;
111109 const electronMenu = yield* ElectronMenu.ElectronMenu;
112110 const environment = yield* DesktopEnvironment.DesktopEnvironment;
113 const appName = yield* electronApp.name;
114111 const context = yield* Effect.context<DesktopApplicationMenuRuntimeServices>();
115112 const runPromise = Effect.runPromiseWith(context);
116113
......@@ -157,9 +154,9 @@ export const make = Effect.gen(function* () {
157154
158155 if (environment.platform === "darwin") {
159156 template.push({
160 label: appName,
157 label: environment.displayName,
161158 submenu: [
162 { role: "about" },
159 { role: "about", label: `About ${environment.displayName}` },
163160 {
164161 label: "Check for Updates...",
165162 click: checkForUpdatesClick,
......@@ -173,11 +170,11 @@ export const make = Effect.gen(function* () {
173170 { type: "separator" },
174171 { role: "services" },
175172 { type: "separator" },
176 { role: "hide" },
173 { role: "hide", label: `Hide ${environment.displayName}` },
177174 { role: "hideOthers" },
178175 { role: "unhide" },
179176 { type: "separator" },
180 { role: "quit" },
177 { role: "quit", label: `Quit ${environment.displayName}` },
181178 ],
182179 });
183180 }