Compare commits

...
Author SHA1 Message Date
Vincent Koc 0532a30c16 fix(publishing): block colons in plugin names 2026-03-23 14:16:16 -07:00
4 changed files with 47 additions and 3 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ export function normalizePackageName(name: string) {
const normalized = trimmed.toLowerCase();
if (!PACKAGE_NAME_PATTERN.test(normalized)) {
throw new ConvexError(
"Package name must be lowercase and npm-safe (example: @scope/name or plugin-name)",
'Package name must be lowercase, npm-safe, and cannot contain ":" (example: @scope/name or plugin-name)',
);
}
return normalized;
+30
View File
@@ -1721,6 +1721,36 @@ describe("packages public queries", () => {
).rejects.toThrow(/Package publish payload/i);
});
it('rejects package names containing ":" in the publish action path', async () => {
const ctx = {
runQuery: vi.fn().mockResolvedValue({
_id: "users:owner",
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
}),
runMutation: vi.fn().mockResolvedValue(null),
scheduler: {
runAfter: vi.fn(),
},
storage: {
get: vi.fn(),
},
};
await expect(
publishPackageForUserInternalHandler(ctx as never, {
actorUserId: "users:owner",
payload: {
name: "demo:plugin",
family: "bundle-plugin",
version: "1.0.0",
changelog: "init",
bundle: { hostTargets: ["desktop"] },
files: [],
},
}),
).rejects.toThrow('cannot contain ":"');
});
it("rejects skill publishes on the package endpoint", async () => {
await expect(
publishPackageForUserInternalHandler({} as never, {
@@ -91,6 +91,18 @@ describe("plugins publish route", () => {
expect(route.__path).toBe("/publish-plugin");
});
it('shows a validation error when the plugin name contains ":"', async () => {
renderPublishRoute();
fireEvent.change(screen.getByPlaceholderText("Plugin name"), {
target: { value: "demo:plugin" },
});
expect(screen.getByText('Plugin name cannot contain ":".')).toBeTruthy();
expect(screen.getByRole("button", { name: "Publish" }).getAttribute("disabled")).not.toBeNull();
expect(publishRelease).not.toHaveBeenCalled();
});
it("publishes a code plugin folder with source metadata and normalized file paths", async () => {
renderPublishRoute();
+4 -2
View File
@@ -79,12 +79,14 @@ function PublishPluginRoute() {
() => oversizedFiles.slice(0, 3).map((file) => file.name),
[oversizedFiles],
);
const invalidNameError = name.trim().includes(":") ? 'Plugin name cannot contain ":".' : null;
const validationError =
oversizedFiles.length > 0
invalidNameError ??
(oversizedFiles.length > 0
? `Each file must be 10MB or smaller: ${oversizedFileNames.join(", ")}`
: totalBytes > MAX_PUBLISH_TOTAL_BYTES
? "Total file size exceeds 50MB."
: null;
: null);
const onPickFiles = async (selected: File[]) => {
const expanded = await expandFilesWithReport(selected, {