// SPDX-License-Identifier: AGPL-3.0-or-later import {ThemeTypes} from '@fluxer/constants/src/UserConstants'; import { WebAuthnAuthenticationOptions, WebAuthnAuthenticationResponse, WebAuthnRegistrationResponse, } from '@fluxer/schema/src/domains/auth/WebAuthnSchemas'; import {UserPartialResponse, UserPrivateResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas'; import { createNamedStringLiteralUnion, createStringType, SnowflakeStringType, } from '@fluxer/schema/src/primitives/SchemaPrimitives'; import { EmailType, GlobalNameType, PasswordType, PhoneNumberType, UsernameType, } from '@fluxer/schema/src/primitives/UserValidators'; import {z} from 'zod'; const RegisterThemeType = createNamedStringLiteralUnion( [ [ThemeTypes.DARK, 'DARK', 'Dark colour theme'], [ThemeTypes.DARK_LEGACY, 'DARK_LEGACY', 'Legacy dark colour theme (original neutral grey palette)'], [ThemeTypes.COAL, 'COAL', 'Coal/darker colour theme'], [ThemeTypes.LIGHT, 'LIGHT', 'Light colour theme'], [ThemeTypes.SYSTEM, 'SYSTEM', 'Follow system colour preference'], ] as const, 'UI theme preference', ); export const RegisterRequest = z.object({ email: EmailType.optional().describe('Email address for the new account'), username: UsernameType.optional().describe('Username for the new account (1-32 characters)'), global_name: GlobalNameType.optional().describe('Display name shown to other users'), password: PasswordType.optional().describe('Password for the new account'), date_of_birth: createStringType(10, 10) .refine((value) => /^\d{4}-\d{2}-\d{2}$/.test(value), 'Invalid date format') .optional() .describe('Date of birth in YYYY-MM-DD format'), consent: z.boolean().default(false).describe('Whether user consents to terms of service'), invite_code: createStringType(0, 256).nullish().describe('Guild invite code to join after registration'), registration_url_code: createStringType(1, 256) .nullish() .describe('Admin-issued registration URL code to use for this registration'), theme: RegisterThemeType.optional().describe('Initial UI theme preference for the new account'), }); export type RegisterRequest = z.infer; export const UsernameSuggestionsRequest = z.object({ global_name: GlobalNameType.describe('Display name to generate username suggestions from'), }); export type UsernameSuggestionsRequest = z.infer; export const LoginRequest = z.object({ email: EmailType.describe('Email address for authentication'), password: PasswordType.describe('Account password'), invite_code: createStringType(0, 256).nullish().describe('Guild invite code to join after login'), }); export type LoginRequest = z.infer; export const LogoutAuthSessionsRequest = z.object({ session_id_hashes: z.array(createStringType()).max(100).describe('Array of session ID hashes to log out (max 100)'), password: PasswordType.optional().describe('Account password for verification'), }); export type LogoutAuthSessionsRequest = z.infer; export const ForgotPasswordRequest = z.object({ email: EmailType.describe('Email address to send password reset link'), }); export type ForgotPasswordRequest = z.infer; export const ResetPasswordRequest = z.object({ token: createStringType(64, 64).describe('Password reset token from email'), password: PasswordType.describe('New password to set'), }); export type ResetPasswordRequest = z.infer; export const ResetPasswordTokenParam = z.object({ token: createStringType(64, 64).describe('Password reset token from email'), }); export type ResetPasswordTokenParam = z.infer; export const ValidateResetPasswordTokenResponse = z.object({ valid: z.boolean().describe('Whether the password reset token is valid and unexpired'), }); export type ValidateResetPasswordTokenResponse = z.infer; export const EmailRevertRequest = z.object({ token: createStringType(64, 64).describe('Email revert token from email'), password: PasswordType.describe('Account password for verification'), }); export type EmailRevertRequest = z.infer; export const VerifyEmailRequest = z.object({ token: createStringType(64, 64).describe('Email verification token from email'), }); export type VerifyEmailRequest = z.infer; export const SudoVerificationSchema = z.object({ password: PasswordType.optional().describe('Account password for sudo verification'), mfa_method: createNamedStringLiteralUnion( [ ['totp', 'TOTP', 'Time-based one-time password authentication via authenticator app'], ['webauthn', 'WebAuthn', 'Security key or biometric authentication'], ], 'MFA method to use for verification', ).optional(), mfa_code: createStringType(1, 32).optional().describe('MFA verification code from an authenticator app'), webauthn_response: WebAuthnAuthenticationResponse.optional().describe('WebAuthn authentication response'), webauthn_challenge: createStringType().optional().describe('WebAuthn challenge string'), }); export const SsoStatusResponse = z.object({ enabled: z.boolean().describe('Whether SSO is enabled for this instance'), enforced: z.boolean().describe('Whether SSO is required for all users'), display_name: z.string().nullable().describe('Display name of the SSO provider'), redirect_uri: z.string().describe('OAuth redirect URI for SSO'), }); export type SsoStatusResponse = z.infer; export const SsoStartResponse = z.object({ authorization_url: z.string().describe('URL to redirect user to for SSO authentication'), state: z.string().describe('State parameter for CSRF protection'), redirect_uri: z.string().describe('OAuth redirect URI used for the SSO provider callback'), }); export type SsoStartResponse = z.infer; export const SsoCompleteResponse = z.object({ token: z.string().describe('Authentication token for the session'), user_id: SnowflakeStringType.describe('ID of the authenticated user'), user: UserPartialResponse.describe('Partial user data for the authenticated account'), redirect_to: z.string().describe('URL to redirect the user to after completion'), }); export type SsoCompleteResponse = z.infer; export const AuthTokenWithUserIdResponse = z.object({ token: z.string().describe('Authentication token for API requests'), user_id: SnowflakeStringType.describe('ID of the authenticated user'), user: UserPartialResponse.describe('Partial user data for the authenticated account'), }); export type AuthTokenWithUserIdResponse = z.infer; export const AuthRegistrationPendingApprovalResponse = z.object({ registration_pending_approval: z.literal(true).describe('Registration succeeded and is waiting for admin approval'), user_id: SnowflakeStringType.describe('ID of the registered account waiting for approval'), }); export type AuthRegistrationPendingApprovalResponse = z.infer; const AuthMfaRequiredResponse = z.object({ mfa: z.literal(true).describe('Indicates MFA is required to complete authentication'), ticket: z.string().describe('MFA ticket to use when completing MFA verification'), allowed_methods: z.array(z.string()).max(10).describe('List of allowed MFA methods'), totp: z.boolean().describe('Whether TOTP authenticator MFA is available'), webauthn: z.boolean().describe('Whether WebAuthn security key MFA is available'), backup_codes: z.boolean().describe('Whether the account has at least one unconsumed backup code'), }); export const AuthLoginResponse = z.union([AuthTokenWithUserIdResponse, AuthMfaRequiredResponse]); export type AuthLoginResponse = z.infer; export const AuthRegisterResponse = z.union([ AuthTokenWithUserIdResponse, AuthMfaRequiredResponse, AuthRegistrationPendingApprovalResponse, ]); export type AuthRegisterResponse = z.infer; export const AuthSessionLocation = z.object({ city: z.string().nullish().describe('The city name reported by the client'), region: z.string().nullish().describe('The region reported by the client'), country: z.string().nullish().describe('The country reported by the client'), }); export type AuthSessionLocation = z.infer; const AuthSessionClientInfo = z.object({ platform: z.string().nullish().describe('The platform reported by the client'), os: z.string().nullish().describe('The operating system reported by the client'), browser: z.string().nullish().describe('The browser reported by the client'), device: z.enum(['mobile', 'desktop']).describe('Device class of the session, decided by the server'), location: AuthSessionLocation.nullish().describe('The geolocation data sent by the client'), }); export const AuthSessionResponse = z.object({ id_hash: z.string().describe('The base64url-encoded session id hash'), client_info: AuthSessionClientInfo.nullish().describe('Client metadata recorded for this session'), masked_ip: z.string().nullable().describe('Semi-redacted IP address recorded for this session'), approx_last_used_at: z.iso.datetime().nullish().describe('Approximate timestamp of the last session activity'), current: z.boolean().describe('Whether this is the current session making the request'), }); export type AuthSessionResponse = z.infer; export const AuthSessionsResponse = z.array(AuthSessionResponse); export type AuthSessionsResponse = z.infer; export const WebAuthnAuthenticationOptionsResponse = WebAuthnAuthenticationOptions.omit({ hints: true, extensions: true, }); export type WebAuthnAuthenticationOptionsResponse = z.infer; export const UsernameSuggestionsResponse = z.object({ suggestions: z.array(z.string()).max(20).describe('List of suggested usernames'), }); export type UsernameSuggestionsResponse = z.infer; export const HandoffInitiateResponse = z.object({ code: z.string().describe('Handoff code to share with the receiving device'), expires_at: z.iso.datetime().describe('ISO 8601 timestamp when the handoff code expires'), poll_secret: z.string().optional().describe('Secret the initiating device must present to retrieve the token'), }); export type HandoffInitiateResponse = z.infer; const HandoffInfoClientInfo = z.object({ platform: z.string().nullish().describe('The platform of the requesting device'), os: z.string().nullish().describe('The operating system of the requesting device'), device: z.enum(['mobile', 'desktop']).describe('Device class of the requesting device, decided by the server'), location: AuthSessionLocation.nullish().describe('The approximate location of the requesting device'), }); export const HandoffInfoResponse = z.object({ status: z.string().describe('Current status of the handoff (pending, expired)'), client_info: HandoffInfoClientInfo.nullish().describe('Client information of the initiating device'), }); export type HandoffInfoResponse = z.infer; export const HandoffStatusResponse = z.object({ status: z.string().describe('Current status of the handoff (pending, completed, expired)'), token: z.string().nullish().describe('Authentication token if handoff is complete'), user_id: SnowflakeStringType.nullish().describe('User ID if handoff is complete'), user: UserPartialResponse.nullish().describe('Partial user data if handoff is complete'), }); export type HandoffStatusResponse = z.infer; export const SsoStartRequest = z.object({ redirect_to: createStringType(0, 2048).nullish().describe('URL to redirect to after SSO completion'), redirect_uri: createStringType(0, 2048).nullish().describe('OAuth redirect URI to use for the SSO provider callback'), }); export type SsoStartRequest = z.infer; export const SsoCompleteRequest = z.object({ code: createStringType(1, 4096).describe('Authorization code from the SSO provider'), state: createStringType(1, 4096).describe('State parameter for CSRF protection'), }); export type SsoCompleteRequest = z.infer; export const MfaTotpRequest = z.object({ code: createStringType().describe('The TOTP code from the authenticator app'), ticket: createStringType().describe('The MFA ticket from the login response'), }); export type MfaTotpRequest = z.infer; export const MfaTicketRequest = z.object({ ticket: createStringType().describe('The MFA ticket from the login response'), }); export type MfaTicketRequest = z.infer; export const AuthorizeIpRequest = z.object({ token: createStringType().describe('The IP authorization token from email'), }); export type AuthorizeIpRequest = z.infer; export const IpAuthorizationPollQuery = z.object({ ticket: createStringType().describe('The IP authorization ticket'), }); export type IpAuthorizationPollQuery = z.infer; export const IpAuthorizationPollResponse = z.object({ completed: z.boolean().describe('Whether the IP authorization has been completed'), token: z.string().nullish().describe('Authentication token if authorization is complete'), user_id: SnowflakeStringType.nullish().describe('User ID if authorization is complete'), user: UserPartialResponse.nullish().describe('Partial user data if authorization is complete'), }); export type IpAuthorizationPollResponse = z.infer; export const WebAuthnAuthenticateRequest = z.object({ response: WebAuthnAuthenticationResponse.describe('WebAuthn authentication response'), challenge: createStringType().describe('The challenge string from authentication options'), }); export type WebAuthnAuthenticateRequest = z.infer; export const WebAuthnMfaRequest = z.object({ response: WebAuthnAuthenticationResponse.describe('WebAuthn authentication response'), challenge: createStringType().describe('The challenge string from authentication options'), ticket: createStringType().describe('The MFA ticket from the login response'), }); export type WebAuthnMfaRequest = z.infer; export const HandoffCompleteRequest = z.object({ code: createStringType().describe('The handoff code from the initiating session'), token: createStringType().optional().describe('The authentication token to transfer'), user_id: createStringType().describe('The user ID associated with the authenticated session'), }); export type HandoffCompleteRequest = z.infer; export const HandoffCodeParam = z.object({ code: createStringType().describe('The handoff code'), }); export type HandoffCodeParam = z.infer; export const HandoffStatusRequest = z.object({ poll_secret: createStringType().describe('The poll secret issued when the handoff was initiated'), }); export type HandoffStatusRequest = z.infer; export const HandoffCancelRequest = z.object({ poll_secret: createStringType().describe('The poll secret issued when the handoff was initiated'), }); export type HandoffCancelRequest = z.infer; export const EnableMfaTotpRequest = z .object({ secret: createStringType(1, 256).describe('The TOTP secret key'), code: createStringType(1, 32).describe('The TOTP verification code'), }) .extend(SudoVerificationSchema.shape); export type EnableMfaTotpRequest = z.infer; export const DisableTotpRequest = z .object({ code: createStringType(1, 32).describe('The TOTP code to verify'), password: PasswordType.optional().describe('Account password for verification'), }) .extend(SudoVerificationSchema.shape); export type DisableTotpRequest = z.infer; export const MfaBackupCodesRequest = z .object({ regenerate: z.boolean().describe('Whether to regenerate backup codes'), password: PasswordType.optional().describe('Account password for verification'), }) .extend(SudoVerificationSchema.shape); export type MfaBackupCodesRequest = z.infer; const MfaBackupCodeResponse = z.object({ code: z.string().describe('The backup code'), consumed: z.boolean().describe('Whether the code has been used'), }); export const MfaBackupCodesResponse = z.object({ backup_codes: z.array(MfaBackupCodeResponse).describe('List of backup codes'), }); export type MfaBackupCodesResponse = z.infer; export const MfaBackupCodesChallengeStartResponse = z.object({ ticket: z.string().describe('Ticket for backup codes challenge actions'), code_expires_at: z.string().describe('ISO8601 timestamp when the verification code expires'), resend_available_at: z.string().describe('ISO8601 timestamp when the code can be resent'), }); export type MfaBackupCodesChallengeStartResponse = z.infer; export const MfaBackupCodesChallengeResendRequest = z.object({ ticket: createStringType().describe('Backup codes challenge ticket identifier'), }); export type MfaBackupCodesChallengeResendRequest = z.infer; export const MfaBackupCodesChallengeVerifyRequest = MfaBackupCodesChallengeResendRequest.extend({ code: createStringType().describe('Verification code sent to the email address'), }); export type MfaBackupCodesChallengeVerifyRequest = z.infer; export const MfaBackupCodesChallengeVerifyResponse = MfaBackupCodesResponse.extend({ verification_proof: z.string().describe('Proof token authorizing backup code regeneration on this ticket'), }); export type MfaBackupCodesChallengeVerifyResponse = z.infer; export const MfaBackupCodesChallengeRegenerateRequest = MfaBackupCodesChallengeResendRequest.extend({ verification_proof: createStringType().describe('Proof token obtained from verifying the email code'), }); export type MfaBackupCodesChallengeRegenerateRequest = z.infer; export const PhoneSendVerificationRequest = z.object({ phone: PhoneNumberType.describe('Phone number to send verification code'), channel: z .enum(['sms', 'inbound_challenge']) .optional() .describe( 'Channel to deliver the OTP on. Defaults to the first available channel from server policy. Server may override to an available fallback when the requested channel is disabled.', ), }); export type PhoneSendVerificationRequest = z.infer; const PhoneSendVerificationDeliveredResponse = z.object({ channel: z .literal('sms') .describe('Channel actually used for delivery (may differ from request when server adjusts)'), }); const PhoneSendVerificationInboundChallengeResponse = z.object({ channel: z.literal('inbound_challenge').describe('The user must send Fluxer an SMS instead of receiving one'), challenge_code: createStringType(4, 12).describe('The numeric code the user must text to our number'), our_number: createStringType(4, 32).describe('The Twilio number the user must text the code to (E.164)'), expires_at: z.iso.datetime().describe('ISO 8601 timestamp when this inbound challenge expires'), reason: z .enum(['voip', 'canadian', 'unknown_line_type', 'expensive_destination', 'account_forced', 'behavioural_risk']) .describe('Why inbound verification is required'), }); export const PhoneSendVerificationResponse = z.union([ PhoneSendVerificationDeliveredResponse, PhoneSendVerificationInboundChallengeResponse, ]); export type PhoneSendVerificationResponse = z.infer; export const PhoneVerifyRequest = z.object({ phone: PhoneNumberType.describe('Phone number being verified'), code: createStringType(1, 32).describe('The verification code'), }); export type PhoneVerifyRequest = z.infer; export const PhoneVerifyResponse = z.object({ verified: z.literal(true).describe('Indicates the phone number was verified successfully'), }); export type PhoneVerifyResponse = z.infer; export const WebAuthnCredentialResponse = z.object({ id: z.string().describe('The credential ID'), name: z.string().describe('User-assigned name for the credential'), created_at: z.string().describe('When the credential was registered'), last_used_at: z.string().nullable().describe('When the credential was last used'), rp_id: z.string().describe('Relying party ID the passkey belongs to'), }); export type WebAuthnCredentialResponse = z.infer; export const WebAuthnCredentialListResponse = z.array(WebAuthnCredentialResponse); export type WebAuthnCredentialListResponse = z.infer; export const WebAuthnChallengeResponse = z.looseObject({ challenge: z.string().describe('The WebAuthn challenge'), }); export type WebAuthnChallengeResponse = z.infer; export const WebAuthnRegisterRequest = z.object({ response: WebAuthnRegistrationResponse.describe('WebAuthn registration response'), challenge: createStringType(1, 1024).describe('The challenge from registration options'), name: createStringType(1, 100).describe('User-assigned name for the credential'), }); export type WebAuthnRegisterRequest = z.infer; export const WebAuthnCredentialUpdateRequest = z .object({ name: createStringType(1, 100).describe('New name for the credential'), }) .extend(SudoVerificationSchema.shape); export type WebAuthnCredentialUpdateRequest = z.infer; export const WebAuthnTwoFactorRequest = z .object({ enabled: z.boolean().describe('Whether registered passkeys count as a second factor when logging in'), }) .extend(SudoVerificationSchema.shape); export type WebAuthnTwoFactorRequest = z.infer; export const WebAuthnTwoFactorResponse = z.object({ user: UserPrivateResponse.describe('The updated account'), backup_codes: z .array(MfaBackupCodeResponse) .nullable() .describe('Backup codes minted by this call, or null when none were minted'), }); export type WebAuthnTwoFactorResponse = z.infer; export const SudoMfaMethodsResponse = z.object({ totp: z.boolean().describe('Whether TOTP is enabled'), webauthn: z.boolean().describe('Whether the account has at least one registered WebAuthn credential'), backup_codes: z.boolean().describe('Whether the account has at least one unconsumed backup code'), has_mfa: z.boolean().describe('Whether the account can satisfy a sudo mode challenge'), }); export type SudoMfaMethodsResponse = z.infer; export const InboundSmsChallengeStartResponse = z.object({ challenge_code: createStringType(4, 12).describe('The numeric code the user must text to our number'), our_number: createStringType(4, 32).describe('The Twilio number the user must text the code to (E.164)'), expires_at: z.string().describe('ISO timestamp at which the challenge becomes invalid'), }); export type InboundSmsChallengeStartResponse = z.infer; export const LogoutAuthSessionsWithVerificationRequest = LogoutAuthSessionsRequest.extend(SudoVerificationSchema.shape);