From 6dc2b5fca0627c4729c51f6a12f1318d942e344e Mon Sep 17 00:00:00 2001 From: Mohamed Melouk <42706279+cobraprojects@users.noreply.github.com> Date: Sat, 9 May 2026 06:54:10 +0300 Subject: [PATCH] rename verifications functions names --- .../app/api/verify-email/resend/route.ts | 46 +++++++----------- apps/blog-next/app/api/verify-email/route.ts | 4 +- apps/blog-next/lib/schemas/auth.ts | 4 ++ .../blog-nuxt/server/api/verify-email.post.ts | 4 +- .../server/api/verify-email/resend.post.ts | 45 ++++++++--------- apps/blog-nuxt/shared/schemas/auth.ts | 4 ++ apps/blog-sveltekit/src/lib/schemas/auth.ts | 4 ++ .../src/routes/api/verify-email/+server.ts | 4 +- .../routes/api/verify-email/resend/+server.ts | 46 +++++++----------- apps/docs/docs/auth/email-verification.md | 24 ++++++---- apps/docs/docs/security.md | 2 +- packages/auth/src/contracts.ts | 14 ++++++ packages/auth/src/index.ts | 9 +++- packages/auth/src/runtime.ts | 48 +++++++++++++++++++ packages/auth/tests/contracts.type.test.ts | 11 ++++- packages/auth/tests/docs-smoke.test.ts | 5 +- packages/auth/tests/package.test.ts | 39 ++++++++------- packages/core/src/portable/holo.ts | 3 ++ 18 files changed, 193 insertions(+), 123 deletions(-) diff --git a/apps/blog-next/app/api/verify-email/resend/route.ts b/apps/blog-next/app/api/verify-email/resend/route.ts index 4edc3efd..cc1c02ab 100644 --- a/apps/blog-next/app/api/verify-email/resend/route.ts +++ b/apps/blog-next/app/api/verify-email/resend/route.ts @@ -1,42 +1,28 @@ -import { verification } from '@holo-js/auth' +import { resendEmailVerification } from '@holo-js/auth' +import { validate } from '@holo-js/forms' -interface ResendVerificationRequestBody { - readonly email?: string -} +import { resendEmailVerificationForm } from '@/lib/schemas/auth' -async function readRequestBody(request: Request): Promise { - const contentType = request.headers.get('content-type') ?? '' - if (!contentType.includes('application/json')) { - return {} - } +export async function POST(request: Request) { + const submission = await validate(request, resendEmailVerificationForm) - const payload = await request.json().catch(() => null) - if (!payload || typeof payload !== 'object') { - return {} + if (!submission.valid) { + return Response.json(submission.fail(), { + status: submission.fail().status, + }) } - const email = typeof payload.email === 'string' ? payload.email.trim() : undefined - return email ? { email } : {} -} - -export async function POST(request: Request) { - const input = await readRequestBody(request) - const { error } = await verification.resend(input) + const { error } = await resendEmailVerification(submission.data.email) if (error) { - return Response.json({ - ok: false as const, + const failure = submission.fail({ status: error.status, errors: error.fields, - }, { - status: error.status, }) + + return Response.json(failure, { status: failure.status }) } - return Response.json({ - ok: true as const, - status: 200, - data: { - message: 'A fresh verification email has been sent.', - }, - }) + return Response.json(submission.success({ + message: 'A fresh verification email has been sent.', + })) } diff --git a/apps/blog-next/app/api/verify-email/route.ts b/apps/blog-next/app/api/verify-email/route.ts index a0c552ae..bc508ee6 100644 --- a/apps/blog-next/app/api/verify-email/route.ts +++ b/apps/blog-next/app/api/verify-email/route.ts @@ -1,4 +1,4 @@ -import { check, verification } from '@holo-js/auth' +import { check, verifyEmail } from '@holo-js/auth' import { validate } from '@holo-js/forms' import { verifyEmailForm } from '@/lib/schemas/auth' @@ -13,7 +13,7 @@ export async function POST(request: Request) { } const wasAuthenticated = await check() - const { error } = await verification.consume(submission.data.token) + const { error } = await verifyEmail(submission.data.token) if (error) { const failure = submission.fail({ status: error.status, diff --git a/apps/blog-next/lib/schemas/auth.ts b/apps/blog-next/lib/schemas/auth.ts index cb5bd8ea..03ef8a17 100644 --- a/apps/blog-next/lib/schemas/auth.ts +++ b/apps/blog-next/lib/schemas/auth.ts @@ -17,6 +17,10 @@ export const forgotPasswordForm = schema({ email: field.string().required('Email is required.').email('Enter a valid email address.'), }) +export const resendEmailVerificationForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), +}) + export const resetPasswordForm = schema({ token: field.string().required('Reset token is required.'), password: field.password().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), diff --git a/apps/blog-nuxt/server/api/verify-email.post.ts b/apps/blog-nuxt/server/api/verify-email.post.ts index bae608c3..3d1ac8f8 100644 --- a/apps/blog-nuxt/server/api/verify-email.post.ts +++ b/apps/blog-nuxt/server/api/verify-email.post.ts @@ -1,4 +1,4 @@ -import { check, verification } from '@holo-js/auth' +import { check, verifyEmail } from '@holo-js/auth' import { validate } from '@holo-js/forms' import { verifyEmailForm } from '#shared/schemas/auth' @@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => { } const wasAuthenticated = await check() - const { error } = await verification.consume(submission.data.token) + const { error } = await verifyEmail(submission.data.token) if (error) { const failure = submission.fail({ status: error.status, diff --git a/apps/blog-nuxt/server/api/verify-email/resend.post.ts b/apps/blog-nuxt/server/api/verify-email/resend.post.ts index 790cd8d7..a01f32db 100644 --- a/apps/blog-nuxt/server/api/verify-email/resend.post.ts +++ b/apps/blog-nuxt/server/api/verify-email/resend.post.ts @@ -1,36 +1,29 @@ -import { verification } from '@holo-js/auth' +import { resendEmailVerification } from '@holo-js/auth' +import { validate } from '@holo-js/forms' -interface ResendVerificationRequestBody { - readonly email?: string -} +import { resendEmailVerificationForm } from '#shared/schemas/auth' export default defineEventHandler(async (event) => { - let payload: ResendVerificationRequestBody | null = {} - try { - payload = await readBody(event) - } catch { - payload = {} + const submission = await validate(event, resendEmailVerificationForm) + + if (!submission.valid) { + const failure = submission.fail() + setResponseStatus(event, failure.status) + return failure } - const email = typeof payload === 'object' - && payload !== null - && typeof payload.email === 'string' - ? payload.email.trim() - : '' - const { error } = await verification.resend(email ? { email } : undefined) + + const { error } = await resendEmailVerification(submission.data.email) if (error) { - setResponseStatus(event, error.status) - return { - ok: false as const, + const failure = submission.fail({ status: error.status, errors: error.fields, - } - } + }) - return { - ok: true as const, - status: 200, - data: { - message: 'A fresh verification email has been sent.', - }, + setResponseStatus(event, failure.status) + return failure } + + return submission.success({ + message: 'A fresh verification email has been sent.', + }) }) diff --git a/apps/blog-nuxt/shared/schemas/auth.ts b/apps/blog-nuxt/shared/schemas/auth.ts index cb5bd8ea..03ef8a17 100644 --- a/apps/blog-nuxt/shared/schemas/auth.ts +++ b/apps/blog-nuxt/shared/schemas/auth.ts @@ -17,6 +17,10 @@ export const forgotPasswordForm = schema({ email: field.string().required('Email is required.').email('Enter a valid email address.'), }) +export const resendEmailVerificationForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), +}) + export const resetPasswordForm = schema({ token: field.string().required('Reset token is required.'), password: field.password().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), diff --git a/apps/blog-sveltekit/src/lib/schemas/auth.ts b/apps/blog-sveltekit/src/lib/schemas/auth.ts index cb5bd8ea..03ef8a17 100644 --- a/apps/blog-sveltekit/src/lib/schemas/auth.ts +++ b/apps/blog-sveltekit/src/lib/schemas/auth.ts @@ -17,6 +17,10 @@ export const forgotPasswordForm = schema({ email: field.string().required('Email is required.').email('Enter a valid email address.'), }) +export const resendEmailVerificationForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), +}) + export const resetPasswordForm = schema({ token: field.string().required('Reset token is required.'), password: field.password().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), diff --git a/apps/blog-sveltekit/src/routes/api/verify-email/+server.ts b/apps/blog-sveltekit/src/routes/api/verify-email/+server.ts index 48d3e75e..8c6dc4fa 100644 --- a/apps/blog-sveltekit/src/routes/api/verify-email/+server.ts +++ b/apps/blog-sveltekit/src/routes/api/verify-email/+server.ts @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit' -import { check, verification } from '@holo-js/auth' +import { check, verifyEmail } from '@holo-js/auth' import { validate } from '@holo-js/forms' import { verifyEmailForm } from '$lib/schemas/auth' @@ -14,7 +14,7 @@ export async function POST({ request }: { request: Request }) { } const authenticationCheck = check() - const verificationResult = verification.consume(submission.data.token) + const verificationResult = verifyEmail(submission.data.token) const [wasAuthenticated, { error }] = await Promise.all([authenticationCheck, verificationResult]) if (error) { const failure = submission.fail({ diff --git a/apps/blog-sveltekit/src/routes/api/verify-email/resend/+server.ts b/apps/blog-sveltekit/src/routes/api/verify-email/resend/+server.ts index edd2f76e..e7f1c249 100644 --- a/apps/blog-sveltekit/src/routes/api/verify-email/resend/+server.ts +++ b/apps/blog-sveltekit/src/routes/api/verify-email/resend/+server.ts @@ -1,43 +1,29 @@ import { json } from '@sveltejs/kit' -import { verification } from '@holo-js/auth' +import { resendEmailVerification } from '@holo-js/auth' +import { validate } from '@holo-js/forms' -interface ResendVerificationRequestBody { - readonly email?: string -} +import { resendEmailVerificationForm } from '$lib/schemas/auth' -async function readRequestBody(request: Request): Promise { - const contentType = request.headers.get('content-type') ?? '' - if (!contentType.includes('application/json')) { - return {} - } +export async function POST({ request }: { request: Request }) { + const submission = await validate(request, resendEmailVerificationForm) - const payload = await request.json().catch(() => null) - if (!payload || typeof payload !== 'object') { - return {} + if (!submission.valid) { + return json(submission.fail(), { + status: submission.fail().status, + }) } - const email = typeof payload.email === 'string' ? payload.email.trim() : undefined - return email ? { email } : {} -} - -export async function POST({ request }: { request: Request }) { - const input = await readRequestBody(request) - const { error } = await verification.resend(input) + const { error } = await resendEmailVerification(submission.data.email) if (error) { - return json({ - ok: false as const, + const failure = submission.fail({ status: error.status, errors: error.fields, - }, { - status: error.status, }) + + return json(failure, { status: failure.status }) } - return json({ - ok: true as const, - status: 200, - data: { - message: 'A fresh verification email has been sent.', - }, - }) + return json(submission.success({ + message: 'A fresh verification email has been sent.', + })) } diff --git a/apps/docs/docs/auth/email-verification.md b/apps/docs/docs/auth/email-verification.md index d7a24e21..8b4912c0 100644 --- a/apps/docs/docs/auth/email-verification.md +++ b/apps/docs/docs/auth/email-verification.md @@ -38,7 +38,7 @@ AUTH_EMAIL_VERIFICATION_ROUTE=/verify-email `APP_URL` is used when the framework builds the email link. Applications should not manually construct the verification URL in normal usage. -The application still owns the verification page and the route that calls `verification.consume(...)`. The framework +The application still owns the verification page and the route that calls `verifyEmail(token)`. The framework owns the redirect target and the generated email link. ## Registration Flow @@ -110,30 +110,36 @@ That lets the app redirect the signed-in user to the verify page instead of reje ## Consuming Verification Tokens -Verification pages consume the token from the emailed link: +Verification pages verify the token from the emailed link: ```ts -import { verification } from '@holo-js/auth' +import { verifyEmail } from '@holo-js/auth' -const { data: verifiedUser, error } = await verification.consume(token) +const { data: verifiedUser, error } = await verifyEmail(token) ``` The verification flow marks the local user as verified and invalidates the token. ## Resending Verification Emails -Applications can resend another verification email with plain object input: +Applications can resend another verification email with a direct email argument: ```ts -import { verification } from '@holo-js/auth' +import { resendEmailVerification } from '@holo-js/auth' -const { error } = await verification.resend({ - email: body.email, -}) +const { error } = await resendEmailVerification(body.email) ``` This is the intended verify-page flow when the user lands on `/verify-email?email=...` after login. +When you are sending a verification email outside a resend route, use the same API shape with the send-oriented name: + +```ts +import { sendEmailVerification } from '@holo-js/auth' + +const { error } = await sendEmailVerification(body.email) +``` + Expected resend failures come back in `error`, for example: - `email_verification_user_missing` diff --git a/apps/docs/docs/security.md b/apps/docs/docs/security.md index 501854e6..8f63fb76 100644 --- a/apps/docs/docs/security.md +++ b/apps/docs/docs/security.md @@ -116,7 +116,7 @@ When `@holo-js/forms` is installed, forms can opt into security directly through Validation failures and auth failures stay separate: - `validate(...)` returns form validation failures such as missing fields, bad formats, CSRF errors, and throttling. -- `login(...)`, `register(...)`, `verification.consume(...)`, `requestPasswordReset(...)`, and `resetPassword(...)` return auth failures in `error`. +- `login(...)`, `register(...)`, `verifyEmail(...)`, `requestPasswordReset(...)`, and `resetPassword(...)` return auth failures in `error`. - Auth failures are plain data with `status` and `fields`, so routes can forward them directly into the normal form response shape. ### Login diff --git a/packages/auth/src/contracts.ts b/packages/auth/src/contracts.ts index 997e4d49..402eb69e 100644 --- a/packages/auth/src/contracts.ts +++ b/packages/auth/src/contracts.ts @@ -183,6 +183,11 @@ export interface AuthPasswordResetRequestOptions { readonly expiresAt?: Date } +export interface AuthEmailVerificationSendOptions { + readonly guard?: string + readonly expiresAt?: Date +} + export interface AuthSessionLoginOptions { readonly remember?: boolean } @@ -234,6 +239,15 @@ export interface AuthFacade extends AuthGuardFacade { resetPassword( input: TInput, ): Promise>> + verifyEmail(token: string): Promise>> + sendEmailVerification(): Promise>> + sendEmailVerification(email: string): Promise>> + sendEmailVerification(email: string | undefined): Promise>> + sendEmailVerification(email: string | undefined, options: AuthEmailVerificationSendOptions): Promise>> + resendEmailVerification(): Promise>> + resendEmailVerification(email: string): Promise>> + resendEmailVerification(email: string | undefined): Promise>> + resendEmailVerification(email: string | undefined, options: AuthEmailVerificationSendOptions): Promise>> hashPassword(password: string): Promise verifyPassword(password: string, digest: string): Promise needsPasswordRehash(digest: string): Promise diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 63422a9a..135ddaa4 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,4 +1,4 @@ -import { check, currentAccessToken, getAuthRuntime, hashPassword, id, impersonate, impersonateById, impersonation, login, loginUsing, loginUsingId, logout, needsPasswordRehash, refreshUser, register, requestPasswordReset, resetPassword, stopImpersonating, tokens, user, verification, verifyPassword } from './runtime' +import { check, currentAccessToken, getAuthRuntime, hashPassword, id, impersonate, impersonateById, impersonation, login, loginUsing, loginUsingId, logout, needsPasswordRehash, refreshUser, register, requestPasswordReset, resendEmailVerification, resetPassword, sendEmailVerification, stopImpersonating, tokens, user, verification, verifyEmail, verifyPassword } from './runtime' export { AUTH_ERROR_CODES, AuthError, defineAuthConfig, isAuthError } from './contracts' export { @@ -21,12 +21,15 @@ export { refreshUser, register, requestPasswordReset, + resendEmailVerification, resetAuthRuntime, resetPassword, + sendEmailVerification, stopImpersonating, tokens, user, verification, + verifyEmail, verifyPassword, } from './runtime' export type { @@ -40,6 +43,7 @@ export type { AuthEmailVerificationConsumeErrorCode, AuthEmailVerificationFacade, AuthEmailVerificationResendErrorCode, + AuthEmailVerificationSendOptions, AuthEstablishedSession, AuthFacade, AuthFieldErrors, @@ -104,10 +108,13 @@ const auth = Object.freeze({ needsPasswordRehash, register, requestPasswordReset, + resendEmailVerification, resetPassword, + sendEmailVerification, stopImpersonating, tokens, verification, + verifyEmail, verifyPassword, guard(name: string) { return getAuthRuntime().guard(name) diff --git a/packages/auth/src/runtime.ts b/packages/auth/src/runtime.ts index 02cc5850..db468893 100644 --- a/packages/auth/src/runtime.ts +++ b/packages/auth/src/runtime.ts @@ -8,6 +8,7 @@ import type { AuthEmailVerificationConsumeErrorCode, AuthEmailVerificationFacade, AuthEmailVerificationResendErrorCode, + AuthEmailVerificationSendOptions, AuthEstablishedSession, AuthFacade, AuthGuardFacade, @@ -1871,6 +1872,16 @@ function createEmailVerificationFacade(): AuthEmailVerificationFacade { }) } +function createEmailVerificationResendInput( + email?: string, + options: AuthEmailVerificationSendOptions = {}, +): { readonly guard?: string, readonly expiresAt?: Date, readonly email?: string } { + return { + ...options, + ...(typeof email === 'string' ? { email } : {}), + } +} + async function requestPasswordResetUsingRuntime( input: TInput, options: AuthPasswordResetRequestOptions = {}, @@ -2221,6 +2232,15 @@ export function getAuthRuntime(): AuthRuntimeFacade { resetPassword(input: TInput) { return resetPasswordUsingRuntime(input) }, + verifyEmail(token: string) { + return verification.consume(token) + }, + sendEmailVerification(email?: string, options?: AuthEmailVerificationSendOptions) { + return verification.resend(createEmailVerificationResendInput(email, options)) + }, + resendEmailVerification(email?: string, options?: AuthEmailVerificationSendOptions) { + return verification.resend(createEmailVerificationResendInput(email, options)) + }, hashPassword(password: string) { return getRuntimeBindings().passwordHasher.hash(password) }, @@ -2372,6 +2392,34 @@ export async function resetPassword( return getAuthRuntime().resetPassword(input) } +export function verifyEmail( + token: string, +): Promise>> { + return getAuthRuntime().verifyEmail(token) +} + +export function sendEmailVerification(): Promise>> +export function sendEmailVerification(email: string): Promise>> +export function sendEmailVerification(email: string | undefined): Promise>> +export function sendEmailVerification(email: string | undefined, options: AuthEmailVerificationSendOptions): Promise>> +export function sendEmailVerification( + email?: string, + options?: AuthEmailVerificationSendOptions, +): Promise>> { + return getAuthRuntime().verification.resend(createEmailVerificationResendInput(email, options)) +} + +export function resendEmailVerification(): Promise>> +export function resendEmailVerification(email: string): Promise>> +export function resendEmailVerification(email: string | undefined): Promise>> +export function resendEmailVerification(email: string | undefined, options: AuthEmailVerificationSendOptions): Promise>> +export function resendEmailVerification( + email?: string, + options?: AuthEmailVerificationSendOptions, +): Promise>> { + return getAuthRuntime().verification.resend(createEmailVerificationResendInput(email, options)) +} + export const tokens: AuthTokenFacade = Object.freeze({ create(user: unknown, options: PersonalAccessTokenCreationOptions) { return getAuthRuntime().tokens.create(user, options) diff --git a/packages/auth/tests/contracts.type.test.ts b/packages/auth/tests/contracts.type.test.ts index f68f62e5..e96f1b41 100644 --- a/packages/auth/tests/contracts.type.test.ts +++ b/packages/auth/tests/contracts.type.test.ts @@ -1,5 +1,5 @@ import { describe, expectTypeOf, it } from 'vitest' -import auth, { AuthError, isAuthError, type AuthErrorCode, type AuthEstablishedSession, type AuthFailure, type AuthGuardFacade, type AuthImpersonationState, type AuthLoginErrorCode, type AuthLogoutResult, type AuthPasswordResetConsumeErrorCode, type AuthPasswordResetRequestErrorCode, type AuthProviderAdapter, type AuthRegistrationErrorCode, type AuthResult, type AuthRuntimeBindings, type AuthUser, type CurrentAuthResponse, type getAuthRuntime, type HoloAuthUser, type register, type user } from '../src' +import auth, { AuthError, isAuthError, type AuthEmailVerificationConsumeErrorCode, type AuthEmailVerificationResendErrorCode, type AuthErrorCode, type AuthEstablishedSession, type AuthFailure, type AuthFieldErrors, type AuthGuardFacade, type AuthImpersonationState, type AuthLoginErrorCode, type AuthLogoutResult, type AuthPasswordResetConsumeErrorCode, type AuthPasswordResetRequestErrorCode, type AuthProviderAdapter, type AuthRegistrationErrorCode, type AuthResult, type AuthRuntimeBindings, type AuthUser, type CurrentAuthResponse, type EmailVerificationTokenResult, type getAuthRuntime, type HoloAuthUser, type register, type user, type verifyEmail } from '../src' import clientAuth, { type refreshUser as refreshClientUser, type useAuth as clientUseAuth, type user as clientUser } from '../src/client' import type { useAuth as useNextAuth } from '../src/next/client' import type { useAuth as useNuxtAuth } from '../src/nuxt' @@ -118,6 +118,12 @@ describe('@holo-js/auth typing', () => { expectTypeOf(auth.impersonation).returns.toEqualTypeOf>() expectTypeOf(auth.stopImpersonating).returns.toEqualTypeOf>() expectTypeOf(auth.logout).returns.toEqualTypeOf>() + expectTypeOf(auth.verifyEmail).parameter(0).toEqualTypeOf() + expectTypeOf(auth.verifyEmail).returns.toEqualTypeOf>>>() + expectTypeOf(auth.sendEmailVerification).parameter(0).toEqualTypeOf() + expectTypeOf(auth.sendEmailVerification).returns.toEqualTypeOf>>>() + expectTypeOf(auth.resendEmailVerification).parameter(0).toEqualTypeOf() + expectTypeOf(auth.resendEmailVerification).returns.toEqualTypeOf>>>() expectTypeOf(clientAuth.user).returns.toEqualTypeOf>() expectTypeOf(clientAuth.useAuth).returns.toEqualTypeOf boolean @@ -150,6 +156,9 @@ describe('@holo-js/auth typing', () => { passwordConfirmation?: readonly string[] }> >() + expectTypeOf>>().toEqualTypeOf< + AuthResult> + >() }) it('keeps legacy custom session runtimes assignable to auth runtime bindings', () => { diff --git a/packages/auth/tests/docs-smoke.test.ts b/packages/auth/tests/docs-smoke.test.ts index 2df9889d..1979cda1 100644 --- a/packages/auth/tests/docs-smoke.test.ts +++ b/packages/auth/tests/docs-smoke.test.ts @@ -92,8 +92,9 @@ describe('auth documentation smoke checks', () => { expect(verification).toContain('APP_URL') expect(verification).toContain('emailVerificationRequired') expect(verification).toContain('emailVerificationRoute') - expect(verification).toContain('verification.consume') - expect(verification).toContain('verification.resend') + expect(verification).toContain('verifyEmail') + expect(verification).toContain('resendEmailVerification') + expect(verification).toContain('sendEmailVerification') expect(verification).toContain('@holo-js/notifications') expect(verification).toContain('@holo-js/mail') expect(verification).not.toContain('notify(created, verificationCreated(token))') diff --git a/packages/auth/tests/package.test.ts b/packages/auth/tests/package.test.ts index 102410e6..2393aa74 100644 --- a/packages/auth/tests/package.test.ts +++ b/packages/auth/tests/package.test.ts @@ -24,12 +24,15 @@ import auth, { refreshUser, register, requestPasswordReset, + resendEmailVerification, resetPassword, resetAuthRuntime, + sendEmailVerification, stopImpersonating, tokens, user, verification, + verifyEmail, verifyPassword, } from '../src' import clientAuth, { @@ -1644,17 +1647,17 @@ describe('@holo-js/auth package runtime', () => { expect(runtime.deliveries[0]?.tokenValue).toBe(token.plainTextToken) const invalidVerificationError = expectAuthFailureCode( - await verification.consume('bad-token'), + await verifyEmail('bad-token'), 'email_verification_token_invalid', ) expect(invalidVerificationError.message).toContain('Invalid email verification token') expectAuthFailureCode( - await verification.consume(`${token.id}.wrong-secret`), + await verifyEmail(`${token.id}.wrong-secret`), 'email_verification_token_expired', ) - const verified = unwrapAuthResult(await verification.consume(token.plainTextToken)) + const verified = unwrapAuthResult(await verifyEmail(token.plainTextToken)) expect(verified).toMatchObject({ id: 1, email: 'ava@example.com', @@ -1662,12 +1665,12 @@ describe('@holo-js/auth package runtime', () => { expect(runtime.usersProvider.users.get(1)?.email_verified_at).toBeInstanceOf(Date) expect(runtime.emailVerificationTokenStore.records.size).toBe(0) - expectAuthFailureCode(await verification.consume(token.plainTextToken), 'email_verification_token_expired') + expectAuthFailureCode(await verifyEmail(token.plainTextToken), 'email_verification_token_expired') const expired = await verification.create(created, { expiresAt: new Date('2026-04-07T00:00:00.000Z'), }) - expectAuthFailureCode(await verification.consume(expired.plainTextToken), 'email_verification_token_expired') + expectAuthFailureCode(await verifyEmail(expired.plainTextToken), 'email_verification_token_expired') }) it('resends verification tokens by email without requiring an active session', async () => { @@ -1684,26 +1687,28 @@ describe('@holo-js/auth package runtime', () => { expect(runtime.deliveries).toHaveLength(1) - const resent = unwrapAuthResult(await verification.resend({ + const sent = unwrapAuthResult(await getAuthRuntime().sendEmailVerification('ava@example.com')) + const resent = unwrapAuthResult(await getAuthRuntime().resendEmailVerification('ava@example.com')) + const topLevelSent = unwrapAuthResult(await sendEmailVerification('ava@example.com')) + const legacyResent = unwrapAuthResult(await verification.resend({ email: 'ava@example.com', })) + expect(sent.plainTextToken).toContain('.') expect(resent.plainTextToken).toContain('.') - expect(runtime.deliveries).toHaveLength(2) - expect(runtime.deliveries[1]).toMatchObject({ + expect(topLevelSent.plainTextToken).toContain('.') + expect(legacyResent.plainTextToken).toContain('.') + expect(runtime.deliveries).toHaveLength(5) + expect(runtime.deliveries[4]).toMatchObject({ type: 'verification', email: 'ava@example.com', - tokenId: resent.id, - tokenValue: resent.plainTextToken, + tokenId: legacyResent.id, + tokenValue: legacyResent.plainTextToken, }) - expectAuthFailureCode(await verification.resend({ - email: 'missing@example.com', - }), 'email_verification_user_missing') + expectAuthFailureCode(await resendEmailVerification('missing@example.com'), 'email_verification_user_missing') - await verification.consume(resent.plainTextToken) - expectAuthFailureCode(await verification.resend({ - email: 'ava@example.com', - }), 'email_already_verified') + await verifyEmail(legacyResent.plainTextToken) + expectAuthFailureCode(await resendEmailVerification('ava@example.com'), 'email_already_verified') }) it('fails verification and password reset flows when a provider cannot persist user changes', async () => { diff --git a/packages/core/src/portable/holo.ts b/packages/core/src/portable/holo.ts index fbaeedc0..a008309f 100644 --- a/packages/core/src/portable/holo.ts +++ b/packages/core/src/portable/holo.ts @@ -299,6 +299,9 @@ export interface HoloAuthRuntimeBinding { resend(options?: { readonly guard?: string, readonly expiresAt?: Date, readonly email?: string }): Promise> consume(plainTextToken: string): Promise> } + verifyEmail(token: string): Promise> + sendEmailVerification(email?: string, options?: { readonly guard?: string, readonly expiresAt?: Date }): Promise> + resendEmailVerification(email?: string, options?: { readonly guard?: string, readonly expiresAt?: Date }): Promise> requestPasswordReset( input: { readonly email: string