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", () => {...@@ -217,7 +217,7 @@ describe("DesktopAppIdentity", () => {
217 const identity = yield* DesktopAppIdentity.DesktopAppIdentity;217 const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
218 yield* identity.configure;218 yield* identity.configure;
219219
220 assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]);220 assert.deepEqual(calls.setName, ["T3 Code Alpha"]);
221 assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)");221 assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)");
222 assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3");222 assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3");
223 assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab");223 assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab");
...@@ -237,6 +237,39 @@ describe("DesktopAppIdentity", () => {...@@ -237,6 +237,39 @@ describe("DesktopAppIdentity", () => {
237 );237 );
238 });238 });
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
240 it.effect("sets the dock icon only when running unpackaged", () => {273 it.effect("sets the dock icon only when running unpackaged", () => {
241 const calls: ElectronAppCalls = {274 const calls: ElectronAppCalls = {
242 setAboutPanelOptions: [],275 setAboutPanelOptions: [],
apps/desktop/src/app/DesktopAppIdentity.ts+6-1
...@@ -91,7 +91,12 @@ export const make = Effect.gen(function* () {...@@ -91,7 +91,12 @@ export const make = Effect.gen(function* () {
9191
92 const configure = Effect.gen(function* () {92 const configure = Effect.gen(function* () {
93 const commitHash = yield* resolveAboutCommitHash;93 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 );
95 yield* electronApp.setAboutPanelOptions({100 yield* electronApp.setAboutPanelOptions({
96 applicationName: environment.displayName,101 applicationName: environment.displayName,
97 applicationVersion: environment.appVersion,102 applicationVersion: environment.appVersion,
apps/desktop/src/window/DesktopApplicationMenu.test.ts+35-1
...@@ -103,6 +103,7 @@ const layerElectronMenu = (...@@ -103,6 +103,7 @@ const layerElectronMenu = (
103const configureMenu = (103const configureMenu = (
104 selectedAction: Deferred.Deferred<string>,104 selectedAction: Deferred.Deferred<string>,
105 applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,105 applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,
106 environment: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {},
106) =>107) =>
107 Effect.gen(function* () {108 Effect.gen(function* () {
108 const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;109 const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
...@@ -116,7 +117,7 @@ const configureMenu = (...@@ -116,7 +117,7 @@ const configureMenu = (
116 Layer.provideMerge(layerElectronDialog),117 Layer.provideMerge(layerElectronDialog),
117 Layer.provideMerge(layerElectronApp),118 Layer.provideMerge(layerElectronApp),
118 Layer.provideMerge(119 Layer.provideMerge(
119 DesktopEnvironment.layer(environmentInput).pipe(120 DesktopEnvironment.layer({ ...environmentInput, ...environment }).pipe(
120 Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),121 Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),
121 ),122 ),
122 ),123 ),
...@@ -125,6 +126,39 @@ const configureMenu = (...@@ -125,6 +126,39 @@ const configureMenu = (
125 );126 );
126127
127describe("DesktopApplicationMenu", () => {128describe("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
128 it.effect("installs the native menu and routes Settings through DesktopWindow", () =>162 it.effect("installs the native menu and routes Settings through DesktopWindow", () =>
129 Effect.gen(function* () {163 Effect.gen(function* () {
130 const selectedAction = yield* Deferred.make<string>();164 const selectedAction = yield* Deferred.make<string>();
apps/desktop/src/window/DesktopApplicationMenu.ts+4-7
...@@ -7,7 +7,6 @@ import * as Schema from "effect/Schema";...@@ -7,7 +7,6 @@ import * as Schema from "effect/Schema";
7import type * as Electron from "electron";7import type * as Electron from "electron";
88
9import { makeComponentLogger } from "../app/DesktopObservability.ts";9import { makeComponentLogger } from "../app/DesktopObservability.ts";
10import * as ElectronApp from "../electron/ElectronApp.ts";
11import * as ElectronDialog from "../electron/ElectronDialog.ts";10import * as ElectronDialog from "../electron/ElectronDialog.ts";
12import * as ElectronMenu from "../electron/ElectronMenu.ts";11import * as ElectronMenu from "../electron/ElectronMenu.ts";
13import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";12import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
...@@ -107,10 +106,8 @@ const handleCheckForUpdatesMenuClick = Effect.gen(function* () {...@@ -107,10 +106,8 @@ const handleCheckForUpdatesMenuClick = Effect.gen(function* () {
107106
108/** @public Service construction is part of the canonical Effect module API. */107/** @public Service construction is part of the canonical Effect module API. */
109export const make = Effect.gen(function* () {108export const make = Effect.gen(function* () {
110 const electronApp = yield* ElectronApp.ElectronApp;
111 const electronMenu = yield* ElectronMenu.ElectronMenu;109 const electronMenu = yield* ElectronMenu.ElectronMenu;
112 const environment = yield* DesktopEnvironment.DesktopEnvironment;110 const environment = yield* DesktopEnvironment.DesktopEnvironment;
113 const appName = yield* electronApp.name;
114 const context = yield* Effect.context<DesktopApplicationMenuRuntimeServices>();111 const context = yield* Effect.context<DesktopApplicationMenuRuntimeServices>();
115 const runPromise = Effect.runPromiseWith(context);112 const runPromise = Effect.runPromiseWith(context);
116113
...@@ -157,9 +154,9 @@ export const make = Effect.gen(function* () {...@@ -157,9 +154,9 @@ export const make = Effect.gen(function* () {
157154
158 if (environment.platform === "darwin") {155 if (environment.platform === "darwin") {
159 template.push({156 template.push({
160 label: appName,157 label: environment.displayName,
161 submenu: [158 submenu: [
162 { role: "about" },159 { role: "about", label: `About ${environment.displayName}` },
163 {160 {
164 label: "Check for Updates...",161 label: "Check for Updates...",
165 click: checkForUpdatesClick,162 click: checkForUpdatesClick,
...@@ -173,11 +170,11 @@ export const make = Effect.gen(function* () {...@@ -173,11 +170,11 @@ export const make = Effect.gen(function* () {
173 { type: "separator" },170 { type: "separator" },
174 { role: "services" },171 { role: "services" },
175 { type: "separator" },172 { type: "separator" },
176 { role: "hide" },173 { role: "hide", label: `Hide ${environment.displayName}` },
177 { role: "hideOthers" },174 { role: "hideOthers" },
178 { role: "unhide" },175 { role: "unhide" },
179 { type: "separator" },176 { type: "separator" },
180 { role: "quit" },177 { role: "quit", label: `Quit ${environment.displayName}` },
181 ],178 ],
182 });179 });
183 }180 }