Skip to content

Commit acd2227

Browse files
committed
Add connection-scoped cookie support
1 parent 82b1c0e commit acd2227

2 files changed

Lines changed: 279 additions & 11 deletions

File tree

src/http-stream.test.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,82 @@ describe("createHttpStream", () => {
137137
}
138138
});
139139

140+
it("propagates cookies across initialize, SSE, session POST, and DELETE", async () => {
141+
const controlledFetch = createControlledFetch({
142+
initializeCookies: ["transport=alpha; Path=/"],
143+
getCookies: ["route=bravo; Path=/"],
144+
});
145+
const stream = createHttpStream("https://agent.example/acp", {
146+
fetch: controlledFetch.fetch,
147+
headers: {
148+
Cookie: "caller=custom; transport=caller",
149+
},
150+
});
151+
const writer = stream.writable.getWriter();
152+
const reader = stream.readable.getReader();
153+
154+
try {
155+
await writer.write(initializeRequest);
156+
await readMessage(reader);
157+
await controlledFetch.sendSse(0, sessionNewResponse);
158+
await readMessage(reader);
159+
await writer.write(promptRequest);
160+
await writer.close();
161+
162+
expect(requestAt(controlledFetch.requests, 0).credentials).toBe(
163+
"include",
164+
);
165+
expect(requestAt(controlledFetch.requests, 0).headers.get("Cookie")).toBe(
166+
"caller=custom; transport=caller",
167+
);
168+
expect(requestAt(controlledFetch.requests, 1).headers.get("Cookie")).toBe(
169+
"transport=caller; caller=custom",
170+
);
171+
expect(requestAt(controlledFetch.requests, 2).headers.get("Cookie")).toBe(
172+
"transport=caller; route=bravo; caller=custom",
173+
);
174+
expect(requestAt(controlledFetch.requests, 3).headers.get("Cookie")).toBe(
175+
"transport=caller; route=bravo; caller=custom",
176+
);
177+
expect(requestAt(controlledFetch.requests, 4).headers.get("Cookie")).toBe(
178+
"transport=caller; route=bravo; caller=custom",
179+
);
180+
expect(
181+
controlledFetch.requests.map((request) => request.credentials),
182+
).toEqual(["include", "include", "include", "include", "include"]);
183+
} finally {
184+
reader.releaseLock();
185+
writer.releaseLock();
186+
}
187+
});
188+
189+
it("omits managed cookies when cookie handling is disabled", async () => {
190+
const controlledFetch = createControlledFetch({
191+
initializeCookies: ["transport=alpha; Path=/"],
192+
});
193+
const stream = createHttpStream("https://agent.example/acp", {
194+
fetch: controlledFetch.fetch,
195+
cookies: "omit",
196+
});
197+
const writer = stream.writable.getWriter();
198+
const reader = stream.readable.getReader();
199+
200+
try {
201+
await writer.write(initializeRequest);
202+
await readMessage(reader);
203+
204+
expect(requestAt(controlledFetch.requests, 0).credentials).toBe("omit");
205+
expect(requestAt(controlledFetch.requests, 1).credentials).toBe("omit");
206+
expect(
207+
requestAt(controlledFetch.requests, 1).headers.get("Cookie"),
208+
).toBeNull();
209+
} finally {
210+
reader.releaseLock();
211+
writer.releaseLock();
212+
await stream.writable.close();
213+
}
214+
});
215+
140216
it("sends DELETE and aborts SSE requests when closed", async () => {
141217
const controlledFetch = createControlledFetch();
142218
const stream = createHttpStream("https://agent.example/acp", {
@@ -330,6 +406,7 @@ interface RecordedRequest {
330406
readonly method: string;
331407
readonly headers: Headers;
332408
readonly body: string;
409+
readonly credentials: RequestCredentials | undefined;
333410
}
334411

335412
interface RecordedSseRequest {
@@ -349,7 +426,14 @@ interface TestClientState {
349426
readonly permissionRequests?: RequestPermissionRequest[];
350427
}
351428

352-
function createControlledFetch(): ControlledFetch {
429+
interface ControlledFetchOptions {
430+
readonly initializeCookies?: readonly string[];
431+
readonly getCookies?: readonly string[];
432+
}
433+
434+
function createControlledFetch(
435+
options: ControlledFetchOptions = {},
436+
): ControlledFetch {
353437
const requests: RecordedRequest[] = [];
354438
const sseRequests: RecordedSseRequest[] = [];
355439
const encoder = new TextEncoder();
@@ -365,11 +449,13 @@ function createControlledFetch(): ControlledFetch {
365449
method,
366450
headers,
367451
body: bodyToString(init?.body),
452+
credentials: init?.credentials,
368453
});
369454

370455
if (method === "POST" && !headers.has(HEADER_CONNECTION_ID)) {
371456
return jsonResponse(initializeResponse, 200, {
372457
[HEADER_CONNECTION_ID]: "connection-1",
458+
...setCookieResponseHeaders(options.initializeCookies),
373459
});
374460
}
375461

@@ -395,7 +481,10 @@ function createControlledFetch(): ControlledFetch {
395481

396482
return new Response(stream.readable, {
397483
status: 200,
398-
headers: { "Content-Type": EVENT_STREAM_MIME_TYPE },
484+
headers: {
485+
"Content-Type": EVENT_STREAM_MIME_TYPE,
486+
...setCookieResponseHeaders(options.getCookies),
487+
},
399488
});
400489
}
401490

@@ -485,3 +574,9 @@ function jsonResponse(
485574
},
486575
});
487576
}
577+
578+
function setCookieResponseHeaders(
579+
cookies: readonly string[] | undefined,
580+
): Record<string, string> {
581+
return cookies ? { "Set-Cookie": cookies.join(", ") } : {};
582+
}

0 commit comments

Comments
 (0)