clawhub/convex/githubRestoreMutations.ts
Val Alexander deb592d4ce
docs: update repository guidelines and improve formatting across multiple files
- Enhanced AGENTS.md with clearer project structure and development commands.
- Updated CHANGELOG.md to reflect recent fixes and additions.
- Improved formatting in CONTRIBUTING.md for better readability.
- Adjusted package.json and configuration files for consistent command structure.
- Refined README.md and VISION.md for clarity and organization.
- Standardized code formatting in various TypeScript files for consistency.

These changes aim to enhance documentation clarity and maintainability across the repository.
2026-03-18 21:56:01 -05:00

84 lines
2.6 KiB
TypeScript

import { v } from "convex/values";
import { internal } from "./_generated/api";
import { internalMutation } from "./functions";
import { assertAdmin } from "./lib/access";
export const evictSquatterSkillForRestoreInternal = internalMutation({
args: {
actorUserId: v.id("users"),
slug: v.string(),
rightfulOwnerUserId: v.id("users"),
},
handler: async (ctx, args) => {
const actor = await ctx.db.get(args.actorUserId);
if (!actor || actor.deletedAt || actor.deactivatedAt) throw new Error("Actor not found");
assertAdmin(actor);
const slug = args.slug.trim().toLowerCase();
if (!slug) throw new Error("Slug required");
const now = Date.now();
const existingSkill = await ctx.db
.query("skills")
.withIndex("by_slug", (q) => q.eq("slug", slug))
.unique();
if (!existingSkill) return { ok: true as const, action: "noop" as const };
if (existingSkill.ownerUserId === args.rightfulOwnerUserId) {
return { ok: true as const, action: "already_owned" as const };
}
const evictedSlug = buildEvictedSlug(slug, now);
// Free the slug immediately (same transaction) by renaming the squatter's skill.
await ctx.db.patch(existingSkill._id, {
slug: evictedSlug,
softDeletedAt: now,
hiddenAt: existingSkill.hiddenAt ?? now,
hiddenBy: existingSkill.hiddenBy ?? actor._id,
updatedAt: now,
});
// Remove from vector search ASAP.
const embeddings = await ctx.db
.query("skillEmbeddings")
.withIndex("by_skill", (q) => q.eq("skillId", existingSkill._id))
.collect();
for (const embedding of embeddings) {
await ctx.db.patch(embedding._id, {
visibility: "deleted",
updatedAt: now,
});
}
// Cleanup the rest asynchronously (versions, fingerprints, installs, etc.)
await ctx.scheduler.runAfter(0, internal.skills.hardDeleteInternal, {
skillId: existingSkill._id,
actorUserId: actor._id,
phase: "versions",
});
await ctx.db.insert("auditLogs", {
actorUserId: actor._id,
action: "slug.reclaim.sync",
targetType: "skill",
targetId: existingSkill._id,
metadata: {
slug,
evictedSlug,
squatterUserId: existingSkill.ownerUserId,
rightfulOwnerUserId: args.rightfulOwnerUserId,
reason: "Synchronous eviction during GitHub restore",
},
createdAt: now,
});
return { ok: true as const, action: "evicted" as const, evictedSlug };
},
});
function buildEvictedSlug(slug: string, now: number) {
const suffix = now.toString(36);
return `${slug}-evicted-${suffix}`;
}