import type { DirectorySyncEffect, DirectorySyncStatus, OrgSsoStatus, SsoBeginError, SsoCompleteError, SsoController, SsoDecisionError, SsoFlow, SsoMutationError, SsoPortalError, SsoProfile, SsoResolutionDecision, SsoRouteDecision, SsoValidateError, SsoWebhookError, SsoWebhookEvent, } from "@trigger.dev/plugins"; import { errAsync, okAsync, type ResultAsync } from "neverthrow"; // The default fallback used when no cloud SSO plugin is installed. // `decideRouteForEmail` returns no_sso so OSS deployments behave // identically to a deployment with no SSO feature at all. Mutation // methods return feature_disabled so callers can surface a clear // "not available" message in UI gated by `isUsingPlugin()`. // // The fallback never touches the database. It still accepts the loader's // Prisma input for signature parity with the real cloud plugin factory // (so the loader can swap implementations without changing its call), // but ignores it entirely. export class SsoFallback { constructor(_prisma?: unknown) {} create(): SsoController { return new SsoFallbackController(); } } class SsoFallbackController implements SsoController { async isUsingPlugin(): Promise { return false; } getStatus(_organizationId: string): ResultAsync { return okAsync({ hasIdpOrg: false, enforced: false, jitProvisioningEnabled: false, jitDefaultRoleId: null, idpOrgId: null, primaryConnectionId: null, domains: [], connections: [], }); } generatePortalLink(_params: { organizationId: string; userId: string; intent: "sso" | "domain_verification" | "dsync"; returnUrl: string; }): ResultAsync<{ url: string }, SsoPortalError> { return errAsync("idp_org_unavailable" as const); } setEnforced(_params: { organizationId: string; enforced: boolean; }): ResultAsync { return errAsync("feature_disabled" as const); } setJitProvisioningEnabled(_params: { organizationId: string; enabled: boolean; }): ResultAsync { return errAsync("feature_disabled" as const); } setJitDefaultRole(_params: { organizationId: string; roleId: string | null; }): ResultAsync { return errAsync("feature_disabled" as const); } updateConfig(_params: { organizationId: string; enforced: boolean; jitProvisioningEnabled: boolean; jitDefaultRoleId: string | null; }): ResultAsync { return errAsync("feature_disabled" as const); } getDirectorySyncStatus( _organizationId: string ): ResultAsync { return okAsync({ hasDirectory: false, hasActiveDirectory: false, allowExternalDomainSync: false, allowManualMembership: true, directoryDefaultRoleId: null, userCount: 0, directories: [], groups: [], }); } setDirectoryGroupRole(_params: { organizationId: string; groupId: string; roleId: string | null; }): ResultAsync<{ effects: DirectorySyncEffect[] }, SsoMutationError> { return errAsync("feature_disabled" as const); } setDirectoryDefaultRole(_params: { organizationId: string; roleId: string | null; }): ResultAsync { return errAsync("feature_disabled" as const); } setAllowExternalDomainSync(_params: { organizationId: string; allowed: boolean; }): ResultAsync { return errAsync("feature_disabled" as const); } // OSS has no directory, so manual membership is always allowed. getMembershipPolicy( _organizationId: string ): ResultAsync<{ manualMembershipAllowed: boolean }, SsoDecisionError> { return okAsync({ manualMembershipAllowed: true }); } setAllowManualMembership(_params: { organizationId: string; allowed: boolean; }): ResultAsync { return errAsync("feature_disabled" as const); } recordMembershipRemoval(_params: { organizationId: string; userId: string; reason: "manual_removal" | "self_leave"; }): ResultAsync { // No plugin → no JIT → nothing to guard against. No-op success. return okAsync(undefined as void); } clearMembershipRemoval(_params: { organizationId: string; userId: string; }): ResultAsync { return okAsync(undefined as void); } decideRouteForEmail(_email: string): ResultAsync { return okAsync({ kind: "no_sso" as const }); } beginAuthorization(_params: { email: string; redirectTo: string; flow: SsoFlow; }): ResultAsync<{ url: string }, SsoBeginError> { return errAsync("feature_disabled" as const); } completeAuthorization(_params: { code: string; state: string }): ResultAsync< { profile: SsoProfile; redirectTo: string; flow: SsoFlow; }, SsoCompleteError > { return errAsync("connection_unknown" as const); } completeIdpInitiatedAuthorization(_params: { code: string; }): ResultAsync<{ profile: SsoProfile; redirectTo: string }, SsoCompleteError> { return errAsync("connection_unknown" as const); } // Fail-open: with no plugin there are no SSO sessions to invalidate, // and the host treats `valid: true` as "leave the session alone". validateSession(_params: { userId: string; idpOrgId: string; connectionId: string; }): ResultAsync<{ valid: boolean }, SsoValidateError> { return okAsync({ valid: true }); } resolveSsoIdentity(_params: { profile: SsoProfile; }): ResultAsync { return errAsync("feature_disabled" as const); } attachSsoIdentity(_params: { userId: string; profile: SsoProfile; }): ResultAsync { return errAsync("feature_disabled" as const); } evaluateJit(_params: { userId: string; idpOrgId: string; }): ResultAsync< { shouldProvision: boolean; organizationId: string; roleId: string | null }, SsoMutationError > { return errAsync("feature_disabled" as const); } verifyWebhook(_params: { rawBody: string; headers: Record; }): ResultAsync<{ event: SsoWebhookEvent }, SsoWebhookError> { return errAsync("feature_disabled" as const); } processWebhookEvent( _event: SsoWebhookEvent ): ResultAsync<{ effects: DirectorySyncEffect[] }, SsoWebhookError> { // No plugin: nothing to verify or act on. The host's webhook proxy // already rejects unverified requests, so a call here just no-ops. return okAsync({ effects: [] }); } }