From dacf9b35c6f2d589edb6f532d1a454368b42f9fe Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 18:07:18 +0000 Subject: [PATCH] fix: add type-safe validation for EmailFlowId and TotpRegistrationId - Add checkedToEmailFlowId validator function to validate UUID format - Replace unsafe type cast with validated return in sendEmailWithCode - Add checkedToTotpRegistrationId validation in kratosInitiateTotp - Update kratosValidateTotp to use proper TotpRegistrationId type This improves type safety by ensuring flow IDs are validated at runtime rather than using unsafe type assertions. --- src/services/kratos/auth-email-no-password.ts | 6 +++++- src/services/kratos/index.ts | 7 +++++++ src/services/kratos/totp.ts | 8 ++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/services/kratos/auth-email-no-password.ts b/src/services/kratos/auth-email-no-password.ts index f63157bce..10bc1c29f 100644 --- a/src/services/kratos/auth-email-no-password.ts +++ b/src/services/kratos/auth-email-no-password.ts @@ -17,6 +17,7 @@ import { checkedToEmailAddress } from "@domain/users" import knex from "knex" +import { checkedToEmailFlowId } from "./index" import { createCookieLoginFlow } from "./cookie" import { CodeExpiredKratosError, @@ -73,7 +74,10 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe }, }) - return data.id as EmailFlowId + const emailFlowId = checkedToEmailFlowId(data.id) + if (emailFlowId instanceof Error) return emailFlowId + + return emailFlowId } catch (err) { return new UnknownKratosError(err) } diff --git a/src/services/kratos/index.ts b/src/services/kratos/index.ts index ced0c52fe..9b8044c1f 100644 --- a/src/services/kratos/index.ts +++ b/src/services/kratos/index.ts @@ -40,6 +40,13 @@ export const checkedToEmailLoginId = (flow: string): EmailLoginId | ValidationEr return flow as EmailLoginId } +export const checkedToEmailFlowId = (flow: string): EmailFlowId | ValidationError => { + if (!flow.match(UuidRegex)) { + return new InvalidFlowId(flow) + } + return flow as EmailFlowId +} + export const checkedToTotpCode = (totpCode: string): TotpCode | ValidationError => { if (totpCode.length !== 6) { return new InvalidTotpCode(totpCode) diff --git a/src/services/kratos/totp.ts b/src/services/kratos/totp.ts index d800ad57c..88e806cbf 100644 --- a/src/services/kratos/totp.ts +++ b/src/services/kratos/totp.ts @@ -5,6 +5,7 @@ import { isAxiosError } from "axios" import { KRATOS_MASTER_USER_PASSWORD } from "@config" +import { checkedToTotpRegistrationId } from "./index" import { AuthenticationKratosError, MissingTotpKratosError, @@ -23,9 +24,12 @@ export const kratosInitiateTotp = async (token: AuthToken) => { return new MissingTotpKratosError() } + const totpRegistrationId = checkedToTotpRegistrationId(res.data.id) + if (totpRegistrationId instanceof Error) return totpRegistrationId + const totpSecret = (totpAttributes.attributes as UiNodeTextAttributes).text .text as TotpSecret - return { totpSecret, totpRegistrationId: res.data.id as TotpRegistrationId } + return { totpSecret, totpRegistrationId } } catch (err) { return new UnknownKratosError(err) } @@ -38,7 +42,7 @@ export const kratosValidateTotp = async ({ }: { authToken: AuthToken totpCode: string - totpRegistrationId: string + totpRegistrationId: TotpRegistrationId }) => { try { await kratosPublic.updateSettingsFlow({