fix(routing): protect publisher aliases from plugin collisions

This commit is contained in:
Vincent Koc
2026-06-23 17:56:48 +08:00
parent ab862d49f1
commit 9b352d7f99
20 changed files with 706 additions and 220 deletions
+9 -1
View File
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { isReservedPublicOwnerHandle } from "./publicRouteReservations";
import {
isReservedOpenClawExtensionHandle,
isReservedPublicOwnerHandle,
} from "./publicRouteReservations";
describe("public route reservations", () => {
it.each(["admin", "clawhub", "docs", "plugins", "skills"])(
@@ -9,6 +12,11 @@ describe("public route reservations", () => {
},
);
it.each(["codex", "tencent"])("reserves @%s as an OpenClaw alias", (handle) => {
expect(isReservedOpenClawExtensionHandle(handle)).toBe(true);
expect(isReservedPublicOwnerHandle(handle)).toBe(false);
});
it("does not normalize at-sign prefixes", () => {
expect(isReservedPublicOwnerHandle("@clawhub")).toBe(false);
});
+9
View File
@@ -1,10 +1,19 @@
import { OPENCLAW_EXTENSION_SLUG_TO_PACKAGE } from "clawhub-schema";
const RESERVED_PUBLIC_OWNER_HANDLES = new Set(["admin", "clawhub", "docs", "plugins", "skills"]);
const RESERVED_OPENCLAW_EXTENSION_HANDLES = new Set(
Object.keys(OPENCLAW_EXTENSION_SLUG_TO_PACKAGE),
);
const RESERVED_UNSCOPED_PACKAGE_NAMES = new Set(["publish"]);
export function isReservedPublicOwnerHandle(handle: string | undefined | null) {
return Boolean(handle && RESERVED_PUBLIC_OWNER_HANDLES.has(handle.trim().toLowerCase()));
}
export function isReservedOpenClawExtensionHandle(handle: string | undefined | null) {
return Boolean(handle && RESERVED_OPENCLAW_EXTENSION_HANDLES.has(handle.trim().toLowerCase()));
}
export function isReservedUnscopedPackageName(name: string | undefined | null) {
return Boolean(name && RESERVED_UNSCOPED_PACKAGE_NAMES.has(name.trim().toLowerCase()));
}
+7
View File
@@ -12,6 +12,7 @@ import { isPackageBlockedFromPublic } from "./lib/packageSecurity";
import { toPublicPublisher } from "./lib/public";
import {
formatReservedPublicOwnerHandleMessage,
isReservedOpenClawExtensionHandle,
isReservedPublicOwnerHandle,
} from "./lib/publicRouteReservations";
import {
@@ -1078,6 +1079,9 @@ async function ensureOrgPublisherHandleWithActor(
if (!normalizePublisherHandle(args.memberHandle)) {
throw new ConvexError("memberHandle required when creating org publisher");
}
if (!existingPublisher && !existingUser && isReservedOpenClawExtensionHandle(handle)) {
throw new ConvexError(formatReservedPublicOwnerHandleMessage(handle));
}
const publisherId = await ctx.db.insert("publishers", {
kind: "org",
@@ -1686,6 +1690,9 @@ async function createOrgPublisherForUser(
if (await isHandleReservedForAnotherUser(ctx, handle, args.actorUserId)) {
throw new ConvexError(`Handle "@${handle}" is reserved for another user`);
}
if (isReservedOpenClawExtensionHandle(handle)) {
throw new ConvexError(formatReservedPublicOwnerHandleMessage(handle));
}
const now = Date.now();
const publisherId = await ctx.db.insert("publishers", {
+8
View File
@@ -14,6 +14,7 @@ import { syncGitHubProfile } from "./lib/githubAccount";
import { toPublicUser } from "./lib/public";
import {
formatReservedPublicOwnerHandleMessage,
isReservedOpenClawExtensionHandle,
isReservedPublicOwnerHandle,
} from "./lib/publicRouteReservations";
import {
@@ -760,6 +761,10 @@ async function canUserClaimHandle(
if (await isHandleReservedForAnotherUser(ctx, normalizedHandle, userId)) return false;
const publisher = await getPublisherByHandle(ctx, normalizedHandle);
if (isReservedOpenClawExtensionHandle(normalizedHandle)) {
const existingUser = await getUserByHandleOrPersonalPublisher(ctx, normalizedHandle);
return existingUser?._id === userId;
}
if (!publisher || publisher.deletedAt || publisher.deactivatedAt) return true;
return publisher.kind === "user" && publisher.linkedUserId === userId;
}
@@ -2056,6 +2061,9 @@ async function ensurePublisherHandleWithActor(
if (existing?.deletedAt || existing?.deactivatedAt) {
throw new Error("Handle belongs to a deleted or deactivated user");
}
if (!existing && isReservedOpenClawExtensionHandle(normalizedHandle)) {
throw new ConvexError(formatReservedPublicOwnerHandleMessage(normalizedHandle));
}
const now = Date.now();
const displayName = args.displayName?.trim() || normalizedHandle;
+1
View File
@@ -5,6 +5,7 @@ export * from "./catalogMetadata.js";
export * from "./docsLinks.js";
export * from "./license.js";
export * from "./openclawContract.js";
export * from "./openClawExtensionSlugs.js";
export * from "./packages.js";
export * from "./pluginCategories.js";
export { ApiRoutes, LegacyApiRoutes } from "./routes.js";
+1
View File
@@ -4,6 +4,7 @@ export * from "./catalogMetadata.js";
export * from "./docsLinks.js";
export * from "./license.js";
export * from "./openclawContract.js";
export * from "./openClawExtensionSlugs.js";
export * from "./packages.js";
export * from "./pluginCategories.js";
export { ApiRoutes, LegacyApiRoutes } from "./routes.js";
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACrD,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACzD,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACrD,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACzD,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
+192
View File
@@ -0,0 +1,192 @@
export declare const OPENCLAW_EXTENSION_SLUG_TO_PACKAGE: {
readonly acpx: "@openclaw/acpx";
readonly "active-memory": "@openclaw/active-memory";
readonly alibaba: "@openclaw/alibaba-provider";
readonly "alibaba-provider": "@openclaw/alibaba-provider";
readonly "amazon-bedrock": "@openclaw/amazon-bedrock-provider";
readonly "amazon-bedrock-mantle": "@openclaw/amazon-bedrock-mantle-provider";
readonly "amazon-bedrock-mantle-provider": "@openclaw/amazon-bedrock-mantle-provider";
readonly "amazon-bedrock-provider": "@openclaw/amazon-bedrock-provider";
readonly anthropic: "@openclaw/anthropic-provider";
readonly "anthropic-provider": "@openclaw/anthropic-provider";
readonly "anthropic-vertex": "@openclaw/anthropic-vertex-provider";
readonly "anthropic-vertex-provider": "@openclaw/anthropic-vertex-provider";
readonly arcee: "@openclaw/arcee-provider";
readonly "arcee-provider": "@openclaw/arcee-provider";
readonly "azure-speech": "@openclaw/azure-speech";
readonly bluebubbles: "@openclaw/bluebubbles";
readonly bonjour: "@openclaw/bonjour";
readonly brave: "@openclaw/brave-plugin";
readonly "brave-plugin": "@openclaw/brave-plugin";
readonly browser: "@openclaw/browser-plugin";
readonly "browser-plugin": "@openclaw/browser-plugin";
readonly byteplus: "@openclaw/byteplus-provider";
readonly "byteplus-provider": "@openclaw/byteplus-provider";
readonly cerebras: "@openclaw/cerebras-provider";
readonly "cerebras-provider": "@openclaw/cerebras-provider";
readonly chutes: "@openclaw/chutes-provider";
readonly "chutes-provider": "@openclaw/chutes-provider";
readonly "cloudflare-ai-gateway": "@openclaw/cloudflare-ai-gateway-provider";
readonly "cloudflare-ai-gateway-provider": "@openclaw/cloudflare-ai-gateway-provider";
readonly codex: "@openclaw/codex";
readonly comfy: "@openclaw/comfy-provider";
readonly "comfy-provider": "@openclaw/comfy-provider";
readonly "copilot-proxy": "@openclaw/copilot-proxy";
readonly deepgram: "@openclaw/deepgram-provider";
readonly "deepgram-provider": "@openclaw/deepgram-provider";
readonly deepinfra: "@openclaw/deepinfra-provider";
readonly "deepinfra-provider": "@openclaw/deepinfra-provider";
readonly deepseek: "@openclaw/deepseek-provider";
readonly "deepseek-provider": "@openclaw/deepseek-provider";
readonly "device-pair": "@openclaw/device-pair";
readonly "diagnostics-otel": "@openclaw/diagnostics-otel";
readonly "diagnostics-prometheus": "@openclaw/diagnostics-prometheus";
readonly diffs: "@openclaw/diffs";
readonly "diffs-language-pack": "@openclaw/diffs-language-pack";
readonly discord: "@openclaw/discord";
readonly "document-extract": "@openclaw/document-extract-plugin";
readonly "document-extract-plugin": "@openclaw/document-extract-plugin";
readonly duckduckgo: "@openclaw/duckduckgo-plugin";
readonly "duckduckgo-plugin": "@openclaw/duckduckgo-plugin";
readonly elevenlabs: "@openclaw/elevenlabs-speech";
readonly "elevenlabs-speech": "@openclaw/elevenlabs-speech";
readonly exa: "@openclaw/exa-plugin";
readonly "exa-plugin": "@openclaw/exa-plugin";
readonly fal: "@openclaw/fal-provider";
readonly "fal-provider": "@openclaw/fal-provider";
readonly feishu: "@openclaw/feishu";
readonly "file-transfer": "@openclaw/file-transfer";
readonly firecrawl: "@openclaw/firecrawl-plugin";
readonly "firecrawl-plugin": "@openclaw/firecrawl-plugin";
readonly fireworks: "@openclaw/fireworks-provider";
readonly "fireworks-provider": "@openclaw/fireworks-provider";
readonly "github-copilot": "@openclaw/github-copilot-provider";
readonly "github-copilot-provider": "@openclaw/github-copilot-provider";
readonly google: "@openclaw/google-plugin";
readonly "google-meet": "@openclaw/google-meet";
readonly "google-plugin": "@openclaw/google-plugin";
readonly googlechat: "@openclaw/googlechat";
readonly gradium: "@openclaw/gradium-speech";
readonly "gradium-speech": "@openclaw/gradium-speech";
readonly groq: "@openclaw/groq-provider";
readonly "groq-provider": "@openclaw/groq-provider";
readonly huggingface: "@openclaw/huggingface-provider";
readonly "huggingface-provider": "@openclaw/huggingface-provider";
readonly imessage: "@openclaw/imessage";
readonly inworld: "@openclaw/inworld-speech";
readonly "inworld-speech": "@openclaw/inworld-speech";
readonly irc: "@openclaw/irc";
readonly kilocode: "@openclaw/kilocode-provider";
readonly "kilocode-provider": "@openclaw/kilocode-provider";
readonly kimi: "@openclaw/kimi-provider";
readonly "kimi-coding": "@openclaw/kimi-provider";
readonly "kimi-provider": "@openclaw/kimi-provider";
readonly line: "@openclaw/line";
readonly litellm: "@openclaw/litellm-provider";
readonly "litellm-provider": "@openclaw/litellm-provider";
readonly "llm-task": "@openclaw/llm-task";
readonly lmstudio: "@openclaw/lmstudio-provider";
readonly "lmstudio-provider": "@openclaw/lmstudio-provider";
readonly lobster: "@openclaw/lobster";
readonly matrix: "@openclaw/matrix";
readonly mattermost: "@openclaw/mattermost";
readonly "memory-core": "@openclaw/memory-core";
readonly "memory-lancedb": "@openclaw/memory-lancedb";
readonly "memory-wiki": "@openclaw/memory-wiki";
readonly microsoft: "@openclaw/microsoft-speech";
readonly "microsoft-foundry": "@openclaw/microsoft-foundry";
readonly "microsoft-speech": "@openclaw/microsoft-speech";
readonly "migrate-claude": "@openclaw/migrate-claude";
readonly "migrate-hermes": "@openclaw/migrate-hermes";
readonly minimax: "@openclaw/minimax-provider";
readonly "minimax-provider": "@openclaw/minimax-provider";
readonly mistral: "@openclaw/mistral-provider";
readonly "mistral-provider": "@openclaw/mistral-provider";
readonly moonshot: "@openclaw/moonshot-provider";
readonly "moonshot-provider": "@openclaw/moonshot-provider";
readonly msteams: "@openclaw/msteams";
readonly "nextcloud-talk": "@openclaw/nextcloud-talk";
readonly nostr: "@openclaw/nostr";
readonly nvidia: "@openclaw/nvidia-provider";
readonly "nvidia-provider": "@openclaw/nvidia-provider";
readonly ollama: "@openclaw/ollama-provider";
readonly "ollama-provider": "@openclaw/ollama-provider";
readonly "open-prose": "@openclaw/open-prose";
readonly openai: "@openclaw/openai-provider";
readonly "openai-provider": "@openclaw/openai-provider";
readonly opencode: "@openclaw/opencode-provider";
readonly "opencode-go": "@openclaw/opencode-go-provider";
readonly "opencode-go-provider": "@openclaw/opencode-go-provider";
readonly "opencode-provider": "@openclaw/opencode-provider";
readonly openrouter: "@openclaw/openrouter-provider";
readonly "openrouter-provider": "@openclaw/openrouter-provider";
readonly openshell: "@openclaw/openshell-sandbox";
readonly "openshell-sandbox": "@openclaw/openshell-sandbox";
readonly perplexity: "@openclaw/perplexity-plugin";
readonly "perplexity-plugin": "@openclaw/perplexity-plugin";
readonly "phone-control": "@openclaw/phone-control";
readonly "qa-channel": "@openclaw/qa-channel";
readonly "qa-lab": "@openclaw/qa-lab";
readonly "qa-matrix": "@openclaw/qa-matrix";
readonly qianfan: "@openclaw/qianfan-provider";
readonly "qianfan-provider": "@openclaw/qianfan-provider";
readonly qqbot: "@openclaw/qqbot";
readonly qwen: "@openclaw/qwen-provider";
readonly "qwen-provider": "@openclaw/qwen-provider";
readonly runway: "@openclaw/runway-provider";
readonly "runway-provider": "@openclaw/runway-provider";
readonly searxng: "@openclaw/searxng-plugin";
readonly "searxng-plugin": "@openclaw/searxng-plugin";
readonly senseaudio: "@openclaw/senseaudio-provider";
readonly "senseaudio-provider": "@openclaw/senseaudio-provider";
readonly sglang: "@openclaw/sglang-provider";
readonly "sglang-provider": "@openclaw/sglang-provider";
readonly signal: "@openclaw/signal";
readonly "skill-workshop": "@openclaw/skill-workshop";
readonly slack: "@openclaw/slack";
readonly stepfun: "@openclaw/stepfun-provider";
readonly "stepfun-provider": "@openclaw/stepfun-provider";
readonly "synology-chat": "@openclaw/synology-chat";
readonly synthetic: "@openclaw/synthetic-provider";
readonly "synthetic-provider": "@openclaw/synthetic-provider";
readonly "talk-voice": "@openclaw/talk-voice";
readonly tavily: "@openclaw/tavily-plugin";
readonly "tavily-plugin": "@openclaw/tavily-plugin";
readonly telegram: "@openclaw/telegram";
readonly tencent: "@openclaw/tencent-provider";
readonly "tencent-provider": "@openclaw/tencent-provider";
readonly "thread-ownership": "@openclaw/thread-ownership";
readonly tlon: "@openclaw/tlon";
readonly together: "@openclaw/together-provider";
readonly "together-provider": "@openclaw/together-provider";
readonly tokenjuice: "@openclaw/tokenjuice";
readonly "tts-local-cli": "@openclaw/tts-local-cli";
readonly twitch: "@openclaw/twitch";
readonly venice: "@openclaw/venice-provider";
readonly "venice-provider": "@openclaw/venice-provider";
readonly "vercel-ai-gateway": "@openclaw/vercel-ai-gateway-provider";
readonly "vercel-ai-gateway-provider": "@openclaw/vercel-ai-gateway-provider";
readonly vllm: "@openclaw/vllm-provider";
readonly "vllm-provider": "@openclaw/vllm-provider";
readonly "voice-call": "@openclaw/voice-call";
readonly volcengine: "@openclaw/volcengine-provider";
readonly "volcengine-provider": "@openclaw/volcengine-provider";
readonly voyage: "@openclaw/voyage-provider";
readonly "voyage-provider": "@openclaw/voyage-provider";
readonly vydra: "@openclaw/vydra-provider";
readonly "vydra-provider": "@openclaw/vydra-provider";
readonly "web-readability": "@openclaw/web-readability-plugin";
readonly "web-readability-plugin": "@openclaw/web-readability-plugin";
readonly webhooks: "@openclaw/webhooks";
readonly whatsapp: "@openclaw/whatsapp";
readonly xai: "@openclaw/xai-plugin";
readonly "xai-plugin": "@openclaw/xai-plugin";
readonly xiaomi: "@openclaw/xiaomi-provider";
readonly "xiaomi-provider": "@openclaw/xiaomi-provider";
readonly zai: "@openclaw/zai-provider";
readonly "zai-provider": "@openclaw/zai-provider";
readonly zalo: "@openclaw/zalo";
readonly zalouser: "@openclaw/zalouser";
};
export declare function getOpenClawExtensionPackageName(slug: string): string | null;
export declare function getOpenClawPackageCandidateNames(requestedName: string): string[];
+203
View File
@@ -0,0 +1,203 @@
export const OPENCLAW_EXTENSION_SLUG_TO_PACKAGE = {
acpx: "@openclaw/acpx",
"active-memory": "@openclaw/active-memory",
alibaba: "@openclaw/alibaba-provider",
"alibaba-provider": "@openclaw/alibaba-provider",
"amazon-bedrock": "@openclaw/amazon-bedrock-provider",
"amazon-bedrock-mantle": "@openclaw/amazon-bedrock-mantle-provider",
"amazon-bedrock-mantle-provider": "@openclaw/amazon-bedrock-mantle-provider",
"amazon-bedrock-provider": "@openclaw/amazon-bedrock-provider",
anthropic: "@openclaw/anthropic-provider",
"anthropic-provider": "@openclaw/anthropic-provider",
"anthropic-vertex": "@openclaw/anthropic-vertex-provider",
"anthropic-vertex-provider": "@openclaw/anthropic-vertex-provider",
arcee: "@openclaw/arcee-provider",
"arcee-provider": "@openclaw/arcee-provider",
"azure-speech": "@openclaw/azure-speech",
bluebubbles: "@openclaw/bluebubbles",
bonjour: "@openclaw/bonjour",
brave: "@openclaw/brave-plugin",
"brave-plugin": "@openclaw/brave-plugin",
browser: "@openclaw/browser-plugin",
"browser-plugin": "@openclaw/browser-plugin",
byteplus: "@openclaw/byteplus-provider",
"byteplus-provider": "@openclaw/byteplus-provider",
cerebras: "@openclaw/cerebras-provider",
"cerebras-provider": "@openclaw/cerebras-provider",
chutes: "@openclaw/chutes-provider",
"chutes-provider": "@openclaw/chutes-provider",
"cloudflare-ai-gateway": "@openclaw/cloudflare-ai-gateway-provider",
"cloudflare-ai-gateway-provider": "@openclaw/cloudflare-ai-gateway-provider",
codex: "@openclaw/codex",
comfy: "@openclaw/comfy-provider",
"comfy-provider": "@openclaw/comfy-provider",
"copilot-proxy": "@openclaw/copilot-proxy",
deepgram: "@openclaw/deepgram-provider",
"deepgram-provider": "@openclaw/deepgram-provider",
deepinfra: "@openclaw/deepinfra-provider",
"deepinfra-provider": "@openclaw/deepinfra-provider",
deepseek: "@openclaw/deepseek-provider",
"deepseek-provider": "@openclaw/deepseek-provider",
"device-pair": "@openclaw/device-pair",
"diagnostics-otel": "@openclaw/diagnostics-otel",
"diagnostics-prometheus": "@openclaw/diagnostics-prometheus",
diffs: "@openclaw/diffs",
"diffs-language-pack": "@openclaw/diffs-language-pack",
discord: "@openclaw/discord",
"document-extract": "@openclaw/document-extract-plugin",
"document-extract-plugin": "@openclaw/document-extract-plugin",
duckduckgo: "@openclaw/duckduckgo-plugin",
"duckduckgo-plugin": "@openclaw/duckduckgo-plugin",
elevenlabs: "@openclaw/elevenlabs-speech",
"elevenlabs-speech": "@openclaw/elevenlabs-speech",
exa: "@openclaw/exa-plugin",
"exa-plugin": "@openclaw/exa-plugin",
fal: "@openclaw/fal-provider",
"fal-provider": "@openclaw/fal-provider",
feishu: "@openclaw/feishu",
"file-transfer": "@openclaw/file-transfer",
firecrawl: "@openclaw/firecrawl-plugin",
"firecrawl-plugin": "@openclaw/firecrawl-plugin",
fireworks: "@openclaw/fireworks-provider",
"fireworks-provider": "@openclaw/fireworks-provider",
"github-copilot": "@openclaw/github-copilot-provider",
"github-copilot-provider": "@openclaw/github-copilot-provider",
google: "@openclaw/google-plugin",
"google-meet": "@openclaw/google-meet",
"google-plugin": "@openclaw/google-plugin",
googlechat: "@openclaw/googlechat",
gradium: "@openclaw/gradium-speech",
"gradium-speech": "@openclaw/gradium-speech",
groq: "@openclaw/groq-provider",
"groq-provider": "@openclaw/groq-provider",
huggingface: "@openclaw/huggingface-provider",
"huggingface-provider": "@openclaw/huggingface-provider",
imessage: "@openclaw/imessage",
inworld: "@openclaw/inworld-speech",
"inworld-speech": "@openclaw/inworld-speech",
irc: "@openclaw/irc",
kilocode: "@openclaw/kilocode-provider",
"kilocode-provider": "@openclaw/kilocode-provider",
kimi: "@openclaw/kimi-provider",
"kimi-coding": "@openclaw/kimi-provider",
"kimi-provider": "@openclaw/kimi-provider",
line: "@openclaw/line",
litellm: "@openclaw/litellm-provider",
"litellm-provider": "@openclaw/litellm-provider",
"llm-task": "@openclaw/llm-task",
lmstudio: "@openclaw/lmstudio-provider",
"lmstudio-provider": "@openclaw/lmstudio-provider",
lobster: "@openclaw/lobster",
matrix: "@openclaw/matrix",
mattermost: "@openclaw/mattermost",
"memory-core": "@openclaw/memory-core",
"memory-lancedb": "@openclaw/memory-lancedb",
"memory-wiki": "@openclaw/memory-wiki",
microsoft: "@openclaw/microsoft-speech",
"microsoft-foundry": "@openclaw/microsoft-foundry",
"microsoft-speech": "@openclaw/microsoft-speech",
"migrate-claude": "@openclaw/migrate-claude",
"migrate-hermes": "@openclaw/migrate-hermes",
minimax: "@openclaw/minimax-provider",
"minimax-provider": "@openclaw/minimax-provider",
mistral: "@openclaw/mistral-provider",
"mistral-provider": "@openclaw/mistral-provider",
moonshot: "@openclaw/moonshot-provider",
"moonshot-provider": "@openclaw/moonshot-provider",
msteams: "@openclaw/msteams",
"nextcloud-talk": "@openclaw/nextcloud-talk",
nostr: "@openclaw/nostr",
nvidia: "@openclaw/nvidia-provider",
"nvidia-provider": "@openclaw/nvidia-provider",
ollama: "@openclaw/ollama-provider",
"ollama-provider": "@openclaw/ollama-provider",
"open-prose": "@openclaw/open-prose",
openai: "@openclaw/openai-provider",
"openai-provider": "@openclaw/openai-provider",
opencode: "@openclaw/opencode-provider",
"opencode-go": "@openclaw/opencode-go-provider",
"opencode-go-provider": "@openclaw/opencode-go-provider",
"opencode-provider": "@openclaw/opencode-provider",
openrouter: "@openclaw/openrouter-provider",
"openrouter-provider": "@openclaw/openrouter-provider",
openshell: "@openclaw/openshell-sandbox",
"openshell-sandbox": "@openclaw/openshell-sandbox",
perplexity: "@openclaw/perplexity-plugin",
"perplexity-plugin": "@openclaw/perplexity-plugin",
"phone-control": "@openclaw/phone-control",
"qa-channel": "@openclaw/qa-channel",
"qa-lab": "@openclaw/qa-lab",
"qa-matrix": "@openclaw/qa-matrix",
qianfan: "@openclaw/qianfan-provider",
"qianfan-provider": "@openclaw/qianfan-provider",
qqbot: "@openclaw/qqbot",
qwen: "@openclaw/qwen-provider",
"qwen-provider": "@openclaw/qwen-provider",
runway: "@openclaw/runway-provider",
"runway-provider": "@openclaw/runway-provider",
searxng: "@openclaw/searxng-plugin",
"searxng-plugin": "@openclaw/searxng-plugin",
senseaudio: "@openclaw/senseaudio-provider",
"senseaudio-provider": "@openclaw/senseaudio-provider",
sglang: "@openclaw/sglang-provider",
"sglang-provider": "@openclaw/sglang-provider",
signal: "@openclaw/signal",
"skill-workshop": "@openclaw/skill-workshop",
slack: "@openclaw/slack",
stepfun: "@openclaw/stepfun-provider",
"stepfun-provider": "@openclaw/stepfun-provider",
"synology-chat": "@openclaw/synology-chat",
synthetic: "@openclaw/synthetic-provider",
"synthetic-provider": "@openclaw/synthetic-provider",
"talk-voice": "@openclaw/talk-voice",
tavily: "@openclaw/tavily-plugin",
"tavily-plugin": "@openclaw/tavily-plugin",
telegram: "@openclaw/telegram",
tencent: "@openclaw/tencent-provider",
"tencent-provider": "@openclaw/tencent-provider",
"thread-ownership": "@openclaw/thread-ownership",
tlon: "@openclaw/tlon",
together: "@openclaw/together-provider",
"together-provider": "@openclaw/together-provider",
tokenjuice: "@openclaw/tokenjuice",
"tts-local-cli": "@openclaw/tts-local-cli",
twitch: "@openclaw/twitch",
venice: "@openclaw/venice-provider",
"venice-provider": "@openclaw/venice-provider",
"vercel-ai-gateway": "@openclaw/vercel-ai-gateway-provider",
"vercel-ai-gateway-provider": "@openclaw/vercel-ai-gateway-provider",
vllm: "@openclaw/vllm-provider",
"vllm-provider": "@openclaw/vllm-provider",
"voice-call": "@openclaw/voice-call",
volcengine: "@openclaw/volcengine-provider",
"volcengine-provider": "@openclaw/volcengine-provider",
voyage: "@openclaw/voyage-provider",
"voyage-provider": "@openclaw/voyage-provider",
vydra: "@openclaw/vydra-provider",
"vydra-provider": "@openclaw/vydra-provider",
"web-readability": "@openclaw/web-readability-plugin",
"web-readability-plugin": "@openclaw/web-readability-plugin",
webhooks: "@openclaw/webhooks",
whatsapp: "@openclaw/whatsapp",
xai: "@openclaw/xai-plugin",
"xai-plugin": "@openclaw/xai-plugin",
xiaomi: "@openclaw/xiaomi-provider",
"xiaomi-provider": "@openclaw/xiaomi-provider",
zai: "@openclaw/zai-provider",
"zai-provider": "@openclaw/zai-provider",
zalo: "@openclaw/zalo",
zalouser: "@openclaw/zalouser",
};
export function getOpenClawExtensionPackageName(slug) {
const normalizedSlug = slug.trim().toLowerCase();
if (!normalizedSlug)
return null;
return (OPENCLAW_EXTENSION_SLUG_TO_PACKAGE[normalizedSlug] ?? null);
}
export function getOpenClawPackageCandidateNames(requestedName) {
if (requestedName.includes("/"))
return [requestedName];
const officialPackageName = getOpenClawExtensionPackageName(requestedName);
return Array.from(new Set([officialPackageName, `@openclaw/${requestedName}`, requestedName].filter((name) => typeof name === "string" && name.length > 0)));
}
//# sourceMappingURL=openClawExtensionSlugs.js.map
File diff suppressed because one or more lines are too long
+1
View File
@@ -5,6 +5,7 @@ export * from "./catalogMetadata.js";
export * from "./docsLinks.js";
export * from "./license.js";
export * from "./openclawContract.js";
export * from "./openClawExtensionSlugs.js";
export * from "./packages.js";
export * from "./pluginCategories.js";
export { ApiRoutes, LegacyApiRoutes } from "./routes.js";
@@ -0,0 +1,214 @@
export const OPENCLAW_EXTENSION_SLUG_TO_PACKAGE = {
acpx: "@openclaw/acpx",
"active-memory": "@openclaw/active-memory",
alibaba: "@openclaw/alibaba-provider",
"alibaba-provider": "@openclaw/alibaba-provider",
"amazon-bedrock": "@openclaw/amazon-bedrock-provider",
"amazon-bedrock-mantle": "@openclaw/amazon-bedrock-mantle-provider",
"amazon-bedrock-mantle-provider": "@openclaw/amazon-bedrock-mantle-provider",
"amazon-bedrock-provider": "@openclaw/amazon-bedrock-provider",
anthropic: "@openclaw/anthropic-provider",
"anthropic-provider": "@openclaw/anthropic-provider",
"anthropic-vertex": "@openclaw/anthropic-vertex-provider",
"anthropic-vertex-provider": "@openclaw/anthropic-vertex-provider",
arcee: "@openclaw/arcee-provider",
"arcee-provider": "@openclaw/arcee-provider",
"azure-speech": "@openclaw/azure-speech",
bluebubbles: "@openclaw/bluebubbles",
bonjour: "@openclaw/bonjour",
brave: "@openclaw/brave-plugin",
"brave-plugin": "@openclaw/brave-plugin",
browser: "@openclaw/browser-plugin",
"browser-plugin": "@openclaw/browser-plugin",
byteplus: "@openclaw/byteplus-provider",
"byteplus-provider": "@openclaw/byteplus-provider",
cerebras: "@openclaw/cerebras-provider",
"cerebras-provider": "@openclaw/cerebras-provider",
chutes: "@openclaw/chutes-provider",
"chutes-provider": "@openclaw/chutes-provider",
"cloudflare-ai-gateway": "@openclaw/cloudflare-ai-gateway-provider",
"cloudflare-ai-gateway-provider": "@openclaw/cloudflare-ai-gateway-provider",
codex: "@openclaw/codex",
comfy: "@openclaw/comfy-provider",
"comfy-provider": "@openclaw/comfy-provider",
"copilot-proxy": "@openclaw/copilot-proxy",
deepgram: "@openclaw/deepgram-provider",
"deepgram-provider": "@openclaw/deepgram-provider",
deepinfra: "@openclaw/deepinfra-provider",
"deepinfra-provider": "@openclaw/deepinfra-provider",
deepseek: "@openclaw/deepseek-provider",
"deepseek-provider": "@openclaw/deepseek-provider",
"device-pair": "@openclaw/device-pair",
"diagnostics-otel": "@openclaw/diagnostics-otel",
"diagnostics-prometheus": "@openclaw/diagnostics-prometheus",
diffs: "@openclaw/diffs",
"diffs-language-pack": "@openclaw/diffs-language-pack",
discord: "@openclaw/discord",
"document-extract": "@openclaw/document-extract-plugin",
"document-extract-plugin": "@openclaw/document-extract-plugin",
duckduckgo: "@openclaw/duckduckgo-plugin",
"duckduckgo-plugin": "@openclaw/duckduckgo-plugin",
elevenlabs: "@openclaw/elevenlabs-speech",
"elevenlabs-speech": "@openclaw/elevenlabs-speech",
exa: "@openclaw/exa-plugin",
"exa-plugin": "@openclaw/exa-plugin",
fal: "@openclaw/fal-provider",
"fal-provider": "@openclaw/fal-provider",
feishu: "@openclaw/feishu",
"file-transfer": "@openclaw/file-transfer",
firecrawl: "@openclaw/firecrawl-plugin",
"firecrawl-plugin": "@openclaw/firecrawl-plugin",
fireworks: "@openclaw/fireworks-provider",
"fireworks-provider": "@openclaw/fireworks-provider",
"github-copilot": "@openclaw/github-copilot-provider",
"github-copilot-provider": "@openclaw/github-copilot-provider",
google: "@openclaw/google-plugin",
"google-meet": "@openclaw/google-meet",
"google-plugin": "@openclaw/google-plugin",
googlechat: "@openclaw/googlechat",
gradium: "@openclaw/gradium-speech",
"gradium-speech": "@openclaw/gradium-speech",
groq: "@openclaw/groq-provider",
"groq-provider": "@openclaw/groq-provider",
huggingface: "@openclaw/huggingface-provider",
"huggingface-provider": "@openclaw/huggingface-provider",
imessage: "@openclaw/imessage",
inworld: "@openclaw/inworld-speech",
"inworld-speech": "@openclaw/inworld-speech",
irc: "@openclaw/irc",
kilocode: "@openclaw/kilocode-provider",
"kilocode-provider": "@openclaw/kilocode-provider",
kimi: "@openclaw/kimi-provider",
"kimi-coding": "@openclaw/kimi-provider",
"kimi-provider": "@openclaw/kimi-provider",
line: "@openclaw/line",
litellm: "@openclaw/litellm-provider",
"litellm-provider": "@openclaw/litellm-provider",
"llm-task": "@openclaw/llm-task",
lmstudio: "@openclaw/lmstudio-provider",
"lmstudio-provider": "@openclaw/lmstudio-provider",
lobster: "@openclaw/lobster",
matrix: "@openclaw/matrix",
mattermost: "@openclaw/mattermost",
"memory-core": "@openclaw/memory-core",
"memory-lancedb": "@openclaw/memory-lancedb",
"memory-wiki": "@openclaw/memory-wiki",
microsoft: "@openclaw/microsoft-speech",
"microsoft-foundry": "@openclaw/microsoft-foundry",
"microsoft-speech": "@openclaw/microsoft-speech",
"migrate-claude": "@openclaw/migrate-claude",
"migrate-hermes": "@openclaw/migrate-hermes",
minimax: "@openclaw/minimax-provider",
"minimax-provider": "@openclaw/minimax-provider",
mistral: "@openclaw/mistral-provider",
"mistral-provider": "@openclaw/mistral-provider",
moonshot: "@openclaw/moonshot-provider",
"moonshot-provider": "@openclaw/moonshot-provider",
msteams: "@openclaw/msteams",
"nextcloud-talk": "@openclaw/nextcloud-talk",
nostr: "@openclaw/nostr",
nvidia: "@openclaw/nvidia-provider",
"nvidia-provider": "@openclaw/nvidia-provider",
ollama: "@openclaw/ollama-provider",
"ollama-provider": "@openclaw/ollama-provider",
"open-prose": "@openclaw/open-prose",
openai: "@openclaw/openai-provider",
"openai-provider": "@openclaw/openai-provider",
opencode: "@openclaw/opencode-provider",
"opencode-go": "@openclaw/opencode-go-provider",
"opencode-go-provider": "@openclaw/opencode-go-provider",
"opencode-provider": "@openclaw/opencode-provider",
openrouter: "@openclaw/openrouter-provider",
"openrouter-provider": "@openclaw/openrouter-provider",
openshell: "@openclaw/openshell-sandbox",
"openshell-sandbox": "@openclaw/openshell-sandbox",
perplexity: "@openclaw/perplexity-plugin",
"perplexity-plugin": "@openclaw/perplexity-plugin",
"phone-control": "@openclaw/phone-control",
"qa-channel": "@openclaw/qa-channel",
"qa-lab": "@openclaw/qa-lab",
"qa-matrix": "@openclaw/qa-matrix",
qianfan: "@openclaw/qianfan-provider",
"qianfan-provider": "@openclaw/qianfan-provider",
qqbot: "@openclaw/qqbot",
qwen: "@openclaw/qwen-provider",
"qwen-provider": "@openclaw/qwen-provider",
runway: "@openclaw/runway-provider",
"runway-provider": "@openclaw/runway-provider",
searxng: "@openclaw/searxng-plugin",
"searxng-plugin": "@openclaw/searxng-plugin",
senseaudio: "@openclaw/senseaudio-provider",
"senseaudio-provider": "@openclaw/senseaudio-provider",
sglang: "@openclaw/sglang-provider",
"sglang-provider": "@openclaw/sglang-provider",
signal: "@openclaw/signal",
"skill-workshop": "@openclaw/skill-workshop",
slack: "@openclaw/slack",
stepfun: "@openclaw/stepfun-provider",
"stepfun-provider": "@openclaw/stepfun-provider",
"synology-chat": "@openclaw/synology-chat",
synthetic: "@openclaw/synthetic-provider",
"synthetic-provider": "@openclaw/synthetic-provider",
"talk-voice": "@openclaw/talk-voice",
tavily: "@openclaw/tavily-plugin",
"tavily-plugin": "@openclaw/tavily-plugin",
telegram: "@openclaw/telegram",
tencent: "@openclaw/tencent-provider",
"tencent-provider": "@openclaw/tencent-provider",
"thread-ownership": "@openclaw/thread-ownership",
tlon: "@openclaw/tlon",
together: "@openclaw/together-provider",
"together-provider": "@openclaw/together-provider",
tokenjuice: "@openclaw/tokenjuice",
"tts-local-cli": "@openclaw/tts-local-cli",
twitch: "@openclaw/twitch",
venice: "@openclaw/venice-provider",
"venice-provider": "@openclaw/venice-provider",
"vercel-ai-gateway": "@openclaw/vercel-ai-gateway-provider",
"vercel-ai-gateway-provider": "@openclaw/vercel-ai-gateway-provider",
vllm: "@openclaw/vllm-provider",
"vllm-provider": "@openclaw/vllm-provider",
"voice-call": "@openclaw/voice-call",
volcengine: "@openclaw/volcengine-provider",
"volcengine-provider": "@openclaw/volcengine-provider",
voyage: "@openclaw/voyage-provider",
"voyage-provider": "@openclaw/voyage-provider",
vydra: "@openclaw/vydra-provider",
"vydra-provider": "@openclaw/vydra-provider",
"web-readability": "@openclaw/web-readability-plugin",
"web-readability-plugin": "@openclaw/web-readability-plugin",
webhooks: "@openclaw/webhooks",
whatsapp: "@openclaw/whatsapp",
xai: "@openclaw/xai-plugin",
"xai-plugin": "@openclaw/xai-plugin",
xiaomi: "@openclaw/xiaomi-provider",
"xiaomi-provider": "@openclaw/xiaomi-provider",
zai: "@openclaw/zai-provider",
"zai-provider": "@openclaw/zai-provider",
zalo: "@openclaw/zalo",
zalouser: "@openclaw/zalouser",
} as const;
export function getOpenClawExtensionPackageName(slug: string): string | null {
const normalizedSlug = slug.trim().toLowerCase();
if (!normalizedSlug) return null;
return (
OPENCLAW_EXTENSION_SLUG_TO_PACKAGE[
normalizedSlug as keyof typeof OPENCLAW_EXTENSION_SLUG_TO_PACKAGE
] ?? null
);
}
export function getOpenClawPackageCandidateNames(requestedName: string): string[] {
if (requestedName.includes("/")) return [requestedName];
const officialPackageName = getOpenClawExtensionPackageName(requestedName);
return Array.from(
new Set(
[officialPackageName, `@openclaw/${requestedName}`, requestedName].filter(
(name): name is string => typeof name === "string" && name.length > 0,
),
),
);
}
+6
View File
@@ -25,6 +25,9 @@ Publisher profiles:
- Canonical page: `/<handle>`
- Legacy compatibility pages: `/user/<handle>`, `/p/<handle>`, `/u/<handle>`,
and `/orgs/<handle>` redirect to `/<handle>`
- Existing publisher handles that predate an official OpenClaw alias remain
reachable at `/user/<handle>` so the alias can keep its canonical top-level
shortcut.
Skills:
@@ -111,6 +114,9 @@ This means official OpenClaw aliases are reserved before skills at the root.
That is intentional: `https://clawhub.ai/codex` must show the official OpenClaw
Codex plugin even if a skill named `codex` exists.
Publisher handle creation and rename paths reserve the full official alias set.
This prevents new profiles from taking over those top-level plugin shortcuts.
## Collision policy
Do not make every `/:owner/:slug` path a universal package route. Owners can
@@ -62,6 +62,23 @@ describe("users route redirect", () => {
});
});
it("keeps existing official-alias publishers reachable from the legacy profile route", async () => {
const route = (await import("../routes/user/$handle")).Route as unknown as {
__config: {
beforeLoad: (args: {
params: { handle: string };
search: Record<string, unknown>;
}) => unknown;
};
};
redirectMock.mockClear();
expect(() =>
route.__config.beforeLoad({ params: { handle: "tencent" }, search: {} }),
).not.toThrow();
expect(redirectMock).not.toHaveBeenCalled();
});
it("redirects legacy org profile routes to publisher profiles", async () => {
const route = (await import("../routes/orgs/$handle")).Route as unknown as {
__config: {
+2 -2
View File
@@ -1,6 +1,7 @@
import { Link } from "@tanstack/react-router";
import { Download } from "lucide-react";
import { formatCompactStat } from "../lib/numberFormat";
import { buildPublisherProfileHref } from "../lib/ownerRoute";
import {
type PublicPublisherListItem,
type PublicPublisherPublishedItem,
@@ -41,8 +42,7 @@ export function PublisherListItem({ publisher, variant = "list" }: PublisherList
return (
<Link
to="/$slug"
params={{ slug: handle }}
to={buildPublisherProfileHref(handle)}
className={`publisher-card publisher-card-${variant}`}
aria-label={`Publisher: ${publisher.displayName}`}
>
+5 -214
View File
@@ -1,214 +1,5 @@
const OPENCLAW_EXTENSION_SLUG_TO_PACKAGE = {
acpx: "@openclaw/acpx",
"active-memory": "@openclaw/active-memory",
alibaba: "@openclaw/alibaba-provider",
"alibaba-provider": "@openclaw/alibaba-provider",
"amazon-bedrock": "@openclaw/amazon-bedrock-provider",
"amazon-bedrock-mantle": "@openclaw/amazon-bedrock-mantle-provider",
"amazon-bedrock-mantle-provider": "@openclaw/amazon-bedrock-mantle-provider",
"amazon-bedrock-provider": "@openclaw/amazon-bedrock-provider",
anthropic: "@openclaw/anthropic-provider",
"anthropic-provider": "@openclaw/anthropic-provider",
"anthropic-vertex": "@openclaw/anthropic-vertex-provider",
"anthropic-vertex-provider": "@openclaw/anthropic-vertex-provider",
arcee: "@openclaw/arcee-provider",
"arcee-provider": "@openclaw/arcee-provider",
"azure-speech": "@openclaw/azure-speech",
bluebubbles: "@openclaw/bluebubbles",
bonjour: "@openclaw/bonjour",
brave: "@openclaw/brave-plugin",
"brave-plugin": "@openclaw/brave-plugin",
browser: "@openclaw/browser-plugin",
"browser-plugin": "@openclaw/browser-plugin",
byteplus: "@openclaw/byteplus-provider",
"byteplus-provider": "@openclaw/byteplus-provider",
cerebras: "@openclaw/cerebras-provider",
"cerebras-provider": "@openclaw/cerebras-provider",
chutes: "@openclaw/chutes-provider",
"chutes-provider": "@openclaw/chutes-provider",
"cloudflare-ai-gateway": "@openclaw/cloudflare-ai-gateway-provider",
"cloudflare-ai-gateway-provider": "@openclaw/cloudflare-ai-gateway-provider",
codex: "@openclaw/codex",
comfy: "@openclaw/comfy-provider",
"comfy-provider": "@openclaw/comfy-provider",
"copilot-proxy": "@openclaw/copilot-proxy",
deepgram: "@openclaw/deepgram-provider",
"deepgram-provider": "@openclaw/deepgram-provider",
deepinfra: "@openclaw/deepinfra-provider",
"deepinfra-provider": "@openclaw/deepinfra-provider",
deepseek: "@openclaw/deepseek-provider",
"deepseek-provider": "@openclaw/deepseek-provider",
"device-pair": "@openclaw/device-pair",
"diagnostics-otel": "@openclaw/diagnostics-otel",
"diagnostics-prometheus": "@openclaw/diagnostics-prometheus",
diffs: "@openclaw/diffs",
"diffs-language-pack": "@openclaw/diffs-language-pack",
discord: "@openclaw/discord",
"document-extract": "@openclaw/document-extract-plugin",
"document-extract-plugin": "@openclaw/document-extract-plugin",
duckduckgo: "@openclaw/duckduckgo-plugin",
"duckduckgo-plugin": "@openclaw/duckduckgo-plugin",
elevenlabs: "@openclaw/elevenlabs-speech",
"elevenlabs-speech": "@openclaw/elevenlabs-speech",
exa: "@openclaw/exa-plugin",
"exa-plugin": "@openclaw/exa-plugin",
fal: "@openclaw/fal-provider",
"fal-provider": "@openclaw/fal-provider",
feishu: "@openclaw/feishu",
"file-transfer": "@openclaw/file-transfer",
firecrawl: "@openclaw/firecrawl-plugin",
"firecrawl-plugin": "@openclaw/firecrawl-plugin",
fireworks: "@openclaw/fireworks-provider",
"fireworks-provider": "@openclaw/fireworks-provider",
"github-copilot": "@openclaw/github-copilot-provider",
"github-copilot-provider": "@openclaw/github-copilot-provider",
google: "@openclaw/google-plugin",
"google-meet": "@openclaw/google-meet",
"google-plugin": "@openclaw/google-plugin",
googlechat: "@openclaw/googlechat",
gradium: "@openclaw/gradium-speech",
"gradium-speech": "@openclaw/gradium-speech",
groq: "@openclaw/groq-provider",
"groq-provider": "@openclaw/groq-provider",
huggingface: "@openclaw/huggingface-provider",
"huggingface-provider": "@openclaw/huggingface-provider",
imessage: "@openclaw/imessage",
inworld: "@openclaw/inworld-speech",
"inworld-speech": "@openclaw/inworld-speech",
irc: "@openclaw/irc",
kilocode: "@openclaw/kilocode-provider",
"kilocode-provider": "@openclaw/kilocode-provider",
kimi: "@openclaw/kimi-provider",
"kimi-coding": "@openclaw/kimi-provider",
"kimi-provider": "@openclaw/kimi-provider",
line: "@openclaw/line",
litellm: "@openclaw/litellm-provider",
"litellm-provider": "@openclaw/litellm-provider",
"llm-task": "@openclaw/llm-task",
lmstudio: "@openclaw/lmstudio-provider",
"lmstudio-provider": "@openclaw/lmstudio-provider",
lobster: "@openclaw/lobster",
matrix: "@openclaw/matrix",
mattermost: "@openclaw/mattermost",
"memory-core": "@openclaw/memory-core",
"memory-lancedb": "@openclaw/memory-lancedb",
"memory-wiki": "@openclaw/memory-wiki",
microsoft: "@openclaw/microsoft-speech",
"microsoft-foundry": "@openclaw/microsoft-foundry",
"microsoft-speech": "@openclaw/microsoft-speech",
"migrate-claude": "@openclaw/migrate-claude",
"migrate-hermes": "@openclaw/migrate-hermes",
minimax: "@openclaw/minimax-provider",
"minimax-provider": "@openclaw/minimax-provider",
mistral: "@openclaw/mistral-provider",
"mistral-provider": "@openclaw/mistral-provider",
moonshot: "@openclaw/moonshot-provider",
"moonshot-provider": "@openclaw/moonshot-provider",
msteams: "@openclaw/msteams",
"nextcloud-talk": "@openclaw/nextcloud-talk",
nostr: "@openclaw/nostr",
nvidia: "@openclaw/nvidia-provider",
"nvidia-provider": "@openclaw/nvidia-provider",
ollama: "@openclaw/ollama-provider",
"ollama-provider": "@openclaw/ollama-provider",
"open-prose": "@openclaw/open-prose",
openai: "@openclaw/openai-provider",
"openai-provider": "@openclaw/openai-provider",
opencode: "@openclaw/opencode-provider",
"opencode-go": "@openclaw/opencode-go-provider",
"opencode-go-provider": "@openclaw/opencode-go-provider",
"opencode-provider": "@openclaw/opencode-provider",
openrouter: "@openclaw/openrouter-provider",
"openrouter-provider": "@openclaw/openrouter-provider",
openshell: "@openclaw/openshell-sandbox",
"openshell-sandbox": "@openclaw/openshell-sandbox",
perplexity: "@openclaw/perplexity-plugin",
"perplexity-plugin": "@openclaw/perplexity-plugin",
"phone-control": "@openclaw/phone-control",
"qa-channel": "@openclaw/qa-channel",
"qa-lab": "@openclaw/qa-lab",
"qa-matrix": "@openclaw/qa-matrix",
qianfan: "@openclaw/qianfan-provider",
"qianfan-provider": "@openclaw/qianfan-provider",
qqbot: "@openclaw/qqbot",
qwen: "@openclaw/qwen-provider",
"qwen-provider": "@openclaw/qwen-provider",
runway: "@openclaw/runway-provider",
"runway-provider": "@openclaw/runway-provider",
searxng: "@openclaw/searxng-plugin",
"searxng-plugin": "@openclaw/searxng-plugin",
senseaudio: "@openclaw/senseaudio-provider",
"senseaudio-provider": "@openclaw/senseaudio-provider",
sglang: "@openclaw/sglang-provider",
"sglang-provider": "@openclaw/sglang-provider",
signal: "@openclaw/signal",
"skill-workshop": "@openclaw/skill-workshop",
slack: "@openclaw/slack",
stepfun: "@openclaw/stepfun-provider",
"stepfun-provider": "@openclaw/stepfun-provider",
"synology-chat": "@openclaw/synology-chat",
synthetic: "@openclaw/synthetic-provider",
"synthetic-provider": "@openclaw/synthetic-provider",
"talk-voice": "@openclaw/talk-voice",
tavily: "@openclaw/tavily-plugin",
"tavily-plugin": "@openclaw/tavily-plugin",
telegram: "@openclaw/telegram",
tencent: "@openclaw/tencent-provider",
"tencent-provider": "@openclaw/tencent-provider",
"thread-ownership": "@openclaw/thread-ownership",
tlon: "@openclaw/tlon",
together: "@openclaw/together-provider",
"together-provider": "@openclaw/together-provider",
tokenjuice: "@openclaw/tokenjuice",
"tts-local-cli": "@openclaw/tts-local-cli",
twitch: "@openclaw/twitch",
venice: "@openclaw/venice-provider",
"venice-provider": "@openclaw/venice-provider",
"vercel-ai-gateway": "@openclaw/vercel-ai-gateway-provider",
"vercel-ai-gateway-provider": "@openclaw/vercel-ai-gateway-provider",
vllm: "@openclaw/vllm-provider",
"vllm-provider": "@openclaw/vllm-provider",
"voice-call": "@openclaw/voice-call",
volcengine: "@openclaw/volcengine-provider",
"volcengine-provider": "@openclaw/volcengine-provider",
voyage: "@openclaw/voyage-provider",
"voyage-provider": "@openclaw/voyage-provider",
vydra: "@openclaw/vydra-provider",
"vydra-provider": "@openclaw/vydra-provider",
"web-readability": "@openclaw/web-readability-plugin",
"web-readability-plugin": "@openclaw/web-readability-plugin",
webhooks: "@openclaw/webhooks",
whatsapp: "@openclaw/whatsapp",
xai: "@openclaw/xai-plugin",
"xai-plugin": "@openclaw/xai-plugin",
xiaomi: "@openclaw/xiaomi-provider",
"xiaomi-provider": "@openclaw/xiaomi-provider",
zai: "@openclaw/zai-provider",
"zai-provider": "@openclaw/zai-provider",
zalo: "@openclaw/zalo",
zalouser: "@openclaw/zalouser",
} as const;
export function getOpenClawExtensionPackageName(slug: string): string | null {
const normalizedSlug = slug.trim().toLowerCase();
if (!normalizedSlug) return null;
return (
OPENCLAW_EXTENSION_SLUG_TO_PACKAGE[
normalizedSlug as keyof typeof OPENCLAW_EXTENSION_SLUG_TO_PACKAGE
] ?? null
);
}
export function getOpenClawPackageCandidateNames(requestedName: string): string[] {
if (requestedName.includes("/")) return [requestedName];
const officialPackageName = getOpenClawExtensionPackageName(requestedName);
return Array.from(
new Set(
[officialPackageName, `@openclaw/${requestedName}`, requestedName].filter(
(name): name is string => typeof name === "string" && name.length > 0,
),
),
);
}
export {
getOpenClawExtensionPackageName,
getOpenClawPackageCandidateNames,
OPENCLAW_EXTENSION_SLUG_TO_PACKAGE,
} from "clawhub-schema";
+6
View File
@@ -1,11 +1,17 @@
import { describe, expect, it } from "vitest";
import {
buildPublisherProfileHref,
isOwnerRouteHandleOrIdSegment,
isOwnerRouteHandleSegment,
isOwnerRouteScopeSegment,
} from "./ownerRoute";
describe("owner route segments", () => {
it("uses the legacy profile route for official alias collisions", () => {
expect(buildPublisherProfileHref("tencent")).toBe("/user/tencent");
expect(buildPublisherProfileHref("steipete")).toBe("/steipete");
});
it("accepts npm-compatible publisher handle characters", () => {
expect(isOwnerRouteHandleSegment("example.tools")).toBe(true);
expect(isOwnerRouteHandleSegment("lab_1")).toBe(true);
+9 -1
View File
@@ -1,3 +1,5 @@
import { getOpenClawExtensionPackageName } from "./openClawExtensionSlugs";
const OWNER_ROUTE_HANDLE_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,38}[a-zA-Z0-9])?$/;
const OWNER_ROUTE_SCOPE_PATTERN = /^@[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,38}[a-zA-Z0-9])?$/;
@@ -22,7 +24,13 @@ function routeSegment(value: string) {
}
export function buildPublisherProfileHref(handle: string) {
return `/${routeSegment(handle)}`;
return isLegacyPublisherProfileHandle(handle)
? `/user/${routeSegment(handle)}`
: `/${routeSegment(handle)}`;
}
export function isLegacyPublisherProfileHandle(handle: string) {
return Boolean(getOpenClawExtensionPackageName(handle));
}
export function buildSkillDetailHref(owner: string, slug: string) {
+11
View File
@@ -92,4 +92,15 @@ describe("slug route resolution", () => {
});
expect(fetchSkillPageDataMock).not.toHaveBeenCalled();
});
it("keeps official OpenClaw aliases ahead of colliding publisher handles", async () => {
queryMock.mockResolvedValue({ _id: "publishers:tencent", handle: "tencent" });
await expect(resolveTopLevelSlugRoute("tencent")).resolves.toEqual({
kind: "plugin",
name: "@openclaw/tencent-provider",
href: "/openclaw/plugins/tencent-provider",
});
expect(fetchSkillPageDataMock).not.toHaveBeenCalled();
});
});
+3 -1
View File
@@ -14,7 +14,7 @@ import { Card, CardContent } from "../../components/ui/card";
import { Skeleton } from "../../components/ui/skeleton";
import { formatCompactStat } from "../../lib/numberFormat";
import { buildPublisherMeta } from "../../lib/og";
import { buildPublisherProfileHref } from "../../lib/ownerRoute";
import { buildPublisherProfileHref, isLegacyPublisherProfileHandle } from "../../lib/ownerRoute";
import type {
PublicPublisher,
PublicPublisherCatalogDisplay,
@@ -25,6 +25,8 @@ import { readPublicDownloadCount } from "../../lib/publicUser";
export const Route = createFileRoute("/user/$handle")({
beforeLoad: ({ params }) => {
if (isLegacyPublisherProfileHandle(params.handle)) return;
throw redirect({
href: buildPublisherProfileHref(params.handle),
replace: true,