From abc80e1e2b57583ccbc28da9b5b2b2b84ef9cdf5 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:18:12 -0400 Subject: [PATCH] Fix server-fn deprecation warnings and add CSRF middleware (#357) * Replace deprecated createServerFn().inputValidator() with .validator() * Add CSRF middleware for server functions * Fix prettier formatting after validator rename * Pass Zod schemas directly to .validator() Drops the (data: unknown) => schema.parse(data) lambdas in favor of standard-schema support. This also makes server-fn callers type-checked against the schema input, which surfaced a dead hideSpam property in SearchTabStrip (the schema never included it and the server hard-codes hideSpam for web requests). * Name the inline project-scoped schema in projects.ts --- .../features/search-tabs/SearchTabStrip.tsx | 1 - src/serverFunctions/ahrefs.ts | 2 +- src/serverFunctions/ai-search.ts | 4 +-- src/serverFunctions/aiSearchAccess.ts | 2 +- src/serverFunctions/audit.ts | 12 ++++---- src/serverFunctions/backlinks.ts | 10 +++---- src/serverFunctions/backlinksAccess.ts | 2 +- src/serverFunctions/billing.ts | 2 +- src/serverFunctions/domain.ts | 10 +++---- src/serverFunctions/gsc.ts | 10 +++---- src/serverFunctions/keywords.ts | 22 +++++++-------- src/serverFunctions/lighthouse.ts | 4 +-- src/serverFunctions/onboarding.ts | 2 +- src/serverFunctions/onboardingChat.ts | 2 +- src/serverFunctions/projects.ts | 14 +++++----- src/serverFunctions/rank-tracking.ts | 28 +++++++++---------- src/serverFunctions/redditConversions.ts | 2 +- src/serverFunctions/sam.ts | 6 ++-- src/serverFunctions/samAccess.ts | 2 +- src/serverFunctions/searchPerformance.ts | 10 ++----- src/start.ts | 7 ++++- 21 files changed, 74 insertions(+), 80 deletions(-) diff --git a/src/client/features/search-tabs/SearchTabStrip.tsx b/src/client/features/search-tabs/SearchTabStrip.tsx index e3fa634..c4c9dd5 100644 --- a/src/client/features/search-tabs/SearchTabStrip.tsx +++ b/src/client/features/search-tabs/SearchTabStrip.tsx @@ -179,7 +179,6 @@ function getSearchTabQueryConfig( projectId, target: input.target, scope: input.scope, - hideSpam: false, }, }), }; diff --git a/src/serverFunctions/ahrefs.ts b/src/serverFunctions/ahrefs.ts index 9b4b82f..735f4e4 100644 --- a/src/serverFunctions/ahrefs.ts +++ b/src/serverFunctions/ahrefs.ts @@ -32,7 +32,7 @@ const ahrefsResponseSchema = z.object({ export const getAhrefsDomainRatings = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => domainRatingsInputSchema.parse(data)) + .validator(domainRatingsInputSchema) .handler(async ({ data }) => { const result: Record = {}; diff --git a/src/serverFunctions/ai-search.ts b/src/serverFunctions/ai-search.ts index a767dfc..ac090d1 100644 --- a/src/serverFunctions/ai-search.ts +++ b/src/serverFunctions/ai-search.ts @@ -26,7 +26,7 @@ async function assertPaidPlan(organizationId: string) { export const lookupBrand = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => brandLookupInputSchema.parse(data)) + .validator(brandLookupInputSchema) .handler(async ({ data, context }) => { await assertPaidPlan(context.organizationId); return getBrandLookup({ ...data, projectId: context.projectId }, context); @@ -34,7 +34,7 @@ export const lookupBrand = createServerFn({ method: "POST" }) export const explorePrompt = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => promptExplorerInputSchema.parse(data)) + .validator(promptExplorerInputSchema) .handler(async ({ data, context }) => { await assertPaidPlan(context.organizationId); return runExplorePrompt({ ...data, projectId: context.projectId }, context); diff --git a/src/serverFunctions/aiSearchAccess.ts b/src/serverFunctions/aiSearchAccess.ts index adf2e07..22e66a1 100644 --- a/src/serverFunctions/aiSearchAccess.ts +++ b/src/serverFunctions/aiSearchAccess.ts @@ -17,7 +17,7 @@ type AiSearchAccessStatus = { export const getAiSearchAccessSetupStatus = createServerFn({ method: "GET" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => aiSearchProjectSchema.parse(data)) + .validator(aiSearchProjectSchema) .handler(async (): Promise => { if (await isHostedServerAuthMode()) { return { enabled: true, errorMessage: null }; diff --git a/src/serverFunctions/audit.ts b/src/serverFunctions/audit.ts index 5aec603..050573e 100644 --- a/src/serverFunctions/audit.ts +++ b/src/serverFunctions/audit.ts @@ -21,7 +21,7 @@ import { export const startAudit = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => startAuditSchema.parse(data)) + .validator(startAuditSchema) .handler(async ({ data, context }) => { // The crawler runs on our Workers compute and isn't credit-metered, so // plan-tier limits are the abuse bound in hosted mode: free accounts get @@ -68,35 +68,35 @@ export const startAudit = createServerFn({ method: "POST" }) export const getAuditStatus = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getAuditStatusSchema.parse(data)) + .validator(getAuditStatusSchema) .handler(async ({ data, context }) => { return AuditService.getStatus(data.auditId, context.projectId); }); export const getAuditResults = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getAuditResultsSchema.parse(data)) + .validator(getAuditResultsSchema) .handler(async ({ data, context }) => { return AuditService.getResults(data.auditId, context.projectId); }); export const getAuditHistory = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getAuditHistorySchema.parse(data)) + .validator(getAuditHistorySchema) .handler(async ({ context }) => { return AuditService.getHistory(context.projectId); }); export const getCrawlProgress = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getCrawlProgressSchema.parse(data)) + .validator(getCrawlProgressSchema) .handler(async ({ data, context }) => { return AuditService.getCrawlProgress(data.auditId, context.projectId); }); export const deleteAudit = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => deleteAuditSchema.parse(data)) + .validator(deleteAuditSchema) .handler(async ({ data, context }) => { await AuditService.remove(data.auditId, context.projectId); return { success: true }; diff --git a/src/serverFunctions/backlinks.ts b/src/serverFunctions/backlinks.ts index d732e3c..68ff160 100644 --- a/src/serverFunctions/backlinks.ts +++ b/src/serverFunctions/backlinks.ts @@ -16,7 +16,7 @@ export const getBacklinksOverview = createServerFn({ method: "POST", }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => backlinksOverviewInputSchema.parse(data)) + .validator(backlinksOverviewInputSchema) .handler(async ({ data, context }) => { const profile = await BacklinksService.profileOverview( { @@ -32,7 +32,7 @@ export const getBacklinksRows = createServerFn({ method: "POST", }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => backlinksRowsPageRequestSchema.parse(data)) + .validator(backlinksRowsPageRequestSchema) .handler(({ data, context }) => BacklinksService.profileBacklinksPage(data, context, WEB_SPAM_OPTIONS), ); @@ -41,9 +41,7 @@ export const getBacklinksReferringDomains = createServerFn({ method: "POST", }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => - referringDomainsPageRequestSchema.parse(data), - ) + .validator(referringDomainsPageRequestSchema) .handler(({ data, context }) => BacklinksService.profileReferringDomainsPage( data, @@ -56,7 +54,7 @@ export const getBacklinksTopPages = createServerFn({ method: "POST", }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => topPagesPageRequestSchema.parse(data)) + .validator(topPagesPageRequestSchema) .handler(({ data, context }) => BacklinksService.profileTopPagesPage(data, context), ); diff --git a/src/serverFunctions/backlinksAccess.ts b/src/serverFunctions/backlinksAccess.ts index 121ff68..80038d6 100644 --- a/src/serverFunctions/backlinksAccess.ts +++ b/src/serverFunctions/backlinksAccess.ts @@ -17,7 +17,7 @@ type BacklinksAccessStatus = { export const getBacklinksAccessSetupStatus = createServerFn({ method: "GET" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => backlinksProjectSchema.parse(data)) + .validator(backlinksProjectSchema) .handler(async (): Promise => { if (await isHostedServerAuthMode()) { return { enabled: true, errorMessage: null }; diff --git a/src/serverFunctions/billing.ts b/src/serverFunctions/billing.ts index 329dc22..49d09de 100644 --- a/src/serverFunctions/billing.ts +++ b/src/serverFunctions/billing.ts @@ -50,7 +50,7 @@ export type BillingUsageEvent = { export const getBillingUsageEvents = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => billingUsageRangeSchema.parse(data)) + .validator(billingUsageRangeSchema) .handler(async ({ data, context }) => { if (!(await isHostedServerAuthMode())) { return []; diff --git a/src/serverFunctions/domain.ts b/src/serverFunctions/domain.ts index c7630d0..a9736c3 100644 --- a/src/serverFunctions/domain.ts +++ b/src/serverFunctions/domain.ts @@ -18,7 +18,7 @@ async function getDomainE2eFixtures() { export const getDomainOverview = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => domainOverviewSchema.parse(data)) + .validator(domainOverviewSchema) .handler(async ({ data, context }) => { if (shouldUseDomainE2eFixtures()) { const fixtures = await getDomainE2eFixtures(); @@ -36,7 +36,7 @@ export const getDomainOverview = createServerFn({ method: "POST" }) export const getDomainKeywordSuggestions = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => domainKeywordSuggestionsSchema.parse(data)) + .validator(domainKeywordSuggestionsSchema) .handler(async ({ data, context }) => DomainService.getSuggestedKeywords( { @@ -50,9 +50,7 @@ export const getDomainKeywordSuggestions = createServerFn({ method: "POST" }) export const getDomainKeywordsPage = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => - domainKeywordsPageRequestSchema.parse(data), - ) + .validator(domainKeywordsPageRequestSchema) .handler(async ({ data, context }) => { if (shouldUseDomainE2eFixtures()) { const fixtures = await getDomainE2eFixtures(); @@ -70,7 +68,7 @@ export const getDomainKeywordsPage = createServerFn({ method: "POST" }) export const getDomainPagesPage = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => domainPagesPageRequestSchema.parse(data)) + .validator(domainPagesPageRequestSchema) .handler(async ({ data, context }) => { if (shouldUseDomainE2eFixtures()) { const fixtures = await getDomainE2eFixtures(); diff --git a/src/serverFunctions/gsc.ts b/src/serverFunctions/gsc.ts index c37ab46..8460459 100644 --- a/src/serverFunctions/gsc.ts +++ b/src/serverFunctions/gsc.ts @@ -32,7 +32,7 @@ export const getGscGrantStatus = createServerFn({ method: "GET" }) export const getGscConnection = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => projectScopedSchema.parse(data)) + .validator(projectScopedSchema) .handler(async ({ context }) => { const [connection, currentUserHasGrant, hosted, gscConfigured] = await Promise.all([ @@ -53,7 +53,7 @@ export const getGscConnection = createServerFn({ method: "POST" }) export const listGscSites = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => projectScopedSchema.parse(data)) + .validator(projectScopedSchema) .handler(async ({ context }) => { const [siteList, connection] = await Promise.all([ GscService.listSitesForUserWithGrantStatus(context.userId), @@ -72,7 +72,7 @@ export const listGscSites = createServerFn({ method: "POST" }) export const setGscSite = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => setSiteSchema.parse(data)) + .validator(setSiteSchema) .handler(async ({ data, context }) => { const connection = await GscService.setSite({ projectId: context.projectId, @@ -94,7 +94,7 @@ export const setGscSite = createServerFn({ method: "POST" }) export const disconnectGsc = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => projectScopedSchema.parse(data)) + .validator(projectScopedSchema) .handler(async ({ context }) => { await GscService.disconnect({ projectId: context.projectId, @@ -113,7 +113,7 @@ export const disconnectGsc = createServerFn({ method: "POST" }) export const startSelfHostedGscLink = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => startSelfHostedLinkSchema.parse(data)) + .validator(startSelfHostedLinkSchema) .handler(async ({ data, context }) => { const publicOrigin = getPublicOrigin(getRequest()); const url = await createSelfHostedGscAuthorizationUrl({ diff --git a/src/serverFunctions/keywords.ts b/src/serverFunctions/keywords.ts index 619f101..debccbb 100644 --- a/src/serverFunctions/keywords.ts +++ b/src/serverFunctions/keywords.ts @@ -24,7 +24,7 @@ async function getKeywordE2eFixtures() { export const researchKeywords = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => researchKeywordsSchema.parse(data)) + .validator(researchKeywordsSchema) .handler(async ({ data, context }) => { if (shouldUseKeywordE2eFixtures()) { const fixtures = await getKeywordE2eFixtures(); @@ -42,7 +42,7 @@ export const researchKeywords = createServerFn({ method: "POST" }) export const saveKeywords = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => saveKeywordsSchema.parse(data)) + .validator(saveKeywordsSchema) .handler(async ({ data, context }) => { return KeywordResearchService.saveKeywords({ ...data, @@ -52,7 +52,7 @@ export const saveKeywords = createServerFn({ method: "POST" }) export const getSavedKeywords = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getSavedKeywordsSchema.parse(data)) + .validator(getSavedKeywordsSchema) .handler(async ({ data, context }) => { return KeywordResearchService.getSavedKeywords({ ...data, @@ -62,7 +62,7 @@ export const getSavedKeywords = createServerFn({ method: "POST" }) export const exportSavedKeywords = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => exportSavedKeywordsSchema.parse(data)) + .validator(exportSavedKeywordsSchema) .handler(async ({ data, context }) => { return KeywordResearchService.exportSavedKeywords({ ...data, @@ -72,7 +72,7 @@ export const exportSavedKeywords = createServerFn({ method: "POST" }) export const updateSavedKeywordTags = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => updateSavedKeywordTagsSchema.parse(data)) + .validator(updateSavedKeywordTagsSchema) .handler(async ({ data, context }) => { return KeywordResearchService.updateSavedKeywordTags({ ...data, @@ -82,7 +82,7 @@ export const updateSavedKeywordTags = createServerFn({ method: "POST" }) export const updateSavedKeywordTag = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => updateSavedKeywordTagSchema.parse(data)) + .validator(updateSavedKeywordTagSchema) .handler(async ({ data, context }) => { return KeywordResearchService.updateSavedKeywordTag({ ...data, @@ -92,7 +92,7 @@ export const updateSavedKeywordTag = createServerFn({ method: "POST" }) export const deleteSavedKeywordTag = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => deleteSavedKeywordTagSchema.parse(data)) + .validator(deleteSavedKeywordTagSchema) .handler(async ({ data, context }) => { return KeywordResearchService.deleteSavedKeywordTag({ ...data, @@ -104,16 +104,14 @@ export const removeSavedKeywords = createServerFn({ method: "POST", }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => removeSavedKeywordsSchema.parse(data)) + .validator(removeSavedKeywordsSchema) .handler(async ({ data, context }) => { return KeywordResearchService.removeSavedKeywords(context.projectId, data); }); export const refreshSavedKeywordMetrics = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => - refreshSavedKeywordMetricsSchema.parse(data), - ) + .validator(refreshSavedKeywordMetricsSchema) .handler(async ({ context }) => { return KeywordResearchService.refreshSavedKeywordMetrics( { projectId: context.projectId }, @@ -123,7 +121,7 @@ export const refreshSavedKeywordMetrics = createServerFn({ method: "POST" }) export const getSerpAnalysis = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => serpAnalysisSchema.parse(data)) + .validator(serpAnalysisSchema) .handler(async ({ data, context }) => KeywordResearchService.getSerpAnalysis( { diff --git a/src/serverFunctions/lighthouse.ts b/src/serverFunctions/lighthouse.ts index f73e3b9..0a1fcdb 100644 --- a/src/serverFunctions/lighthouse.ts +++ b/src/serverFunctions/lighthouse.ts @@ -45,7 +45,7 @@ async function getAuditLighthouseData(input: { export const getAuditLighthouseIssues = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => lighthouseAuditIssueSchema.parse(data)) + .validator(lighthouseAuditIssueSchema) .handler(async ({ data, context }) => { const lighthouse = await getAuditLighthouseData({ projectId: context.projectId, @@ -68,7 +68,7 @@ export const getAuditLighthouseIssues = createServerFn({ method: "POST" }) export const exportAuditLighthouseIssues = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => lighthouseAuditExportSchema.parse(data)) + .validator(lighthouseAuditExportSchema) .handler(async ({ data, context }) => { const lighthouse = await getAuditLighthouseData({ projectId: context.projectId, diff --git a/src/serverFunctions/onboarding.ts b/src/serverFunctions/onboarding.ts index f1eab11..e7e38db 100644 --- a/src/serverFunctions/onboarding.ts +++ b/src/serverFunctions/onboarding.ts @@ -66,7 +66,7 @@ export const getOnboardingAnswers = createServerFn({ method: "GET" }) export const saveOnboardingAnswers = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => onboardingAnswersSchema.parse(data)) + .validator(onboardingAnswersSchema) .handler(async ({ data, context }) => { const now = new Date().toISOString(); const completedAt = data.completed ? now : undefined; diff --git a/src/serverFunctions/onboardingChat.ts b/src/serverFunctions/onboardingChat.ts index 609ee25..69adf03 100644 --- a/src/serverFunctions/onboardingChat.ts +++ b/src/serverFunctions/onboardingChat.ts @@ -39,7 +39,7 @@ const saveSiteSchema = z.object({ // Persists the site + default location for the onboarding project. export const saveOnboardingSite = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => saveSiteSchema.parse(data)) + .validator(saveSiteSchema) .handler(async ({ data, context }) => { const project = await ProjectRepository.getProjectForOrganization( data.projectId, diff --git a/src/serverFunctions/projects.ts b/src/serverFunctions/projects.ts index c937459..cc1032f 100644 --- a/src/serverFunctions/projects.ts +++ b/src/serverFunctions/projects.ts @@ -12,6 +12,8 @@ import { } from "@/types/schemas/projects"; import { z } from "zod"; +const projectScopedSchema = z.object({ projectId: z.string().min(1) }); + export const getProjects = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) .handler(async ({ context }) => @@ -20,21 +22,21 @@ export const getProjects = createServerFn({ method: "POST" }) export const createProject = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => createProjectSchema.parse(data)) + .validator(createProjectSchema) .handler(async ({ data, context }) => ProjectService.createProject(context.organizationId, data), ); export const updateProject = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => updateProjectSchema.parse(data)) + .validator(updateProjectSchema) .handler(async ({ data, context }) => ProjectService.updateProject(context.organizationId, data), ); export const archiveProject = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => archiveProjectSchema.parse(data)) + .validator(archiveProjectSchema) .handler(async ({ data, context }) => ProjectService.archiveProject(context.organizationId, data), ); @@ -47,16 +49,14 @@ export const getArchivedProjects = createServerFn({ method: "POST" }) export const restoreProject = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => restoreProjectSchema.parse(data)) + .validator(restoreProjectSchema) .handler(async ({ data, context }) => ProjectService.restoreProject(context.organizationId, data), ); export const getProjectAccess = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => - z.object({ projectId: z.string().min(1) }).parse(data), - ) + .validator(projectScopedSchema) .handler(async ({ data, context }) => { return ProjectService.getProjectForOrganization( context.organizationId, diff --git a/src/serverFunctions/rank-tracking.ts b/src/serverFunctions/rank-tracking.ts index c77b4f3..abca737 100644 --- a/src/serverFunctions/rank-tracking.ts +++ b/src/serverFunctions/rank-tracking.ts @@ -59,21 +59,21 @@ async function requireConfig(configId: string, projectId: string) { export const getRankTrackingConfigs = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getConfigsSchema.parse(data)) + .validator(getConfigsSchema) .handler(async ({ context }) => { return RankTrackingRepository.getConfigsForProject(context.projectId); }); export const getRankTrackingConfigSummaries = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getConfigsSchema.parse(data)) + .validator(getConfigsSchema) .handler(async ({ context }) => { return RankTrackingRepository.getConfigSummaries(context.projectId); }); export const createRankTrackingConfig = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => createConfigSchema.parse(data)) + .validator(createConfigSchema) .handler(async ({ data, context }) => { const result = await RankTrackingService.createConfig({ projectId: context.projectId, @@ -104,7 +104,7 @@ export const createRankTrackingConfig = createServerFn({ method: "POST" }) export const updateRankTrackingConfig = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => updateConfigSchema.parse(data)) + .validator(updateConfigSchema) .handler(async ({ data, context }) => { await RankTrackingService.updateConfig(data.configId, context.projectId, { domain: data.domain, @@ -120,7 +120,7 @@ export const updateRankTrackingConfig = createServerFn({ method: "POST" }) export const triggerRankCheck = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => triggerCheckSchema.parse(data)) + .validator(triggerCheckSchema) .handler(async ({ data, context }) => { const isHosted = await isHostedServerAuthMode(); if (isHosted && !(await customerHasPaidPlan(context.organizationId))) { @@ -157,7 +157,7 @@ export const triggerRankCheck = createServerFn({ method: "POST" }) export const getLatestRankResults = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getLatestResultsSchema.parse(data)) + .validator(getLatestResultsSchema) .handler(async ({ data, context }) => { return getLatestResults( data.configId, @@ -168,21 +168,21 @@ export const getLatestRankResults = createServerFn({ method: "POST" }) export const getLatestRankRun = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getLatestRunSchema.parse(data)) + .validator(getLatestRunSchema) .handler(async ({ data, context }) => { return RankTrackingService.getLatestRun(data.configId, context.projectId); }); export const estimateRankCheckCost = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => estimateCostSchema.parse(data)) + .validator(estimateCostSchema) .handler(async ({ data, context }) => { return RankTrackingService.estimateCost(data.configId, context.projectId); }); export const addTrackingKeywords = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => addKeywordsSchema.parse(data)) + .validator(addKeywordsSchema) .handler(async ({ data, context }) => { const result = await RankTrackingService.addKeywords( data.configId, @@ -252,7 +252,7 @@ export const addTrackingKeywords = createServerFn({ method: "POST" }) export const removeTrackingKeywords = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => removeKeywordsSchema.parse(data)) + .validator(removeKeywordsSchema) .handler(async ({ data, context }) => { await RankTrackingService.removeKeywords( data.configId, @@ -264,7 +264,7 @@ export const removeTrackingKeywords = createServerFn({ method: "POST" }) export const refreshTrackingKeywordMetrics = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => refreshMetricsSchema.parse(data)) + .validator(refreshMetricsSchema) .handler(async ({ data, context }) => { const result = await RankTrackingService.refreshKeywordMetrics( data.configId, @@ -290,7 +290,7 @@ export const refreshTrackingKeywordMetrics = createServerFn({ method: "POST" }) export const getRankKeywordHistory = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getKeywordHistorySchema.parse(data)) + .validator(getKeywordHistorySchema) .handler(async ({ data, context }): Promise => { await requireConfig(data.configId, context.projectId); return RankTrackingRepository.getKeywordHistory( @@ -302,7 +302,7 @@ export const getRankKeywordHistory = createServerFn({ method: "POST" }) export const getRankConfigTrend = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getConfigTrendSchema.parse(data)) + .validator(getConfigTrendSchema) .handler(async ({ data, context }): Promise => { await requireConfig(data.configId, context.projectId); const rows = await RankTrackingRepository.getConfigTrend( @@ -330,7 +330,7 @@ export const getRankConfigTrend = createServerFn({ method: "POST" }) export const getRankPositionMatrix = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => getPositionMatrixSchema.parse(data)) + .validator(getPositionMatrixSchema) .handler(async ({ data, context }): Promise => { await requireConfig(data.configId, context.projectId); return RankTrackingRepository.getPositionMatrix( diff --git a/src/serverFunctions/redditConversions.ts b/src/serverFunctions/redditConversions.ts index c4b1b56..0de3e9b 100644 --- a/src/serverFunctions/redditConversions.ts +++ b/src/serverFunctions/redditConversions.ts @@ -11,7 +11,7 @@ const conversionInputSchema = z.object({ export const captureRedditConversionEvent = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => conversionInputSchema.parse(data)) + .validator(conversionInputSchema) .handler(async ({ data, context }) => { const status = await captureRedditConversion({ attribution: data.attribution, diff --git a/src/serverFunctions/sam.ts b/src/serverFunctions/sam.ts index ebe22d0..4a05d98 100644 --- a/src/serverFunctions/sam.ts +++ b/src/serverFunctions/sam.ts @@ -15,7 +15,7 @@ const projectScopedSchema = z.object({ projectId: z.string().min(1) }); // Lists the SAM chat sessions for a project (newest first) for the side-panel. export const listSamSessions = createServerFn({ method: "GET" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => projectScopedSchema.parse(data)) + .validator(projectScopedSchema) .handler(async ({ context }) => { return SamSessionRepository.listSessionsForProject( context.projectId, @@ -27,7 +27,7 @@ export const listSamSessions = createServerFn({ method: "GET" }) // connection keyed by that id. export const createSamSession = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => projectScopedSchema.parse(data)) + .validator(projectScopedSchema) .handler(async ({ context }) => { const session = await SamSessionRepository.createSession({ projectId: context.projectId, @@ -46,7 +46,7 @@ const archiveSchema = z.object({ sessionId: z.string().min(1) }); // unarchive can restore it. There is no unarchive UI yet. export const archiveSamSession = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) - .inputValidator((data: unknown) => archiveSchema.parse(data)) + .validator(archiveSchema) .handler(async ({ data, context }) => { // Authorize against the session's project (the canonical project-access // path), not the caller's org directly. diff --git a/src/serverFunctions/samAccess.ts b/src/serverFunctions/samAccess.ts index b8b1d91..c7ca53b 100644 --- a/src/serverFunctions/samAccess.ts +++ b/src/serverFunctions/samAccess.ts @@ -21,7 +21,7 @@ type SamAccessStatus = { // deployments always have the key provisioned, so only self-hosted is checked. export const getSamAccessSetupStatus = createServerFn({ method: "GET" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => projectScopedSchema.parse(data)) + .validator(projectScopedSchema) .handler(async (): Promise => { if (await isHostedServerAuthMode()) { return { enabled: true, errorMessage: null }; diff --git a/src/serverFunctions/searchPerformance.ts b/src/serverFunctions/searchPerformance.ts index be3593c..b3723b3 100644 --- a/src/serverFunctions/searchPerformance.ts +++ b/src/serverFunctions/searchPerformance.ts @@ -64,7 +64,7 @@ function isExpectedConnectionFailure(error: unknown): boolean { */ export const getSearchPerformanceReport = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => searchPerformanceInputSchema.parse(data)) + .validator(searchPerformanceInputSchema) .handler(async ({ data, context }) => { const { startDate, endDate } = resolveDateRange({ dateRange: data.dateRange, @@ -137,9 +137,7 @@ export const getSearchPerformanceReport = createServerFn({ method: "POST" }) */ export const getSearchPerformanceTable = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => - searchPerformanceTableInputSchema.parse(data), - ) + .validator(searchPerformanceTableInputSchema) .handler(async ({ data, context }) => { const { startDate, endDate } = resolveDateRange({ dateRange: data.dateRange, @@ -185,9 +183,7 @@ export const getSearchPerformanceTable = createServerFn({ method: "POST" }) */ export const exportSearchPerformanceTable = createServerFn({ method: "POST" }) .middleware(requireProjectContext) - .inputValidator((data: unknown) => - searchPerformanceTableExportInputSchema.parse(data), - ) + .validator(searchPerformanceTableExportInputSchema) .handler(async ({ data, context }) => { const { startDate, endDate } = resolveDateRange({ dateRange: data.dateRange, diff --git a/src/start.ts b/src/start.ts index 26c6eeb..44ec32d 100644 --- a/src/start.ts +++ b/src/start.ts @@ -1,6 +1,11 @@ -import { createStart } from "@tanstack/react-start"; +import { createCsrfMiddleware, createStart } from "@tanstack/react-start"; import { globalServerFunctionMiddleware } from "@/serverFunctions/middleware"; +const csrfMiddleware = createCsrfMiddleware({ + filter: (ctx) => ctx.handlerType === "serverFn", +}); + export const startInstance = createStart(() => ({ + requestMiddleware: [csrfMiddleware], functionMiddleware: globalServerFunctionMiddleware, }));