import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"; import { Schema } from "effect"; import { ApiKeyManagementError } from "./api-key-errors"; import { UserStoreError, WorkOSError } from "./errors"; import { NoOrganization, SessionAuth } from "./middleware"; const AuthUser = Schema.Struct({ id: Schema.String, email: Schema.String, name: Schema.NullOr(Schema.String), avatarUrl: Schema.NullOr(Schema.String), }); const AuthOrganization = Schema.Struct({ id: Schema.String, name: Schema.String, }); const AuthMeResponse = Schema.Struct({ user: AuthUser, organization: Schema.NullOr(AuthOrganization), }); const AuthOrganizationSummary = Schema.Struct({ id: Schema.String, name: Schema.String, }); const AuthOrganizationsResponse = Schema.Struct({ organizations: Schema.Array(AuthOrganizationSummary), activeOrganizationId: Schema.NullOr(Schema.String), }); const SwitchOrganizationBody = Schema.Struct({ organizationId: Schema.String, }); const CreateOrganizationBody = Schema.Struct({ name: Schema.String, }); const CreateOrganizationResponse = Schema.Struct({ id: Schema.String, name: Schema.String, }); // `state` is optional — some WorkOS-initiated redirects arrive at the // callback without the state we set on /auth/login. The CSRF check is // only enforced when state is present (see callback handler). const AuthCallbackSearch = Schema.Struct({ code: Schema.String, state: Schema.optional(Schema.String), }); const PendingInvitationInviter = Schema.Struct({ email: Schema.String, name: Schema.NullOr(Schema.String), }); const PendingInvitation = Schema.Struct({ id: Schema.String, organizationId: Schema.String, organizationName: Schema.String, createdAt: Schema.String, inviter: Schema.NullOr(PendingInvitationInviter), }); const PendingInvitationsResponse = Schema.Struct({ invitations: Schema.Array(PendingInvitation), }); const AcceptInvitationBody = Schema.Struct({ invitationId: Schema.String, }); const AcceptInvitationResponse = Schema.Struct({ id: Schema.String, name: Schema.String, }); const ApiKeySummary = Schema.Struct({ id: Schema.String, name: Schema.String, obfuscatedValue: Schema.String, createdAt: Schema.String, updatedAt: Schema.String, lastUsedAt: Schema.NullOr(Schema.String), }); const ApiKeysResponse = Schema.Struct({ apiKeys: Schema.Array(ApiKeySummary), }); const CreateApiKeyBody = Schema.Struct({ name: Schema.String, }); const CreatedApiKeyResponse = Schema.Struct({ id: Schema.String, name: Schema.String, obfuscatedValue: Schema.String, createdAt: Schema.String, updatedAt: Schema.String, lastUsedAt: Schema.NullOr(Schema.String), value: Schema.String, }); const ApiKeyParams = { apiKeyId: Schema.String }; export const AUTH_PATHS = { login: "/api/auth/login", logout: "/api/auth/logout", callback: "/api/auth/callback", switchOrganization: "/api/auth/switch-organization", } as const; const AuthErrors = [UserStoreError, WorkOSError] as const; const ApiKeyErrors = [ApiKeyManagementError, NoOrganization, UserStoreError, WorkOSError] as const; /** Public auth endpoints — no authentication required */ export class CloudAuthPublicApi extends HttpApiGroup.make("cloudAuthPublic") .add(HttpApiEndpoint.get("login", "/auth/login")) .add( HttpApiEndpoint.get("callback", "/auth/callback", { query: AuthCallbackSearch, error: AuthErrors, }), ) {} /** Session auth endpoints — require a logged-in user, may not have an org */ export class CloudAuthApi extends HttpApiGroup.make("cloudAuth") .add( HttpApiEndpoint.get("me", "/auth/me", { success: AuthMeResponse, error: AuthErrors, }), ) .add(HttpApiEndpoint.post("logout", "/auth/logout")) .add( HttpApiEndpoint.get("organizations", "/auth/organizations", { success: AuthOrganizationsResponse, error: WorkOSError, }), ) .add( HttpApiEndpoint.post("switchOrganization", "/auth/switch-organization", { payload: SwitchOrganizationBody, error: WorkOSError, }), ) .add( HttpApiEndpoint.post("createOrganization", "/auth/create-organization", { payload: CreateOrganizationBody, success: CreateOrganizationResponse, error: AuthErrors, }), ) .add( HttpApiEndpoint.get("pendingInvitations", "/auth/pending-invitations", { success: PendingInvitationsResponse, error: WorkOSError, }), ) .add( HttpApiEndpoint.post("acceptInvitation", "/auth/accept-invitation", { payload: AcceptInvitationBody, success: AcceptInvitationResponse, error: AuthErrors, }), ) .add( HttpApiEndpoint.get("listApiKeys", "/auth/api-keys", { success: ApiKeysResponse, error: ApiKeyErrors, }), ) .add( HttpApiEndpoint.post("createApiKey", "/auth/api-keys", { payload: CreateApiKeyBody, success: CreatedApiKeyResponse, error: ApiKeyErrors, }), ) .add( HttpApiEndpoint.delete("revokeApiKey", "/auth/api-keys/:apiKeyId", { params: ApiKeyParams, error: ApiKeyErrors, }), ) .middleware(SessionAuth) {}