Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ jobs:
- name: API contract boundary audit
run: bun run check:api-validation:strict

- name: Shared utils enforcement audit
run: bun run check:utils

- name: Zustand v5 selector audit
run: bun run check:zustand-v5

Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { normalizeEmail } from '@sim/utils/string'
import { useRouter, useSearchParams } from 'next/navigation'
import { requestJson } from '@/lib/api/client/request'
import { forgetPasswordContract } from '@/lib/api/contracts'
Expand Down Expand Up @@ -45,7 +46,7 @@ const validateEmailField = (emailValue: string): string[] => {
return errors
}

const validation = quickValidateEmail(emailValue.trim().toLowerCase())
const validation = quickValidateEmail(normalizeEmail(emailValue))
if (!validation.isValid) {
errors.push(validation.reason || 'Please enter a valid email address.')
}
Expand Down Expand Up @@ -159,7 +160,7 @@ export default function LoginPage({

const formData = new FormData(e.currentTarget)
const emailRaw = formData.get('email') as string
const email = emailRaw.trim().toLowerCase()
const email = normalizeEmail(emailRaw)

const emailValidationErrors = validateEmailField(email)
setEmailErrors(emailValidationErrors)
Expand Down Expand Up @@ -277,7 +278,7 @@ export default function LoginPage({
return
}

const emailValidation = quickValidateEmail(forgotPasswordEmail.trim().toLowerCase())
const emailValidation = quickValidateEmail(normalizeEmail(forgotPasswordEmail))
if (!emailValidation.isValid) {
setResetStatus({
type: 'error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
TableRow,
Tooltip,
} from '@sim/emcn'
import { formatDateTime } from '@sim/utils/formatting'
import { useQueryClient } from '@tanstack/react-query'
import { RefreshCw } from 'lucide-react'
import { useRouter } from 'next/navigation'
Expand Down Expand Up @@ -68,7 +69,7 @@ const STATUS_BADGE_VARIANT: Record<string, 'orange' | 'blue' | 'green' | 'red' |
function formatDate(value: string | null): string {
if (!value) return '—'
try {
return new Date(value).toLocaleString()
return formatDateTime(new Date(value))
} catch {
return value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type ReactNode, useRef, useState } from 'react'
import { Streamdown } from 'streamdown'
import 'streamdown/styles.css'
import { Avatar, AvatarFallback, AvatarImage, Chip, cn } from '@sim/emcn'
import { formatDate } from '@sim/utils/formatting'
import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types'
import { mapReleases, releasesEndpoint } from '@/app/(landing)/changelog/utils'

Expand Down Expand Up @@ -53,14 +54,6 @@ function isContributorsLabel(children: ReactNode): boolean {
return /^\s*contributors\s*:?\s*$/i.test(String(children))
}

function formatDate(value: string): string {
return new Date(value).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}

export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) {
const [entries, setEntries] = useState<ChangelogEntry[]>(initialEntries)
const [loading, setLoading] = useState<boolean>(false)
Expand Down Expand Up @@ -133,7 +126,9 @@ export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) {
</div>
) : null}
</div>
<span className='text-[12px] text-[var(--text-muted)]'>{formatDate(entry.date)}</span>
<span className='text-[12px] text-[var(--text-muted)]'>
{formatDate(new Date(entry.date))}
</span>
</div>

<div aria-hidden='true' className='mt-[9px] mb-3 h-px bg-[var(--border)]' />
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/auth/oauth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createSign } from 'crypto'
import { db } from '@sim/db'
import { account, credential } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { getPostgresErrorCode, toError } from '@sim/utils/errors'
import { and, desc, eq } from 'drizzle-orm'
import { withLeaderLock } from '@/lib/concurrency/leader-lock'
import { coalesceLocally } from '@/lib/concurrency/singleflight'
Expand Down Expand Up @@ -281,7 +281,7 @@ export async function safeAccountInsert(
await db.insert(account).values(data)
logger.info(`Created new ${context.provider} account for user`, { userId: data.userId })
} catch (error: any) {
if (error?.code === '23505') {
if (getPostgresErrorCode(error) === '23505') {
logger.error(`Duplicate ${context.provider} account detected, credential already exists`, {
userId: data.userId,
identifier: context.identifier,
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/billing/invoices/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @vitest-environment node
*/
import { createMockRequest, dbChainMock, dbChainMockFns } from '@sim/testing'
import { generateShortId } from '@sim/utils/id'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockGetSession, mockGetStripeClient, mockStripeInvoicesList } = vi.hoisted(() => ({
Expand All @@ -25,7 +26,7 @@ import { GET } from '@/app/api/billing/invoices/route'

function makeInvoice(overrides: Record<string, unknown> = {}) {
return {
id: `in_${Math.random().toString(36).slice(2)}`,
id: `in_${generateShortId()}`,
number: 'INV-1',
created: 1700000000,
total: 1000,
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/cron/renew-subscriptions/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
redisConfigMockFns,
resetDbChainMock,
} from '@sim/testing'
import { sleep } from '@sim/utils/helpers'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockVerifyCronAuth } = vi.hoisted(() => ({
Expand All @@ -37,7 +38,7 @@ function createRequest() {
)
}

const flushMicrotasks = () => new Promise((resolve) => setTimeout(resolve, 0))
const flushMicrotasks = () => sleep(0)

describe('Teams subscription renewal route (fire-and-forget)', () => {
beforeEach(() => {
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/files/public/[token]/otp/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { normalizeEmail } from '@sim/utils/string'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { renderOTPEmail } from '@/components/emails'
Expand Down Expand Up @@ -71,7 +72,7 @@ export const POST = withRouteHandler(
const { token } = parsed.data.params
// Normalize once so allow-list matching, OTP storage, and the verify lookup
// all key off the same value (allow-list entries are stored lowercase).
const email = parsed.data.body.email.trim().toLowerCase()
const email = normalizeEmail(parsed.data.body.email)

const resolved = await resolveActiveShareByToken(token)
if (!resolved) {
Expand Down Expand Up @@ -133,7 +134,7 @@ export const PUT = withRouteHandler(
if (!parsed.success) return parsed.response
const { token } = parsed.data.params
const { otp } = parsed.data.body
const email = parsed.data.body.email.trim().toLowerCase()
const email = normalizeEmail(parsed.data.body.email)

const resolved = await resolveActiveShareByToken(token)
if (!resolved) {
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/files/public/[token]/sso/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { normalizeEmail } from '@sim/utils/string'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { publicFileSSOContract } from '@/lib/api/contracts/public-shares'
Expand Down Expand Up @@ -53,7 +54,7 @@ export const POST = withRouteHandler(
const parsed = await parseRequest(publicFileSSOContract, request, context)
if (!parsed.success) return parsed.response
const { token } = parsed.data.params
const email = parsed.data.body.email.trim().toLowerCase()
const email = normalizeEmail(parsed.data.body.email)

const resolved = await resolveActiveShareByToken(token)
if (!resolved) {
Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { functionExecuteContract } from '@/lib/api/contracts'
import { parseRequest } from '@/lib/api/server'
Expand Down Expand Up @@ -1021,7 +1022,7 @@ async function maybeExportSandboxFileToWorkspace(args: {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to export sandbox file',
error: getErrorMessage(error, 'Failed to export sandbox file'),
output: { result: null, stdout: cleanStdout(stdout), executionTime },
},
{ status: 400 }
Expand Down Expand Up @@ -1165,7 +1166,7 @@ async function maybeExportSandboxFilesToWorkspace(args: {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Invalid sandbox output destination',
error: getErrorMessage(error, 'Invalid sandbox output destination'),
output: {
result: null,
stdout: cleanStdout(args.stdout),
Expand Down Expand Up @@ -1220,7 +1221,7 @@ async function maybeExportSandboxFilesToWorkspace(args: {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to export sandbox files',
error: getErrorMessage(error, 'Failed to export sandbox files'),
output: {
result: null,
stdout: cleanStdout(args.stdout),
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/logs/export/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { dbReplica } from '@sim/db'
import { workflow, workflowExecutionLogs } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { and, desc, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
Expand Down Expand Up @@ -139,7 +140,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
} catch (rowError) {
logger.warn('Skipping unserializable execution data for export row', {
executionId: r.executionId,
error: rowError instanceof Error ? rowError.message : String(rowError),
error: getErrorMessage(rowError),
})
}
const line = [
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/mcp/servers/test-connection/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { truncate } from '@sim/utils/string'
import type { NextRequest } from 'next/server'
import { mcpServerTestBodySchema } from '@/lib/api/contracts/mcp'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
Expand Down Expand Up @@ -59,7 +60,7 @@ function sanitizeConnectionError(error: unknown): string {
}

const firstLine = error.message.split('\n')[0]
return firstLine.length > 200 ? `${firstLine.slice(0, 200)}...` : firstLine
return truncate(firstLine, 200)
}

/**
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/memory/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { memory } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getPostgresErrorCode } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull, like } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
Expand Down Expand Up @@ -224,7 +225,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
{ status: 200 }
)
} catch (error: any) {
if (error.code === '23505') {
if (getPostgresErrorCode(error) === '23505') {
return NextResponse.json(
{ success: false, error: { message: 'Memory with this key already exists' } },
{ status: 409 }
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/mothership/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
: 'Mothership execute error',
{
requestId,
error: error instanceof Error ? error.message : 'Unknown error',
error: getErrorMessage(error, 'Unknown error'),
}
)
send({
type: 'error',
error: error instanceof Error ? error.message : 'Internal server error',
error: getErrorMessage(error, 'Internal server error'),
})
} finally {
allowExplicitAbort = false
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/organizations/[id]/invitations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { createLogger } from '@sim/logger'
import { isOrgAdminRole } from '@sim/platform-authz/workspace'
import { getErrorMessage } from '@sim/utils/errors'
import { normalizeEmail } from '@sim/utils/string'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import {
Expand Down Expand Up @@ -178,7 +179,7 @@ export const POST = withRouteHandler(
new Set(
invitationEmails
.map((raw) => {
const normalized = raw.trim().toLowerCase()
const normalized = normalizeEmail(raw)
return quickValidateEmail(normalized).isValid ? normalized : null
})
.filter((email): email is string => !!email)
Expand Down Expand Up @@ -572,7 +573,7 @@ export const POST = withRouteHandler(
(email) => pendingEmails.includes(email) && !memberUserIdByEmail.has(email)
),
invalidEmails: invitationEmails.filter(
(email) => !quickValidateEmail(email.trim().toLowerCase()).isValid
(email) => !quickValidateEmail(normalizeEmail(email)).isValid
),
workspaceGrantsPerInvite: validGrants.length,
...(seatValidation
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/organizations/[id]/members/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { isOrgAdminRole } from '@sim/platform-authz/workspace'
import { normalizeEmail } from '@sim/utils/string'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import {
Expand Down Expand Up @@ -212,7 +213,7 @@ export const POST = withRouteHandler(
const { email, role = 'member' } = parsed.data.body

// Validate and normalize email
const normalizedEmail = email.trim().toLowerCase()
const normalizedEmail = normalizeEmail(email)
const validation = quickValidateEmail(normalizedEmail)
if (!validation.isValid) {
return NextResponse.json(
Expand Down
Loading
Loading