/* @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@convex-dev/auth/server", () => ({ getAuthUserId: vi.fn(), authTables: {}, })); vi.mock("./lib/skillPublish", () => ({ fetchText: vi.fn().mockResolvedValue("# skill"), })); const { getAuthUserId } = await import("@convex-dev/auth/server"); const { getReadme: getSkillReadme, getFilePreview: getSkillFilePreview, getFileText: getSkillFileText, getGitHubSkillContent, } = await import("./skills"); const getSkillReadmeHandler = getSkillReadme as unknown as { _handler: Function }; const getSkillFilePreviewHandler = getSkillFilePreview as unknown as { _handler: Function }; const getSkillFileTextHandler = getSkillFileText as unknown as { _handler: Function }; const getGitHubSkillContentHandler = getGitHubSkillContent as unknown as { _handler: Function }; function makeSkillVersion() { return { _id: "skillVersions:1", _creationTime: 1, skillId: "skills:1", version: "1.0.0", changelog: "init", files: [ { path: "SKILL.md", size: 10, storageId: "_storage:1", sha256: "abc", contentType: "text/markdown", }, ], }; } function makeActionCtx(args: { skill?: Record | null; version?: Record | null; actor?: Record | null; publisherMemberRole?: "owner" | "admin" | "publisher" | null; publisherAccess?: boolean; storedBlob?: Blob | null; }) { return { runQuery: vi.fn(async (_endpoint: unknown, payload: Record) => { if (payload.versionId && args.version) return args.version ?? null; if (payload.skillId && args.skill) return args.skill ?? null; if (payload.publisherId && payload.userId === args.actor?._id) { if (Array.isArray(payload.allowedPublisherRoles)) { if (args.publisherAccess !== undefined) return args.publisherAccess; if (payload.legacyOwnerUserId) return payload.legacyOwnerUserId === args.actor?._id; return Boolean(args.publisherMemberRole); } return args.publisherMemberRole ?? null; } if (payload.userId === args.actor?._id) { return args.actor ?? null; } throw new Error("Unexpected endpoint"); }), storage: { get: vi.fn().mockResolvedValue(args.storedBlob ?? new Blob(["# skill"])), }, } as never; } describe("version file access actions", () => { beforeEach(() => { vi.mocked(getAuthUserId).mockReset(); vi.mocked(getAuthUserId).mockResolvedValue(null as never); }); it("blocks unauthenticated access to hidden skill versions", async () => { const ctx = makeActionCtx({ version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).rejects.toThrow("Version not available"); }); it("blocks unauthenticated access to pending-publication versions on public skills", async () => { const ctx = makeActionCtx({ version: { ...makeSkillVersion(), publicationStatus: "pending" }, skill: { _id: "skills:1", ownerUserId: "users:owner", softDeletedAt: undefined, moderationStatus: "active", moderationFlags: [], stats: { downloads: 0, stars: 0, installsAllTime: 0, versions: 1, comments: 0, }, }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).rejects.toThrow("Version not available"); }); it("allows owners to read pending-publication versions", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never); const ctx = makeActionCtx({ actor: { _id: "users:owner", role: "user" }, version: { ...makeSkillVersion(), publicationStatus: "pending" }, skill: { _id: "skills:1", ownerUserId: "users:owner", softDeletedAt: undefined, moderationStatus: "active", moderationFlags: [], stats: { downloads: 0, stars: 0, installsAllTime: 0, versions: 1, comments: 0, }, }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).resolves.toEqual({ path: "SKILL.md", text: "# skill" }); }); it("allows owners to read hidden skill versions", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never); const ctx = makeActionCtx({ actor: { _id: "users:owner", role: "user" }, version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).resolves.toEqual({ path: "SKILL.md", text: "# skill" }); }); it("does not let stale ownerUserId read publisher-owned hidden skill versions", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never); const ctx = makeActionCtx({ actor: { _id: "users:owner", role: "user" }, publisherMemberRole: null, publisherAccess: false, version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", ownerPublisherId: "publishers:org", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).rejects.toThrow("Version not available"); }); it("allows org collaborators to read hidden skill versions", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:member" as never); const ctx = makeActionCtx({ actor: { _id: "users:member", role: "user" }, publisherMemberRole: "publisher", publisherAccess: true, version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", ownerPublisherId: "publishers:org", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).resolves.toEqual({ path: "SKILL.md", text: "# skill" }); }); it("allows linked personal publisher users to read hidden skill versions", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never); const ctx = makeActionCtx({ actor: { _id: "users:owner", role: "user" }, publisherMemberRole: null, publisherAccess: true, version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:legacy-owner", ownerPublisherId: "publishers:owner", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).resolves.toEqual({ path: "SKILL.md", text: "# skill" }); }); it("allows legacy no-link personal publisher owners to read hidden skill versions", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never); const ctx = makeActionCtx({ actor: { _id: "users:owner", role: "user" }, publisherMemberRole: null, version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", ownerPublisherId: "publishers:owner", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).resolves.toEqual({ path: "SKILL.md", text: "# skill" }); }); it("does not honor stale personal publisher memberships for hidden skill versions", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:friend" as never); const ctx = makeActionCtx({ actor: { _id: "users:friend", role: "user" }, publisherMemberRole: "owner", publisherAccess: false, version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", ownerPublisherId: "publishers:owner", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillReadmeHandler._handler(ctx, { versionId: "skillVersions:1" } as never), ).rejects.toThrow("Version not available"); }); it("allows owners to read hidden skill files", async () => { vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never); const ctx = makeActionCtx({ actor: { _id: "users:owner", role: "user" }, version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillFileTextHandler._handler(ctx, { versionId: "skillVersions:1", path: "SKILL.md", } as never), ).resolves.toMatchObject({ path: "SKILL.md", text: "# skill" }); }); it("returns opaque files as download-only previews with exact metadata", async () => { const version = { ...makeSkillVersion(), files: [ { path: "assets/payload.bin", size: 4, storageId: "_storage:opaque", sha256: "d".repeat(64), contentType: "application/octet-stream", }, ], }; const ctx = makeActionCtx({ version, storedBlob: new Blob([Uint8Array.from([0, 1, 2, 255])], { type: "application/octet-stream", }), skill: { _id: "skills:1", ownerUserId: "users:owner", stats: {}, softDeletedAt: undefined, moderationStatus: "active", moderationFlags: [], }, }); await expect( getSkillFilePreviewHandler._handler(ctx, { versionId: "skillVersions:1", path: "assets/payload.bin", } as never), ).resolves.toEqual({ path: "assets/payload.bin", text: null, size: 4, sha256: "d".repeat(64), }); }); it.each([ ["report.pdf", "application/pdf"], ["page.html", "text/html"], ["diagram.svg", "image/svg+xml"], ["config.xml", "application/xml"], ])( "previews valid UTF-8 document %s as escaped text regardless of extension", async (path, contentType) => { const text = "valid UTF-8"; const version = { ...makeSkillVersion(), files: [ { path, size: text.length, storageId: "_storage:rich", sha256: "e".repeat(64), contentType, }, ], }; const ctx = makeActionCtx({ version, storedBlob: new Blob([text], { type: contentType }), skill: { _id: "skills:1", ownerUserId: "users:owner", stats: {}, softDeletedAt: undefined, moderationStatus: "active", moderationFlags: [], }, }); await expect( getSkillFilePreviewHandler._handler(ctx, { versionId: "skillVersions:1", path, } as never), ).resolves.toEqual({ path, text, size: text.length, sha256: "e".repeat(64), }); }, ); it("blocks unauthenticated file reads from hidden skill versions", async () => { const ctx = makeActionCtx({ version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "pending.scan", moderationFlags: [], }, }); await expect( getSkillFileTextHandler._handler(ctx, { versionId: "skillVersions:1", path: "SKILL.md", } as never), ).rejects.toThrow("Version not available"); }); it("blocks public reads from malware-blocked skill files", async () => { const ctx = makeActionCtx({ version: makeSkillVersion(), skill: { _id: "skills:1", ownerUserId: "users:owner", softDeletedAt: undefined, moderationStatus: "hidden", moderationReason: "scanner.vt.malicious", moderationFlags: ["blocked.malware"], }, }); await expect( getSkillFileTextHandler._handler(ctx, { versionId: "skillVersions:1", path: "SKILL.md", } as never), ).rejects.toThrow("Version not available"); }); it("returns null instead of throwing for public reads from malware-blocked GitHub skill content", async () => { const skill = { _id: "skills:github", _creationTime: 1, slug: "github-demo", displayName: "GitHub Demo", summary: "Summary", ownerUserId: "users:owner", canonicalSkillId: undefined, forkOf: undefined, latestVersionId: undefined, installKind: "github", githubCurrentStatus: "present", githubCurrentContentHash: "hash-a", tags: {}, badges: undefined, stats: { downloads: 1, installsCurrent: 1, installsAllTime: 1, stars: 1, versions: 0, comments: 0, }, createdAt: 1, updatedAt: 2, softDeletedAt: undefined, moderationStatus: "hidden", moderationFlags: ["blocked.malware"], moderationReason: "scanner.vt.malicious", }; const ctx = { db: { get: vi.fn(async (id: string) => (id === "skills:github" ? skill : null)), query: vi.fn(() => { throw new Error("Content should not be read when the skill is not publicly readable"); }), }, }; await expect( getGitHubSkillContentHandler._handler(ctx, { skillId: "skills:github", kind: "readme", } as never), ).resolves.toBeNull(); }); it("still allows public access to visible skill files", async () => { const ctx = makeActionCtx({ version: makeSkillVersion(), skill: { _id: "skills:1", _creationTime: 1, slug: "demo", displayName: "Demo", summary: "Summary", ownerUserId: "users:owner", canonicalSkillId: undefined, forkOf: undefined, latestVersionId: "skillVersions:1", tags: {}, badges: undefined, stats: { downloads: 1, installsCurrent: 1, installsAllTime: 1, stars: 1, versions: 1, comments: 0, }, createdAt: 1, updatedAt: 2, softDeletedAt: undefined, moderationStatus: "active", moderationFlags: [], moderationReason: undefined, }, }); await expect( getSkillFileTextHandler._handler(ctx, { versionId: "skillVersions:1", path: "SKILL.md", } as never), ).resolves.toMatchObject({ path: "SKILL.md", text: "# skill" }); }); });