Skip to content

Commit 93db8ef

Browse files
committed
feat: support custom gateway headers
1 parent f1cc58a commit 93db8ef

25 files changed

Lines changed: 428 additions & 28 deletions

desktop/garyx-desktop/src/main/gary-client.test.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,45 @@ test("fetchThreadHistory preserves kind parity fields for committed reducers", a
8686
}
8787
});
8888

89+
test("fetchThreadHistory sends configured gateway headers", async () => {
90+
const originalFetch = globalThis.fetch;
91+
let capturedHeaders = null;
92+
globalThis.fetch = async (_url, options) => {
93+
capturedHeaders = new Headers(options?.headers);
94+
return new Response(
95+
JSON.stringify({
96+
ok: true,
97+
messages: [],
98+
pending_user_inputs: [],
99+
}),
100+
{ status: 200, statusText: "OK" },
101+
);
102+
};
103+
104+
try {
105+
await fetchThreadHistory(
106+
{
107+
gatewayUrl: "http://127.0.0.1:31337",
108+
gatewayAuthToken: "test-token",
109+
gatewayHeaders: [
110+
"X-Garyx-Proxy: proxy-token",
111+
"X-Trace-Id=trace-123",
112+
].join("\n"),
113+
},
114+
{
115+
threadId: "thread::header-test",
116+
afterIndex: 0,
117+
},
118+
);
119+
120+
assert.equal(capturedHeaders.get("Authorization"), "Bearer test-token");
121+
assert.equal(capturedHeaders.get("X-Garyx-Proxy"), "proxy-token");
122+
assert.equal(capturedHeaders.get("X-Trace-Id"), "trace-123");
123+
} finally {
124+
globalThis.fetch = originalFetch;
125+
}
126+
});
127+
89128
test("getTask fetches task detail and preserves backing workflow thread id", async () => {
90129
const originalFetch = globalThis.fetch;
91130
const urls = [];

desktop/garyx-desktop/src/main/gary-client.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ import type {
119119
StartWorkflowThreadInput,
120120
StartWorkflowThreadResult,
121121
} from "@shared/contracts";
122+
import { parseGatewayHeadersBlock } from "../shared/gateway-headers.ts";
122123
import {
123124
decideStreamSeq,
124125
isControlTranscriptMessage,
@@ -1099,6 +1100,16 @@ function applyGatewayAuthHeader(
10991100
return headers;
11001101
}
11011102

1103+
function applyGatewayCustomHeaders(
1104+
headers: Headers,
1105+
gatewayHeaders: string | null | undefined,
1106+
): Headers {
1107+
for (const [name, value] of Object.entries(parseGatewayHeadersBlock(gatewayHeaders))) {
1108+
headers.set(name, value);
1109+
}
1110+
return headers;
1111+
}
1112+
11021113
function isLocalGatewayUrl(gatewayUrl: string): boolean {
11031114
try {
11041115
const parsed = new URL(normalizeGatewayUrl(gatewayUrl));
@@ -1323,7 +1334,10 @@ export async function streamThreadEvents(
13231334
): Promise<void> {
13241335
const afterSeq = Math.max(0, Math.trunc(options?.afterSeq ?? 0));
13251336
const headers = applyGatewayAuthHeader(
1326-
new Headers({ Accept: "text/event-stream" }),
1337+
applyGatewayCustomHeaders(
1338+
new Headers({ Accept: "text/event-stream" }),
1339+
settings.gatewayHeaders,
1340+
),
13271341
settings.gatewayAuthToken,
13281342
);
13291343
headers.set("Last-Event-ID", String(afterSeq));
@@ -1548,7 +1562,7 @@ export async function requestJson<T>(
15481562
init?: RequestInit,
15491563
): Promise<T> {
15501564
const headers = applyGatewayAuthHeader(
1551-
new Headers(init?.headers),
1565+
applyGatewayCustomHeaders(new Headers(init?.headers), settings.gatewayHeaders),
15521566
settings.gatewayAuthToken,
15531567
);
15541568
headers.set("Accept", "application/json");
@@ -1583,11 +1597,12 @@ export async function requestJson<T>(
15831597
async function requestJsonFromGatewayUrl<T>(
15841598
gatewayUrl: string,
15851599
gatewayAuthToken: string,
1600+
gatewayHeaders: string | null | undefined,
15861601
path: string,
15871602
init?: RequestInit,
15881603
): Promise<T> {
15891604
const headers = applyGatewayAuthHeader(
1590-
new Headers(init?.headers),
1605+
applyGatewayCustomHeaders(new Headers(init?.headers), gatewayHeaders),
15911606
gatewayAuthToken,
15921607
);
15931608
headers.set("Accept", "application/json");
@@ -2988,7 +3003,7 @@ export async function checkConnection(
29883003
}
29893004

29903005
export async function probeGateway(
2991-
input: { gatewayUrl: string; gatewayAuthToken: string },
3006+
input: { gatewayUrl: string; gatewayAuthToken: string; gatewayHeaders?: string },
29923007
): Promise<GatewayProbeResult> {
29933008
const normalizedGatewayUrl = normalizeGatewayUrl(input.gatewayUrl);
29943009
const path = "/runtime";
@@ -3007,6 +3022,7 @@ export async function probeGateway(
30073022
const runtime = await requestJsonFromGatewayUrl<RuntimePayload>(
30083023
normalizedGatewayUrl,
30093024
input.gatewayAuthToken,
3025+
input.gatewayHeaders,
30103026
path,
30113027
{
30123028
signal: AbortSignal.timeout(5000),

desktop/garyx-desktop/src/main/index.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,13 +688,24 @@ function registerIpcHandlers(): void {
688688

689689
ipcMain.handle(
690690
"garyx:add-gateway-profile",
691-
async (_event, input: { label?: string; gatewayUrl?: string; gatewayAuthToken?: string }) => {
691+
async (
692+
_event,
693+
input: {
694+
label?: string;
695+
gatewayUrl?: string;
696+
gatewayAuthToken?: string;
697+
gatewayHeaders?: string;
698+
},
699+
) => {
692700
return addDesktopGatewayProfile({
693701
label: typeof input?.label === "string" ? input.label : "",
694702
gatewayUrl: String(input?.gatewayUrl || ""),
695703
gatewayAuthToken: typeof input?.gatewayAuthToken === "string"
696704
? input.gatewayAuthToken
697705
: "",
706+
gatewayHeaders: typeof input?.gatewayHeaders === "string"
707+
? input.gatewayHeaders
708+
: "",
698709
});
699710
},
700711
);
@@ -708,6 +719,7 @@ function registerIpcHandlers(): void {
708719
label?: string;
709720
gatewayUrl?: string;
710721
gatewayAuthToken?: string;
722+
gatewayHeaders?: string;
711723
},
712724
) => {
713725
const state = await updateDesktopGatewayProfile({
@@ -717,6 +729,9 @@ function registerIpcHandlers(): void {
717729
gatewayAuthToken: typeof input?.gatewayAuthToken === "string"
718730
? input.gatewayAuthToken
719731
: undefined,
732+
gatewayHeaders: typeof input?.gatewayHeaders === "string"
733+
? input.gatewayHeaders
734+
: undefined,
720735
});
721736
if (mainWindow && !mainWindow.isDestroyed()) {
722737
restartThreadEventForwarders();
@@ -1494,7 +1509,7 @@ function registerIpcHandlers(): void {
14941509
"garyx:check-connection",
14951510
async (
14961511
_event,
1497-
input?: { gatewayUrl?: string; gatewayAuthToken?: string },
1512+
input?: { gatewayUrl?: string; gatewayAuthToken?: string; gatewayHeaders?: string },
14981513
) => {
14991514
const settings = await resolveSettings();
15001515
const nextSettings = input
@@ -1508,6 +1523,10 @@ function registerIpcHandlers(): void {
15081523
typeof input.gatewayAuthToken === "string"
15091524
? input.gatewayAuthToken
15101525
: settings.gatewayAuthToken,
1526+
gatewayHeaders:
1527+
typeof input.gatewayHeaders === "string"
1528+
? input.gatewayHeaders
1529+
: settings.gatewayHeaders,
15111530
}
15121531
: settings;
15131532
return checkConnection(nextSettings);
@@ -1518,7 +1537,7 @@ function registerIpcHandlers(): void {
15181537
"garyx:probe-gateway",
15191538
async (
15201539
_event,
1521-
input: { gatewayUrl: string; gatewayAuthToken: string },
1540+
input: { gatewayUrl: string; gatewayAuthToken: string; gatewayHeaders?: string },
15221541
) => {
15231542
return probeGateway(input);
15241543
},

desktop/garyx-desktop/src/main/store.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
type StartWorkflowThreadResult,
2727
} from '@shared/contracts';
2828
import { desktopStateWithoutThread } from '@shared/desktop-state';
29+
import { normalizeGatewayHeadersBlock } from '../shared/gateway-headers.ts';
2930
import {
3031
archiveRemoteThread,
3132
createRemoteAutomation,
@@ -274,6 +275,7 @@ function normalizeSettings(value?: Partial<DesktopSettings>): DesktopSettings {
274275
typeof value?.gatewayAuthToken === 'string'
275276
? value.gatewayAuthToken.trim()
276277
: DEFAULT_DESKTOP_SETTINGS.gatewayAuthToken,
278+
gatewayHeaders: normalizeGatewayHeadersBlock(value?.gatewayHeaders),
277279
accountId: value?.accountId?.trim() || DEFAULT_DESKTOP_SETTINGS.accountId,
278280
fromId: value?.fromId?.trim() || DEFAULT_DESKTOP_SETTINGS.fromId,
279281
timeoutSeconds: Math.max(
@@ -336,6 +338,7 @@ function normalizeGatewayProfile(
336338
gatewayUrl,
337339
gatewayAuthToken:
338340
typeof value?.gatewayAuthToken === 'string' ? value.gatewayAuthToken.trim() : '',
341+
gatewayHeaders: normalizeGatewayHeadersBlock(value?.gatewayHeaders),
339342
updatedAt,
340343
};
341344
}
@@ -381,6 +384,7 @@ function profileFromSettings(
381384
label: gatewayProfileLabel(gatewayUrl),
382385
gatewayUrl,
383386
gatewayAuthToken: settings.gatewayAuthToken.trim(),
387+
gatewayHeaders: normalizeGatewayHeadersBlock(settings.gatewayHeaders),
384388
updatedAt,
385389
};
386390
}
@@ -1011,11 +1015,13 @@ export async function addDesktopGatewayProfile(input: {
10111015
label?: string;
10121016
gatewayUrl: string;
10131017
gatewayAuthToken?: string;
1018+
gatewayHeaders?: string;
10141019
}): Promise<DesktopState> {
10151020
const profile = normalizeGatewayProfile({
10161021
label: input.label,
10171022
gatewayUrl: input.gatewayUrl,
10181023
gatewayAuthToken: input.gatewayAuthToken,
1024+
gatewayHeaders: input.gatewayHeaders,
10191025
updatedAt: new Date().toISOString(),
10201026
});
10211027
if (!profile) {
@@ -1040,6 +1046,7 @@ export async function updateDesktopGatewayProfile(input: {
10401046
label?: string;
10411047
gatewayUrl: string;
10421048
gatewayAuthToken?: string;
1049+
gatewayHeaders?: string;
10431050
}): Promise<DesktopState> {
10441051
const current = await getLocalDesktopState();
10451052
const normalizedId = input.profileId.trim();
@@ -1053,6 +1060,9 @@ export async function updateDesktopGatewayProfile(input: {
10531060
gatewayAuthToken: typeof input.gatewayAuthToken === 'string'
10541061
? input.gatewayAuthToken
10551062
: existing.gatewayAuthToken,
1063+
gatewayHeaders: typeof input.gatewayHeaders === 'string'
1064+
? input.gatewayHeaders
1065+
: existing.gatewayHeaders,
10561066
// Editing keeps the row where it was; only newly added profiles take a
10571067
// fresh timestamp.
10581068
updatedAt: existing.updatedAt,
@@ -1079,6 +1089,7 @@ export async function updateDesktopGatewayProfile(input: {
10791089
...current.settings,
10801090
gatewayUrl: nextProfile.gatewayUrl,
10811091
gatewayAuthToken: nextProfile.gatewayAuthToken,
1092+
gatewayHeaders: nextProfile.gatewayHeaders,
10821093
})
10831094
: current.settings,
10841095
};

desktop/garyx-desktop/src/renderer/src/GatewaySettingsPanel.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,14 @@ type GatewaySettingsPanelProps = {
147147
label?: string;
148148
gatewayUrl: string;
149149
gatewayAuthToken?: string;
150+
gatewayHeaders?: string;
150151
}) => Promise<void>;
151152
onUpdateGatewayProfile?: (input: {
152153
profileId: string;
153154
label?: string;
154155
gatewayUrl: string;
155156
gatewayAuthToken?: string;
157+
gatewayHeaders?: string;
156158
}) => Promise<void>;
157159
onDeleteGatewayProfile?: (profileId: string) => Promise<void>;
158160
onMutateGatewayDraft?: DraftMutator;
@@ -1241,19 +1243,22 @@ function GatewayProfileDialog({
12411243
label?: string;
12421244
gatewayUrl: string;
12431245
gatewayAuthToken?: string;
1246+
gatewayHeaders?: string;
12441247
}) => Promise<void>;
12451248
}) {
12461249
const { t } = useI18n();
12471250
const [label, setLabel] = useState('');
12481251
const [gatewayUrl, setGatewayUrl] = useState('');
12491252
const [gatewayAuthToken, setGatewayAuthToken] = useState('');
1253+
const [gatewayHeaders, setGatewayHeaders] = useState('');
12501254
const [saving, setSaving] = useState(false);
12511255

12521256
useEffect(() => {
12531257
if (open) {
12541258
setLabel(profile?.label ?? '');
12551259
setGatewayUrl(profile?.gatewayUrl ?? '');
12561260
setGatewayAuthToken(profile?.gatewayAuthToken ?? '');
1261+
setGatewayHeaders(profile?.gatewayHeaders ?? '');
12571262
}
12581263
}, [open, profile]);
12591264

@@ -1270,6 +1275,7 @@ function GatewayProfileDialog({
12701275
setLabel('');
12711276
setGatewayUrl('');
12721277
setGatewayAuthToken('');
1278+
setGatewayHeaders('');
12731279
}
12741280

12751281
async function handleSave() {
@@ -1278,7 +1284,7 @@ function GatewayProfileDialog({
12781284
}
12791285
setSaving(true);
12801286
try {
1281-
await onSubmit({ label, gatewayUrl, gatewayAuthToken });
1287+
await onSubmit({ label, gatewayUrl, gatewayAuthToken, gatewayHeaders });
12821288
resetFields();
12831289
onOpenChange(false);
12841290
} finally {
@@ -1338,6 +1344,18 @@ function GatewayProfileDialog({
13381344
onChange={(event) => setGatewayAuthToken(event.target.value)}
13391345
/>
13401346
</label>
1347+
<label className="gateway-setup-field">
1348+
<span>{t('Headers')}</span>
1349+
<Textarea
1350+
autoCapitalize="off"
1351+
autoComplete="off"
1352+
className="gateway-profile-headers-editor"
1353+
placeholder="X-Garyx-Gateway: value"
1354+
spellCheck={false}
1355+
value={gatewayHeaders}
1356+
onChange={(event) => setGatewayHeaders(event.target.value)}
1357+
/>
1358+
</label>
13411359
</div>
13421360
<DialogFooter>
13431361
<Button
@@ -2257,6 +2275,13 @@ export function GatewaySettingsPanel({
22572275
<span className="gateway-profile-row-copy">
22582276
<span className="gateway-profile-row-name">{profile.label}</span>
22592277
<span className="gateway-profile-row-url">{profile.gatewayUrl}</span>
2278+
{countNonEmptyLines(profile.gatewayHeaders) > 0 ? (
2279+
<span className="gateway-profile-row-url">
2280+
{t('{count} custom headers', {
2281+
count: countNonEmptyLines(profile.gatewayHeaders),
2282+
})}
2283+
</span>
2284+
) : null}
22602285
</span>
22612286
{isCurrent ? (
22622287
<span className="gateway-profile-current">{t('Current')}</span>

desktop/garyx-desktop/src/renderer/src/GatewaySwitcher.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function GatewayIdentityBar({
7979
label: gatewayHostLabel(currentGatewayUrl),
8080
gatewayUrl: currentGatewayUrl.trim(),
8181
gatewayAuthToken: '',
82+
gatewayHeaders: '',
8283
updatedAt: '',
8384
});
8485
}

0 commit comments

Comments
 (0)