Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e23a42a
fix: hide package resources when owners are banned
vyctorbrzezowski May 16, 2026
0b80fea
fix(api): attribute unban package restores to moderator
vyctorbrzezowski May 16, 2026
edb266b
fix(web): clarify package effects in ban confirmations
vyctorbrzezowski May 18, 2026
352734f
fix(api): continue package ban batches during in-flight bans
vyctorbrzezowski May 21, 2026
83684eb
Merge remote-tracking branch 'origin/main' into review/pr-2283
Patrick-Erichsen May 27, 2026
40dbc00
fix: keep package unban restore scoped to ban batch
Patrick-Erichsen May 27, 2026
63231d5
fix: retimestamp package releases during repeated bans
Patrick-Erichsen May 27, 2026
ce9ce08
merge origin main into package ban resources
Patrick-Erichsen May 27, 2026
66b3468
fix: start package ban batches after user ban commit
Patrick-Erichsen May 27, 2026
1e8d93f
fix: preserve manual package moderation after unban
Patrick-Erichsen May 27, 2026
fdc00f2
fix: cover personal publisher package sanctions
Patrick-Erichsen May 27, 2026
c9969f5
fix: restore personal publisher packages in autoban remediation
Patrick-Erichsen May 27, 2026
65867bb
fix: bound package publish token revocation batches
Patrick-Erichsen May 27, 2026
8b358f0
merge latest main into package ban resources
Patrick-Erichsen May 27, 2026
ee5a4ad
merge latest main into package ban resources
Patrick-Erichsen May 27, 2026
2c0f359
fix: clear package ban reason during remediation restore
Patrick-Erichsen May 27, 2026
bc55fd8
fix: block direct package ban restores
Patrick-Erichsen May 27, 2026
2f75169
fix: scan linked personal publisher packages during sanctions
Patrick-Erichsen May 27, 2026
a2bd693
fix: tighten package sanction restore batches
Patrick-Erichsen May 27, 2026
59ed73a
Merge remote-tracking branch 'origin/main' into review/pr-2283
Patrick-Erichsen May 27, 2026
70eb0ae
fix: block personal publisher publishes after owner ban
Patrick-Erichsen May 27, 2026
f0960d0
fix: allow initial package ban cleanup before commit
Patrick-Erichsen May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
### Fixes

- API: fix `GET /api/v1/skills` pagination so `cursor` advances to the next page instead of repeating the first page for supported non-trending sorts (#2275) (thanks @vyctorbrzezowski, @enerj).
- Security/API: hide owned package/plugin catalog entries, revoke package publish tokens, and restore only matching ban-hidden packages on user unban (thanks @vyctorbrzezowski).
- API: block public raw skill files when moderation already blocks downloads and reject skill tags that point at another skill's version (thanks @vyctorbrzezowski).
- Web: stop stale unban restore batches from reactivating skills after the owner is banned again or deactivated (thanks @vyctorbrzezowski).
- Security/API: reject direct skill owner transfers when the skill is hidden, suspicious, or malicious (thanks @vyctorbrzezowski).
Expand Down
333 changes: 332 additions & 1 deletion convex/autobanRemediation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const {
recomputeLatestSkillModerationInternal,
restoreOwnedSkillsForAutobanRemediationBatchInternal,
} = await import("./skills");
const { remediateAutobansInternal } = await import("./users");
const { listRestorableAutobanPackageCandidatesPageInternal, remediateAutobansInternal } =
await import("./users");

type WrappedHandler<TArgs, TResult = unknown> = {
_handler: (ctx: unknown, args: TArgs) => Promise<TResult>;
Expand All @@ -44,8 +45,20 @@ const restorePackagesHandler = (
ownerUserId: string;
bannedAt: number;
cursor?: string;
scope?: "ownerUserId" | "personalPublisher";
}>
)._handler;
const listPackageCandidatesHandler = (
listRestorableAutobanPackageCandidatesPageInternal as unknown as WrappedHandler<
{
ownerUserId: string;
bannedAt: number;
cursor?: string;
scope?: "ownerUserId" | "personalPublisher";
},
{ packageIds: string[]; isDone: boolean; continueCursor: string | null }
>
)._handler;
const remediateAutobansHandler = (
remediateAutobansInternal as unknown as WrappedHandler<
{
Expand Down Expand Up @@ -1041,6 +1054,7 @@ describe("autoban remediation package restore", () => {
"packages:demo",
expect.objectContaining({
softDeletedAt: undefined,
softDeletedReason: undefined,
latestReleaseId: "packageReleases:good",
tags: { latest: "packageReleases:good" },
}),
Expand All @@ -1055,6 +1069,323 @@ describe("autoban remediation package restore", () => {
);
});

it("restores packages owned through the user's personal publisher", async () => {
const bannedAt = 1778569308754;
const patch = vi.fn();
const insert = vi.fn();
const scheduler = { runAfter: vi.fn() };
const query = vi.fn((table: string) => {
if (table === "packages") {
return {
withIndex: (name: string) => {
expect(name).toBe("by_owner_publisher");
return {
order: () => ({
paginate: vi.fn(async () => ({
page: [
{
_id: "packages:personal",
name: "@scope/personal",
normalizedName: "@scope/personal",
displayName: "@scope/personal",
family: "external-code-plugin",
ownerUserId: "users:publishing-actor",
ownerPublisherId: "publishers:personal",
softDeletedAt: bannedAt,
scanStatus: "clean",
tags: { latest: "packageReleases:good" },
latestReleaseId: "packageReleases:good",
stats: {},
compatibility: {},
capabilities: {},
verification: {},
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
],
isDone: true,
continueCursor: null,
})),
}),
};
},
};
}
if (table === "packageReleases") {
return {
withIndex: (name: string) => {
expect(name).toBe("by_package");
return {
collect: vi.fn(async () => [
{
_id: "packageReleases:good",
packageId: "packages:personal",
version: "1.0.0",
changelog: "",
integritySha256: "good-sha",
compatibility: {},
capabilities: {},
verification: {},
softDeletedAt: bannedAt,
llmAnalysis: { status: "clean" },
distTags: ["latest"],
createdAt: 1,
},
]),
};
},
};
}
if (
table === "packageSearchDigest" ||
table === "packageCapabilitySearchDigest" ||
table === "packagePluginCategorySearchDigest"
) {
return {
withIndex: () => ({
unique: vi.fn(async () => null),
collect: vi.fn(async () => []),
}),
};
}
throw new Error(`Unexpected table ${table}`);
});

const result = (await restorePackagesHandler(
{
db: {
query,
patch,
insert,
get: vi.fn(async (id: string) => {
if (id === "users:target") {
return {
_id: "users:target",
role: "user",
personalPublisherId: "publishers:personal",
};
}
if (id === "publishers:personal") {
return { _id: id, kind: "user", linkedUserId: "users:target" };
}
return null;
}),
replace: vi.fn(),
delete: vi.fn(),
normalizeId: vi.fn(() => null),
},
scheduler,
} as never,
{
actorUserId: "users:admin",
ownerUserId: "users:target",
bannedAt,
scope: "personalPublisher",
},
)) as { restoredCount: number; restoredReleases: number; skippedMalicious: number };

expect(result).toMatchObject({
restoredCount: 1,
restoredReleases: 1,
skippedMalicious: 0,
});
expect(patch).toHaveBeenCalledWith("packageReleases:good", { softDeletedAt: undefined });
expect(patch).toHaveBeenCalledWith(
"packages:personal",
expect.objectContaining({ softDeletedAt: undefined }),
);
});

it("lists restorable personal-publisher package candidates for dry-run counts", async () => {
const bannedAt = 1778569308754;
const result = await listPackageCandidatesHandler(
{
db: {
get: vi.fn(async (id: string) =>
id === "users:target" ? { _id: id, personalPublisherId: "publishers:personal" } : null,
),
query: vi.fn((table: string) => {
expect(table).toBe("packages");
return {
withIndex: (name: string) => {
expect(name).toBe("by_owner_publisher");
return {
order: () => ({
paginate: vi.fn(async () => ({
page: [
{
_id: "packages:legacy-duplicate",
ownerUserId: "users:target",
softDeletedAt: bannedAt,
scanStatus: "clean",
},
{
_id: "packages:personal",
ownerUserId: "users:publishing-actor",
softDeletedAt: bannedAt,
scanStatus: "clean",
},
],
isDone: true,
continueCursor: null,
})),
}),
};
},
};
}),
},
} as never,
{
ownerUserId: "users:target",
bannedAt,
scope: "personalPublisher",
},
);

expect(result).toEqual({
packageIds: ["packages:personal"],
isDone: true,
continueCursor: null,
});
});

it("does not count org-owned legacy package rows as autoban restore candidates", async () => {
const bannedAt = 1778569308754;
const result = await listPackageCandidatesHandler(
{
db: {
get: vi.fn(async (id: string) => {
if (id === "users:target") {
return { _id: id, personalPublisherId: "publishers:personal" };
}
if (id === "publishers:org") return { _id: id, kind: "org" };
return null;
}),
query: vi.fn((table: string) => {
expect(table).toBe("packages");
return {
withIndex: (name: string) => {
expect(name).toBe("by_owner");
return {
order: () => ({
paginate: vi.fn(async () => ({
page: [
{
_id: "packages:org",
ownerUserId: "users:target",
ownerPublisherId: "publishers:org",
softDeletedAt: bannedAt,
scanStatus: "clean",
},
{
_id: "packages:legacy-personal",
ownerUserId: "users:target",
ownerPublisherId: undefined,
softDeletedAt: bannedAt,
scanStatus: "clean",
},
],
isDone: true,
continueCursor: null,
})),
}),
};
},
};
}),
},
} as never,
{
ownerUserId: "users:target",
bannedAt,
},
);

expect(result).toEqual({
packageIds: ["packages:legacy-personal"],
isDone: true,
continueCursor: null,
});
});

it("lists linked legacy personal-publisher package candidates without users.personalPublisherId", async () => {
const bannedAt = 1778569308754;
const result = await listPackageCandidatesHandler(
{
db: {
get: vi.fn(async (id: string) =>
id === "users:target" ? { _id: id, personalPublisherId: undefined } : null,
),
query: vi.fn((table: string) => {
if (table === "publishers") {
return {
withIndex: (
name: string,
cb: (q: { eq: (field: string, value: string) => unknown }) => unknown,
) => {
expect(name).toBe("by_linked_user");
let linkedUserId = "";
cb({
eq: (field: string, value: string) => {
if (field === "linkedUserId") linkedUserId = value;
return {};
},
});
return {
unique: vi.fn(async () =>
linkedUserId === "users:target"
? {
_id: "publishers:personal",
kind: "user",
linkedUserId: "users:target",
}
: null,
),
};
},
};
}
expect(table).toBe("packages");
return {
withIndex: (name: string) => {
expect(name).toBe("by_owner_publisher");
return {
order: () => ({
paginate: vi.fn(async () => ({
page: [
{
_id: "packages:personal",
ownerUserId: "users:publishing-actor",
softDeletedAt: bannedAt,
scanStatus: "clean",
},
],
isDone: true,
continueCursor: null,
})),
}),
};
},
};
}),
},
} as never,
{
ownerUserId: "users:target",
bannedAt,
scope: "personalPublisher",
},
);

expect(result).toEqual({
packageIds: ["packages:personal"],
isDone: true,
continueCursor: null,
});
});

it("skips timestamp-matched packages when no non-malicious release can be selected", async () => {
const bannedAt = 1778569308754;
const patch = vi.fn();
Expand Down
Loading
Loading