Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
Expand All @@ -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),
});
}

Expand Down
4 changes: 3 additions & 1 deletion packages/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 33 additions & 0 deletions packages/auth/src/__tests__/backend-handler.test.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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 }));
});
});
3 changes: 2 additions & 1 deletion packages/auth/src/backend-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
createDevEmailTrapProvider,
createNodemailerProvider,
createResendProvider,
parseDevEmailTrapMaxEmails,
isDevEmailTrapConfigured,
} from './providers';

Expand Down Expand Up @@ -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),
}),
}),
);
Expand Down
9 changes: 9 additions & 0 deletions packages/auth/src/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +110 to +117
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions “reuse a shared positive-integer parser” for DEV_EMAIL_TRAP_MAX_EMAILS, but the template worker still has its own toPositiveInteger() helper in apps/ottabase-template-app-tanstack/worker/lib/email-provider.ts with the same logic. Consider switching that code to import parseDevEmailTrapMaxEmails (or moving the parser to a truly shared utilities module) so the behavior can’t drift over time, or adjust the PR description if sharing across packages isn’t intended.

Copilot uses AI. Check for mistakes.

/**
* Options for configuring OAuth providers
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/ottaeditor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down
8 changes: 8 additions & 0 deletions packages/ottaeditor/src/tools/StepsTool/StepsTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
12 changes: 11 additions & 1 deletion packages/ottaeditor/src/tools/StepsTool/StepsTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down