capsite/webapp: rotate the cookie in place
The cookie moves out of the immutable init into a MapStore (key shape "cookie", value M.string()) in the instance's DO storage. Handles get its read-only view via the new WebApp.withHeaderMap, which reads at every send, so a rotation reaches handles already out and the webapp id, and with it the cache's wiring, stays put (checklist goal 6). Whoever holds the id can rotate, by RPC (WebAppFactory.cookies(id)) or POST /at/<id>/cookie; the diigo account owner revokes by logging out. Durable state: instances minted earlier keep the cookie in init.headers. The first handle() moves it to the store and rewrites init without it, in one transaction. Code from before this commit would then see no cookie, so rolling back past it needs the reverse move. webapp gains a package.json and a vitest workerd pool for the tests. Generated-by: Claude Opus 5.5
This commit is contained in:
1 parent
9179bdbf38
commit
10414c2ce6
9 files changed
+2512
-14
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import { env } from "cloudflare:workers";
|
||||
import { runInDurableObject } from "cloudflare:test";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import worker, { type WebAppInit } from "../src/index.js";
|
||||
|
||||
type TestEnv = { WEBAPP_DO: DurableObjectNamespace<import("../src/index.js").WebAppDo> };
|
||||
const testEnv = env as unknown as TestEnv;
|
||||
|
||||
const URL_ = "https://diigo.example/api/";
|
||||
|
||||
/** the cookie header of each outbound fetch */
|
||||
function spyCookies(): (string | null)[] {
|
||||
const seen: (string | null)[] = [];
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (_input, init) => {
|
||||
seen.push(new Headers(init?.headers).get("cookie"));
|
||||
return new Response("ok");
|
||||
});
|
||||
return seen;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const fresh = () => testEnv.WEBAPP_DO.get(testEnv.WEBAPP_DO.newUniqueId());
|
||||
|
||||
/** `init` as stored */
|
||||
const storedInit = (stub: DurableObjectStub) =>
|
||||
runInDurableObject(stub, (_obj, state) => state.storage.kv.get<WebAppInit>("init"));
|
||||
|
||||
describe("cookie rotation", () => {
|
||||
it("mint keeps the cookie out of init; sends carry it", async () => {
|
||||
const seen = spyCookies();
|
||||
const stub = fresh();
|
||||
await stub.configure({ url: URL_, headers: { Cookie: "session=a", accept: "application/json" } });
|
||||
expect(await storedInit(stub)).toEqual({ url: URL_, headers: { accept: "application/json" } });
|
||||
using app = await stub.handle();
|
||||
await app.get();
|
||||
expect(seen).toEqual(["session=a"]);
|
||||
});
|
||||
|
||||
it("a rotation reaches handles already out", async () => {
|
||||
const seen = spyCookies();
|
||||
const stub = fresh();
|
||||
await stub.configure({ url: URL_, headers: { cookie: "session=a" } });
|
||||
using app = await stub.handle();
|
||||
await app.pathjoin("x").get();
|
||||
using cookies = await stub.cookies();
|
||||
await cookies.set("cookie", "session=b");
|
||||
await app.pathjoin("x").get();
|
||||
expect(seen).toEqual(["session=a", "session=b"]);
|
||||
});
|
||||
|
||||
it("an instance minted before rotation moves its cookie to the store on first use", async () => {
|
||||
const seen = spyCookies();
|
||||
const stub = fresh();
|
||||
// the shape configure() stored until the cookie store
|
||||
await runInDurableObject(stub, (_obj, state) =>
|
||||
state.storage.put("init", { url: URL_, headers: { cookie: "session=old" }, msPerCall: 0 }),
|
||||
);
|
||||
using app = await stub.handle();
|
||||
await app.get();
|
||||
expect(await storedInit(stub)).toEqual({ url: URL_, headers: {}, msPerCall: 0 });
|
||||
using cookies = await stub.cookies();
|
||||
expect(await cookies.get("cookie")).toBe("session=old");
|
||||
await cookies.set("cookie", "session=new");
|
||||
using again = await stub.handle();
|
||||
await again.get();
|
||||
expect(seen).toEqual(["session=old", "session=new"]);
|
||||
});
|
||||
|
||||
it("POST /at/<id>/cookie rotates, by the same id that drives", async () => {
|
||||
const seen = spyCookies();
|
||||
const id = testEnv.WEBAPP_DO.newUniqueId();
|
||||
const stub = testEnv.WEBAPP_DO.get(id);
|
||||
await stub.configure({ url: URL_ });
|
||||
const rotate = (cookie: string) =>
|
||||
worker.fetch(
|
||||
new Request(`https://webapp.example/at/${id}/cookie`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ cookie }),
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
testEnv,
|
||||
);
|
||||
expect(await (await rotate("session=first")).json()).toEqual({ ok: true });
|
||||
expect(await (await rotate("session=second")).json()).toEqual({ ok: true });
|
||||
using app = await stub.handle();
|
||||
await app.get();
|
||||
expect(seen).toEqual(["session=second"]);
|
||||
});
|
||||
|
||||
it("a fixed header contradicting the stored cookie fails at send, naming no values", async () => {
|
||||
spyCookies();
|
||||
const stub = fresh();
|
||||
await stub.configure({ url: URL_, headers: { cookie: "session=a" } });
|
||||
using app = await stub.handle();
|
||||
const err = await app
|
||||
.withHeader("cookie", "session=mine")
|
||||
.get()
|
||||
.then(
|
||||
() => "",
|
||||
(e: unknown) => String(e),
|
||||
);
|
||||
expect(err).toContain("headers: contradicts on cookie");
|
||||
expect(err).not.toContain("session=");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@cloudflare/vitest-plugin/types"]
|
||||
},
|
||||
"include": ["./**/*", "../worker-configuration.d.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user