diff --git a/apps/ottabase-template-app-tanstack/worker/lib/email-provider.ts b/apps/ottabase-template-app-tanstack/worker/lib/email-provider.ts index d2b1c93b8..e5b920ff1 100644 --- a/apps/ottabase-template-app-tanstack/worker/lib/email-provider.ts +++ b/apps/ottabase-template-app-tanstack/worker/lib/email-provider.ts @@ -4,7 +4,7 @@ import { createKvEmailTrapStore, type DevEmailTrapStore, } from '@ottabase/email/providers/dev-trap'; -import { isDevEmailTrapConfigured } from '@ottabase/auth/providers'; +import { isDevEmailTrapConfigured, parseDevEmailTrapMaxEmails } from '@ottabase/auth/providers'; import type { CloudflareEnv } from '../../cloudflare-env'; export type AppEmailProvider = 'auto' | 'dev-trap' | 'resend' | 'ses' | 'nodemailer'; @@ -17,15 +17,6 @@ export interface ResolvedMailerResult { error?: string; } -function toPositiveInteger(value: string | undefined, fallback: number): number { - const parsed = Number(value); - if (!Number.isFinite(parsed) || parsed <= 0) { - return fallback; - } - - return Math.floor(parsed); -} - export function isDevTrapAvailable(env: CloudflareEnv): boolean { return isDevEmailTrapConfigured(env as any) && !!env.OBCF_KV; } @@ -36,7 +27,7 @@ export function getDevEmailTrapStore(env: CloudflareEnv): DevEmailTrapStore | nu } return createKvEmailTrapStore(env.OBCF_KV as any, { - maxEntries: toPositiveInteger(env.DEV_EMAIL_TRAP_MAX_EMAILS, 50), + maxEntries: parseDevEmailTrapMaxEmails(env.DEV_EMAIL_TRAP_MAX_EMAILS, 50), }); } diff --git a/packages/auth/README.md b/packages/auth/README.md index 3a6361e43..65f878189 100644 --- a/packages/auth/README.md +++ b/packages/auth/README.md @@ -495,7 +495,9 @@ const emailProvider = createDevEmailTrapProvider(env, { ``` This is intended for local development. It captures rendered emails in KV so your app can expose a local inbox instead -of sending through SMTP or an external API. +of sending through SMTP or an external API. When you set `DEV_EMAIL_TRAP_MAX_EMAILS`, values are truncated to an integer +and anything less than 1 or invalid falls back to the default inbox size instead of shrinking the trap to a single +message. ## Session Utilities diff --git a/packages/auth/src/__tests__/backend-handler.test.ts b/packages/auth/src/__tests__/backend-handler.test.ts index d00f94a99..01c4d3694 100644 --- a/packages/auth/src/__tests__/backend-handler.test.ts +++ b/packages/auth/src/__tests__/backend-handler.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import { createAuthConfig } from '../backend-handler'; +import * as emailTrapModule from '@ottabase/email/providers/dev-trap'; function createMockD1() { const prepare = vi.fn((sql: string) => { @@ -165,4 +166,36 @@ describe('Auth Backend Handler', () => { expect(result).toBe(true); expect(d1.prepare).toHaveBeenCalledWith(expect.stringContaining('UPDATE users SET email_verified')); }); + + it('uses the fallback dev trap inbox size when DEV_EMAIL_TRAP_MAX_EMAILS is not positive', () => { + const createStoreSpy = vi.spyOn(emailTrapModule, 'createKvEmailTrapStore'); + const env = { + OBCF_D1: createMockD1() as any, + OBCF_KV: {} as any, + AUTH_SECRET: 'test-secret', + ENVIRONMENT: 'test', + DEV_EMAIL_TRAP_ENABLED: 'true', + DEV_EMAIL_TRAP_MAX_EMAILS: '-5', + }; + + createAuthConfig(env as any); + + expect(createStoreSpy).toHaveBeenCalledWith(env.OBCF_KV, expect.objectContaining({ maxEntries: 50 })); + }); + + it('uses the fallback dev trap inbox size when DEV_EMAIL_TRAP_MAX_EMAILS is a fraction below 1', () => { + const createStoreSpy = vi.spyOn(emailTrapModule, 'createKvEmailTrapStore'); + const env = { + OBCF_D1: createMockD1() as any, + OBCF_KV: {} as any, + AUTH_SECRET: 'test-secret', + ENVIRONMENT: 'test', + DEV_EMAIL_TRAP_ENABLED: 'true', + DEV_EMAIL_TRAP_MAX_EMAILS: '0.5', + }; + + createAuthConfig(env as any); + + expect(createStoreSpy).toHaveBeenCalledWith(env.OBCF_KV, expect.objectContaining({ maxEntries: 50 })); + }); }); diff --git a/packages/auth/src/backend-handler.ts b/packages/auth/src/backend-handler.ts index f8dfbfa94..e829f220b 100644 --- a/packages/auth/src/backend-handler.ts +++ b/packages/auth/src/backend-handler.ts @@ -27,6 +27,7 @@ import { createDevEmailTrapProvider, createNodemailerProvider, createResendProvider, + parseDevEmailTrapMaxEmails, isDevEmailTrapConfigured, } from './providers'; @@ -227,7 +228,7 @@ export function createAuthConfig(env: AuthEnv, options?: CreateAuthConfigOptions providers.push( createDevEmailTrapProvider(env, { store: createKvEmailTrapStore(env.OBCF_KV as any, { - maxEntries: Math.max(1, Number(env.DEV_EMAIL_TRAP_MAX_EMAILS) || 50), + maxEntries: parseDevEmailTrapMaxEmails(env.DEV_EMAIL_TRAP_MAX_EMAILS, 50), }), }), ); diff --git a/packages/auth/src/providers.ts b/packages/auth/src/providers.ts index 3a07d57f6..9df059e1f 100644 --- a/packages/auth/src/providers.ts +++ b/packages/auth/src/providers.ts @@ -107,6 +107,15 @@ export function isDevEmailTrapConfigured(env: ProviderEnv): boolean { return explicit === true && !!env.OBCF_KV; } +export function parseDevEmailTrapMaxEmails(value: string | undefined, fallback = 50): number { + const maxEmails = Math.floor(Number(value)); + if (!Number.isFinite(maxEmails) || maxEmails < 1) { + return fallback; + } + + return maxEmails; +} + /** * Options for configuring OAuth providers */ diff --git a/packages/ottaeditor/README.md b/packages/ottaeditor/README.md index fe17d36c3..d250d3642 100644 --- a/packages/ottaeditor/README.md +++ b/packages/ottaeditor/README.md @@ -187,6 +187,8 @@ interface StepsData { } ``` +Validation is defensive for imported content: malformed `items` entries are rejected instead of throwing during save-time checks. + ## Styling All custom plugins use common classes from `ottaeditor-common.css` (imported by `editorjs-brandkit-theme.css`): diff --git a/packages/ottaeditor/src/tools/StepsTool/StepsTool.test.ts b/packages/ottaeditor/src/tools/StepsTool/StepsTool.test.ts index be752605e..00a4ace29 100644 --- a/packages/ottaeditor/src/tools/StepsTool/StepsTool.test.ts +++ b/packages/ottaeditor/src/tools/StepsTool/StepsTool.test.ts @@ -81,4 +81,12 @@ describe('StepsTool', () => { expect(tool.validate({ items: [] })).toBe(false); expect(tool.validate({ items: [{ title: 'Ready', content: '' }] })).toBe(true); }); + + it('rejects malformed payloads during validation without throwing', () => { + const { tool } = createTool(); + + expect(tool.validate({ items: [null as any] })).toBe(false); + expect(tool.validate({ items: [{ title: null as any, content: '' }] })).toBe(false); + expect(tool.validate({ items: [{ title: '', content: 123 as any }] })).toBe(false); + }); }); diff --git a/packages/ottaeditor/src/tools/StepsTool/StepsTool.ts b/packages/ottaeditor/src/tools/StepsTool/StepsTool.ts index d4f28be09..30571b715 100644 --- a/packages/ottaeditor/src/tools/StepsTool/StepsTool.ts +++ b/packages/ottaeditor/src/tools/StepsTool/StepsTool.ts @@ -356,7 +356,17 @@ export default class StepsTool implements BlockTool { } validate(savedData: StepsData): boolean { - return Array.isArray(savedData.items) && savedData.items.some((item) => item.title.trim() || item.content.trim()); + if (!Array.isArray(savedData?.items)) { + return false; + } + + return savedData.items.some((item) => { + if (!item || typeof item.title !== 'string' || typeof item.content !== 'string') { + return false; + } + + return item.title.trim() !== '' || item.content.trim() !== ''; + }); } destroy(): void {