import { ConvexError, v } from "convex/values"; import { internal } from "./_generated/api"; import type { Doc, Id } from "./_generated/dataModel"; import type { MutationCtx, QueryCtx } from "./_generated/server"; import { internalMutation, internalQuery, mutation, query } from "./functions"; import { requireUser } from "./lib/access"; import { deleteGitHubSkillScansForSource } from "./lib/githubSkillScans"; import { adjustGlobalPublicSkillsCount, getPublicSkillVisibilityDelta } from "./lib/globalStats"; import { isOfficialPublisher } from "./lib/officialPublishers"; import { toPublicSkill } from "./lib/public"; import { getOwnerPublisher, getPersonalPublisherForUserOrFallback, isPublisherActive, isPublisherRoleAllowed, requirePublisherRole, } from "./lib/publishers"; import { assertGenericGitHubSkillSyncEnabled, getRuntimeRolloutCapabilities, isLegacyNvidiaSkillSource, } from "./lib/rolloutCapabilities"; import { syncSkillSearchDigestForSkill } from "./lib/skillSearchDigest"; const GITHUB_SKILL_SCAN_CLEANUP_BATCH_SIZE = 25; const SKILLS_SH_ALIAS_SOURCE_SKILL_LIMIT = 500; type PublicGitHubSkillSource = Pick< Doc<"githubSkillSources">, | "_id" | "repo" | "defaultBranch" | "lastSyncStatus" | "lastSyncError" | "lastSyncErrorAt" | "displayManifestStatus" | "displayManifestFetchedAt" | "displayManifestCommit" | "lastSyncIssues" | "lastSyncInvalidSkills" | "createdAt" | "updatedAt" > & { ownerPublisher: Pick, "_id" | "handle" | "displayName"> | null; skills: Array< Pick, "_id" | "slug" | "displayName" | "githubPath" | "githubCurrentStatus"> >; }; export const getByIdInternal = internalQuery({ args: { sourceId: v.id("githubSkillSources") }, handler: async (ctx, args) => ctx.db.get(args.sourceId), }); export const getSkillsShAliasTargetInternal = internalQuery({ args: { repo: v.string(), path: v.string() }, handler: async (ctx, args) => { const repo = args.repo.trim().toLowerCase(); const path = args.path.trim().replace(/^\/+|\/+$/g, ""); if (!repo || !path) return null; const source = await ctx.db .query("githubSkillSources") .withIndex("by_repo", (q) => q.eq("repo", repo)) .unique(); if (!source) return null; const skills = await ctx.db .query("skills") .withIndex("by_github_source", (q) => q.eq("githubSourceId", source._id)) .take(SKILLS_SH_ALIAS_SOURCE_SKILL_LIMIT + 1); if (skills.length > SKILLS_SH_ALIAS_SOURCE_SKILL_LIMIT) return null; const matches = skills.filter( (skill) => skill.githubPath === path && (skill.githubCurrentRepo ?? source.repo).toLowerCase() === repo && skill.installKind === "github" && skill.githubCurrentStatus === "present" && (skill.githubScanStatus === "clean" || skill.githubScanStatus === "suspicious") && Boolean(skill.githubCurrentCommit) && Boolean(skill.githubCurrentContentHash) && Boolean(toPublicSkill(skill)), ); if (matches.length !== 1) return null; const skill = matches[0]!; const publisher = await getOwnerPublisher(ctx, { ownerPublisherId: skill.ownerPublisherId, ownerUserId: skill.ownerUserId, }); if (!publisher) return null; const handle = publisher.handle?.trim(); if (!handle) return null; return { source, skill, publisher: { handle, displayName: publisher.displayName ?? handle }, canonicalRef: `@${handle}/${skill.slug}`, canonicalRoute: `/${encodeURIComponent(handle)}/skills/${encodeURIComponent(skill.slug)}`, }; }, }); async function toPublicGitHubSkillSource( ctx: Pick, source: Doc<"githubSkillSources">, ): Promise { const skills = await ctx.db .query("skills") .withIndex("by_github_source", (q) => q.eq("githubSourceId", source._id)) .collect(); const visibleGitHubSkills = skills .filter((skill) => skill.installKind === "github" && !skill.softDeletedAt) .sort((a, b) => a.displayName.localeCompare(b.displayName)) .map((skill) => ({ _id: skill._id, slug: skill.slug, displayName: skill.displayName, githubPath: skill.githubPath, githubCurrentStatus: skill.githubCurrentStatus, })); const ownerPublisher = source.ownerPublisherId ? await ctx.db.get(source.ownerPublisherId) : null; return { _id: source._id as Id<"githubSkillSources">, repo: source.repo, ownerPublisher: ownerPublisher ? { _id: ownerPublisher._id, handle: ownerPublisher.handle, displayName: ownerPublisher.displayName, } : null, defaultBranch: source.defaultBranch, lastSyncStatus: source.lastSyncStatus, lastSyncError: source.lastSyncError, lastSyncErrorAt: source.lastSyncErrorAt, displayManifestStatus: source.displayManifestStatus, displayManifestFetchedAt: source.displayManifestFetchedAt, displayManifestCommit: source.displayManifestCommit, lastSyncIssues: source.lastSyncIssues, lastSyncInvalidSkills: source.lastSyncInvalidSkills, createdAt: source.createdAt, updatedAt: source.updatedAt, skills: visibleGitHubSkills, }; } export const listForPublisher = query({ args: { ownerPublisherId: v.id("publishers") }, handler: async (ctx, args): Promise => { const { userId } = await requireUser(ctx); await requirePublisherRole(ctx, { publisherId: args.ownerPublisherId, userId, allowed: ["admin"], }); const sources = await ctx.db .query("githubSkillSources") .withIndex("by_owner_publisher", (q) => q.eq("ownerPublisherId", args.ownerPublisherId)) .collect(); const visibleSources = getRuntimeRolloutCapabilities().githubSkillSync.runtimeEnabled ? sources : sources.filter((source) => isLegacyNvidiaSkillSource(source.repo)); const sortedSources = visibleSources.sort((a, b) => b.updatedAt - a.updatedAt); return await Promise.all(sortedSources.map((source) => toPublicGitHubSkillSource(ctx, source))); }, }); export const listForManageableOfficialPublishers = query({ args: {}, handler: async (ctx): Promise => { const { userId, user } = await requireUser(ctx); const memberships = await ctx.db .query("publisherMembers") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(); const ownerPublisherIds = new Set>(); for (const membership of memberships) { if (!isPublisherRoleAllowed(membership.role, ["admin"])) continue; const publisher = await ctx.db.get(membership.publisherId); if ( !publisher || !isPublisherActive(publisher) || !(await isOfficialPublisher(ctx, publisher)) ) { continue; } ownerPublisherIds.add(publisher._id); } const personalPublisher = await getPersonalPublisherForUserOrFallback(ctx, user); if ( personalPublisher && isPublisherActive(personalPublisher) && (await isOfficialPublisher(ctx, personalPublisher)) ) { ownerPublisherIds.add(personalPublisher._id); } const sourceGroups = await Promise.all( [...ownerPublisherIds].map((ownerPublisherId) => ctx.db .query("githubSkillSources") .withIndex("by_owner_publisher", (q) => q.eq("ownerPublisherId", ownerPublisherId)) .collect(), ), ); const sources = sourceGroups.flat(); const visibleSources = getRuntimeRolloutCapabilities().githubSkillSync.runtimeEnabled ? sources : sources.filter((source) => isLegacyNvidiaSkillSource(source.repo)); const sortedSources = visibleSources.sort((a, b) => b.updatedAt - a.updatedAt); return await Promise.all(sortedSources.map((source) => toPublicGitHubSkillSource(ctx, source))); }, }); export async function deleteForPublisherHandler( ctx: MutationCtx, args: { ownerPublisherId: Id<"publishers">; sourceId: Id<"githubSkillSources">; now?: number; }, ) { const { userId } = await requireUser(ctx); await requirePublisherRole(ctx, { publisherId: args.ownerPublisherId, userId, allowed: ["admin"], }); const source = await ctx.db.get(args.sourceId); if (!source || source.ownerPublisherId !== args.ownerPublisherId) { throw new ConvexError("GitHub source not found."); } assertGenericGitHubSkillSyncEnabled(source.repo); const now = args.now ?? Date.now(); const sourceUpdatedAt = Math.max(now, source.updatedAt + 1); const contents = await ctx.db .query("githubSkillContents") .withIndex("by_github_source", (q) => q.eq("githubSourceId", args.sourceId)) .collect(); for (const content of contents) { await ctx.db.delete(content._id); } const [pendingCandidates, failedCandidates, legacyCandidates] = await Promise.all([ ctx.db .query("githubSkillCandidates") .withIndex("by_github_source_and_lifecycle_status", (q) => q.eq("githubSourceId", args.sourceId).eq("lifecycleStatus", "pending"), ) .collect(), ctx.db .query("githubSkillCandidates") .withIndex("by_github_source_and_lifecycle_status", (q) => q.eq("githubSourceId", args.sourceId).eq("lifecycleStatus", "failed"), ) .collect(), ctx.db .query("githubSkillCandidates") .withIndex("by_github_source_and_lifecycle_status", (q) => q.eq("githubSourceId", args.sourceId).eq("lifecycleStatus", undefined), ) .collect(), ]); const candidates = [...pendingCandidates, ...failedCandidates, ...legacyCandidates]; for (const candidate of candidates) { const skill = await ctx.db.get(candidate.skillId); if (skill?.githubPendingCandidateId === candidate._id) { await ctx.db.patch(skill._id, { githubPendingCandidateId: undefined, updatedAt: now, }); await ctx.db.patch(candidate._id, { lifecycleStatus: "canceled", canceledAt: now, cancellationReason: "github.source.disconnected", updatedAt: now, }); } } const skills = await ctx.db .query("skills") .withIndex("by_github_source", (q) => q.eq("githubSourceId", args.sourceId)) .collect(); let deletedSkills = 0; let publicSkillDelta = 0; for (const skill of skills) { if (skill.installKind !== "github") continue; const nextSkill: Doc<"skills"> = { ...skill, softDeletedAt: skill.softDeletedAt ?? now, githubCurrentStatus: "missing", githubRemovedAt: skill.githubRemovedAt ?? now, updatedAt: now, }; publicSkillDelta += getPublicSkillVisibilityDelta(skill, nextSkill); await ctx.db.patch(skill._id, { softDeletedAt: nextSkill.softDeletedAt, githubCurrentStatus: nextSkill.githubCurrentStatus, githubRemovedAt: nextSkill.githubRemovedAt, updatedAt: now, }); await syncSkillSearchDigestForSkill(ctx, nextSkill); deletedSkills += 1; } if (publicSkillDelta !== 0) { await adjustGlobalPublicSkillsCount(ctx, publicSkillDelta, now); } await ctx.db.patch(args.sourceId, { ownerPublisherId: undefined, disconnectedOwnerPublisherId: args.ownerPublisherId, authorizationStatus: "revoked", authorizationCheckedAt: now, authorizationError: "GitHub source disconnected by publisher.", updatedAt: sourceUpdatedAt, }); return { ok: true as const, deletedSkills }; } export async function cleanupDeletedSourceScansHandler( ctx: MutationCtx, args: { sourceId: Id<"githubSkillSources"> }, ) { const deleted = await deleteGitHubSkillScansForSource( ctx, args.sourceId, GITHUB_SKILL_SCAN_CLEANUP_BATCH_SIZE, ); const done = deleted < GITHUB_SKILL_SCAN_CLEANUP_BATCH_SIZE; if (deleted > 0) { await ctx.scheduler.runAfter(0, internal.securityScan.pruneExpiredSkillScanRequestsInternal, { batchSize: 10, }); } if (!done) { await ctx.scheduler.runAfter( 0, internal.githubSkillSources.cleanupDeletedSourceScansInternal, args, ); } return { ok: true as const, deleted, done }; } export const cleanupDeletedSourceScansInternal = internalMutation({ args: { sourceId: v.id("githubSkillSources") }, handler: cleanupDeletedSourceScansHandler, }); export const deleteForPublisher: ReturnType = mutation({ args: { ownerPublisherId: v.id("publishers"), sourceId: v.id("githubSkillSources"), }, handler: async (ctx, args) => deleteForPublisherHandler(ctx, args), });