Files
triggerdotdev--trigger.dev/internal-packages/sso/src/fallback.ts
T
2026-07-13 13:32:57 +08:00

240 lines
6.6 KiB
TypeScript

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<boolean> {
return false;
}
getStatus(_organizationId: string): ResultAsync<OrgSsoStatus, SsoDecisionError> {
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<void, SsoMutationError> {
return errAsync("feature_disabled" as const);
}
setJitProvisioningEnabled(_params: {
organizationId: string;
enabled: boolean;
}): ResultAsync<void, SsoMutationError> {
return errAsync("feature_disabled" as const);
}
setJitDefaultRole(_params: {
organizationId: string;
roleId: string | null;
}): ResultAsync<void, SsoMutationError> {
return errAsync("feature_disabled" as const);
}
updateConfig(_params: {
organizationId: string;
enforced: boolean;
jitProvisioningEnabled: boolean;
jitDefaultRoleId: string | null;
}): ResultAsync<void, SsoMutationError> {
return errAsync("feature_disabled" as const);
}
getDirectorySyncStatus(
_organizationId: string
): ResultAsync<DirectorySyncStatus, SsoDecisionError> {
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<void, SsoMutationError> {
return errAsync("feature_disabled" as const);
}
setAllowExternalDomainSync(_params: {
organizationId: string;
allowed: boolean;
}): ResultAsync<void, SsoMutationError> {
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<void, SsoMutationError> {
return errAsync("feature_disabled" as const);
}
recordMembershipRemoval(_params: {
organizationId: string;
userId: string;
reason: "manual_removal" | "self_leave";
}): ResultAsync<void, SsoMutationError> {
// No plugin → no JIT → nothing to guard against. No-op success.
return okAsync(undefined as void);
}
clearMembershipRemoval(_params: {
organizationId: string;
userId: string;
}): ResultAsync<void, SsoMutationError> {
return okAsync(undefined as void);
}
decideRouteForEmail(_email: string): ResultAsync<SsoRouteDecision, SsoDecisionError> {
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<SsoResolutionDecision, SsoMutationError> {
return errAsync("feature_disabled" as const);
}
attachSsoIdentity(_params: {
userId: string;
profile: SsoProfile;
}): ResultAsync<void, SsoMutationError> {
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<string, string>;
}): 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: [] });
}
}