mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(feishu): harden onboarding and webhook validation
This commit is contained in:
@@ -22,6 +22,20 @@ function makeEvent(
|
||||
};
|
||||
}
|
||||
|
||||
function makePostEvent(content: unknown) {
|
||||
return {
|
||||
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
|
||||
message: {
|
||||
message_id: "msg_1",
|
||||
chat_id: "oc_chat1",
|
||||
chat_type: "group",
|
||||
message_type: "post",
|
||||
content: JSON.stringify(content),
|
||||
mentions: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("parseFeishuMessageEvent – mentionedBot", () => {
|
||||
const BOT_OPEN_ID = "ou_bot_123";
|
||||
|
||||
@@ -85,64 +99,31 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
|
||||
|
||||
it("returns mentionedBot=true for post message with at (no top-level mentions)", () => {
|
||||
const BOT_OPEN_ID = "ou_bot_123";
|
||||
const postContent = JSON.stringify({
|
||||
const event = makePostEvent({
|
||||
content: [
|
||||
[{ tag: "at", user_id: BOT_OPEN_ID, user_name: "claw" }],
|
||||
[{ tag: "text", text: "What does this document say" }],
|
||||
],
|
||||
});
|
||||
const event = {
|
||||
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
|
||||
message: {
|
||||
message_id: "msg_1",
|
||||
chat_id: "oc_chat1",
|
||||
chat_type: "group",
|
||||
message_type: "post",
|
||||
content: postContent,
|
||||
mentions: [],
|
||||
},
|
||||
};
|
||||
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
|
||||
expect(ctx.mentionedBot).toBe(true);
|
||||
});
|
||||
|
||||
it("returns mentionedBot=false for post message with no at", () => {
|
||||
const postContent = JSON.stringify({
|
||||
const event = makePostEvent({
|
||||
content: [[{ tag: "text", text: "hello" }]],
|
||||
});
|
||||
const event = {
|
||||
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
|
||||
message: {
|
||||
message_id: "msg_1",
|
||||
chat_id: "oc_chat1",
|
||||
chat_type: "group",
|
||||
message_type: "post",
|
||||
content: postContent,
|
||||
mentions: [],
|
||||
},
|
||||
};
|
||||
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
|
||||
expect(ctx.mentionedBot).toBe(false);
|
||||
});
|
||||
|
||||
it("returns mentionedBot=false for post message with at for another user", () => {
|
||||
const postContent = JSON.stringify({
|
||||
const event = makePostEvent({
|
||||
content: [
|
||||
[{ tag: "at", user_id: "ou_other", user_name: "other" }],
|
||||
[{ tag: "text", text: "hello" }],
|
||||
],
|
||||
});
|
||||
const event = {
|
||||
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
|
||||
message: {
|
||||
message_id: "msg_1",
|
||||
chat_id: "oc_chat1",
|
||||
chat_type: "group",
|
||||
message_type: "post",
|
||||
content: postContent,
|
||||
mentions: [],
|
||||
},
|
||||
};
|
||||
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
|
||||
expect(ctx.mentionedBot).toBe(false);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,24 @@ vi.mock("./send.js", () => ({
|
||||
getMessageFeishu: mockGetMessageFeishu,
|
||||
}));
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}),
|
||||
} as RuntimeEnv;
|
||||
}
|
||||
|
||||
async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessageEvent }) {
|
||||
await handleFeishuMessage({
|
||||
cfg: params.cfg,
|
||||
event: params.event,
|
||||
runtime: createRuntimeEnv(),
|
||||
});
|
||||
}
|
||||
|
||||
describe("handleFeishuMessage command authorization", () => {
|
||||
const mockFinalizeInboundContext = vi.fn((ctx: unknown) => ctx);
|
||||
const mockDispatchReplyFromConfig = vi
|
||||
@@ -96,17 +114,7 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
},
|
||||
};
|
||||
|
||||
await handleFeishuMessage({
|
||||
cfg,
|
||||
event,
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}),
|
||||
} as RuntimeEnv,
|
||||
});
|
||||
await dispatchMessage({ cfg, event });
|
||||
|
||||
expect(mockResolveCommandAuthorizedFromAuthorizers).toHaveBeenCalledWith({
|
||||
useAccessGroups: true,
|
||||
@@ -151,17 +159,7 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
},
|
||||
};
|
||||
|
||||
await handleFeishuMessage({
|
||||
cfg,
|
||||
event,
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}),
|
||||
} as RuntimeEnv,
|
||||
});
|
||||
await dispatchMessage({ cfg, event });
|
||||
|
||||
expect(mockReadAllowFromStore).toHaveBeenCalledWith("feishu");
|
||||
expect(mockResolveCommandAuthorizedFromAuthorizers).not.toHaveBeenCalled();
|
||||
@@ -198,17 +196,7 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
},
|
||||
};
|
||||
|
||||
await handleFeishuMessage({
|
||||
cfg,
|
||||
event,
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}),
|
||||
} as RuntimeEnv,
|
||||
});
|
||||
await dispatchMessage({ cfg, event });
|
||||
|
||||
expect(mockUpsertPairingRequest).toHaveBeenCalledWith({
|
||||
channel: "feishu",
|
||||
@@ -262,17 +250,7 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
},
|
||||
};
|
||||
|
||||
await handleFeishuMessage({
|
||||
cfg,
|
||||
event,
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}),
|
||||
} as RuntimeEnv,
|
||||
});
|
||||
await dispatchMessage({ cfg, event });
|
||||
|
||||
expect(mockResolveCommandAuthorizedFromAuthorizers).toHaveBeenCalledWith({
|
||||
useAccessGroups: true,
|
||||
|
||||
@@ -2,6 +2,28 @@ import { describe, expect, it } from "vitest";
|
||||
import { FeishuConfigSchema } from "./config-schema.js";
|
||||
|
||||
describe("FeishuConfigSchema webhook validation", () => {
|
||||
it("applies top-level defaults", () => {
|
||||
const result = FeishuConfigSchema.parse({});
|
||||
expect(result.domain).toBe("feishu");
|
||||
expect(result.connectionMode).toBe("websocket");
|
||||
expect(result.webhookPath).toBe("/feishu/events");
|
||||
expect(result.dmPolicy).toBe("pairing");
|
||||
expect(result.groupPolicy).toBe("allowlist");
|
||||
expect(result.requireMention).toBe(true);
|
||||
});
|
||||
|
||||
it("does not force top-level policy defaults into account config", () => {
|
||||
const result = FeishuConfigSchema.parse({
|
||||
accounts: {
|
||||
main: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.accounts?.main?.dmPolicy).toBeUndefined();
|
||||
expect(result.accounts?.main?.groupPolicy).toBeUndefined();
|
||||
expect(result.accounts?.main?.requireMention).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects top-level webhook mode without verificationToken", () => {
|
||||
const result = FeishuConfigSchema.safeParse({
|
||||
connectionMode: "webhook",
|
||||
|
||||
@@ -112,6 +112,31 @@ export const FeishuGroupSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
const FeishuSharedConfigShape = {
|
||||
webhookHost: z.string().optional(),
|
||||
webhookPort: z.number().int().positive().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
configWrites: z.boolean().optional(),
|
||||
dmPolicy: DmPolicySchema.optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
groupPolicy: GroupPolicySchema.optional(),
|
||||
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
dms: z.record(z.string(), DmConfigSchema).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||
renderMode: RenderModeSchema,
|
||||
streaming: StreamingModeSchema,
|
||||
tools: FeishuToolsConfigSchema,
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-account configuration.
|
||||
* All fields are optional - missing fields inherit from top-level config.
|
||||
@@ -127,28 +152,7 @@ export const FeishuAccountConfigSchema = z
|
||||
domain: FeishuDomainSchema.optional(),
|
||||
connectionMode: FeishuConnectionModeSchema.optional(),
|
||||
webhookPath: z.string().optional(),
|
||||
webhookHost: z.string().optional(),
|
||||
webhookPort: z.number().int().positive().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
configWrites: z.boolean().optional(),
|
||||
dmPolicy: DmPolicySchema.optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
groupPolicy: GroupPolicySchema.optional(),
|
||||
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
dms: z.record(z.string(), DmConfigSchema).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||
renderMode: RenderModeSchema,
|
||||
streaming: StreamingModeSchema, // Enable streaming card mode (default: true)
|
||||
tools: FeishuToolsConfigSchema,
|
||||
...FeishuSharedConfigShape,
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -163,29 +167,11 @@ export const FeishuConfigSchema = z
|
||||
domain: FeishuDomainSchema.optional().default("feishu"),
|
||||
connectionMode: FeishuConnectionModeSchema.optional().default("websocket"),
|
||||
webhookPath: z.string().optional().default("/feishu/events"),
|
||||
webhookHost: z.string().optional(),
|
||||
webhookPort: z.number().int().positive().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
configWrites: z.boolean().optional(),
|
||||
...FeishuSharedConfigShape,
|
||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
requireMention: z.boolean().optional().default(true),
|
||||
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
||||
topicSessionMode: TopicSessionModeSchema,
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
dms: z.record(z.string(), DmConfigSchema).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
||||
streaming: StreamingModeSchema, // Enable streaming card mode (default: true)
|
||||
tools: FeishuToolsConfigSchema,
|
||||
// Dynamic agent creation for DM users
|
||||
dynamicAgentCreation: DynamicAgentCreationSchema,
|
||||
// Multi-account configuration
|
||||
|
||||
@@ -38,6 +38,16 @@ vi.mock("./runtime.js", () => ({
|
||||
|
||||
import { downloadImageFeishu, downloadMessageResourceFeishu, sendMediaFeishu } from "./media.js";
|
||||
|
||||
function expectPathIsolatedToTmpRoot(pathValue: string, key: string): void {
|
||||
expect(pathValue).not.toContain(key);
|
||||
expect(pathValue).not.toContain("..");
|
||||
|
||||
const tmpRoot = path.resolve(os.tmpdir());
|
||||
const resolved = path.resolve(pathValue);
|
||||
const rel = path.relative(tmpRoot, resolved);
|
||||
expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
|
||||
}
|
||||
|
||||
describe("sendMediaFeishu msg_type routing", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -217,13 +227,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
||||
|
||||
expect(result.buffer).toEqual(Buffer.from("image-data"));
|
||||
expect(capturedPath).toBeDefined();
|
||||
expect(capturedPath).not.toContain(imageKey);
|
||||
expect(capturedPath).not.toContain("..");
|
||||
|
||||
const tmpRoot = path.resolve(os.tmpdir());
|
||||
const resolved = path.resolve(capturedPath as string);
|
||||
const rel = path.relative(tmpRoot, resolved);
|
||||
expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
|
||||
expectPathIsolatedToTmpRoot(capturedPath as string, imageKey);
|
||||
});
|
||||
|
||||
it("uses isolated temp paths for message resource downloads", async () => {
|
||||
@@ -246,13 +250,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
||||
|
||||
expect(result.buffer).toEqual(Buffer.from("resource-data"));
|
||||
expect(capturedPath).toBeDefined();
|
||||
expect(capturedPath).not.toContain(fileKey);
|
||||
expect(capturedPath).not.toContain("..");
|
||||
|
||||
const tmpRoot = path.resolve(os.tmpdir());
|
||||
const resolved = path.resolve(capturedPath as string);
|
||||
const rel = path.relative(tmpRoot, resolved);
|
||||
expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
|
||||
expectPathIsolatedToTmpRoot(capturedPath as string, fileKey);
|
||||
});
|
||||
|
||||
it("rejects invalid image keys before calling feishu api", async () => {
|
||||
|
||||
@@ -78,6 +78,41 @@ function buildConfig(params: {
|
||||
} as ClawdbotConfig;
|
||||
}
|
||||
|
||||
async function withRunningWebhookMonitor(
|
||||
params: {
|
||||
accountId: string;
|
||||
path: string;
|
||||
verificationToken: string;
|
||||
},
|
||||
run: (url: string) => Promise<void>,
|
||||
) {
|
||||
const port = await getFreePort();
|
||||
const cfg = buildConfig({
|
||||
accountId: params.accountId,
|
||||
path: params.path,
|
||||
port,
|
||||
verificationToken: params.verificationToken,
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const monitorPromise = monitorFeishuProvider({
|
||||
config: cfg,
|
||||
runtime,
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
const url = `http://127.0.0.1:${port}${params.path}`;
|
||||
await waitUntilServerReady(url);
|
||||
|
||||
try {
|
||||
await run(url);
|
||||
} finally {
|
||||
abortController.abort();
|
||||
await monitorPromise;
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
stopFeishuMonitor();
|
||||
});
|
||||
@@ -99,76 +134,50 @@ describe("Feishu webhook security hardening", () => {
|
||||
|
||||
it("returns 415 for POST requests without json content type", async () => {
|
||||
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
||||
const port = await getFreePort();
|
||||
const path = "/hook-content-type";
|
||||
const cfg = buildConfig({
|
||||
accountId: "content-type",
|
||||
path,
|
||||
port,
|
||||
verificationToken: "verify_token",
|
||||
});
|
||||
await withRunningWebhookMonitor(
|
||||
{
|
||||
accountId: "content-type",
|
||||
path: "/hook-content-type",
|
||||
verificationToken: "verify_token",
|
||||
},
|
||||
async (url) => {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "text/plain" },
|
||||
body: "{}",
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const monitorPromise = monitorFeishuProvider({
|
||||
config: cfg,
|
||||
runtime,
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
await waitUntilServerReady(`http://127.0.0.1:${port}${path}`);
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "text/plain" },
|
||||
body: "{}",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(415);
|
||||
expect(await response.text()).toBe("Unsupported Media Type");
|
||||
|
||||
abortController.abort();
|
||||
await monitorPromise;
|
||||
expect(response.status).toBe(415);
|
||||
expect(await response.text()).toBe("Unsupported Media Type");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("rate limits webhook burst traffic with 429", async () => {
|
||||
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
||||
const port = await getFreePort();
|
||||
const path = "/hook-rate-limit";
|
||||
const cfg = buildConfig({
|
||||
accountId: "rate-limit",
|
||||
path,
|
||||
port,
|
||||
verificationToken: "verify_token",
|
||||
});
|
||||
await withRunningWebhookMonitor(
|
||||
{
|
||||
accountId: "rate-limit",
|
||||
path: "/hook-rate-limit",
|
||||
verificationToken: "verify_token",
|
||||
},
|
||||
async (url) => {
|
||||
let saw429 = false;
|
||||
for (let i = 0; i < 130; i += 1) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "text/plain" },
|
||||
body: "{}",
|
||||
});
|
||||
if (response.status === 429) {
|
||||
saw429 = true;
|
||||
expect(await response.text()).toBe("Too Many Requests");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const monitorPromise = monitorFeishuProvider({
|
||||
config: cfg,
|
||||
runtime,
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
await waitUntilServerReady(`http://127.0.0.1:${port}${path}`);
|
||||
|
||||
let saw429 = false;
|
||||
for (let i = 0; i < 130; i += 1) {
|
||||
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "text/plain" },
|
||||
body: "{}",
|
||||
});
|
||||
if (response.status === 429) {
|
||||
saw429 = true;
|
||||
expect(await response.text()).toBe("Too Many Requests");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(saw429).toBe(true);
|
||||
|
||||
abortController.abort();
|
||||
await monitorPromise;
|
||||
expect(saw429).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +104,25 @@ async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void>
|
||||
);
|
||||
}
|
||||
|
||||
async function promptFeishuCredentials(prompter: WizardPrompter): Promise<{
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
}> {
|
||||
const appId = String(
|
||||
await prompter.text({
|
||||
message: "Enter Feishu App ID",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
const appSecret = String(
|
||||
await prompter.text({
|
||||
message: "Enter Feishu App Secret",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
return { appId, appSecret };
|
||||
}
|
||||
|
||||
function setFeishuGroupPolicy(
|
||||
cfg: ClawdbotConfig,
|
||||
groupPolicy: "open" | "allowlist" | "disabled",
|
||||
@@ -210,18 +229,9 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
},
|
||||
};
|
||||
} else {
|
||||
appId = String(
|
||||
await prompter.text({
|
||||
message: "Enter Feishu App ID",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
appSecret = String(
|
||||
await prompter.text({
|
||||
message: "Enter Feishu App Secret",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
const entered = await promptFeishuCredentials(prompter);
|
||||
appId = entered.appId;
|
||||
appSecret = entered.appSecret;
|
||||
}
|
||||
} else if (hasConfigCreds) {
|
||||
const keep = await prompter.confirm({
|
||||
@@ -229,32 +239,14 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keep) {
|
||||
appId = String(
|
||||
await prompter.text({
|
||||
message: "Enter Feishu App ID",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
appSecret = String(
|
||||
await prompter.text({
|
||||
message: "Enter Feishu App Secret",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
const entered = await promptFeishuCredentials(prompter);
|
||||
appId = entered.appId;
|
||||
appSecret = entered.appSecret;
|
||||
}
|
||||
} else {
|
||||
appId = String(
|
||||
await prompter.text({
|
||||
message: "Enter Feishu App ID",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
appSecret = String(
|
||||
await prompter.text({
|
||||
message: "Enter Feishu App Secret",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
const entered = await promptFeishuCredentials(prompter);
|
||||
appId = entered.appId;
|
||||
appSecret = entered.appSecret;
|
||||
}
|
||||
|
||||
if (appId && appSecret) {
|
||||
|
||||
Reference in New Issue
Block a user