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
5 changes: 3 additions & 2 deletions apps/sim/app/(auth)/verify/use-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { normalizeEmail } from '@sim/utils/string'
import { useRouter, useSearchParams } from 'next/navigation'
import { client, useSession } from '@/lib/auth/auth-client'
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
Expand Down Expand Up @@ -100,7 +101,7 @@ export function useVerification({
setErrorMessage('')

try {
const normalizedEmail = email.trim().toLowerCase()
const normalizedEmail = normalizeEmail(email)
const response = await client.emailOtp.verifyEmail({
email: normalizedEmail,
otp,
Expand Down Expand Up @@ -173,7 +174,7 @@ export function useVerification({
setIsLoading(true)
setErrorMessage('')

const normalizedEmail = email.trim().toLowerCase()
const normalizedEmail = normalizeEmail(email)
client.emailOtp
.sendVerificationOtp({
email: normalizedEmail,
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/credential-sets/invite/[token]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { normalizeEmail } from '@sim/utils/string'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import {
Expand All @@ -17,7 +18,6 @@ import {
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { normalizeEmail } from '@/lib/invitations/core'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'

const logger = createLogger('CredentialSetInviteToken')
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/invitations/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { invitation, invitationWorkspaceGrant } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { normalizeEmail } from '@sim/utils/string'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import {
Expand All @@ -13,7 +14,7 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { cancelInvitation, getInvitationById, normalizeEmail } from '@/lib/invitations/core'
import { cancelInvitation, getInvitationById } from '@/lib/invitations/core'
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('InvitationsAPI')
Expand Down
25 changes: 11 additions & 14 deletions apps/sim/app/api/tools/crowdstrike/query/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { isRecordLike } from '@sim/utils/object'
import { type NextRequest, NextResponse } from 'next/server'
import { crowdstrikeQueryContract } from '@/lib/api/contracts/tools/crowdstrike'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
Expand Down Expand Up @@ -31,10 +32,6 @@ function getCloudBaseUrl(cloud: CrowdStrikeCloud): string {
return cloudMap[cloud]
}

function isJsonRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function getString(value: unknown): string | null {
return typeof value === 'string' ? value : null
}
Expand All @@ -56,32 +53,32 @@ function getRecordArray(value: unknown): JsonRecord[] {
return []
}

return value.filter(isJsonRecord)
return value.filter(isRecordLike)
}

function getResourcesArray(data: unknown): unknown[] {
const root = getResponseRoot(data)
if (!isJsonRecord(root) || !Array.isArray(root.resources)) {
if (!isRecordLike(root) || !Array.isArray(root.resources)) {
return []
}

return root.resources
}

function getRecordResources(data: unknown): JsonRecord[] {
return getResourcesArray(data).filter(isJsonRecord)
return getResourcesArray(data).filter(isRecordLike)
}

function getStringResources(data: unknown): string[] {
return getStringArray(getResourcesArray(data))
}

function getResponseRoot(data: unknown): unknown {
if (!isJsonRecord(data)) {
if (!isRecordLike(data)) {
return null
}

if (isJsonRecord(data.body)) {
if (isRecordLike(data.body)) {
return data.body
}

Expand All @@ -90,7 +87,7 @@ function getResponseRoot(data: unknown): unknown {

function getPagination(data: unknown) {
const root = getResponseRoot(data)
if (!isJsonRecord(root) || !isJsonRecord(root.meta) || !isJsonRecord(root.meta.pagination)) {
if (!isRecordLike(root) || !isRecordLike(root.meta) || !isRecordLike(root.meta.pagination)) {
return null
}

Expand All @@ -102,13 +99,13 @@ function getPagination(data: unknown) {
}

function getErrorMessage(data: unknown, fallback: string): string {
if (!isJsonRecord(data)) {
if (!isRecordLike(data)) {
return fallback
}

const errors = Array.isArray(data.errors) ? data.errors : []
const firstError = errors[0]
if (isJsonRecord(firstError)) {
if (isRecordLike(firstError)) {
const firstMessage = getString(firstError.message) ?? getString(firstError.code)
if (firstMessage) {
return firstMessage
Expand Down Expand Up @@ -179,7 +176,7 @@ async function getAccessToken(params: CrowdStrikeBaseParams): Promise<string> {
throw new Error(getErrorMessage(data, 'Failed to authenticate with CrowdStrike'))
}

if (!isJsonRecord(data) || typeof data.access_token !== 'string') {
if (!isRecordLike(data) || typeof data.access_token !== 'string') {
throw new Error('CrowdStrike authentication did not return an access token')
}

Expand Down Expand Up @@ -234,7 +231,7 @@ function normalizeAggregationBucket(resource: JsonRecord): CrowdStrikeSensorAggr
count: getNumber(resource.count),
from: getNumber(resource.from),
keyAsString: getString(resource.key_as_string),
label: isJsonRecord(resource.label) ? resource.label : null,
label: isRecordLike(resource.label) ? resource.label : null,
stringFrom: getString(resource.string_from),
stringTo: getString(resource.string_to),
subAggregates: getRecordArray(resource.sub_aggregates).map(normalizeAggregationResult),
Expand Down
35 changes: 16 additions & 19 deletions apps/sim/app/api/tools/image/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { sleep } from '@sim/utils/helpers'
import { isRecordLike } from '@sim/utils/object'
import { type NextRequest, NextResponse } from 'next/server'
import {
type ImageToolBody,
Expand Down Expand Up @@ -431,10 +432,6 @@ const FALAI_IMAGE_MODEL_CONFIGS: Record<string, FalAIImageModelConfig> = {
},
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function getStringProperty(
record: Record<string, unknown> | undefined,
key: string
Expand All @@ -452,7 +449,7 @@ function getNumberProperty(
}

function firstRecord(value: unknown): Record<string, unknown> | undefined {
return Array.isArray(value) ? value.find(isRecord) : undefined
return Array.isArray(value) ? value.find(isRecordLike) : undefined
}

function pickAllowed(
Expand Down Expand Up @@ -575,7 +572,7 @@ async function generateWithOpenAI(
maxBytes: MAX_IMAGE_JSON_BYTES,
label: 'OpenAI image response',
})
if (!isRecord(data)) {
if (!isRecordLike(data)) {
throw new Error('Invalid OpenAI image response')
}

Expand Down Expand Up @@ -669,27 +666,27 @@ async function generateWithGemini(
maxBytes: MAX_IMAGE_JSON_BYTES,
label: 'Gemini image response',
})
if (!isRecord(data)) {
if (!isRecordLike(data)) {
throw new Error('Invalid Gemini image response')
}

const candidate = firstRecord(data.candidates)
const content = isRecord(candidate?.content) ? candidate.content : undefined
const content = isRecordLike(candidate?.content) ? candidate.content : undefined
const parts = Array.isArray(content?.parts) ? content.parts : []
const textPart = parts.find((part) => isRecord(part) && typeof part.text === 'string')
const textPart = parts.find((part) => isRecordLike(part) && typeof part.text === 'string')
const imagePart = parts.find((part) => {
if (!isRecord(part)) return false
return isRecord(part.inlineData) || isRecord(part.inline_data)
if (!isRecordLike(part)) return false
return isRecordLike(part.inlineData) || isRecordLike(part.inline_data)
})

if (!isRecord(imagePart)) {
if (!isRecordLike(imagePart)) {
logger.error(`[${requestId}] Gemini response missing image part`)
throw new Error('No image data found in Gemini response')
}

const inlineData = isRecord(imagePart.inlineData)
const inlineData = isRecordLike(imagePart.inlineData)
? imagePart.inlineData
: isRecord(imagePart.inline_data)
: isRecordLike(imagePart.inline_data)
? imagePart.inline_data
: undefined
const base64Image = getStringProperty(inlineData, 'data')
Expand All @@ -712,7 +709,7 @@ async function generateWithGemini(
fileName: `gemini-${model}.${extensionFromContentType(contentType)}`,
provider: 'gemini',
model,
description: isRecord(textPart) ? getStringProperty(textPart, 'text') : undefined,
description: isRecordLike(textPart) ? getStringProperty(textPart, 'text') : undefined,
}
}

Expand All @@ -722,7 +719,7 @@ function buildFalAIQueueUrl(endpoint: string, requestId: string, path: 'status'

function getFalAIErrorMessage(error: unknown): string {
if (typeof error === 'string') return error
if (isRecord(error)) {
if (isRecordLike(error)) {
return (
getStringProperty(error, 'message') ||
getStringProperty(error, 'detail') ||
Expand Down Expand Up @@ -835,7 +832,7 @@ async function generateWithFalAI(
maxBytes: MAX_IMAGE_JSON_BYTES,
label: 'Fal.ai create response',
})
if (!isRecord(createData)) {
if (!isRecordLike(createData)) {
throw new Error('Invalid Fal.ai queue response')
}

Expand Down Expand Up @@ -878,7 +875,7 @@ async function generateWithFalAI(
maxBytes: MAX_IMAGE_JSON_BYTES,
label: 'Fal.ai status response',
})
if (!isRecord(statusData)) {
if (!isRecordLike(statusData)) {
throw new Error('Invalid Fal.ai status response')
}

Expand Down Expand Up @@ -910,7 +907,7 @@ async function generateWithFalAI(
maxBytes: MAX_IMAGE_JSON_BYTES,
label: 'Fal.ai result response',
})
if (!isRecord(resultData)) {
if (!isRecordLike(resultData)) {
throw new Error('Invalid Fal.ai result response')
}

Expand Down
17 changes: 7 additions & 10 deletions apps/sim/app/api/tools/video/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { sleep } from '@sim/utils/helpers'
import { generateId } from '@sim/utils/id'
import { isRecordLike } from '@sim/utils/object'
import { type NextRequest, NextResponse } from 'next/server'
import { videoProviders, videoToolContract } from '@/lib/api/contracts/tools/media/video'
import { getValidationErrorMessage, parseRequest, validationErrorResponse } from '@/lib/api/server'
Expand Down Expand Up @@ -1081,10 +1082,6 @@ function formatFalAIDuration(
return String(duration)
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function getStringProperty(
record: Record<string, unknown> | undefined,
key: string
Expand Down Expand Up @@ -1159,7 +1156,7 @@ function getFalAIValidationError(

function getFalAIErrorMessage(error: unknown): string {
if (typeof error === 'string') return error
if (isRecord(error)) return getStringProperty(error, 'message') || JSON.stringify(error)
if (isRecordLike(error)) return getStringProperty(error, 'message') || JSON.stringify(error)
return 'Unknown error'
}

Expand Down Expand Up @@ -1236,7 +1233,7 @@ async function generateWithFalAI(
}

const createData = await readVideoJson<unknown>(createResponse, 'Fal.ai queue response')
if (!isRecord(createData)) {
if (!isRecordLike(createData)) {
throw new Error('Invalid Fal.ai queue response')
}

Expand Down Expand Up @@ -1273,7 +1270,7 @@ async function generateWithFalAI(
}

const statusData = await readVideoJson<unknown>(statusResponse, 'Fal.ai status response')
if (!isRecord(statusData)) {
if (!isRecordLike(statusData)) {
throw new Error('Invalid Fal.ai status response')
}

Expand All @@ -1300,12 +1297,12 @@ async function generateWithFalAI(
}

const resultData = await readVideoJson<unknown>(resultResponse, 'Fal.ai result response')
if (!isRecord(resultData)) {
if (!isRecordLike(resultData)) {
throw new Error('Invalid Fal.ai result response')
}

const videoOutput = isRecord(resultData.video) ? resultData.video : undefined
const fallbackOutput = isRecord(resultData.output) ? resultData.output : undefined
const videoOutput = isRecordLike(resultData.video) ? resultData.video : undefined
const fallbackOutput = isRecordLike(resultData.output) ? resultData.output : undefined
const videoUrl =
getStringProperty(videoOutput, 'url') || getStringProperty(fallbackOutput, 'url')
if (!videoUrl) {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/workspaces/invitations/batch/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createLogger } from '@sim/logger'
import { normalizeEmail } from '@sim/utils/string'
import { type NextRequest, NextResponse } from 'next/server'
import { batchWorkspaceInvitationsContract } from '@/lib/api/contracts/invitations'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { normalizeEmail } from '@/lib/invitations/core'
import {
createWorkspaceInvitation,
prepareWorkspaceInvitationContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export {
type StreamLoopOptions,
type StreamLoopState,
} from './stream-context'
export { finalizeResidualToolCalls, isRecord } from './stream-helpers'
export { finalizeResidualToolCalls } from './stream-helpers'
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { isRecordLike } from '@sim/utils/object'
import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome'
import type { MothershipStreamV1ToolUI } from '@/lib/copilot/generated/mothership-stream-v1'
import {
Expand Down Expand Up @@ -81,12 +82,8 @@ export type ToolResultPhasePayload = {
success?: boolean
}

export function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}

export function asPayloadRecord(value: unknown): StreamPayload | undefined {
return isRecord(value) ? value : undefined
return isRecordLike(value) ? value : undefined
}

export function getToolUI(ui?: MothershipStreamV1ToolUI): StreamToolUI | undefined {
Expand Down
Loading
Loading