import { HttpApiBuilder, HttpApi } from "effect/unstable/httpapi";import { HttpRouter, HttpServer } from "effect/unstable/http";import { describe, expect, it } from "@effect/vitest";import { Data, Effect, Layer } from "effect";import type { Effect as EffectType } from "effect/Effect"; import { CloudAuthPublicApi } from "./api";import { CloudAuthPublicHandlers } from "./handlers";import { UserStoreService } from "./context";import { WorkOSError } from "./errors";import { WorkOSAuth } from "./workos"; const TestAuthPublicApi = HttpApi.make("cloudWeb").add(CloudAuthPublicApi);type EffectSuccess<T> = T extends EffectType<infer A, infer _E, infer _R> ? A : never;type AuthenticateWithCodeResult = EffectSuccess< ReturnType<WorkOSAuth["Service"]["authenticateWithCode"]>>;const fakeUser: AuthenticateWithCodeResult["user"] = { object: "user", id: "user_1", email: "test@example.com", emailVerified: true, profilePictureUrl: null, firstName: null, lastName: null, lastSignInAt: null, locale: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", externalId: null, metadata: {},}; class UnstubbedWorkOSMethod extends Data.TaggedError("UnstubbedWorkOSMethod")<{ method: string;}> {} const makeAuthFetch = (workos: Partial<WorkOSAuth["Service"]>) => { const WorkOSTest = Layer.succeed( WorkOSAuth, new Proxy(workos as WorkOSAuth["Service"], { get: (target, prop) => { if (prop in target) return target[prop as keyof typeof target]; return () => Effect.fail( new UnstubbedWorkOSMethod({ method: typeof prop === "string" ? prop : (prop.description ?? "symbol"), }), ); }, }), ); const UserStoreTest = Layer.succeed(UserStoreService)({ use: <A>() => Effect.sync(() => undefined as A), }); const apiLayer = HttpApiBuilder.layer(TestAuthPublicApi).pipe( Layer.provide(CloudAuthPublicHandlers), Layer.provideMerge(WorkOSTest), Layer.provideMerge(UserStoreTest), Layer.provideMerge(HttpServer.layerServices), Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })), ); const web = HttpRouter.toWebHandler(apiLayer, { disableLogger: true }); return web.handler as (request: Request) => Promise<Response>;}; describe("Auth callback handlers", () => { it.effect("routes login", () => Effect.gen(function* () { let observedState: string | undefined; const fetch = makeAuthFetch({ getAuthorizationUrl: (_redirectUri: string, state?: string) => { observedState = state; return `https://auth.example.test?state=${state}`; }, }); const response = yield* Effect.promise(() => fetch(new Request("http://test.local/auth/login")), ); expect(response.status).toBe(302); expect(observedState).toMatch(/^[0-9a-f]{64}$/); expect(response.headers.get("location")).toBe( `https://auth.example.test?state=${observedState}`, ); const setCookie = response.headers.get("set-cookie") ?? ""; expect(setCookie).toContain(`wos-login-state=${observedState}`); expect(setCookie).toContain("Max-Age=600"); }), ); it.effect("rejects callback state without the matching login state cookie", () => Effect.gen(function* () { let authenticateCalls = 0; const fetch = makeAuthFetch({ authenticateWithCode: () => Effect.sync(() => { authenticateCalls++; const result: AuthenticateWithCodeResult = { user: fakeUser, accessToken: "access_token", refreshToken: "refresh_token", sealedSession: "sealed_session", }; return result; }), }); const response = yield* Effect.promise(() => fetch( new Request("http://test.local/auth/callback?code=attacker-code&state=attacker-state"), ), ); expect(response.status).toBe(400); expect(authenticateCalls).toBe(0); }), ); // Some WorkOS-initiated redirects don't include a state parameter. // The schema treats state as optional; the handler skips the CSRF // check when state is absent and proceeds with the code exchange. it.effect("accepts a callback without state", () => Effect.gen(function* () { const fetch = makeAuthFetch({ authenticateWithCode: () => Effect.succeed({ user: fakeUser, accessToken: "access_token", refreshToken: "refresh_token", organizationId: "org_1", sealedSession: "sealed_session_no_state", } satisfies AuthenticateWithCodeResult), }); const response = yield* Effect.promise(() => fetch(new Request("http://test.local/auth/callback?code=stateless-code")), ); expect(response.status).toBe(302); expect(response.headers.get("location")).toBe("/"); const setCookie = response.headers.get("set-cookie") ?? ""; expect(setCookie).toContain("wos-session=sealed_session_no_state"); }), ); it.effect("sets the session cookie and clears login state on matching callback state", () => Effect.gen(function* () { const fetch = makeAuthFetch({ authenticateWithCode: () => Effect.succeed({ user: fakeUser, accessToken: "access_token", refreshToken: "refresh_token", organizationId: "org_1", sealedSession: "sealed_session", } satisfies AuthenticateWithCodeResult), }); const response = yield* Effect.promise(() => fetch( new Request("http://test.local/auth/callback?code=code&state=state_1", { headers: { cookie: "wos-login-state=state_1" }, }), ), ); expect(response.status).toBe(302); expect(response.headers.get("location")).toBe("/"); const setCookie = response.headers.get("set-cookie") ?? ""; expect(setCookie).toContain("wos-session=sealed_session"); expect(setCookie).toContain("Max-Age=604800"); expect(setCookie).toContain("wos-login-state="); expect(setCookie).toContain("Max-Age=0"); }), ); // Regression: an invited user signing in for the first time has only a // *pending* membership (the WorkOS-side representation of an unaccepted // invitation). The callback used to pick `data[0]` regardless of status // and call refreshSession into that org, which 400s and surfaces as // WorkOSError → 500. We now skip pending memberships entirely. it.effect("does not refresh into a pending membership; leaves session org-less", () => Effect.gen(function* () { let refreshCalls = 0; const fetch = makeAuthFetch({ authenticateWithCode: () => Effect.succeed({ user: fakeUser, accessToken: "access_token", refreshToken: "refresh_token", sealedSession: "sealed_session_no_org", } satisfies AuthenticateWithCodeResult), // The handler only reads `data[*].organizationId` and // `data[*].status`, so we stub a minimal shape matching that // contract instead of hand-rolling the full WorkOS SDK types. // oxlint-disable-next-line executor/no-double-cast listUserMemberships: (() => Effect.succeed({ data: [ { organizationId: "org_pending", status: "pending", }, ], })) as unknown as WorkOSAuth["Service"]["listUserMemberships"], refreshSession: () => Effect.sync(() => { refreshCalls++; return null; }), }); const response = yield* Effect.promise(() => fetch( new Request("http://test.local/auth/callback?code=code&state=state_2", { headers: { cookie: "wos-login-state=state_2" }, }), ), ); expect(response.status).toBe(302); expect(response.headers.get("location")).toBe("/"); expect(refreshCalls).toBe(0); const setCookie = response.headers.get("set-cookie") ?? ""; expect(setCookie).toContain("wos-session=sealed_session_no_org"); }), ); // Regression: even with an active membership, if WorkOS rejects the // refresh (e.g. membership just revoked, brief race), the callback // should still succeed with an org-less session rather than 500. it.effect("falls through to org-less session when refresh fails", () => Effect.gen(function* () { const fetch = makeAuthFetch({ authenticateWithCode: () => Effect.succeed({ user: fakeUser, accessToken: "access_token", refreshToken: "refresh_token", sealedSession: "sealed_session_fallback", } satisfies AuthenticateWithCodeResult), // Same minimal-shape stub as above — see comment in the // pending-membership test. // oxlint-disable-next-line executor/no-double-cast listUserMemberships: (() => Effect.succeed({ data: [ { organizationId: "org_active", status: "active", }, ], })) as unknown as WorkOSAuth["Service"]["listUserMemberships"], refreshSession: (() => Effect.fail(new WorkOSError())) as WorkOSAuth["Service"]["refreshSession"], }); const response = yield* Effect.promise(() => fetch( new Request("http://test.local/auth/callback?code=code&state=state_3", { headers: { cookie: "wos-login-state=state_3" }, }), ), ); expect(response.status).toBe(302); expect(response.headers.get("location")).toBe("/"); const setCookie = response.headers.get("set-cookie") ?? ""; expect(setCookie).toContain("wos-session=sealed_session_fallback"); }), );});
import { HttpApiBuilder, HttpApi } from "effect/unstable/httpapi";
import { HttpRouter, HttpServer } from "effect/unstable/http";
import { describe, expect, it } from "@effect/vitest";
import { Data, Effect, Layer } from "effect";
import type { Effect as EffectType } from "effect/Effect";
import { CloudAuthPublicApi } from "./api";
import { CloudAuthPublicHandlers } from "./handlers";
import { UserStoreService } from "./context";
import { WorkOSError } from "./errors";
import { WorkOSAuth } from "./workos";
const TestAuthPublicApi = HttpApi.make("cloudWeb").add(CloudAuthPublicApi);
type EffectSuccess<T> = T extends EffectType<infer A, infer _E, infer _R> ? A : never;
type AuthenticateWithCodeResult = EffectSuccess<
ReturnType<WorkOSAuth["Service"]["authenticateWithCode"]>
>;
const fakeUser: AuthenticateWithCodeResult["user"] = {
object: "user",
id: "user_1",
email: "test@example.com",
emailVerified: true,
profilePictureUrl: null,
firstName: null,
lastName: null,
lastSignInAt: null,
locale: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
externalId: null,
metadata: {},
};
class UnstubbedWorkOSMethod extends Data.TaggedError("UnstubbedWorkOSMethod")<{
method: string;
}> {}
const makeAuthFetch = (workos: Partial<WorkOSAuth["Service"]>) => {
const WorkOSTest = Layer.succeed(
WorkOSAuth,
new Proxy(workos as WorkOSAuth["Service"], {
get: (target, prop) => {
if (prop in target) return target[prop as keyof typeof target];
return () =>
Effect.fail(
new UnstubbedWorkOSMethod({
method: typeof prop === "string" ? prop : (prop.description ?? "symbol"),
}),
);
},
}),
);
const UserStoreTest = Layer.succeed(UserStoreService)({
use: <A>() => Effect.sync(() => undefined as A),
});
const apiLayer = HttpApiBuilder.layer(TestAuthPublicApi).pipe(
Layer.provide(CloudAuthPublicHandlers),
Layer.provideMerge(WorkOSTest),
Layer.provideMerge(UserStoreTest),
Layer.provideMerge(HttpServer.layerServices),
Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),
);
const web = HttpRouter.toWebHandler(apiLayer, { disableLogger: true });
return web.handler as (request: Request) => Promise<Response>;
};
describe("Auth callback handlers", () => {
it.effect("routes login", () =>
Effect.gen(function* () {
let observedState: string | undefined;
const fetch = makeAuthFetch({
getAuthorizationUrl: (_redirectUri: string, state?: string) => {
observedState = state;
return `https://auth.example.test?state=${state}`;
},
});
const response = yield* Effect.promise(() =>
fetch(new Request("http://test.local/auth/login")),
);
expect(response.status).toBe(302);
expect(observedState).toMatch(/^[0-9a-f]{64}$/);
expect(response.headers.get("location")).toBe(
`https://auth.example.test?state=${observedState}`,
);
const setCookie = response.headers.get("set-cookie") ?? "";
expect(setCookie).toContain(`wos-login-state=${observedState}`);
expect(setCookie).toContain("Max-Age=600");
}),
);
it.effect("rejects callback state without the matching login state cookie", () =>
Effect.gen(function* () {
let authenticateCalls = 0;
const fetch = makeAuthFetch({
authenticateWithCode: () =>
Effect.sync(() => {
authenticateCalls++;
const result: AuthenticateWithCodeResult = {
user: fakeUser,
accessToken: "access_token",
refreshToken: "refresh_token",
sealedSession: "sealed_session",
};
return result;
}),
});
const response = yield* Effect.promise(() =>
fetch(
new Request("http://test.local/auth/callback?code=attacker-code&state=attacker-state"),
),
);
expect(response.status).toBe(400);
expect(authenticateCalls).toBe(0);
}),
);
// Some WorkOS-initiated redirects don't include a state parameter.
// The schema treats state as optional; the handler skips the CSRF
// check when state is absent and proceeds with the code exchange.
it.effect("accepts a callback without state", () =>
Effect.gen(function* () {
const fetch = makeAuthFetch({
authenticateWithCode: () =>
Effect.succeed({
user: fakeUser,
accessToken: "access_token",
refreshToken: "refresh_token",
organizationId: "org_1",
sealedSession: "sealed_session_no_state",
} satisfies AuthenticateWithCodeResult),
});
const response = yield* Effect.promise(() =>
fetch(new Request("http://test.local/auth/callback?code=stateless-code")),
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
const setCookie = response.headers.get("set-cookie") ?? "";
expect(setCookie).toContain("wos-session=sealed_session_no_state");
}),
);
it.effect("sets the session cookie and clears login state on matching callback state", () =>
Effect.gen(function* () {
const fetch = makeAuthFetch({
authenticateWithCode: () =>
Effect.succeed({
user: fakeUser,
accessToken: "access_token",
refreshToken: "refresh_token",
organizationId: "org_1",
sealedSession: "sealed_session",
} satisfies AuthenticateWithCodeResult),
});
const response = yield* Effect.promise(() =>
fetch(
new Request("http://test.local/auth/callback?code=code&state=state_1", {
headers: { cookie: "wos-login-state=state_1" },
}),
),
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
const setCookie = response.headers.get("set-cookie") ?? "";
expect(setCookie).toContain("wos-session=sealed_session");
expect(setCookie).toContain("Max-Age=604800");
expect(setCookie).toContain("wos-login-state=");
expect(setCookie).toContain("Max-Age=0");
}),
);
// Regression: an invited user signing in for the first time has only a
// *pending* membership (the WorkOS-side representation of an unaccepted
// invitation). The callback used to pick `data[0]` regardless of status
// and call refreshSession into that org, which 400s and surfaces as
// WorkOSError → 500. We now skip pending memberships entirely.
it.effect("does not refresh into a pending membership; leaves session org-less", () =>
Effect.gen(function* () {
let refreshCalls = 0;
const fetch = makeAuthFetch({
authenticateWithCode: () =>
Effect.succeed({
user: fakeUser,
accessToken: "access_token",
refreshToken: "refresh_token",
sealedSession: "sealed_session_no_org",
} satisfies AuthenticateWithCodeResult),
// The handler only reads `data[*].organizationId` and
// `data[*].status`, so we stub a minimal shape matching that
// contract instead of hand-rolling the full WorkOS SDK types.
// oxlint-disable-next-line executor/no-double-cast
listUserMemberships: (() =>
Effect.succeed({
data: [
{
organizationId: "org_pending",
status: "pending",
},
],
})) as unknown as WorkOSAuth["Service"]["listUserMemberships"],
refreshSession: () =>
Effect.sync(() => {
refreshCalls++;
return null;
}),
});
const response = yield* Effect.promise(() =>
fetch(
new Request("http://test.local/auth/callback?code=code&state=state_2", {
headers: { cookie: "wos-login-state=state_2" },
}),
),
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
expect(refreshCalls).toBe(0);
const setCookie = response.headers.get("set-cookie") ?? "";
expect(setCookie).toContain("wos-session=sealed_session_no_org");
}),
);
// Regression: even with an active membership, if WorkOS rejects the
// refresh (e.g. membership just revoked, brief race), the callback
// should still succeed with an org-less session rather than 500.
it.effect("falls through to org-less session when refresh fails", () =>
Effect.gen(function* () {
const fetch = makeAuthFetch({
authenticateWithCode: () =>
Effect.succeed({
user: fakeUser,
accessToken: "access_token",
refreshToken: "refresh_token",
sealedSession: "sealed_session_fallback",
} satisfies AuthenticateWithCodeResult),
// Same minimal-shape stub as above — see comment in the
// pending-membership test.
// oxlint-disable-next-line executor/no-double-cast
listUserMemberships: (() =>
Effect.succeed({
data: [
{
organizationId: "org_active",
status: "active",
},
],
})) as unknown as WorkOSAuth["Service"]["listUserMemberships"],
refreshSession: (() =>
Effect.fail(new WorkOSError())) as WorkOSAuth["Service"]["refreshSession"],
});
const response = yield* Effect.promise(() =>
fetch(
new Request("http://test.local/auth/callback?code=code&state=state_3", {
headers: { cookie: "wos-login-state=state_3" },
}),
),
);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
const setCookie = response.headers.get("set-cookie") ?? "";
expect(setCookie).toContain("wos-session=sealed_session_fallback");
}),
);
});