mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
Tests: fast-path gateway auth bypass discovery
This commit is contained in:
1
extensions/mattermost/gateway-auth-api.ts
Normal file
1
extensions/mattermost/gateway-auth-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { resolveMattermostGatewayAuthBypassPaths as resolveGatewayAuthBypassPaths } from "./src/gateway-auth-bypass.js";
|
||||
@@ -5,14 +5,16 @@ import {
|
||||
createScopedChannelConfigAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
collectMattermostSlashCallbackPaths,
|
||||
resolveMattermostGatewayAuthBypassPaths,
|
||||
} from "./gateway-auth-bypass.js";
|
||||
import {
|
||||
listMattermostAccountIds,
|
||||
resolveDefaultMattermostAccountId,
|
||||
resolveMattermostAccount,
|
||||
type ResolvedMattermostAccount,
|
||||
} from "./mattermost/accounts.js";
|
||||
import type { MattermostSlashCommandConfig } from "./mattermost/slash-commands.js";
|
||||
import type { MattermostConfig } from "./types.js";
|
||||
|
||||
export const mattermostMeta = {
|
||||
id: "mattermost",
|
||||
@@ -27,8 +29,6 @@ export const mattermostMeta = {
|
||||
quickstartAllowFrom: true,
|
||||
} as const;
|
||||
|
||||
const DEFAULT_SLASH_CALLBACK_PATH = "/api/channels/mattermost/command";
|
||||
|
||||
export function normalizeMattermostAllowEntry(entry: string): string {
|
||||
return normalizeLowercaseStringOrEmpty(
|
||||
entry
|
||||
@@ -50,62 +50,7 @@ export function formatMattermostAllowEntry(entry: string): string {
|
||||
return normalizeLowercaseStringOrEmpty(trimmed.replace(/^(mattermost|user):/i, ""));
|
||||
}
|
||||
|
||||
export function collectMattermostSlashCallbackPaths(
|
||||
raw?: Partial<MattermostSlashCommandConfig>,
|
||||
): string[] {
|
||||
const callbackPath = (() => {
|
||||
const trimmed = raw?.callbackPath?.trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_SLASH_CALLBACK_PATH;
|
||||
}
|
||||
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
||||
})();
|
||||
const callbackUrl = raw?.callbackUrl?.trim();
|
||||
const paths = new Set<string>([callbackPath]);
|
||||
if (callbackUrl) {
|
||||
try {
|
||||
const pathname = new URL(callbackUrl).pathname;
|
||||
if (pathname) {
|
||||
paths.add(pathname);
|
||||
}
|
||||
} catch {
|
||||
// Keep the normalized callback path when the configured URL is invalid.
|
||||
}
|
||||
}
|
||||
return [...paths];
|
||||
}
|
||||
|
||||
export function resolveMattermostGatewayAuthBypassPaths(cfg: {
|
||||
channels?: Record<string, unknown>;
|
||||
}): string[] {
|
||||
const base = cfg.channels?.mattermost as MattermostConfig | undefined;
|
||||
const callbackPaths = new Set(
|
||||
collectMattermostSlashCallbackPaths(
|
||||
base?.commands as Partial<MattermostSlashCommandConfig> | undefined,
|
||||
).filter(
|
||||
(path) =>
|
||||
path === "/api/channels/mattermost/command" || path.startsWith("/api/channels/mattermost/"),
|
||||
),
|
||||
);
|
||||
const accounts = base?.accounts ?? {};
|
||||
for (const account of Object.values(accounts)) {
|
||||
const accountConfig =
|
||||
account && typeof account === "object" && !Array.isArray(account)
|
||||
? (account as {
|
||||
commands?: Parameters<typeof collectMattermostSlashCallbackPaths>[0];
|
||||
})
|
||||
: undefined;
|
||||
for (const path of collectMattermostSlashCallbackPaths(accountConfig?.commands)) {
|
||||
if (
|
||||
path === "/api/channels/mattermost/command" ||
|
||||
path.startsWith("/api/channels/mattermost/")
|
||||
) {
|
||||
callbackPaths.add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...callbackPaths];
|
||||
}
|
||||
export { collectMattermostSlashCallbackPaths, resolveMattermostGatewayAuthBypassPaths };
|
||||
|
||||
export const mattermostConfigAdapter = createScopedChannelConfigAdapter<ResolvedMattermostAccount>({
|
||||
sectionKey: "mattermost",
|
||||
|
||||
38
extensions/mattermost/src/gateway-auth-bypass.test.ts
Normal file
38
extensions/mattermost/src/gateway-auth-bypass.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectMattermostSlashCallbackPaths,
|
||||
resolveMattermostGatewayAuthBypassPaths,
|
||||
} from "./gateway-auth-bypass.js";
|
||||
|
||||
describe("Mattermost gateway auth bypass paths", () => {
|
||||
it("normalizes slash callback paths and callback URL paths", () => {
|
||||
expect(
|
||||
collectMattermostSlashCallbackPaths({
|
||||
callbackPath: "api/channels/mattermost/command",
|
||||
callbackUrl: "https://gateway.example.com/api/channels/mattermost/custom",
|
||||
}),
|
||||
).toEqual(["/api/channels/mattermost/command", "/api/channels/mattermost/custom"]);
|
||||
});
|
||||
|
||||
it("keeps only Mattermost channel callback paths", () => {
|
||||
expect(
|
||||
resolveMattermostGatewayAuthBypassPaths({
|
||||
channels: {
|
||||
mattermost: {
|
||||
commands: {
|
||||
callbackPath: "/api/channels/mattermost/command",
|
||||
callbackUrl: "https://gateway.example.com/api/channels/nostr/default/profile",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
commands: {
|
||||
callbackPath: "/api/channels/mattermost/work",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual(["/api/channels/mattermost/command", "/api/channels/mattermost/work"]);
|
||||
});
|
||||
});
|
||||
83
extensions/mattermost/src/gateway-auth-bypass.ts
Normal file
83
extensions/mattermost/src/gateway-auth-bypass.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
const DEFAULT_SLASH_CALLBACK_PATH = "/api/channels/mattermost/command";
|
||||
|
||||
type MattermostSlashCommandConfigInput = {
|
||||
callbackPath?: unknown;
|
||||
callbackUrl?: unknown;
|
||||
};
|
||||
|
||||
type MattermostAccountConfigInput = {
|
||||
commands?: MattermostSlashCommandConfigInput;
|
||||
};
|
||||
|
||||
type MattermostConfigInput = MattermostAccountConfigInput & {
|
||||
accounts?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function readTrimmedString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function normalizeCallbackPath(value: unknown): string {
|
||||
const trimmed = readTrimmedString(value);
|
||||
if (!trimmed) {
|
||||
return DEFAULT_SLASH_CALLBACK_PATH;
|
||||
}
|
||||
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
||||
}
|
||||
|
||||
function readMattermostCommands(value: unknown): MattermostSlashCommandConfigInput | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as MattermostSlashCommandConfigInput)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function isMattermostBypassPath(path: string): boolean {
|
||||
return path === DEFAULT_SLASH_CALLBACK_PATH || path.startsWith("/api/channels/mattermost/");
|
||||
}
|
||||
|
||||
export function collectMattermostSlashCallbackPaths(
|
||||
raw?: MattermostSlashCommandConfigInput,
|
||||
): string[] {
|
||||
const paths = new Set<string>([normalizeCallbackPath(raw?.callbackPath)]);
|
||||
const callbackUrl = readTrimmedString(raw?.callbackUrl);
|
||||
if (callbackUrl) {
|
||||
try {
|
||||
const pathname = new URL(callbackUrl).pathname;
|
||||
if (pathname) {
|
||||
paths.add(pathname);
|
||||
}
|
||||
} catch {
|
||||
// Keep the normalized callback path when the configured URL is invalid.
|
||||
}
|
||||
}
|
||||
return [...paths];
|
||||
}
|
||||
|
||||
export function resolveMattermostGatewayAuthBypassPaths(cfg: {
|
||||
channels?: Record<string, unknown>;
|
||||
}): string[] {
|
||||
const base =
|
||||
cfg.channels?.mattermost && typeof cfg.channels.mattermost === "object"
|
||||
? (cfg.channels.mattermost as MattermostConfigInput)
|
||||
: undefined;
|
||||
const callbackPaths = new Set(
|
||||
collectMattermostSlashCallbackPaths(readMattermostCommands(base?.commands)).filter(
|
||||
isMattermostBypassPath,
|
||||
),
|
||||
);
|
||||
const accounts = base?.accounts ?? {};
|
||||
for (const account of Object.values(accounts)) {
|
||||
const accountConfig =
|
||||
account && typeof account === "object" && !Array.isArray(account)
|
||||
? (account as MattermostAccountConfigInput)
|
||||
: undefined;
|
||||
for (const path of collectMattermostSlashCallbackPaths(
|
||||
readMattermostCommands(accountConfig?.commands),
|
||||
)) {
|
||||
if (isMattermostBypassPath(path)) {
|
||||
callbackPaths.add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...callbackPaths];
|
||||
}
|
||||
63
src/channels/plugins/gateway-auth-bypass.test.ts
Normal file
63
src/channels/plugins/gateway-auth-bypass.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { loadBundledPluginPublicArtifactModuleSyncMock } = vi.hoisted(() => ({
|
||||
loadBundledPluginPublicArtifactModuleSyncMock: vi.fn(
|
||||
({ artifactBasename, dirName }: { artifactBasename: string; dirName: string }) => {
|
||||
if (dirName === "mattermost" && artifactBasename === "gateway-auth-api.js") {
|
||||
return {
|
||||
resolveGatewayAuthBypassPaths: () => [
|
||||
" /api/channels/mattermost/command ",
|
||||
"",
|
||||
null,
|
||||
"/api/channels/mattermost/work",
|
||||
],
|
||||
};
|
||||
}
|
||||
if (dirName === "broken" && artifactBasename === "gateway-auth-api.js") {
|
||||
throw new Error("broken gateway auth artifact");
|
||||
}
|
||||
throw new Error(
|
||||
`Unable to resolve bundled plugin public surface ${dirName}/${artifactBasename}`,
|
||||
);
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/public-surface-loader.js", () => ({
|
||||
loadBundledPluginPublicArtifactModuleSync: loadBundledPluginPublicArtifactModuleSyncMock,
|
||||
}));
|
||||
|
||||
import { resolveBundledChannelGatewayAuthBypassPaths } from "./gateway-auth-bypass.js";
|
||||
|
||||
describe("bundled channel gateway auth bypass fast path", () => {
|
||||
it("loads the narrow gateway auth artifact for configured channels", () => {
|
||||
const paths = resolveBundledChannelGatewayAuthBypassPaths({
|
||||
channelId: "mattermost",
|
||||
cfg: { channels: { mattermost: {} } },
|
||||
});
|
||||
|
||||
expect(paths).toEqual(["/api/channels/mattermost/command", "/api/channels/mattermost/work"]);
|
||||
expect(loadBundledPluginPublicArtifactModuleSyncMock).toHaveBeenCalledWith({
|
||||
dirName: "mattermost",
|
||||
artifactBasename: "gateway-auth-api.js",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats missing gateway auth artifacts as no bypass paths", () => {
|
||||
expect(
|
||||
resolveBundledChannelGatewayAuthBypassPaths({
|
||||
channelId: "discord",
|
||||
cfg: { channels: { discord: {} } },
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("surfaces errors from present gateway auth artifacts", () => {
|
||||
expect(() =>
|
||||
resolveBundledChannelGatewayAuthBypassPaths({
|
||||
channelId: "broken",
|
||||
cfg: { channels: { broken: {} } },
|
||||
}),
|
||||
).toThrow("broken gateway auth artifact");
|
||||
});
|
||||
});
|
||||
32
src/channels/plugins/gateway-auth-bypass.ts
Normal file
32
src/channels/plugins/gateway-auth-bypass.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { loadBundledPluginPublicArtifactModuleSync } from "../../plugins/public-surface-loader.js";
|
||||
|
||||
type GatewayAuthBypassApi = {
|
||||
resolveGatewayAuthBypassPaths?: (params: { cfg: OpenClawConfig }) => readonly unknown[];
|
||||
};
|
||||
|
||||
const GATEWAY_AUTH_API_ARTIFACT_BASENAME = "gateway-auth-api.js";
|
||||
const MISSING_PUBLIC_SURFACE_PREFIX = "Unable to resolve bundled plugin public surface ";
|
||||
|
||||
function loadBundledChannelGatewayAuthApi(channelId: string): GatewayAuthBypassApi | undefined {
|
||||
try {
|
||||
return loadBundledPluginPublicArtifactModuleSync<GatewayAuthBypassApi>({
|
||||
dirName: channelId,
|
||||
artifactBasename: GATEWAY_AUTH_API_ARTIFACT_BASENAME,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith(MISSING_PUBLIC_SURFACE_PREFIX)) {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveBundledChannelGatewayAuthBypassPaths(params: {
|
||||
channelId: string;
|
||||
cfg: OpenClawConfig;
|
||||
}): string[] {
|
||||
const api = loadBundledChannelGatewayAuthApi(params.channelId);
|
||||
const paths = api?.resolveGatewayAuthBypassPaths?.({ cfg: params.cfg }) ?? [];
|
||||
return paths.flatMap((path) => (typeof path === "string" && path.trim() ? [path.trim()] : []));
|
||||
}
|
||||
@@ -335,6 +335,7 @@ export type ChannelLogoutContext<ResolvedAccount = unknown> = {
|
||||
export type ChannelGatewayAdapter<ResolvedAccount = unknown> = {
|
||||
startAccount?: (ctx: ChannelGatewayContext<ResolvedAccount>) => Promise<unknown>;
|
||||
stopAccount?: (ctx: ChannelGatewayContext<ResolvedAccount>) => Promise<void>;
|
||||
/** Keep gateway auth bypass resolution mirrored through a lightweight top-level `gateway-auth-api.ts` artifact. */
|
||||
resolveGatewayAuthBypassPaths?: (params: { cfg: OpenClawConfig }) => string[];
|
||||
loginWithQrStart?: (params: {
|
||||
accountId?: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { TlsOptions } from "node:tls";
|
||||
import type { WebSocketServer } from "ws";
|
||||
import { A2UI_PATH, CANVAS_WS_PATH, handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
|
||||
import type { CanvasHostHandler } from "../canvas-host/server.js";
|
||||
import { resolveBundledChannelGatewayAuthBypassPaths } from "../channels/plugins/gateway-auth-bypass.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
@@ -77,9 +78,6 @@ type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
|
||||
const HOOK_AUTH_FAILURE_LIMIT = 20;
|
||||
const HOOK_AUTH_FAILURE_WINDOW_MS = 60_000;
|
||||
|
||||
let bundledChannelsModulePromise:
|
||||
| Promise<typeof import("../channels/plugins/bundled.js")>
|
||||
| undefined;
|
||||
let identityAvatarModulePromise: Promise<typeof import("../agents/identity-avatar.js")> | undefined;
|
||||
let controlUiModulePromise: Promise<typeof import("./control-ui.js")> | undefined;
|
||||
let embeddingsHttpModulePromise: Promise<typeof import("./embeddings-http.js")> | undefined;
|
||||
@@ -92,11 +90,6 @@ let sessionHistoryHttpModulePromise:
|
||||
let sessionKillHttpModulePromise: Promise<typeof import("./session-kill-http.js")> | undefined;
|
||||
let toolsInvokeHttpModulePromise: Promise<typeof import("./tools-invoke-http.js")> | undefined;
|
||||
|
||||
function getBundledChannelsModule() {
|
||||
bundledChannelsModulePromise ??= import("../channels/plugins/bundled.js");
|
||||
return bundledChannelsModulePromise;
|
||||
}
|
||||
|
||||
function getIdentityAvatarModule() {
|
||||
identityAvatarModulePromise ??= import("../agents/identity-avatar.js");
|
||||
return identityAvatarModulePromise;
|
||||
@@ -202,21 +195,12 @@ async function resolvePluginGatewayAuthBypassPaths(
|
||||
if (!configuredChannels || Object.keys(configuredChannels).length === 0) {
|
||||
return paths;
|
||||
}
|
||||
const { getBundledChannelPlugin, getBundledChannelSetupPlugin } =
|
||||
await getBundledChannelsModule();
|
||||
for (const channelId of Object.keys(configuredChannels)) {
|
||||
const setupPlugin = getBundledChannelSetupPlugin(channelId);
|
||||
const plugin = setupPlugin?.gateway?.resolveGatewayAuthBypassPaths
|
||||
? setupPlugin
|
||||
: getBundledChannelPlugin(channelId);
|
||||
if (!plugin) {
|
||||
continue;
|
||||
}
|
||||
for (const path of plugin.gateway?.resolveGatewayAuthBypassPaths?.({ cfg: configSnapshot }) ??
|
||||
[]) {
|
||||
if (typeof path === "string" && path.trim()) {
|
||||
paths.add(path.trim());
|
||||
}
|
||||
for (const path of resolveBundledChannelGatewayAuthBypassPaths({
|
||||
channelId,
|
||||
cfg: configSnapshot,
|
||||
})) {
|
||||
paths.add(path);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
|
||||
Reference in New Issue
Block a user