// ---------------------------------------------------------------------------
// WorkOS AuthKit — Effect-native sealed session management
// ---------------------------------------------------------------------------
import { Context, Effect, Layer } from "effect";
import { WorkOS } from "@workos-inc/node/worker";
import { WorkOSError, tryPromiseService, withServiceLogging } from "./errors";
import { server } from "../env";
const COOKIE_NAME = "wos-session";
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
const make = Effect.gen(function* () {
const apiKey = server.WORKOS_API_KEY;
const clientId = server.WORKOS_CLIENT_ID;
const cookiePassword = server.WORKOS_COOKIE_PASSWORD;
if (!cookiePassword || cookiePassword.length < 32) {
return yield* Effect.die(new Error("WORKOS_COOKIE_PASSWORD must be at least 32 characters"));
}
const workos = new WorkOS({ apiKey, clientId });
const use = (fn: (wos: WorkOS) => Promise) =>
withServiceLogging(
"workos",
() => new WorkOSError(),
tryPromiseService(() => fn(workos)),
);
const authenticateSealedSession = (sessionData: string) =>
Effect.gen(function* () {
if (!sessionData) return null;
const session = workos.userManagement.loadSealedSession({
sessionData,
cookiePassword,
});
const result = yield* use(() => session.authenticate());
if (result.authenticated) {
return {
userId: result.user.id,
email: result.user.email,
firstName: result.user.firstName,
lastName: result.user.lastName,
avatarUrl: result.user.profilePictureUrl,
organizationId: result.organizationId,
sessionId: result.sessionId,
refreshedSession: undefined as string | undefined,
};
}
if (result.reason === "no_session_cookie_provided") return null;
// Try refreshing
const refreshed = yield* use(() => session.refresh()).pipe(
Effect.orElseSucceed(() => ({ authenticated: false as const })),
);
if (!refreshed.authenticated || !("sealedSession" in refreshed) || !refreshed.sealedSession)
return null;
return {
userId: refreshed.user.id,
email: refreshed.user.email,
firstName: refreshed.user.firstName,
lastName: refreshed.user.lastName,
avatarUrl: refreshed.user.profilePictureUrl,
organizationId: refreshed.organizationId,
sessionId: refreshed.sessionId,
refreshedSession: refreshed.sealedSession,
};
});
return {
getAuthorizationUrl: (redirectUri: string) =>
workos.userManagement.getAuthorizationUrl({
provider: "authkit",
redirectUri,
clientId,
}),
authenticateWithCode: (code: string) =>
use((wos) =>
wos.userManagement.authenticateWithCode({
code,
clientId,
session: { sealSession: true, cookiePassword },
}),
),
/** Create a new organization in WorkOS. */
createOrganization: (name: string) =>
use((wos) => wos.organizations.createOrganization({ name })),
/** Add a user to an organization as a member. */
createMembership: (organizationId: string, userId: string) =>
use((wos) =>
wos.userManagement.createOrganizationMembership({
organizationId,
userId,
}),
),
/**
* Refresh a sealed session, optionally switching to a new organization.
* Returns the new sealed session string or null if refresh failed.
*/
refreshSession: (sessionData: string, organizationId?: string) =>
Effect.gen(function* () {
const session = workos.userManagement.loadSealedSession({
sessionData,
cookiePassword,
});
const refreshed = yield* use(() =>
session.refresh(organizationId ? { organizationId } : undefined),
);
if (!refreshed.authenticated || !("sealedSession" in refreshed)) return null;
return refreshed.sealedSession ?? null;
}),
/**
* Authenticate a sealed session string. Returns the user info plus
* any refreshed session that needs to be set on the response.
* Returns null if the session is missing or invalid.
*/
authenticateSealedSession,
/** Authenticate from a Request — convenience wrapper around `authenticateSealedSession`. */
authenticateRequest: (request: Request) =>
Effect.gen(function* () {
const sessionData = parseCookie(request.headers.get("cookie"), COOKIE_NAME);
if (!sessionData) return null;
return yield* authenticateSealedSession(sessionData);
}),
/** List organization memberships with user details. */
listOrgMembers: (organizationId: string) =>
use((wos) =>
wos.userManagement.listOrganizationMemberships({
organizationId,
statuses: ["active", "pending"],
}),
),
/** Get a user by ID. */
getUser: (userId: string) => use((wos) => wos.userManagement.getUser(userId)),
/** Send an organization invitation. */
sendInvitation: (params: { email: string; organizationId: string; roleSlug?: string }) =>
use((wos) =>
wos.userManagement.sendInvitation({
email: params.email,
organizationId: params.organizationId,
roleSlug: params.roleSlug,
}),
),
/** Remove an organization membership. */
deleteOrgMembership: (membershipId: string) =>
use((wos) => wos.userManagement.deleteOrganizationMembership(membershipId)),
/** Get the role for a membership. */
getOrgMembership: (membershipId: string) =>
use((wos) => wos.userManagement.getOrganizationMembership(membershipId)),
/** Update a membership's role. */
updateOrgMembershipRole: (membershipId: string, roleSlug: string) =>
use((wos) => wos.userManagement.updateOrganizationMembership(membershipId, { roleSlug })),
/** List available roles for an organization. */
listOrgRoles: (organizationId: string) =>
use((wos) => wos.organizations.listOrganizationRoles({ organizationId })),
};
});
export type WorkOSAuthService = Effect.Effect.Success;
export class WorkOSAuth extends Context.Tag("@executor/cloud/WorkOSAuth")<
WorkOSAuth,
WorkOSAuthService
>() {
static Default = Layer.effect(this, make).pipe(Layer.annotateSpans({ module: "WorkOSAuth" }));
}
const parseCookie = (cookieHeader: string | null, name: string): string | null => {
if (!cookieHeader) return null;
const match = cookieHeader
.split(";")
.map((c) => c.trim())
.find((c) => c.startsWith(`${name}=`));
if (!match) return null;
return match.slice(name.length + 1) || null;
};