11import { v } from 'convex/values'
22import { internal } from './_generated/api'
33import type { Id } from './_generated/dataModel'
4+ import type { ActionCtx } from './_generated/server'
45import { action , internalAction , internalMutation } from './_generated/server'
56import { buildDeterministicZip } from './lib/skillZip'
67
@@ -136,6 +137,13 @@ type PendingScanSkill = {
136137 checkCount : number
137138}
138139
140+ type SkillActivationCandidate = {
141+ moderationStatus ?: string
142+ moderationReason ?: string
143+ moderationFlags ?: string [ ]
144+ softDeletedAt ?: number
145+ }
146+
139147type PollPendingScansResult = {
140148 processed : number
141149 updated : number
@@ -228,6 +236,23 @@ type SyncModerationReasonsResult = {
228236 done : boolean
229237}
230238
239+ const VT_PENDING_REASONS = new Set ( [ 'pending.scan' , 'scanner.vt.pending' , 'pending.scan.stale' ] )
240+
241+ function shouldActivateWhenVtUnavailable ( skill : SkillActivationCandidate | null | undefined ) {
242+ if ( ! skill || skill . softDeletedAt ) return false
243+ if ( skill . moderationFlags ?. includes ( 'blocked.malware' ) ) return false
244+ if ( skill . moderationStatus === 'active' ) return false
245+ const reason = skill . moderationReason
246+ return typeof reason === 'string' && VT_PENDING_REASONS . has ( reason )
247+ }
248+
249+ async function activateSkillWhenVtUnavailable ( ctx : ActionCtx , skillId : Id < 'skills' > ) {
250+ const skill = await ctx . runQuery ( internal . skills . getSkillByIdInternal , { skillId } )
251+ if ( ! shouldActivateWhenVtUnavailable ( skill ) ) return
252+
253+ await ctx . runMutation ( internal . skills . setSkillModerationStatusActiveInternal , { skillId } )
254+ }
255+
231256export const fetchResults = action ( {
232257 args : {
233258 sha256hash : v . optional ( v . string ( ) ) ,
@@ -306,19 +331,11 @@ export const scanWithVirusTotal = internalAction({
306331 const apiKey = process . env . VT_API_KEY
307332 if ( ! apiKey ) {
308333 console . log ( 'VT_API_KEY not configured, skipping scan — activating skill' )
309- // Activate the skill so it appears in search despite no VT scan.
310334 const version = await ctx . runQuery ( internal . skills . getVersionByIdInternal , {
311335 versionId : args . versionId ,
312336 } )
313337 if ( version ) {
314- const skill = await ctx . runQuery ( internal . skills . getSkillByIdInternal , {
315- skillId : version . skillId ,
316- } )
317- if ( skill ?. moderationReason !== 'quality.low' ) {
318- await ctx . runMutation ( internal . skills . setSkillModerationStatusActiveInternal , {
319- skillId : version . skillId ,
320- } )
321- }
338+ await activateSkillWhenVtUnavailable ( ctx , version . skillId )
322339 }
323340 return
324341 }
@@ -538,14 +555,7 @@ export const pollPendingScans = internalAction({
538555 versionId,
539556 vtAnalysis : { status : 'stale' , checkedAt : Date . now ( ) } ,
540557 } )
541- // Activate the skill so it appears in search — absence of a VT
542- // verdict should not permanently hide a published skill.
543- const skill = await ctx . runQuery ( internal . skills . getSkillByIdInternal , { skillId } )
544- if ( skill ?. moderationReason !== 'quality.low' ) {
545- await ctx . runMutation ( internal . skills . setSkillModerationStatusActiveInternal , {
546- skillId,
547- } )
548- }
558+ await activateSkillWhenVtUnavailable ( ctx , skillId )
549559 staled ++
550560 }
551561 continue
@@ -571,14 +581,7 @@ export const pollPendingScans = internalAction({
571581 versionId,
572582 vtAnalysis : { status : 'stale' , checkedAt : Date . now ( ) } ,
573583 } )
574- // Activate the skill so it appears in search — absence of a VT
575- // verdict should not permanently hide a published skill.
576- const skill = await ctx . runQuery ( internal . skills . getSkillByIdInternal , { skillId } )
577- if ( skill ?. moderationReason !== 'quality.low' ) {
578- await ctx . runMutation ( internal . skills . setSkillModerationStatusActiveInternal , {
579- skillId,
580- } )
581- }
584+ await activateSkillWhenVtUnavailable ( ctx , skillId )
582585 staled ++
583586 }
584587 continue
@@ -680,6 +683,10 @@ async function requestRescan(apiKey: string, sha256hash: string): Promise<boolea
680683 }
681684}
682685
686+ export const __test = {
687+ shouldActivateWhenVtUnavailable,
688+ }
689+
683690/**
684691 * Backfill function to process ALL pending skills at once
685692 * Run manually to clear backlog
0 commit comments