import { DEFAULT_ACCOUNT_ID, MarkdownConfigSchema, PAIRING_APPROVED_MESSAGE, addWildcardAllowFrom, applyAccountNameToChannelSection, buildChannelConfigSchema, chunkTextForOutbound, createReplyPrefixOptions, deleteAccountFromConfigSection, emptyPluginConfigSchema, formatAllowFromLowercase, formatDocsLink, formatPairingApproveHint, jsonResult, maybeInjectDisclosure, mergeAllowFromEntries, migrateBaseNameToDefaultAccount, normalizeAccountId, promptAccountId, readJsonBodyWithLimit, readStringParam, registerWebhookTarget, rejectNonPostWebhookRequest, requestBodyErrorToText, resolveChannelAccountConfigBasePath, resolveDisclosureConfig, resolveSenderCommandAuthorization, resolveWebhookPath, resolveWebhookTargets, setAccountEnabledInConfigSection } from "minion/plugin-sdk";
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/account-id";
import { readFileSync } from "node:fs";
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk";
import { ProxyAgent, fetch as fetch$1 } from "undici";
import { z } from "zod";

//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
var __exportAll = (all, no_symbols) => {
	let target = {};
	for (var name in all) {
		__defProp(target, name, {
			get: all[name],
			enumerable: true
		});
	}
	if (!no_symbols) {
		__defProp(target, Symbol.toStringTag, { value: "Module" });
	}
	return target;
};

//#endregion
//#region extensions/zalo/src/token.ts
function resolveZaloToken(config, accountId) {
	const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID$2;
	const isDefaultAccount = resolvedAccountId === DEFAULT_ACCOUNT_ID$2;
	const baseConfig = config;
	const accountConfig = resolvedAccountId !== DEFAULT_ACCOUNT_ID$2 ? baseConfig?.accounts?.[resolvedAccountId] : void 0;
	if (accountConfig) {
		const token = accountConfig.botToken?.trim();
		if (token) return {
			token,
			source: "config"
		};
		const tokenFile = accountConfig.tokenFile?.trim();
		if (tokenFile) try {
			const fileToken = readFileSync(tokenFile, "utf8").trim();
			if (fileToken) return {
				token: fileToken,
				source: "configFile"
			};
		} catch {}
	}
	if (isDefaultAccount) {
		const token = baseConfig?.botToken?.trim();
		if (token) return {
			token,
			source: "config"
		};
		const tokenFile = baseConfig?.tokenFile?.trim();
		if (tokenFile) try {
			const fileToken = readFileSync(tokenFile, "utf8").trim();
			if (fileToken) return {
				token: fileToken,
				source: "configFile"
			};
		} catch {}
		const envToken = process.env.ZALO_BOT_TOKEN?.trim();
		if (envToken) return {
			token: envToken,
			source: "env"
		};
	}
	return {
		token: "",
		source: "none"
	};
}

//#endregion
//#region extensions/zalo/src/accounts.ts
function listConfiguredAccountIds(cfg) {
	const accounts = (cfg.channels?.zalo)?.accounts;
	if (!accounts || typeof accounts !== "object") return [];
	return Object.keys(accounts).filter(Boolean);
}
function listZaloAccountIds(cfg) {
	const ids = listConfiguredAccountIds(cfg);
	if (ids.length === 0) return [DEFAULT_ACCOUNT_ID$1];
	return ids.toSorted((a, b) => a.localeCompare(b));
}
function resolveDefaultZaloAccountId(cfg) {
	const zaloConfig = cfg.channels?.zalo;
	if (zaloConfig?.defaultAccount?.trim()) return zaloConfig.defaultAccount.trim();
	const ids = listZaloAccountIds(cfg);
	if (ids.includes(DEFAULT_ACCOUNT_ID$1)) return DEFAULT_ACCOUNT_ID$1;
	return ids[0] ?? DEFAULT_ACCOUNT_ID$1;
}
function resolveAccountConfig(cfg, accountId) {
	const accounts = (cfg.channels?.zalo)?.accounts;
	if (!accounts || typeof accounts !== "object") return;
	return accounts[accountId];
}
function mergeZaloAccountConfig(cfg, accountId) {
	const { accounts: _ignored, defaultAccount: _ignored2, ...base } = cfg.channels?.zalo ?? {};
	const account = resolveAccountConfig(cfg, accountId) ?? {};
	return {
		...base,
		...account
	};
}
function resolveZaloAccount(params) {
	const accountId = normalizeAccountId$1(params.accountId);
	const baseEnabled = (params.cfg.channels?.zalo)?.enabled !== false;
	const merged = mergeZaloAccountConfig(params.cfg, accountId);
	const accountEnabled = merged.enabled !== false;
	const enabled = baseEnabled && accountEnabled;
	const tokenResolution = resolveZaloToken(params.cfg.channels?.zalo, accountId);
	return {
		accountId,
		name: merged.name?.trim() || void 0,
		enabled,
		token: tokenResolution.token,
		tokenSource: tokenResolution.source,
		config: merged
	};
}
function listEnabledZaloAccounts(cfg) {
	return listZaloAccountIds(cfg).map((accountId) => resolveZaloAccount({
		cfg,
		accountId
	})).filter((account) => account.enabled);
}

//#endregion
//#region extensions/zalo/src/api.ts
/**
* Call the Zalo Bot API
*/
async function callZaloApi(method, token, body, options) {
	const url = `${ZALO_API_BASE}/bot${token}/${method}`;
	const controller = new AbortController();
	const timeoutId = options?.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : void 0;
	const fetcher = options?.fetch ?? fetch;
	try {
		const data = await (await fetcher(url, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: body ? JSON.stringify(body) : void 0,
			signal: controller.signal
		})).json();
		if (!data.ok) throw new ZaloApiError(data.description ?? `Zalo API error: ${method}`, data.error_code, data.description);
		return data;
	} finally {
		if (timeoutId) clearTimeout(timeoutId);
	}
}
/**
* Validate bot token and get bot info
*/
async function getMe(token, timeoutMs, fetcher) {
	return callZaloApi("getMe", token, void 0, {
		timeoutMs,
		fetch: fetcher
	});
}
/**
* Send a text message
*/
async function sendMessage(token, params, fetcher) {
	return callZaloApi("sendMessage", token, params, { fetch: fetcher });
}
/**
* Send a photo message
*/
async function sendPhoto(token, params, fetcher) {
	return callZaloApi("sendPhoto", token, params, { fetch: fetcher });
}
/**
* Get updates using long polling (dev/testing only)
* Note: Zalo returns a single update per call, not an array like Telegram
*/
async function getUpdates(token, params, fetcher) {
	const pollTimeoutSec = params?.timeout ?? 30;
	const timeoutMs = (pollTimeoutSec + 5) * 1e3;
	return callZaloApi("getUpdates", token, { timeout: String(pollTimeoutSec) }, {
		timeoutMs,
		fetch: fetcher
	});
}
/**
* Set webhook URL for receiving updates
*/
async function setWebhook(token, params, fetcher) {
	return callZaloApi("setWebhook", token, params, { fetch: fetcher });
}
/**
* Delete webhook configuration
*/
async function deleteWebhook(token, fetcher) {
	return callZaloApi("deleteWebhook", token, void 0, { fetch: fetcher });
}
var ZALO_API_BASE, ZaloApiError;
var init_api = __esmMin((() => {
	ZALO_API_BASE = "https://bot-api.zaloplatforms.com";
	ZaloApiError = class extends Error {
		constructor(message, errorCode, description) {
			super(message);
			this.errorCode = errorCode;
			this.description = description;
			this.name = "ZaloApiError";
		}
		/** True if this is a long-polling timeout (no updates available) */
		get isPollingTimeout() {
			return this.errorCode === 408;
		}
	};
}));

//#endregion
//#region extensions/zalo/src/proxy.ts
function resolveZaloProxyFetch(proxyUrl) {
	const trimmed = proxyUrl?.trim();
	if (!trimmed) return;
	const cached = proxyCache.get(trimmed);
	if (cached) return cached;
	const agent = new ProxyAgent(trimmed);
	const fetcher = (input, init) => fetch$1(input, {
		...init,
		dispatcher: agent
	});
	proxyCache.set(trimmed, fetcher);
	return fetcher;
}
var proxyCache;
var init_proxy = __esmMin((() => {
	proxyCache = /* @__PURE__ */ new Map();
}));

//#endregion
//#region extensions/zalo/src/send.ts
init_api();
init_proxy();
function resolveSendContext(options) {
	if (options.cfg) {
		const account = resolveZaloAccount({
			cfg: options.cfg,
			accountId: options.accountId
		});
		return {
			token: options.token || account.token,
			fetcher: resolveZaloProxyFetch(options.proxy ?? account.config.proxy)
		};
	}
	const token = options.token ?? resolveZaloToken(void 0, options.accountId).token;
	const proxy = options.proxy;
	return {
		token,
		fetcher: resolveZaloProxyFetch(proxy)
	};
}
async function sendMessageZalo(chatId, text, options = {}) {
	const { token, fetcher } = resolveSendContext(options);
	if (!token) return {
		ok: false,
		error: "No Zalo bot token configured"
	};
	if (!chatId?.trim()) return {
		ok: false,
		error: "No chat_id provided"
	};
	if (options.mediaUrl) return sendPhotoZalo(chatId, options.mediaUrl, {
		...options,
		token,
		caption: text || options.caption
	});
	try {
		const response = await sendMessage(token, {
			chat_id: chatId.trim(),
			text: text.slice(0, 2e3)
		}, fetcher);
		if (response.ok && response.result) return {
			ok: true,
			messageId: response.result.message_id
		};
		return {
			ok: false,
			error: "Failed to send message"
		};
	} catch (err) {
		return {
			ok: false,
			error: err instanceof Error ? err.message : String(err)
		};
	}
}
async function sendPhotoZalo(chatId, photoUrl, options = {}) {
	const { token, fetcher } = resolveSendContext(options);
	if (!token) return {
		ok: false,
		error: "No Zalo bot token configured"
	};
	if (!chatId?.trim()) return {
		ok: false,
		error: "No chat_id provided"
	};
	if (!photoUrl?.trim()) return {
		ok: false,
		error: "No photo URL provided"
	};
	try {
		const response = await sendPhoto(token, {
			chat_id: chatId.trim(),
			photo: photoUrl.trim(),
			caption: options.caption?.slice(0, 2e3)
		}, fetcher);
		if (response.ok && response.result) return {
			ok: true,
			messageId: response.result.message_id
		};
		return {
			ok: false,
			error: "Failed to send photo"
		};
	} catch (err) {
		return {
			ok: false,
			error: err instanceof Error ? err.message : String(err)
		};
	}
}

//#endregion
//#region extensions/zalo/src/actions.ts
const providerId = "zalo";
function listEnabledAccounts(cfg) {
	return listEnabledZaloAccounts(cfg).filter((account) => account.enabled && account.tokenSource !== "none");
}
const zaloMessageActions = {
	listActions: ({ cfg }) => {
		if (listEnabledAccounts(cfg).length === 0) return [];
		const actions = new Set(["send"]);
		return Array.from(actions);
	},
	supportsButtons: () => false,
	extractToolSend: ({ args }) => {
		if ((typeof args.action === "string" ? args.action.trim() : "") !== "sendMessage") return null;
		const to = typeof args.to === "string" ? args.to : void 0;
		if (!to) return null;
		return {
			to,
			accountId: typeof args.accountId === "string" ? args.accountId.trim() : void 0
		};
	},
	handleAction: async ({ action, params, cfg, accountId }) => {
		if (action === "send") {
			const to = readStringParam(params, "to", { required: true });
			const content = readStringParam(params, "message", {
				required: true,
				allowEmpty: true
			});
			const mediaUrl = readStringParam(params, "media", { trim: false });
			const result = await sendMessageZalo(to ?? "", content ?? "", {
				accountId: accountId ?? void 0,
				mediaUrl: mediaUrl ?? void 0,
				cfg
			});
			if (!result.ok) return jsonResult({
				ok: false,
				error: result.error ?? "Failed to send Zalo message"
			});
			return jsonResult({
				ok: true,
				to,
				messageId: result.messageId
			});
		}
		throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
	}
};

//#endregion
//#region extensions/zalo/src/config-schema.ts
const allowFromEntry = z.union([z.string(), z.number()]);
const zaloAccountSchema = z.object({
	name: z.string().optional(),
	enabled: z.boolean().optional(),
	markdown: MarkdownConfigSchema,
	botToken: z.string().optional(),
	tokenFile: z.string().optional(),
	webhookUrl: z.string().optional(),
	webhookSecret: z.string().optional(),
	webhookPath: z.string().optional(),
	dmPolicy: z.enum([
		"pairing",
		"allowlist",
		"open",
		"disabled"
	]).optional(),
	allowFrom: z.array(allowFromEntry).optional(),
	mediaMaxMb: z.number().optional(),
	proxy: z.string().optional(),
	responsePrefix: z.string().optional()
});
const ZaloConfigSchema = zaloAccountSchema.extend({
	accounts: z.object({}).catchall(zaloAccountSchema).optional(),
	defaultAccount: z.string().optional()
});

//#endregion
//#region extensions/zalo/src/onboarding.ts
const channel = "zalo";
function setZaloDmPolicy(cfg, dmPolicy) {
	const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.zalo?.allowFrom) : void 0;
	return {
		...cfg,
		channels: {
			...cfg.channels,
			zalo: {
				...cfg.channels?.zalo,
				dmPolicy,
				...allowFrom ? { allowFrom } : {}
			}
		}
	};
}
function setZaloUpdateMode(cfg, accountId, mode, webhookUrl, webhookSecret, webhookPath) {
	const isDefault = accountId === DEFAULT_ACCOUNT_ID;
	if (mode === "polling") {
		if (isDefault) {
			const { webhookUrl: _url, webhookSecret: _secret, webhookPath: _path, ...rest } = cfg.channels?.zalo ?? {};
			return {
				...cfg,
				channels: {
					...cfg.channels,
					zalo: rest
				}
			};
		}
		const accounts = { ...cfg.channels?.zalo?.accounts };
		const { webhookUrl: _url, webhookSecret: _secret, webhookPath: _path, ...rest } = accounts[accountId] ?? {};
		accounts[accountId] = rest;
		return {
			...cfg,
			channels: {
				...cfg.channels,
				zalo: {
					...cfg.channels?.zalo,
					accounts
				}
			}
		};
	}
	if (isDefault) return {
		...cfg,
		channels: {
			...cfg.channels,
			zalo: {
				...cfg.channels?.zalo,
				webhookUrl,
				webhookSecret,
				webhookPath
			}
		}
	};
	const accounts = { ...cfg.channels?.zalo?.accounts };
	accounts[accountId] = {
		...accounts[accountId],
		webhookUrl,
		webhookSecret,
		webhookPath
	};
	return {
		...cfg,
		channels: {
			...cfg.channels,
			zalo: {
				...cfg.channels?.zalo,
				accounts
			}
		}
	};
}
async function notePrivacyNotice(prompter) {
	await prompter.note([
		"This assistant uses AI models to process your messages.",
		"(GDPR Art. 13 §2, EU AI Act Art. 50 §1)",
		"• Your messages may be sent to AI model providers for processing.",
		"• Conversation history may be stored locally for context.",
		"• You may request access, correction, or erasure of your data.",
		`Privacy notice: ${formatDocsLink("/privacy", "privacy-notice")}`,
		"By continuing setup you acknowledge this data processing."
	].join("\n"), "Privacy notice");
}
async function noteZaloTokenHelp(prompter) {
	await prompter.note([
		"1) Open Zalo Bot Platform: https://bot.zaloplatforms.com",
		"2) Create a bot and get the token",
		"3) Token looks like 12345689:abc-xyz",
		"Tip: you can also set ZALO_BOT_TOKEN in your env.",
		"Docs: https://docs.minion.ai/channels/zalo"
	].join("\n"), "Zalo bot token");
}
async function promptZaloAllowFrom(params) {
	const { cfg, prompter, accountId } = params;
	const existingAllowFrom = resolveZaloAccount({
		cfg,
		accountId
	}).config.allowFrom ?? [];
	const entry = await prompter.text({
		message: "Zalo allowFrom (user id)",
		placeholder: "123456789",
		initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : void 0,
		validate: (value) => {
			const raw = String(value ?? "").trim();
			if (!raw) return "Required";
			if (!/^\d+$/.test(raw)) return "Use a numeric Zalo user id";
		}
	});
	const unique = mergeAllowFromEntries(existingAllowFrom, [String(entry).trim()]);
	if (accountId === DEFAULT_ACCOUNT_ID) return {
		...cfg,
		channels: {
			...cfg.channels,
			zalo: {
				...cfg.channels?.zalo,
				enabled: true,
				dmPolicy: "allowlist",
				allowFrom: unique
			}
		}
	};
	return {
		...cfg,
		channels: {
			...cfg.channels,
			zalo: {
				...cfg.channels?.zalo,
				enabled: true,
				accounts: {
					...cfg.channels?.zalo?.accounts,
					[accountId]: {
						...cfg.channels?.zalo?.accounts?.[accountId],
						enabled: cfg.channels?.zalo?.accounts?.[accountId]?.enabled ?? true,
						dmPolicy: "allowlist",
						allowFrom: unique
					}
				}
			}
		}
	};
}
const dmPolicy = {
	label: "Zalo",
	channel,
	policyKey: "channels.zalo.dmPolicy",
	allowFromKey: "channels.zalo.allowFrom",
	getCurrent: (cfg) => cfg.channels?.zalo?.dmPolicy ?? "pairing",
	setPolicy: (cfg, policy) => setZaloDmPolicy(cfg, policy),
	promptAllowFrom: async ({ cfg, prompter, accountId }) => {
		return promptZaloAllowFrom({
			cfg,
			prompter,
			accountId: accountId && normalizeAccountId(accountId) ? normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID : resolveDefaultZaloAccountId(cfg)
		});
	}
};
const zaloOnboardingAdapter = {
	channel,
	dmPolicy,
	getStatus: async ({ cfg }) => {
		const configured = listZaloAccountIds(cfg).some((accountId) => Boolean(resolveZaloAccount({
			cfg,
			accountId
		}).token));
		return {
			channel,
			configured,
			statusLines: [`Zalo: ${configured ? "configured" : "needs token"}`],
			selectionHint: configured ? "recommended · configured" : "recommended · newcomer-friendly",
			quickstartScore: configured ? 1 : 10
		};
	},
	configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds, forceAllowFrom }) => {
		await notePrivacyNotice(prompter);
		const zaloOverride = accountOverrides.zalo?.trim();
		const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg);
		let zaloAccountId = zaloOverride ? normalizeAccountId(zaloOverride) : defaultZaloAccountId;
		if (shouldPromptAccountIds && !zaloOverride) zaloAccountId = await promptAccountId({
			cfg,
			prompter,
			label: "Zalo",
			currentId: zaloAccountId,
			listAccountIds: listZaloAccountIds,
			defaultAccountId: defaultZaloAccountId
		});
		let next = cfg;
		const resolvedAccount = resolveZaloAccount({
			cfg: next,
			accountId: zaloAccountId
		});
		const accountConfigured = Boolean(resolvedAccount.token);
		const canUseEnv = zaloAccountId === DEFAULT_ACCOUNT_ID && Boolean(process.env.ZALO_BOT_TOKEN?.trim());
		const hasConfigToken = Boolean(resolvedAccount.config.botToken || resolvedAccount.config.tokenFile);
		let token = null;
		if (!accountConfigured) await noteZaloTokenHelp(prompter);
		if (canUseEnv && !resolvedAccount.config.botToken) if (await prompter.confirm({
			message: "ZALO_BOT_TOKEN detected. Use env var?",
			initialValue: true
		})) next = {
			...next,
			channels: {
				...next.channels,
				zalo: {
					...next.channels?.zalo,
					enabled: true
				}
			}
		};
		else token = String(await prompter.text({
			message: "Enter Zalo bot token",
			validate: (value) => value?.trim() ? void 0 : "Required"
		})).trim();
		else if (hasConfigToken) {
			if (!await prompter.confirm({
				message: "Zalo token already configured. Keep it?",
				initialValue: true
			})) token = String(await prompter.text({
				message: "Enter Zalo bot token",
				validate: (value) => value?.trim() ? void 0 : "Required"
			})).trim();
		} else token = String(await prompter.text({
			message: "Enter Zalo bot token",
			validate: (value) => value?.trim() ? void 0 : "Required"
		})).trim();
		if (token) if (zaloAccountId === DEFAULT_ACCOUNT_ID) next = {
			...next,
			channels: {
				...next.channels,
				zalo: {
					...next.channels?.zalo,
					enabled: true,
					botToken: token
				}
			}
		};
		else next = {
			...next,
			channels: {
				...next.channels,
				zalo: {
					...next.channels?.zalo,
					enabled: true,
					accounts: {
						...next.channels?.zalo?.accounts,
						[zaloAccountId]: {
							...next.channels?.zalo?.accounts?.[zaloAccountId],
							enabled: true,
							botToken: token
						}
					}
				}
			}
		};
		if (await prompter.confirm({
			message: "Use webhook mode for Zalo?",
			initialValue: false
		})) {
			const webhookUrl = String(await prompter.text({
				message: "Webhook URL (https://...) ",
				validate: (value) => value?.trim()?.startsWith("https://") ? void 0 : "HTTPS URL required"
			})).trim();
			const defaultPath = (() => {
				try {
					return new URL(webhookUrl).pathname || "/zalo-webhook";
				} catch {
					return "/zalo-webhook";
				}
			})();
			const webhookSecret = String(await prompter.text({
				message: "Webhook secret (8-256 chars)",
				validate: (value) => {
					const raw = String(value ?? "");
					if (raw.length < 8 || raw.length > 256) return "8-256 chars";
				}
			})).trim();
			const webhookPath = String(await prompter.text({
				message: "Webhook path (optional)",
				initialValue: defaultPath
			})).trim();
			next = setZaloUpdateMode(next, zaloAccountId, "webhook", webhookUrl, webhookSecret, webhookPath || void 0);
		} else next = setZaloUpdateMode(next, zaloAccountId, "polling");
		if (forceAllowFrom) next = await promptZaloAllowFrom({
			cfg: next,
			prompter,
			accountId: zaloAccountId
		});
		return {
			cfg: next,
			accountId: zaloAccountId
		};
	}
};

//#endregion
//#region extensions/zalo/src/probe.ts
init_api();
async function probeZalo(token, timeoutMs = 5e3, fetcher) {
	if (!token?.trim()) return {
		ok: false,
		error: "No token provided",
		elapsedMs: 0
	};
	const startTime = Date.now();
	try {
		const response = await getMe(token.trim(), timeoutMs, fetcher);
		const elapsedMs = Date.now() - startTime;
		if (response.ok && response.result) return {
			ok: true,
			bot: response.result,
			elapsedMs
		};
		return {
			ok: false,
			error: "Invalid response from Zalo API",
			elapsedMs
		};
	} catch (err) {
		const elapsedMs = Date.now() - startTime;
		if (err instanceof ZaloApiError) return {
			ok: false,
			error: err.description ?? err.message,
			elapsedMs
		};
		if (err instanceof Error) {
			if (err.name === "AbortError") return {
				ok: false,
				error: `Request timed out after ${timeoutMs}ms`,
				elapsedMs
			};
			return {
				ok: false,
				error: err.message,
				elapsedMs
			};
		}
		return {
			ok: false,
			error: String(err),
			elapsedMs
		};
	}
}

//#endregion
//#region extensions/zalo/src/status-issues.ts
const isRecord = (value) => Boolean(value && typeof value === "object");
const asString = (value) => typeof value === "string" ? value : typeof value === "number" ? String(value) : void 0;
function readZaloAccountStatus(value) {
	if (!isRecord(value)) return null;
	return {
		accountId: value.accountId,
		enabled: value.enabled,
		configured: value.configured,
		dmPolicy: value.dmPolicy
	};
}
function collectZaloStatusIssues(accounts) {
	const issues = [];
	for (const entry of accounts) {
		const account = readZaloAccountStatus(entry);
		if (!account) continue;
		const accountId = asString(account.accountId) ?? "default";
		const enabled = account.enabled !== false;
		const configured = account.configured === true;
		if (!enabled || !configured) continue;
		if (account.dmPolicy === "open") issues.push({
			channel: "zalo",
			accountId,
			kind: "config",
			message: "Zalo dmPolicy is \"open\", allowing any user to message the bot without pairing.",
			fix: "Set channels.zalo.dmPolicy to \"pairing\" or \"allowlist\" to restrict access."
		});
	}
	return issues;
}

//#endregion
//#region extensions/zalo/src/runtime.ts
function setZaloRuntime(next) {
	runtime = next;
}
function getZaloRuntime() {
	if (!runtime) throw new Error("Zalo runtime not initialized");
	return runtime;
}
var runtime;
var init_runtime = __esmMin((() => {
	runtime = null;
}));

//#endregion
//#region extensions/zalo/src/monitor.ts
var monitor_exports = /* @__PURE__ */ __exportAll({
	handleZaloWebhookRequest: () => handleZaloWebhookRequest,
	monitorZaloProvider: () => monitorZaloProvider,
	registerZaloWebhookTarget: () => registerZaloWebhookTarget
});
function logVerbose(core, runtime, message) {
	if (core.logging.shouldLogVerbose()) runtime.log?.(`[zalo] ${message}`);
}
function isSenderAllowed(senderId, allowFrom) {
	if (allowFrom.includes("*")) return true;
	const normalizedSenderId = senderId.toLowerCase();
	return allowFrom.some((entry) => {
		return entry.toLowerCase().replace(/^(zalo|zl):/i, "") === normalizedSenderId;
	});
}
function registerZaloWebhookTarget(target) {
	return registerWebhookTarget(webhookTargets, target).unregister;
}
async function handleZaloWebhookRequest(req, res) {
	const resolved = resolveWebhookTargets(req, webhookTargets);
	if (!resolved) return false;
	const { targets } = resolved;
	if (rejectNonPostWebhookRequest(req, res)) return true;
	const headerToken = String(req.headers["x-bot-api-secret-token"] ?? "");
	const matching = targets.filter((entry) => entry.secret === headerToken);
	if (matching.length === 0) {
		res.statusCode = 401;
		res.end("unauthorized");
		return true;
	}
	if (matching.length > 1) {
		res.statusCode = 401;
		res.end("ambiguous webhook target");
		return true;
	}
	const target = matching[0];
	const body = await readJsonBodyWithLimit(req, {
		maxBytes: 1024 * 1024,
		timeoutMs: 3e4,
		emptyObjectOnEmpty: false
	});
	if (!body.ok) {
		res.statusCode = body.code === "PAYLOAD_TOO_LARGE" ? 413 : body.code === "REQUEST_BODY_TIMEOUT" ? 408 : 400;
		res.end(body.code === "REQUEST_BODY_TIMEOUT" ? requestBodyErrorToText("REQUEST_BODY_TIMEOUT") : body.error);
		return true;
	}
	const raw = body.value;
	const record = raw && typeof raw === "object" ? raw : null;
	const update = record && record.ok === true && record.result ? record.result : record ?? void 0;
	if (!update?.event_name) {
		res.statusCode = 400;
		res.end("invalid payload");
		return true;
	}
	target.statusSink?.({ lastInboundAt: Date.now() });
	processUpdate(update, target.token, target.account, target.config, target.runtime, target.core, target.mediaMaxMb, target.statusSink, target.fetcher).catch((err) => {
		target.runtime.error?.(`[${target.account.accountId}] Zalo webhook failed: ${String(err)}`);
	});
	res.statusCode = 200;
	res.end("ok");
	return true;
}
function startPollingLoop(params) {
	const { token, account, config, runtime, core, abortSignal, isStopped, mediaMaxMb, statusSink, fetcher } = params;
	const pollTimeout = 30;
	const poll = async () => {
		if (isStopped() || abortSignal.aborted) return;
		try {
			const response = await getUpdates(token, { timeout: pollTimeout }, fetcher);
			if (response.ok && response.result) {
				statusSink?.({ lastInboundAt: Date.now() });
				await processUpdate(response.result, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher);
			}
		} catch (err) {
			if (err instanceof ZaloApiError && err.isPollingTimeout) {} else if (!isStopped() && !abortSignal.aborted) {
				console.error(`[${account.accountId}] Zalo polling error:`, err);
				await new Promise((resolve) => setTimeout(resolve, 5e3));
			}
		}
		if (!isStopped() && !abortSignal.aborted) setImmediate(poll);
	};
	poll();
}
async function processUpdate(update, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher) {
	const { event_name, message } = update;
	if (!message) return;
	switch (event_name) {
		case "message.text.received":
			await handleTextMessage(message, token, account, config, runtime, core, statusSink, fetcher);
			break;
		case "message.image.received":
			await handleImageMessage(message, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher);
			break;
		case "message.sticker.received":
			console.log(`[${account.accountId}] Received sticker from ${message.from.id}`);
			break;
		case "message.unsupported.received":
			console.log(`[${account.accountId}] Received unsupported message type from ${message.from.id}`);
			break;
	}
}
async function handleTextMessage(message, token, account, config, runtime, core, statusSink, fetcher) {
	const { text } = message;
	if (!text?.trim()) return;
	await processMessageWithPipeline({
		message,
		token,
		account,
		config,
		runtime,
		core,
		text,
		mediaPath: void 0,
		mediaType: void 0,
		statusSink,
		fetcher
	});
}
async function handleImageMessage(message, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher) {
	const { photo, caption } = message;
	let mediaPath;
	let mediaType;
	if (photo) try {
		const maxBytes = mediaMaxMb * 1024 * 1024;
		const fetched = await core.channel.media.fetchRemoteMedia({ url: photo });
		const saved = await core.channel.media.saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", maxBytes);
		mediaPath = saved.path;
		mediaType = saved.contentType;
	} catch (err) {
		console.error(`[${account.accountId}] Failed to download Zalo image:`, err);
	}
	await processMessageWithPipeline({
		message,
		token,
		account,
		config,
		runtime,
		core,
		text: caption,
		mediaPath,
		mediaType,
		statusSink,
		fetcher
	});
}
async function processMessageWithPipeline(params) {
	const { message, token, account, config, runtime, core, text, mediaPath, mediaType, statusSink, fetcher } = params;
	const { from, chat, message_id, date } = message;
	const isGroup = chat.chat_type === "GROUP";
	const chatId = chat.id;
	const senderId = from.id;
	const senderName = from.name;
	const dmPolicy = account.config.dmPolicy ?? "pairing";
	const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
	const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
	const { senderAllowedForCommands, commandAuthorized } = await resolveSenderCommandAuthorization({
		cfg: config,
		rawBody,
		isGroup,
		dmPolicy,
		configuredAllowFrom: configAllowFrom,
		senderId,
		isSenderAllowed,
		readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalo"),
		shouldComputeCommandAuthorized: (body, cfg) => core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
		resolveCommandAuthorizedFromAuthorizers: (params) => core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params)
	});
	if (!isGroup) {
		if (dmPolicy === "disabled") {
			logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
			return;
		}
		if (dmPolicy !== "open") {
			if (!senderAllowedForCommands) {
				if (dmPolicy === "pairing") {
					const { code, created } = await core.channel.pairing.upsertPairingRequest({
						channel: "zalo",
						id: senderId,
						meta: { name: senderName ?? void 0 }
					});
					if (created) {
						logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
						try {
							await sendMessage(token, {
								chat_id: chatId,
								text: core.channel.pairing.buildPairingReply({
									channel: "zalo",
									idLine: `Your Zalo user id: ${senderId}`,
									code
								})
							}, fetcher);
							statusSink?.({ lastOutboundAt: Date.now() });
						} catch (err) {
							logVerbose(core, runtime, `zalo pairing reply failed for ${senderId}: ${String(err)}`);
						}
					}
				} else logVerbose(core, runtime, `Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`);
				return;
			}
		}
	}
	const route = core.channel.routing.resolveAgentRoute({
		cfg: config,
		channel: "zalo",
		accountId: account.accountId,
		peer: {
			kind: isGroup ? "group" : "direct",
			id: chatId
		}
	});
	if (isGroup && core.channel.commands.isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
		logVerbose(core, runtime, `zalo: drop control command from unauthorized sender ${senderId}`);
		return;
	}
	const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
	const storePath = core.channel.session.resolveStorePath(config.session?.store, { agentId: route.agentId });
	const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
	const previousTimestamp = core.channel.session.readSessionUpdatedAt({
		storePath,
		sessionKey: route.sessionKey
	});
	const body = core.channel.reply.formatAgentEnvelope({
		channel: "Zalo",
		from: fromLabel,
		timestamp: date ? date * 1e3 : void 0,
		previousTimestamp,
		envelope: envelopeOptions,
		body: rawBody
	});
	const ctxPayload = core.channel.reply.finalizeInboundContext({
		Body: body,
		BodyForAgent: rawBody,
		RawBody: rawBody,
		CommandBody: rawBody,
		From: isGroup ? `zalo:group:${chatId}` : `zalo:${senderId}`,
		To: `zalo:${chatId}`,
		SessionKey: route.sessionKey,
		AccountId: route.accountId,
		ChatType: isGroup ? "group" : "direct",
		ConversationLabel: fromLabel,
		SenderName: senderName || void 0,
		SenderId: senderId,
		CommandAuthorized: commandAuthorized,
		Provider: "zalo",
		Surface: "zalo",
		MessageSid: message_id,
		MediaPath: mediaPath,
		MediaType: mediaType,
		MediaUrl: mediaPath,
		OriginatingChannel: "zalo",
		OriginatingTo: `zalo:${chatId}`
	});
	await core.channel.session.recordInboundSession({
		storePath,
		sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
		ctx: ctxPayload,
		onRecordError: (err) => {
			runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
		}
	});
	const tableMode = core.channel.text.resolveMarkdownTableMode({
		cfg: config,
		channel: "zalo",
		accountId: account.accountId
	});
	const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
		cfg: config,
		agentId: route.agentId,
		channel: "zalo",
		accountId: account.accountId
	});
	await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
		ctx: ctxPayload,
		cfg: config,
		dispatcherOptions: {
			...prefixOptions,
			deliver: async (payload) => {
				await deliverZaloReply({
					payload,
					token,
					chatId,
					runtime,
					core,
					config,
					accountId: account.accountId,
					statusSink,
					fetcher,
					tableMode
				});
			},
			onError: (err, info) => {
				runtime.error?.(`[${account.accountId}] Zalo ${info.kind} reply failed: ${String(err)}`);
			}
		},
		replyOptions: { onModelSelected }
	});
}
async function deliverZaloReply(params) {
	const { payload, token, chatId, runtime, core, config, accountId, statusSink, fetcher } = params;
	const tableMode = params.tableMode ?? "code";
	const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
	const mediaList = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
	if (mediaList.length > 0) {
		let first = true;
		for (const mediaUrl of mediaList) {
			const caption = first ? text : void 0;
			first = false;
			try {
				await sendPhoto(token, {
					chat_id: chatId,
					photo: mediaUrl,
					caption
				}, fetcher);
				statusSink?.({ lastOutboundAt: Date.now() });
			} catch (err) {
				runtime.error?.(`Zalo photo send failed: ${String(err)}`);
			}
		}
		return;
	}
	if (text) {
		const chunkMode = core.channel.text.resolveChunkMode(config, "zalo", accountId);
		const chunks = core.channel.text.chunkMarkdownTextWithMode(text, ZALO_TEXT_LIMIT, chunkMode);
		for (const chunk of chunks) try {
			await sendMessage(token, {
				chat_id: chatId,
				text: chunk
			}, fetcher);
			statusSink?.({ lastOutboundAt: Date.now() });
		} catch (err) {
			runtime.error?.(`Zalo message send failed: ${String(err)}`);
		}
	}
}
async function monitorZaloProvider(options) {
	const { token, account, config, runtime, abortSignal, useWebhook, webhookUrl, webhookSecret, webhookPath, statusSink, fetcher: fetcherOverride } = options;
	const core = getZaloRuntime();
	const effectiveMediaMaxMb = account.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
	const fetcher = fetcherOverride ?? resolveZaloProxyFetch(account.config.proxy);
	let stopped = false;
	const stopHandlers = [];
	const stop = () => {
		stopped = true;
		for (const handler of stopHandlers) handler();
	};
	if (useWebhook) {
		if (!webhookUrl || !webhookSecret) throw new Error("Zalo webhookUrl and webhookSecret are required for webhook mode");
		if (!webhookUrl.startsWith("https://")) throw new Error("Zalo webhook URL must use HTTPS");
		if (webhookSecret.length < 8 || webhookSecret.length > 256) throw new Error("Zalo webhook secret must be 8-256 characters");
		const path = resolveWebhookPath({
			webhookPath,
			webhookUrl,
			defaultPath: null
		});
		if (!path) throw new Error("Zalo webhookPath could not be derived");
		await setWebhook(token, {
			url: webhookUrl,
			secret_token: webhookSecret
		}, fetcher);
		const unregister = registerZaloWebhookTarget({
			token,
			account,
			config,
			runtime,
			core,
			path,
			secret: webhookSecret,
			statusSink: (patch) => statusSink?.(patch),
			mediaMaxMb: effectiveMediaMaxMb,
			fetcher
		});
		stopHandlers.push(unregister);
		abortSignal.addEventListener("abort", () => {
			deleteWebhook(token, fetcher).catch(() => {});
		}, { once: true });
		return { stop };
	}
	try {
		await deleteWebhook(token, fetcher);
	} catch {}
	startPollingLoop({
		token,
		account,
		config,
		runtime,
		core,
		abortSignal,
		isStopped: () => stopped,
		mediaMaxMb: effectiveMediaMaxMb,
		statusSink,
		fetcher
	});
	return { stop };
}
var ZALO_TEXT_LIMIT, DEFAULT_MEDIA_MAX_MB, webhookTargets;
var init_monitor = __esmMin((() => {
	init_api();
	init_proxy();
	init_runtime();
	ZALO_TEXT_LIMIT = 2e3;
	DEFAULT_MEDIA_MAX_MB = 5;
	webhookTargets = /* @__PURE__ */ new Map();
}));

//#endregion
//#region extensions/zalo/src/channel.ts
init_proxy();
const meta = {
	id: "zalo",
	label: "Zalo",
	selectionLabel: "Zalo (Bot API)",
	docsPath: "/channels/zalo",
	docsLabel: "zalo",
	blurb: "Vietnam-focused messaging platform with Bot API.",
	aliases: ["zl"],
	order: 80,
	quickstartAllowFrom: true
};
function normalizeZaloMessagingTarget(raw) {
	const trimmed = raw?.trim();
	if (!trimmed) return;
	return trimmed.replace(/^(zalo|zl):/i, "");
}
const zaloDock = {
	id: "zalo",
	capabilities: {
		chatTypes: ["direct"],
		media: true,
		blockStreaming: true
	},
	outbound: { textChunkLimit: 2e3 },
	config: {
		resolveAllowFrom: ({ cfg, accountId }) => (resolveZaloAccount({
			cfg,
			accountId
		}).config.allowFrom ?? []).map((entry) => String(entry)),
		formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({
			allowFrom,
			stripPrefixRe: /^(zalo|zl):/i
		})
	},
	groups: { resolveRequireMention: () => true },
	threading: { resolveReplyToMode: () => "off" }
};
const zaloPlugin = {
	id: "zalo",
	meta,
	onboarding: zaloOnboardingAdapter,
	capabilities: {
		chatTypes: ["direct"],
		media: true,
		reactions: false,
		threads: false,
		polls: false,
		nativeCommands: false,
		blockStreaming: true
	},
	reload: { configPrefixes: ["channels.zalo"] },
	configSchema: buildChannelConfigSchema(ZaloConfigSchema),
	config: {
		listAccountIds: (cfg) => listZaloAccountIds(cfg),
		resolveAccount: (cfg, accountId) => resolveZaloAccount({
			cfg,
			accountId
		}),
		defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg),
		setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
			cfg,
			sectionKey: "zalo",
			accountId,
			enabled,
			allowTopLevel: true
		}),
		deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
			cfg,
			sectionKey: "zalo",
			accountId,
			clearBaseFields: [
				"botToken",
				"tokenFile",
				"name"
			]
		}),
		isConfigured: (account) => Boolean(account.token?.trim()),
		describeAccount: (account) => ({
			accountId: account.accountId,
			name: account.name,
			enabled: account.enabled,
			configured: Boolean(account.token?.trim()),
			tokenSource: account.tokenSource
		}),
		resolveAllowFrom: ({ cfg, accountId }) => (resolveZaloAccount({
			cfg,
			accountId
		}).config.allowFrom ?? []).map((entry) => String(entry)),
		formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({
			allowFrom,
			stripPrefixRe: /^(zalo|zl):/i
		})
	},
	security: { resolveDmPolicy: ({ cfg, accountId, account }) => {
		const basePath = resolveChannelAccountConfigBasePath({
			cfg,
			channelKey: "zalo",
			accountId: accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID
		});
		return {
			policy: account.config.dmPolicy ?? "pairing",
			allowFrom: account.config.allowFrom ?? [],
			policyPath: `${basePath}dmPolicy`,
			allowFromPath: basePath,
			approveHint: formatPairingApproveHint("zalo"),
			normalizeEntry: (raw) => raw.replace(/^(zalo|zl):/i, "")
		};
	} },
	groups: { resolveRequireMention: () => true },
	threading: { resolveReplyToMode: () => "off" },
	actions: zaloMessageActions,
	messaging: {
		normalizeTarget: normalizeZaloMessagingTarget,
		targetResolver: {
			looksLikeId: (raw) => {
				const trimmed = raw.trim();
				if (!trimmed) return false;
				return /^\d{3,}$/.test(trimmed);
			},
			hint: "<chatId>"
		}
	},
	directory: {
		self: async () => null,
		listPeers: async ({ cfg, accountId, query, limit }) => {
			const account = resolveZaloAccount({
				cfg,
				accountId
			});
			const q = query?.trim().toLowerCase() || "";
			return Array.from(new Set((account.config.allowFrom ?? []).map((entry) => String(entry).trim()).filter((entry) => Boolean(entry) && entry !== "*").map((entry) => entry.replace(/^(zalo|zl):/i, "")))).filter((id) => q ? id.toLowerCase().includes(q) : true).slice(0, limit && limit > 0 ? limit : void 0).map((id) => ({
				kind: "user",
				id
			}));
		},
		listGroups: async () => []
	},
	setup: {
		resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
		applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
			cfg,
			channelKey: "zalo",
			accountId,
			name
		}),
		validateInput: ({ accountId, input }) => {
			if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) return "ZALO_BOT_TOKEN can only be used for the default account.";
			if (!input.useEnv && !input.token && !input.tokenFile) return "Zalo requires token or --token-file (or --use-env).";
			return null;
		},
		applyAccountConfig: ({ cfg, accountId, input }) => {
			const namedConfig = applyAccountNameToChannelSection({
				cfg,
				channelKey: "zalo",
				accountId,
				name: input.name
			});
			const next = accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({
				cfg: namedConfig,
				channelKey: "zalo"
			}) : namedConfig;
			if (accountId === DEFAULT_ACCOUNT_ID) return {
				...next,
				channels: {
					...next.channels,
					zalo: {
						...next.channels?.zalo,
						enabled: true,
						...input.useEnv ? {} : input.tokenFile ? { tokenFile: input.tokenFile } : input.token ? { botToken: input.token } : {}
					}
				}
			};
			return {
				...next,
				channels: {
					...next.channels,
					zalo: {
						...next.channels?.zalo,
						enabled: true,
						accounts: {
							...next.channels?.zalo?.accounts,
							[accountId]: {
								...next.channels?.zalo?.accounts?.[accountId],
								enabled: true,
								...input.tokenFile ? { tokenFile: input.tokenFile } : input.token ? { botToken: input.token } : {}
							}
						}
					}
				}
			};
		}
	},
	pairing: {
		idLabel: "zaloUserId",
		normalizeAllowEntry: (entry) => entry.replace(/^(zalo|zl):/i, ""),
		notifyApproval: async ({ cfg, id }) => {
			const account = resolveZaloAccount({ cfg });
			if (!account.token) throw new Error("Zalo token not configured");
			await sendMessageZalo(id, PAIRING_APPROVED_MESSAGE, { token: account.token });
		}
	},
	outbound: {
		deliveryMode: "direct",
		chunker: chunkTextForOutbound,
		chunkerMode: "text",
		textChunkLimit: 2e3,
		sendText: async ({ to, text, accountId, cfg }) => {
			const { enabled: disclosureEnabled, text: disclosureText } = resolveDisclosureConfig(cfg.gateway?.aiDisclosure);
			const result = await sendMessageZalo(to, maybeInjectDisclosure(`zalo:${accountId ?? "default"}:${to}`, text, disclosureText, disclosureEnabled), {
				accountId: accountId ?? void 0,
				cfg
			});
			return {
				channel: "zalo",
				ok: result.ok,
				messageId: result.messageId ?? "",
				error: result.error ? new Error(result.error) : void 0
			};
		},
		sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
			const { enabled: disclosureEnabled, text: disclosureText } = resolveDisclosureConfig(cfg.gateway?.aiDisclosure);
			const result = await sendMessageZalo(to, maybeInjectDisclosure(`zalo:${accountId ?? "default"}:${to}`, text, disclosureText, disclosureEnabled), {
				accountId: accountId ?? void 0,
				mediaUrl,
				cfg
			});
			return {
				channel: "zalo",
				ok: result.ok,
				messageId: result.messageId ?? "",
				error: result.error ? new Error(result.error) : void 0
			};
		}
	},
	status: {
		defaultRuntime: {
			accountId: DEFAULT_ACCOUNT_ID,
			running: false,
			lastStartAt: null,
			lastStopAt: null,
			lastError: null
		},
		collectStatusIssues: collectZaloStatusIssues,
		buildChannelSummary: ({ snapshot }) => ({
			configured: snapshot.configured ?? false,
			tokenSource: snapshot.tokenSource ?? "none",
			running: snapshot.running ?? false,
			mode: snapshot.mode ?? null,
			lastStartAt: snapshot.lastStartAt ?? null,
			lastStopAt: snapshot.lastStopAt ?? null,
			lastError: snapshot.lastError ?? null,
			probe: snapshot.probe,
			lastProbeAt: snapshot.lastProbeAt ?? null
		}),
		probeAccount: async ({ account, timeoutMs }) => probeZalo(account.token, timeoutMs, resolveZaloProxyFetch(account.config.proxy)),
		buildAccountSnapshot: ({ account, runtime }) => {
			const configured = Boolean(account.token?.trim());
			return {
				accountId: account.accountId,
				name: account.name,
				enabled: account.enabled,
				configured,
				tokenSource: account.tokenSource,
				running: runtime?.running ?? false,
				lastStartAt: runtime?.lastStartAt ?? null,
				lastStopAt: runtime?.lastStopAt ?? null,
				lastError: runtime?.lastError ?? null,
				mode: account.config.webhookUrl ? "webhook" : "polling",
				lastInboundAt: runtime?.lastInboundAt ?? null,
				lastOutboundAt: runtime?.lastOutboundAt ?? null,
				dmPolicy: account.config.dmPolicy ?? "pairing"
			};
		}
	},
	gateway: { startAccount: async (ctx) => {
		const account = ctx.account;
		const token = account.token.trim();
		let zaloBotLabel = "";
		const fetcher = resolveZaloProxyFetch(account.config.proxy);
		try {
			const probe = await probeZalo(token, 2500, fetcher);
			const name = probe.ok ? probe.bot?.name?.trim() : null;
			if (name) zaloBotLabel = ` (${name})`;
			ctx.setStatus({
				accountId: account.accountId,
				bot: probe.bot
			});
		} catch {}
		ctx.log?.info(`[${account.accountId}] starting provider${zaloBotLabel}`);
		const { monitorZaloProvider } = await Promise.resolve().then(() => (init_monitor(), monitor_exports));
		return monitorZaloProvider({
			token,
			account,
			config: ctx.cfg,
			runtime: ctx.runtime,
			abortSignal: ctx.abortSignal,
			useWebhook: Boolean(account.config.webhookUrl),
			webhookUrl: account.config.webhookUrl,
			webhookSecret: account.config.webhookSecret,
			webhookPath: account.config.webhookPath,
			fetcher,
			statusSink: (patch) => ctx.setStatus({
				accountId: ctx.accountId,
				...patch
			})
		});
	} }
};

//#endregion
//#region extensions/zalo/index.ts
init_monitor();
init_runtime();
const plugin = {
	id: "zalo",
	name: "Zalo",
	description: "Zalo channel plugin (Bot API)",
	configSchema: emptyPluginConfigSchema(),
	register(api) {
		setZaloRuntime(api.runtime);
		api.registerChannel({
			plugin: zaloPlugin,
			dock: zaloDock
		});
		api.registerHttpHandler(handleZaloWebhookRequest);
	}
};

//#endregion
export { plugin as default };