From 5538311335f84ac74bae0ee12041e41cbafa0cd6 Mon Sep 17 00:00:00 2001 From: David Mosiah Date: Sun, 24 May 2026 10:53:52 -0300 Subject: [PATCH] fix: allow owner package skill slug overlap --- convex/packages.public.test.ts | 88 ++++++++++++++++++++++++++++++++++ convex/packages.ts | 23 +++++++-- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/convex/packages.public.test.ts b/convex/packages.public.test.ts index b699edd51d..23a594f486 100644 --- a/convex/packages.public.test.ts +++ b/convex/packages.public.test.ts @@ -3486,6 +3486,94 @@ describe("packages public queries", () => { expect(ctx.runMutation).not.toHaveBeenCalled(); }); + it("allows package publishes when the matching skill slug belongs to the same owner", async () => { + const ctx = { + runQuery: vi + .fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + _id: "users:owner", + githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000, + }) + .mockResolvedValueOnce({ + _id: "users:owner", + role: "user", + githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000, + }) + .mockResolvedValueOnce({ + _id: "skills:demo", + slug: "demo-plugin", + ownerUserId: "users:owner", + ownerPublisherId: "publishers:owner", + }), + runMutation: vi.fn(async () => ({ + publisherId: "publishers:owner", + linkedUserId: "users:owner", + })), + scheduler: { runAfter: vi.fn() }, + storage: { get: vi.fn() }, + }; + + await expect( + publishPackageForUserInternalHandler(ctx as never, { + actorUserId: "users:owner", + payload: { + name: "demo-plugin", + displayName: "Demo Plugin", + family: "bundle-plugin", + version: "1.0.0", + changelog: "init", + bundle: { hostTargets: ["desktop"] }, + files: [], + }, + }), + ).rejects.toThrow("openclaw.plugin.json is required"); + }); + + it("rejects package publishes when the matching skill slug belongs to another owner", async () => { + const ctx = { + runQuery: vi + .fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + _id: "users:owner", + githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000, + }) + .mockResolvedValueOnce({ + _id: "users:owner", + role: "user", + githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000, + }) + .mockResolvedValueOnce({ + _id: "skills:demo", + slug: "demo-plugin", + ownerUserId: "users:other", + ownerPublisherId: "publishers:other", + }), + runMutation: vi.fn(async () => ({ + publisherId: "publishers:owner", + linkedUserId: "users:owner", + })), + scheduler: { runAfter: vi.fn() }, + storage: { get: vi.fn() }, + }; + + await expect( + publishPackageForUserInternalHandler(ctx as never, { + actorUserId: "users:owner", + payload: { + name: "demo-plugin", + displayName: "Demo Plugin", + family: "bundle-plugin", + version: "1.0.0", + changelog: "init", + bundle: { hostTargets: ["desktop"] }, + files: [], + }, + }), + ).rejects.toThrow('Package name collides with existing skill slug "demo-plugin"'); + }); + it("rejects runtime id changes on an existing code plugin package", async () => { const ctx = makeInsertReleaseCtx(makePackageDoc({ runtimeId: "demo.plugin" })); diff --git a/convex/packages.ts b/convex/packages.ts index ebcc2acbb9..72e9269d26 100644 --- a/convex/packages.ts +++ b/convex/packages.ts @@ -4535,6 +4535,17 @@ function doesTrustedPublisherMatchPublishToken( ); } +function doesPackagePublishOwnMatchingSkillSlug( + skill: Pick, "ownerUserId" | "ownerPublisherId">, + ownerUserId: Id<"users">, + ownerPublisherId: Id<"publishers"> | undefined, +) { + if (skill.ownerPublisherId && ownerPublisherId) { + return skill.ownerPublisherId === ownerPublisherId; + } + return skill.ownerUserId === ownerUserId; +} + async function publishPackageImpl( ctx: Parameters[0] & Pick, auth: PackagePublishAuthContext, @@ -4674,10 +4685,14 @@ async function publishPackageImpl( throw new ConvexError(getPublishTotalSizeError("package")); } - const existingSkill = await runQueryRef(ctx, internalRefs.skills.getSkillBySlugInternal, { - slug: name, - }); - if (existingSkill) { + const existingSkill = await runQueryRef, + "ownerUserId" | "ownerPublisherId" + > | null>(ctx, internalRefs.skills.getSkillBySlugInternal, { slug: name }); + if ( + existingSkill && + !doesPackagePublishOwnMatchingSkillSlug(existingSkill, ownerUserId, ownerPublisherId) + ) { throw new ConvexError(`Package name collides with existing skill slug "${name}"`); } if (family === "code-plugin" && (!effectiveSource?.repo || !effectiveSource?.commit)) {