import * as Cloudflare from "alchemy/Cloudflare/Workers/Runtime";import { HttpApiBuilder } from "effect/unstable/httpapi";import { Cause, Effect } from "effect"; import { UserStoreService } from "../auth/context";import { AuthContext } from "../auth/middleware";import { WorkOSAuth } from "../auth/workos";import { AutumnService } from "../services/autumn";import { OrgHttpApi } from "./compose";import { Forbidden } from "./api";import { getMemberLimitForPlan, selectActiveMemberLimitPlan } from "./member-limits"; const requireAdmin = Effect.gen(function* () { const auth = yield* AuthContext; const workos = yield* WorkOSAuth; const memberships = yield* workos.listOrgMembers(auth.organizationId); const currentMembership = memberships.data.find((m) => m.userId === auth.accountId); if (!currentMembership || currentMembership.role?.slug !== "admin") { return yield* new Forbidden(); }}); // Target-ownership checks — independent of caller privilege. `requireAdmin`// confirms the caller is an admin of their session's org; these confirm the// resource they're about to mutate actually lives in that same org. Without// this, an admin of org A who obtained a membership/domain id from org B// (leak, screenshot, support context) could trigger the WorkOS SDK against// org B's resource — the workspace API key is workspace-wide and WorkOS// does not enforce per-org ownership on delete/update by id. Failures// (not found OR org mismatch) both surface as Forbidden so we don't leak// existence of ids outside the caller's org.const assertMembershipInSessionOrg = (membershipId: string) => Effect.gen(function* () { const auth = yield* AuthContext; const workos = yield* WorkOSAuth; const membership = yield* workos .getOrgMembership(membershipId) .pipe(Effect.catchCause(() => Effect.succeed(null))); if (!membership || membership.organizationId !== auth.organizationId) { return yield* new Forbidden(); } }); const assertDomainInSessionOrg = (domainId: string) => Effect.gen(function* () { const auth = yield* AuthContext; const workos = yield* WorkOSAuth; const domain = yield* workos .getOrganizationDomain(domainId) .pipe(Effect.catchCause(() => Effect.succeed(null))); if (!domain || domain.organizationId !== auth.organizationId) { return yield* new Forbidden(); } }); // Compute live seat usage from WorkOS truth (active+pending memberships +// pending invitations) and look up the per-plan cap from MEMBER_LIMITS.// Recomputed on every call — no event-counting drift.const getMemberSeats = (organizationId: string) => Effect.gen(function* () { const autumn = yield* AutumnService; const workos = yield* WorkOSAuth; const customer = yield* autumn.use((client) => client.customers.getOrCreate({ customerId: organizationId }), ); const planId = selectActiveMemberLimitPlan(customer.subscriptions); const limit = getMemberLimitForPlan(planId); const memberships = yield* workos.listOrgMembers(organizationId); const invitations = yield* workos.listPendingInvitations(organizationId); return { used: memberships.data.length + invitations.data.length, granted: limit ?? 0, unlimited: limit === null, }; }); const reserveMemberSlot = Effect.gen(function* () { const auth = yield* AuthContext; const seats = yield* getMemberSeats(auth.organizationId).pipe( Effect.tap((s) => Effect.logInfo("members.check").pipe( Effect.annotateLogs({ "org.id": auth.organizationId, "members.used": s.used, "members.granted": s.granted, "members.unlimited": s.unlimited, }), ), ), Effect.catchCause((cause) => Effect.gen(function* () { yield* Effect.logError("members.seats lookup failed; failing closed").pipe( Effect.annotateLogs({ "org.id": auth.organizationId, cause: Cause.pretty(cause) }), ); return yield* new Forbidden(); }), ), ); if (!seats.unlimited && seats.used >= seats.granted) { return yield* new Forbidden(); }}); export const OrgHandlers = HttpApiBuilder.group(OrgHttpApi, "org", (handlers) => handlers .handle("listMembers", () => Effect.gen(function* () { const auth = yield* AuthContext; const workos = yield* WorkOSAuth; // The list endpoint falls back to safe display defaults if the seats // lookup errors — we never want a transient Autumn or WorkOS hiccup // to blank the members page. The actual cap gate lives in // `reserveMemberSlot`, which fails closed. const seats = yield* getMemberSeats(auth.organizationId).pipe( Effect.catchTag("AutumnError", (error) => Effect.logError("listMembers.seats: autumn lookup failed").pipe( Effect.annotateLogs({ "org.id": auth.organizationId, error: String(error.cause) }), Effect.as({ used: 0, granted: 0, unlimited: false }), ), ), ); const memberships = yield* workos.listOrgMembers(auth.organizationId); yield* Effect.logInfo("listMembers.seats").pipe( Effect.annotateLogs({ "org.id": auth.organizationId, "members.count": memberships.data.length, "seats.used": seats.used, "seats.granted": seats.granted, "seats.unlimited": seats.unlimited, }), ); const members = yield* Effect.all( memberships.data.map((m) => Effect.gen(function* () { const user = yield* workos.getUser(m.userId); return { id: m.id, userId: m.userId, email: user.email, name: [user.firstName, user.lastName].filter(Boolean).join(" ") || null, avatarUrl: user.profilePictureUrl ?? null, role: m.role?.slug ?? "member", status: m.status, lastActiveAt: user.lastSignInAt ?? null, isCurrentUser: m.userId === auth.accountId, }; }), ), { concurrency: 5 }, ); return { members, seats }; }), ) .handle("listRoles", () => Effect.gen(function* () { const auth = yield* AuthContext; const workos = yield* WorkOSAuth; const result = yield* workos.listOrgRoles(auth.organizationId); return { roles: result.data.map((r) => ({ slug: r.slug, name: r.name, })), }; }), ) .handle("invite", ({ payload }) => Effect.gen(function* () { yield* requireAdmin; const auth = yield* AuthContext; const workos = yield* WorkOSAuth; yield* reserveMemberSlot; const invitation = yield* workos.sendInvitation({ email: payload.email, organizationId: auth.organizationId, roleSlug: payload.roleSlug, }); return { id: invitation.id, email: invitation.email }; }), ) .handle("removeMember", ({ params }) => Effect.gen(function* () { yield* requireAdmin; yield* assertMembershipInSessionOrg(params.membershipId); const workos = yield* WorkOSAuth; yield* workos.deleteOrgMembership(params.membershipId); return { success: true }; }), ) .handle("updateMemberRole", ({ params, payload }) => Effect.gen(function* () { yield* requireAdmin; yield* assertMembershipInSessionOrg(params.membershipId); const workos = yield* WorkOSAuth; yield* workos.updateOrgMembershipRole(params.membershipId, payload.roleSlug); return { success: true }; }), ) .handle("listDomains", () => Effect.gen(function* () { const auth = yield* AuthContext; const workos = yield* WorkOSAuth; const org = yield* workos.getOrganization(auth.organizationId); const domains = yield* Effect.all( org.domains.map((d) => Effect.gen(function* () { const full = yield* workos.getOrganizationDomain(d.id); return { id: full.id, domain: full.domain, state: full.state, verificationToken: full.verificationToken, verificationPrefix: full.verificationPrefix, }; }), ), { concurrency: 5 }, ); return { domains }; }), ) .handle("getDomainVerificationLink", () => Effect.gen(function* () { yield* requireAdmin; const auth = yield* AuthContext; const autumn = yield* AutumnService; const check = yield* autumn .use((client) => client.check({ customerId: auth.organizationId, featureId: "domain-verification", }), ) .pipe(Effect.orElseSucceed(() => ({ allowed: false }))); if (!check.allowed) { return yield* new Forbidden(); } const workos = yield* WorkOSAuth; const env = yield* Cloudflare.WorkerEnvironment.typed<Env>(); const { link } = yield* workos.generateDomainVerificationPortalLink( auth.organizationId, env.VITE_PUBLIC_SITE_URL ? `${env.VITE_PUBLIC_SITE_URL}/org` : "/org", ); return { link }; }), ) .handle("deleteDomain", ({ params }) => Effect.gen(function* () { yield* requireAdmin; yield* assertDomainInSessionOrg(params.domainId); const workos = yield* WorkOSAuth; yield* workos.deleteOrganizationDomain(params.domainId); return { success: true }; }), ) .handle("updateOrgName", ({ payload }) => Effect.gen(function* () { yield* requireAdmin; const auth = yield* AuthContext; const workos = yield* WorkOSAuth; const users = yield* UserStoreService; const org = yield* workos.updateOrganization(auth.organizationId, payload.name); yield* users.use((s) => s.upsertOrganization({ id: org.id, name: org.name })); return { name: org.name }; }), ),);
import * as Cloudflare from "alchemy/Cloudflare/Workers/Runtime";
import { HttpApiBuilder } from "effect/unstable/httpapi";
import { Cause, Effect } from "effect";
import { UserStoreService } from "../auth/context";
import { AuthContext } from "../auth/middleware";
import { WorkOSAuth } from "../auth/workos";
import { AutumnService } from "../services/autumn";
import { OrgHttpApi } from "./compose";
import { Forbidden } from "./api";
import { getMemberLimitForPlan, selectActiveMemberLimitPlan } from "./member-limits";
const requireAdmin = Effect.gen(function* () {
const auth = yield* AuthContext;
const workos = yield* WorkOSAuth;
const memberships = yield* workos.listOrgMembers(auth.organizationId);
const currentMembership = memberships.data.find((m) => m.userId === auth.accountId);
if (!currentMembership || currentMembership.role?.slug !== "admin") {
return yield* new Forbidden();
}
});
// Target-ownership checks — independent of caller privilege. `requireAdmin`
// confirms the caller is an admin of their session's org; these confirm the
// resource they're about to mutate actually lives in that same org. Without
// this, an admin of org A who obtained a membership/domain id from org B
// (leak, screenshot, support context) could trigger the WorkOS SDK against
// org B's resource — the workspace API key is workspace-wide and WorkOS
// does not enforce per-org ownership on delete/update by id. Failures
// (not found OR org mismatch) both surface as Forbidden so we don't leak
// existence of ids outside the caller's org.
const assertMembershipInSessionOrg = (membershipId: string) =>
Effect.gen(function* () {
const auth = yield* AuthContext;
const workos = yield* WorkOSAuth;
const membership = yield* workos
.getOrgMembership(membershipId)
.pipe(Effect.catchCause(() => Effect.succeed(null)));
if (!membership || membership.organizationId !== auth.organizationId) {
return yield* new Forbidden();
}
});
const assertDomainInSessionOrg = (domainId: string) =>
Effect.gen(function* () {
const auth = yield* AuthContext;
const workos = yield* WorkOSAuth;
const domain = yield* workos
.getOrganizationDomain(domainId)
.pipe(Effect.catchCause(() => Effect.succeed(null)));
if (!domain || domain.organizationId !== auth.organizationId) {
return yield* new Forbidden();
}
});
// Compute live seat usage from WorkOS truth (active+pending memberships +
// pending invitations) and look up the per-plan cap from MEMBER_LIMITS.
// Recomputed on every call — no event-counting drift.
const getMemberSeats = (organizationId: string) =>
Effect.gen(function* () {
const autumn = yield* AutumnService;
const workos = yield* WorkOSAuth;
const customer = yield* autumn.use((client) =>
client.customers.getOrCreate({ customerId: organizationId }),
);
const planId = selectActiveMemberLimitPlan(customer.subscriptions);
const limit = getMemberLimitForPlan(planId);
const memberships = yield* workos.listOrgMembers(organizationId);
const invitations = yield* workos.listPendingInvitations(organizationId);
return {
used: memberships.data.length + invitations.data.length,
granted: limit ?? 0,
unlimited: limit === null,
};
});
const reserveMemberSlot = Effect.gen(function* () {
const auth = yield* AuthContext;
const seats = yield* getMemberSeats(auth.organizationId).pipe(
Effect.tap((s) =>
Effect.logInfo("members.check").pipe(
Effect.annotateLogs({
"org.id": auth.organizationId,
"members.used": s.used,
"members.granted": s.granted,
"members.unlimited": s.unlimited,
}),
),
),
Effect.catchCause((cause) =>
Effect.gen(function* () {
yield* Effect.logError("members.seats lookup failed; failing closed").pipe(
Effect.annotateLogs({ "org.id": auth.organizationId, cause: Cause.pretty(cause) }),
);
return yield* new Forbidden();
}),
),
);
if (!seats.unlimited && seats.used >= seats.granted) {
return yield* new Forbidden();
}
});
export const OrgHandlers = HttpApiBuilder.group(OrgHttpApi, "org", (handlers) =>
handlers
.handle("listMembers", () =>
Effect.gen(function* () {
const auth = yield* AuthContext;
const workos = yield* WorkOSAuth;
// The list endpoint falls back to safe display defaults if the seats
// lookup errors — we never want a transient Autumn or WorkOS hiccup
// to blank the members page. The actual cap gate lives in
// `reserveMemberSlot`, which fails closed.
const seats = yield* getMemberSeats(auth.organizationId).pipe(
Effect.catchTag("AutumnError", (error) =>
Effect.logError("listMembers.seats: autumn lookup failed").pipe(
Effect.annotateLogs({ "org.id": auth.organizationId, error: String(error.cause) }),
Effect.as({ used: 0, granted: 0, unlimited: false }),
),
),
);
const memberships = yield* workos.listOrgMembers(auth.organizationId);
yield* Effect.logInfo("listMembers.seats").pipe(
Effect.annotateLogs({
"org.id": auth.organizationId,
"members.count": memberships.data.length,
"seats.used": seats.used,
"seats.granted": seats.granted,
"seats.unlimited": seats.unlimited,
}),
);
const members = yield* Effect.all(
memberships.data.map((m) =>
Effect.gen(function* () {
const user = yield* workos.getUser(m.userId);
return {
id: m.id,
userId: m.userId,
email: user.email,
name: [user.firstName, user.lastName].filter(Boolean).join(" ") || null,
avatarUrl: user.profilePictureUrl ?? null,
role: m.role?.slug ?? "member",
status: m.status,
lastActiveAt: user.lastSignInAt ?? null,
isCurrentUser: m.userId === auth.accountId,
};
}),
),
{ concurrency: 5 },
);
return { members, seats };
}),
)
.handle("listRoles", () =>
Effect.gen(function* () {
const auth = yield* AuthContext;
const workos = yield* WorkOSAuth;
const result = yield* workos.listOrgRoles(auth.organizationId);
return {
roles: result.data.map((r) => ({
slug: r.slug,
name: r.name,
})),
};
}),
)
.handle("invite", ({ payload }) =>
Effect.gen(function* () {
yield* requireAdmin;
const auth = yield* AuthContext;
const workos = yield* WorkOSAuth;
yield* reserveMemberSlot;
const invitation = yield* workos.sendInvitation({
email: payload.email,
organizationId: auth.organizationId,
roleSlug: payload.roleSlug,
});
return { id: invitation.id, email: invitation.email };
}),
)
.handle("removeMember", ({ params }) =>
Effect.gen(function* () {
yield* requireAdmin;
yield* assertMembershipInSessionOrg(params.membershipId);
const workos = yield* WorkOSAuth;
yield* workos.deleteOrgMembership(params.membershipId);
return { success: true };
}),
)
.handle("updateMemberRole", ({ params, payload }) =>
Effect.gen(function* () {
yield* requireAdmin;
yield* assertMembershipInSessionOrg(params.membershipId);
const workos = yield* WorkOSAuth;
yield* workos.updateOrgMembershipRole(params.membershipId, payload.roleSlug);
return { success: true };
}),
)
.handle("listDomains", () =>
Effect.gen(function* () {
const auth = yield* AuthContext;
const workos = yield* WorkOSAuth;
const org = yield* workos.getOrganization(auth.organizationId);
const domains = yield* Effect.all(
org.domains.map((d) =>
Effect.gen(function* () {
const full = yield* workos.getOrganizationDomain(d.id);
return {
id: full.id,
domain: full.domain,
state: full.state,
verificationToken: full.verificationToken,
verificationPrefix: full.verificationPrefix,
};
}),
),
{ concurrency: 5 },
);
return { domains };
}),
)
.handle("getDomainVerificationLink", () =>
Effect.gen(function* () {
yield* requireAdmin;
const auth = yield* AuthContext;
const autumn = yield* AutumnService;
const check = yield* autumn
.use((client) =>
client.check({
customerId: auth.organizationId,
featureId: "domain-verification",
}),
)
.pipe(Effect.orElseSucceed(() => ({ allowed: false })));
if (!check.allowed) {
return yield* new Forbidden();
}
const workos = yield* WorkOSAuth;
const env = yield* Cloudflare.WorkerEnvironment.typed<Env>();
const { link } = yield* workos.generateDomainVerificationPortalLink(
auth.organizationId,
env.VITE_PUBLIC_SITE_URL ? `${env.VITE_PUBLIC_SITE_URL}/org` : "/org",
);
return { link };
}),
)
.handle("deleteDomain", ({ params }) =>
Effect.gen(function* () {
yield* requireAdmin;
yield* assertDomainInSessionOrg(params.domainId);
const workos = yield* WorkOSAuth;
yield* workos.deleteOrganizationDomain(params.domainId);
return { success: true };
}),
)
.handle("updateOrgName", ({ payload }) =>
Effect.gen(function* () {
yield* requireAdmin;
const auth = yield* AuthContext;
const workos = yield* WorkOSAuth;
const users = yield* UserStoreService;
const org = yield* workos.updateOrganization(auth.organizationId, payload.name);
yield* users.use((s) => s.upsertOrganization({ id: org.id, name: org.name }));
return { name: org.name };
}),
),
);