UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,671 lines 61.5 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { t as createNonExitingRuntime } from "./runtime-B4lgFmsS.js";
import { a as shouldLogVerbose, r as logVerbose, t as danger } from "./globals-GTrXU4s9.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import { n as normalizePluginHttpPath } from "./http-route-overlap-D5FGt6xV.js";
import { f as saveMediaStream } from "./store-C9M_0A1p.js";
import { i as shouldComputeCommandAuthorized } from "./command-detection-iKcBJ8it.js";
import { m as resolveSendableOutboundReplyParts } from "./reply-payload-D-VMfpYI.js";
import { a as resolveInboundLastRouteSessionKey, i as resolveAgentRoute } from "./resolve-route-xcIS8NFL.js";
import { n as firstDefined } from "./allow-from-Ddq5GeWG.js";
import { a as warnMissingProviderGroupPolicyFallbackOnce, n as resolveAllowlistProviderRuntimeGroupPolicy, r as resolveDefaultGroupPolicy } from "./runtime-group-policy-CSD1Is8G.js";
import { l as resolvePinnedMainDmOwnerFromAllowlist } from "./dm-policy-shared-BjnswXJt.js";
import { r as chunkMarkdownText } from "./chunk-Cpb8JO1x.js";
import "./runtime-config-snapshot-CDoFLjqb.js";
import { t as waitForAbortSignal } from "./abort-signal-BAyXz5Zx.js";
import "./runtime-env-D_2q8-VK.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { n as buildMentionRegexes, r as matchesMentionPatterns } from "./mentions-Be0DZkGs.js";
import { t as finalizeInboundContext } from "./inbound-context-CZx-NgvC.js";
import "./routing-CQ63ICPX.js";
import { r as formatInboundEnvelope } from "./envelope-IMcsY0tl.js";
import { h as hasFinalChannelTurnDispatch } from "./kernel-BdraY_qZ.js";
import "./history-6XlO5h6s.js";
import { t as createChannelHistoryWindow } from "./history-window-B0hyPVKC.js";
import { n as recordChannelActivity } from "./channel-activity-4piA219h.js";
import { a as readChannelAllowFromStore, d as upsertChannelPairingRequest } from "./pairing-store-CxANz_ON.js";
import "./allow-from-DCaV3hzI.js";
import "./reply-history-Y9HXucO9.js";
import "./reply-runtime--Kbd63wS.js";
import "./reply-dispatch-runtime-B3glKPcA.js";
import { c as createMessageReceiveContext } from "./channel-outbound-B3_Zy-kG.js";
import { a as isRequestBodyLimitError, c as requestBodyErrorToText, s as readRequestBodyWithLimit } from "./http-body-DASeqdKJ.js";
import "./media-store-B6kmSr5u.js";
import { n as resolveConfiguredBindingRoute, r as resolveRuntimeConversationBindingRoute, t as ensureConfiguredBindingRouteReady } from "./binding-routing-C-pxzG2P.js";
import "./conversation-runtime-VjM1-D7a.js";
import { t as resolvePairingIdLabel } from "./pairing-labels-BrpGMnBC.js";
import "./command-auth-native-D4ML0UKp.js";
import { a as resolveInboundSessionEnvelopeContext, o as formatLocationText, s as toLocationContext } from "./channel-inbound-bl7VmTdr.js";
import { i as resolveStableChannelMessageIngress } from "./message-access-mwxdTUGC.js";
import "./channel-ingress-runtime-rGltVkKC.js";
import { t as createChannelPairingChallengeIssuer } from "./channel-pairing-Bk6_JhTm.js";
import { t as createClaimableDedupe } from "./persistent-dedupe-tc3aSkBu.js";
import { a as createWebhookInFlightLimiter, i as beginWebhookRequestPipelineOrReject } from "./webhook-request-guards-gS2khsvD.js";
import "./webhook-ingress-Dgd3Jtks.js";
import { t as normalizeWebhookPath } from "./webhook-path-CaYfbDPb.js";
import { i as resolveSingleWebhookTarget, n as registerWebhookTargetWithPluginRoute } from "./webhook-targets-Beqkt_FW.js";
import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-9LM3JYQX.js";
import { a as resolveLineGroupConfigEntry, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-CsrscOdC.js";
import { D as showLoadingAnimation, S as pushMessagesLine, T as replyMessageLine, c as processLineMessage, d as createImageMessage, f as createLocationMessage, g as getUserDisplayName, k as buildTemplateMessageFromPayload, m as createTextMessageWithQuickReplies, p as createQuickReplyItems, u as createFlexMessage, w as pushTextMessageWithQuickReplies, x as pushMessageLine } from "./markdown-to-line-B11KtHgF.js";
import { a as buildPath, i as mergeHeaders, n as HTTPFetchClient, r as convertResponseToReadable } from "./messagingApiClient-D72iuAe_.js";
import crypto from "node:crypto";
//#region extensions/line/src/bot-access.ts
function normalizeLineAllowEntry(value) {
	const trimmed = String(value).trim();
	if (!trimmed) return "";
	if (trimmed === "*") return "*";
	return trimmed.replace(/^line:(?:user:)?/i, "");
}
const normalizeAllowFrom = (list) => {
	const entries = (list ?? []).map((value) => normalizeLineAllowEntry(value)).filter(Boolean);
	return {
		entries,
		hasWildcard: entries.includes("*"),
		hasEntries: entries.length > 0
	};
};
//#endregion
//#region node_modules/@line/bot-sdk/dist/messaging-api/api/messagingApiBlobClient.js
/**
* LINE Messaging API
* This document describes LINE Messaging API.
*
* The version of the OpenAPI document: 0.0.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* @see {@link LineBotClient} for a unified interface that wraps this client.
*/
var MessagingApiBlobClient = class {
	httpClient;
	constructor(config) {
		const baseURL = config.baseURL || "https://api-data.line.me";
		const defaultHeaders = mergeHeaders(config.defaultHeaders, { Authorization: "Bearer " + config.channelAccessToken });
		this.httpClient = new HTTPFetchClient({
			defaultHeaders,
			baseURL
		});
	}
	/**
	* Download image, video, and audio data sent from users.
	* @param messageId Message ID of video or audio
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#get-content"> Documentation</a>
	*/
	async getMessageContent(messageId) {
		return (await this.getMessageContentWithHttpInfo(messageId)).body;
	}
	/**
	* Download image, video, and audio data sent from users..
	* This method includes HttpInfo object to return additional information.
	* @param messageId Message ID of video or audio
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#get-content"> Documentation</a>
	*/
	async getMessageContentWithHttpInfo(messageId) {
		const requestPath = buildPath("/v2/bot/message/{messageId}/content", { messageId });
		const response = await this.httpClient.get(requestPath);
		return {
			httpResponse: response,
			body: convertResponseToReadable(response)
		};
	}
	/**
	* Get a preview image of the image or video
	* @param messageId Message ID of image or video
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#get-image-or-video-preview"> Documentation</a>
	*/
	async getMessageContentPreview(messageId) {
		return (await this.getMessageContentPreviewWithHttpInfo(messageId)).body;
	}
	/**
	* Get a preview image of the image or video.
	* This method includes HttpInfo object to return additional information.
	* @param messageId Message ID of image or video
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#get-image-or-video-preview"> Documentation</a>
	*/
	async getMessageContentPreviewWithHttpInfo(messageId) {
		const requestPath = buildPath("/v2/bot/message/{messageId}/content/preview", { messageId });
		const response = await this.httpClient.get(requestPath);
		return {
			httpResponse: response,
			body: convertResponseToReadable(response)
		};
	}
	/**
	* Verify the preparation status of a video or audio for getting
	* @param messageId Message ID of video or audio
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#verify-video-or-audio-preparation-status"> Documentation</a>
	*/
	async getMessageContentTranscodingByMessageId(messageId) {
		return (await this.getMessageContentTranscodingByMessageIdWithHttpInfo(messageId)).body;
	}
	/**
	* Verify the preparation status of a video or audio for getting.
	* This method includes HttpInfo object to return additional information.
	* @param messageId Message ID of video or audio
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#verify-video-or-audio-preparation-status"> Documentation</a>
	*/
	async getMessageContentTranscodingByMessageIdWithHttpInfo(messageId) {
		const requestPath = buildPath("/v2/bot/message/{messageId}/content/transcoding", { messageId });
		const res = await this.httpClient.get(requestPath);
		const text = await res.text();
		return {
			httpResponse: res,
			body: text ? JSON.parse(text) : null
		};
	}
	/**
	* Download rich menu image.
	* @param richMenuId ID of the rich menu with the image to be downloaded
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#download-rich-menu-image"> Documentation</a>
	*/
	async getRichMenuImage(richMenuId) {
		return (await this.getRichMenuImageWithHttpInfo(richMenuId)).body;
	}
	/**
	* Download rich menu image..
	* This method includes HttpInfo object to return additional information.
	* @param richMenuId ID of the rich menu with the image to be downloaded
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#download-rich-menu-image"> Documentation</a>
	*/
	async getRichMenuImageWithHttpInfo(richMenuId) {
		const requestPath = buildPath("/v2/bot/richmenu/{richMenuId}/content", { richMenuId });
		const response = await this.httpClient.get(requestPath);
		return {
			httpResponse: response,
			body: convertResponseToReadable(response)
		};
	}
	/**
	* Upload rich menu image
	* @param richMenuId The ID of the rich menu to attach the image to
	* @param body
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#upload-rich-menu-image"> Documentation</a>
	*/
	async setRichMenuImage(richMenuId, body) {
		return (await this.setRichMenuImageWithHttpInfo(richMenuId, body)).body;
	}
	/**
	* Upload rich menu image.
	* This method includes HttpInfo object to return additional information.
	* @param richMenuId The ID of the rich menu to attach the image to
	* @param body
	*
	* @see <a href="https://developers.line.biz/en/reference/messaging-api/#upload-rich-menu-image"> Documentation</a>
	*/
	async setRichMenuImageWithHttpInfo(richMenuId, body) {
		const requestPath = buildPath("/v2/bot/richmenu/{richMenuId}/content", { richMenuId });
		const params = body;
		const res = await this.httpClient.postBinaryContent(requestPath, params);
		const text = await res.text();
		return {
			httpResponse: res,
			body: text ? JSON.parse(text) : null
		};
	}
};
//#endregion
//#region extensions/line/src/download.ts
async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024) {
	const saved = await saveMediaStream(await new MessagingApiBlobClient({ channelAccessToken }).getMessageContent(messageId), void 0, "inbound", maxBytes);
	logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
	return {
		path: saved.path,
		contentType: saved.contentType,
		size: saved.size
	};
}
//#endregion
//#region extensions/line/src/auto-reply-delivery.ts
async function deliverLineAutoReply(params) {
	const { payload, lineData, replyToken, accountId, to, textLimit, deps } = params;
	let replyTokenUsed = params.replyTokenUsed;
	const pushLineMessages = async (messages) => {
		if (messages.length === 0) return;
		for (let i = 0; i < messages.length; i += 5) await deps.pushMessagesLine(to, messages.slice(i, i + 5), {
			cfg: params.cfg,
			accountId
		});
	};
	const sendLineMessages = async (messages, allowReplyToken) => {
		if (messages.length === 0) return;
		let remaining = messages;
		if (allowReplyToken && replyToken && !replyTokenUsed) {
			const replyBatch = remaining.slice(0, 5);
			try {
				await deps.replyMessageLine(replyToken, replyBatch, {
					cfg: params.cfg,
					accountId
				});
			} catch (err) {
				deps.onReplyError?.(err);
				await pushLineMessages(replyBatch);
			}
			replyTokenUsed = true;
			remaining = remaining.slice(replyBatch.length);
		}
		if (remaining.length > 0) await pushLineMessages(remaining);
	};
	const richMessages = [];
	const hasQuickReplies = Boolean(lineData.quickReplies?.length);
	if (lineData.flexMessage) richMessages.push(deps.createFlexMessage(lineData.flexMessage.altText.slice(0, 400), lineData.flexMessage.contents));
	if (lineData.templateMessage) {
		const templateMsg = deps.buildTemplateMessageFromPayload(lineData.templateMessage);
		if (templateMsg) richMessages.push(templateMsg);
	}
	if (lineData.location) richMessages.push(deps.createLocationMessage(lineData.location));
	const processed = payload.text ? deps.processLineMessage(payload.text) : {
		text: "",
		flexMessages: []
	};
	for (const flexMsg of processed.flexMessages) richMessages.push(deps.createFlexMessage(flexMsg.altText.slice(0, 400), flexMsg.contents));
	const chunks = processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
	const mediaMessages = resolveSendableOutboundReplyParts(payload).mediaUrls.map((url) => url?.trim()).filter((url) => Boolean(url)).map((url) => deps.createImageMessage(url));
	if (chunks.length > 0) {
		const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0;
		if (hasQuickReplies && hasRichOrMedia) try {
			await sendLineMessages([...richMessages, ...mediaMessages], false);
		} catch (err) {
			deps.onReplyError?.(err);
		}
		const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
			to,
			chunks,
			quickReplies: lineData.quickReplies,
			replyToken,
			replyTokenUsed,
			cfg: params.cfg,
			accountId,
			replyMessageLine: deps.replyMessageLine,
			pushMessageLine: deps.pushMessageLine,
			pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies,
			createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies
		});
		replyTokenUsed = nextReplyTokenUsed;
		if (!hasQuickReplies || !hasRichOrMedia) {
			await sendLineMessages(richMessages, false);
			if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
		}
	} else {
		const combined = [...richMessages, ...mediaMessages];
		if (hasQuickReplies && combined.length === 0) {
			const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
				to,
				chunks: [buildLineQuickReplyFallbackText(lineData.quickReplies)],
				quickReplies: lineData.quickReplies,
				replyToken,
				replyTokenUsed,
				cfg: params.cfg,
				accountId,
				replyMessageLine: deps.replyMessageLine,
				pushMessageLine: deps.pushMessageLine,
				pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies,
				createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
				onReplyError: deps.onReplyError
			});
			replyTokenUsed = nextReplyTokenUsed;
		} else {
			if (hasQuickReplies && combined.length > 0) {
				const quickReply = deps.createQuickReplyItems(lineData.quickReplies);
				const targetIndex = replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1;
				combined[targetIndex] = {
					...combined[targetIndex],
					quickReply
				};
			}
			await sendLineMessages(combined, true);
		}
	}
	return { replyTokenUsed };
}
//#endregion
//#region extensions/line/src/bot-message-context.ts
function getLineSourceInfo(source) {
	if (!source) return {
		userId: void 0,
		groupId: void 0,
		roomId: void 0,
		isGroup: false
	};
	return {
		userId: source.type === "user" ? source.userId : source.type === "group" ? source.userId : source.type === "room" ? source.userId : void 0,
		groupId: source.type === "group" ? source.groupId : void 0,
		roomId: source.type === "room" ? source.roomId : void 0,
		isGroup: source.type === "group" || source.type === "room"
	};
}
function buildPeerId(source) {
	if (!source) return "unknown";
	const groupKey = normalizeOptionalString(source.type === "group" ? source.groupId : void 0) ?? normalizeOptionalString(source.type === "room" ? source.roomId : void 0);
	if (groupKey) return groupKey;
	if (source.type === "user" && source.userId) return source.userId;
	return "unknown";
}
async function resolveLineInboundRoute(params) {
	recordChannelActivity({
		channel: "line",
		accountId: params.account.accountId,
		direction: "inbound"
	});
	const { userId, groupId, roomId, isGroup } = getLineSourceInfo(params.source);
	const peerId = buildPeerId(params.source);
	let route = resolveAgentRoute({
		cfg: params.cfg,
		channel: "line",
		accountId: params.account.accountId,
		peer: {
			kind: isGroup ? "group" : "direct",
			id: peerId
		}
	});
	const configuredRoute = resolveConfiguredBindingRoute({
		cfg: params.cfg,
		route,
		conversation: {
			channel: "line",
			accountId: params.account.accountId,
			conversationId: peerId
		}
	});
	let configuredBinding = configuredRoute.bindingResolution;
	const configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
	route = configuredRoute.route;
	const runtimeRoute = resolveRuntimeConversationBindingRoute({
		route,
		conversation: {
			channel: "line",
			accountId: params.account.accountId,
			conversationId: peerId
		}
	});
	route = runtimeRoute.route;
	if (runtimeRoute.bindingRecord) {
		configuredBinding = null;
		logVerbose(runtimeRoute.boundSessionKey ? `line: routed via bound conversation ${peerId} -> ${runtimeRoute.boundSessionKey}` : `line: plugin-bound conversation ${peerId}`);
	}
	if (configuredBinding) {
		const ensured = await ensureConfiguredBindingRouteReady({
			cfg: params.cfg,
			bindingResolution: configuredBinding
		});
		if (!ensured.ok) {
			logVerbose(`line: configured ACP binding unavailable for ${peerId} -> ${configuredBindingSessionKey}: ${ensured.error}`);
			throw new Error(`Configured ACP binding unavailable: ${ensured.error}`);
		}
		logVerbose(`line: using configured ACP binding for ${peerId} -> ${configuredBindingSessionKey}`);
	}
	return {
		userId,
		groupId,
		roomId,
		isGroup,
		peerId,
		route
	};
}
const STICKER_PACKAGES = {
	"1": "Moon & James",
	"2": "Cony & Brown",
	"3": "Brown & Friends",
	"4": "Moon Special",
	"789": "LINE Characters",
	"6136": "Cony's Happy Life",
	"6325": "Brown's Life",
	"6359": "Choco",
	"6362": "Sally",
	"6370": "Edward",
	"11537": "Cony",
	"11538": "Brown",
	"11539": "Moon"
};
function describeStickerKeywords(sticker) {
	const keywords = sticker.keywords;
	if (keywords && keywords.length > 0) return keywords.slice(0, 3).join(", ");
	const stickerText = sticker.text;
	if (stickerText) return stickerText;
	return "";
}
function extractMessageText(message) {
	if (message.type === "text") return message.text;
	if (message.type === "location") {
		const loc = message;
		return formatLocationText({
			latitude: loc.latitude,
			longitude: loc.longitude,
			name: loc.title,
			address: loc.address
		}) ?? "";
	}
	if (message.type === "sticker") {
		const sticker = message;
		const packageName = STICKER_PACKAGES[sticker.packageId] ?? "sticker";
		const keywords = describeStickerKeywords(sticker);
		if (keywords) return `[Sent a ${packageName} sticker: ${keywords}]`;
		return `[Sent a ${packageName} sticker]`;
	}
	return "";
}
function extractMediaPlaceholder(message) {
	switch (message.type) {
		case "image": return "<media:image>";
		case "video": return "<media:video>";
		case "audio": return "<media:audio>";
		case "file": return "<media:document>";
		default: return "";
	}
}
function resolveLineConversationLabel(params) {
	return params.isGroup ? params.groupId ? `group:${params.groupId}` : params.roomId ? `room:${params.roomId}` : "unknown-group" : params.senderLabel;
}
function resolveLineAddresses(params) {
	const fromAddress = params.isGroup ? params.groupId ? `line:group:${params.groupId}` : params.roomId ? `line:room:${params.roomId}` : `line:${params.peerId}` : `line:${params.userId ?? params.peerId}`;
	return {
		fromAddress,
		toAddress: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`,
		originatingTo: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`
	};
}
async function finalizeLineInboundContext(params) {
	const { fromAddress, toAddress, originatingTo } = resolveLineAddresses({
		isGroup: params.source.isGroup,
		groupId: params.source.groupId,
		roomId: params.source.roomId,
		userId: params.source.userId,
		peerId: params.source.peerId
	});
	const senderId = params.source.userId ?? "unknown";
	const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
	const conversationLabel = resolveLineConversationLabel({
		isGroup: params.source.isGroup,
		groupId: params.source.groupId,
		roomId: params.source.roomId,
		senderLabel
	});
	const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
		cfg: params.cfg,
		agentId: params.route.agentId,
		sessionKey: params.route.sessionKey
	});
	const body = formatInboundEnvelope({
		channel: "LINE",
		from: conversationLabel,
		timestamp: params.timestamp,
		body: params.rawBody,
		chatType: params.source.isGroup ? "group" : "direct",
		sender: { id: senderId },
		previousTimestamp,
		envelope: envelopeOptions
	});
	const ctxPayload = finalizeInboundContext({
		Body: body,
		BodyForAgent: params.rawBody,
		RawBody: params.rawBody,
		CommandBody: params.rawBody,
		From: fromAddress,
		To: toAddress,
		SessionKey: params.route.sessionKey,
		AccountId: params.route.accountId,
		ChatType: params.source.isGroup ? "group" : "direct",
		ConversationLabel: conversationLabel,
		GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
		SenderId: senderId,
		Provider: "line",
		Surface: "line",
		MessageSid: params.messageSid,
		Timestamp: params.timestamp,
		MediaPath: params.media.firstPath,
		MediaType: params.media.firstContentType,
		MediaUrl: params.media.firstPath,
		MediaPaths: params.media.paths,
		MediaUrls: params.media.paths,
		MediaTypes: params.media.types,
		...params.locationContext,
		CommandAuthorized: params.commandAuthorized,
		OriginatingChannel: "line",
		OriginatingTo: originatingTo,
		GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
			groupId: params.source.groupId,
			roomId: params.source.roomId
		})?.systemPrompt) : void 0,
		InboundHistory: params.inboundHistory
	});
	const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
		dmScope: params.cfg.session?.dmScope,
		allowFrom: params.account.config.allowFrom,
		normalizeEntry: (entry) => normalizeAllowFrom([entry]).entries[0]
	}) : null;
	const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
		route: params.route,
		sessionKey: params.route.sessionKey
	});
	if (shouldLogVerbose()) {
		const preview = body.slice(0, 200).replace(/\n/g, "\\n");
		const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
		logVerbose(`${params.verboseLog.kind === "inbound" ? "line inbound" : "line postback"}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
	}
	return {
		ctxPayload,
		replyToken: params.event.replyToken,
		turn: {
			storePath,
			record: {
				updateLastRoute: !params.source.isGroup ? {
					sessionKey: inboundLastRouteSessionKey,
					channel: "line",
					to: params.source.userId ?? params.source.peerId,
					accountId: params.route.accountId,
					mainDmOwnerPin: inboundLastRouteSessionKey === params.route.mainSessionKey && pinnedMainDmOwner && params.source.userId ? {
						ownerRecipient: pinnedMainDmOwner,
						senderRecipient: params.source.userId,
						onSkip: ({ ownerRecipient, senderRecipient }) => {
							logVerbose(`line: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`);
						}
					} : void 0
				} : void 0,
				onRecordError: (err) => {
					logVerbose(`line: failed updating session meta: ${String(err)}`);
				}
			}
		}
	};
}
async function buildLineMessageContext(params) {
	const { event, allMedia, cfg, account, commandAuthorized, groupHistories, historyLimit } = params;
	const source = event.source;
	const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
		source,
		cfg,
		account
	});
	const message = event.message;
	const messageId = message.id;
	const timestamp = event.timestamp;
	const textContent = extractMessageText(message);
	const placeholder = extractMediaPlaceholder(message);
	let rawBody = textContent || placeholder;
	if (!rawBody && allMedia.length > 0) rawBody = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
	if (!rawBody && allMedia.length === 0) return null;
	let locationContext;
	if (message.type === "location") {
		const loc = message;
		locationContext = toLocationContext({
			latitude: loc.latitude,
			longitude: loc.longitude,
			name: loc.title,
			address: loc.address
		});
	}
	const historyKey = isGroup ? peerId : void 0;
	const inboundHistory = historyKey && groupHistories && (historyLimit ?? 0) > 0 ? createChannelHistoryWindow({ historyMap: groupHistories }).buildInboundHistory({
		historyKey,
		limit: historyLimit ?? 0
	}) : void 0;
	const finalized = await finalizeLineInboundContext({
		cfg,
		account,
		event,
		route,
		source: {
			userId,
			groupId,
			roomId,
			isGroup,
			peerId
		},
		rawBody,
		timestamp,
		messageSid: messageId,
		commandAuthorized,
		media: {
			firstPath: allMedia[0]?.path,
			firstContentType: allMedia[0]?.contentType,
			paths: allMedia.length > 0 ? allMedia.map((m) => m.path) : void 0,
			types: allMedia.length > 0 ? allMedia.map((m) => m.contentType).filter(Boolean) : void 0
		},
		locationContext,
		verboseLog: {
			kind: "inbound",
			mediaCount: allMedia.length
		},
		inboundHistory
	});
	return {
		ctxPayload: finalized.ctxPayload,
		turn: finalized.turn,
		event,
		userId,
		groupId,
		roomId,
		isGroup,
		route,
		replyToken: event.replyToken,
		accountId: account.accountId
	};
}
async function buildLinePostbackContext(params) {
	const { event, cfg, account, commandAuthorized } = params;
	const source = event.source;
	const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
		source,
		cfg,
		account
	});
	const timestamp = event.timestamp;
	const rawData = event.postback?.data?.trim() ?? "";
	if (!rawData) return null;
	let rawBody = rawData;
	if (rawData.includes("line.action=")) {
		const searchParams = new URLSearchParams(rawData);
		const action = searchParams.get("line.action") ?? "";
		const device = searchParams.get("line.device");
		rawBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
	}
	const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
	const finalized = await finalizeLineInboundContext({
		cfg,
		account,
		event,
		route,
		source: {
			userId,
			groupId,
			roomId,
			isGroup,
			peerId
		},
		rawBody,
		timestamp,
		messageSid,
		commandAuthorized,
		media: {
			firstPath: "",
			firstContentType: void 0,
			paths: void 0,
			types: void 0
		},
		verboseLog: { kind: "postback" }
	});
	return {
		ctxPayload: finalized.ctxPayload,
		turn: finalized.turn,
		event,
		userId,
		groupId,
		roomId,
		isGroup,
		route,
		replyToken: event.replyToken,
		accountId: account.accountId
	};
}
//#endregion
//#region extensions/line/src/bot-handlers.ts
const LINE_DOWNLOADABLE_MESSAGE_TYPES = new Set([
	"image",
	"video",
	"audio",
	"file"
]);
function isDownloadableLineMessageType(messageType) {
	return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
}
const LINE_WEBHOOK_REPLAY_WINDOW_MS = 600 * 1e3;
const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
function normalizeLineIngressEntry(value) {
	return normalizeLineAllowEntry(value) || null;
}
var LineRetryableWebhookError = class extends Error {
	constructor(message, options) {
		super(message, options);
		this.name = "LineRetryableWebhookError";
	}
};
function createLineWebhookReplayCache() {
	return createClaimableDedupe({
		ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
		memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
	});
}
function buildLineWebhookReplayKey(event, accountId) {
	if (event.type === "message") {
		const messageId = event.message?.id?.trim();
		if (messageId) return {
			key: `${accountId}|message:${messageId}`,
			eventId: `message:${messageId}`
		};
	}
	const eventId = event.webhookEventId?.trim();
	if (!eventId) return null;
	const source = event.source;
	const sourceId = source?.type === "group" ? `group:${source.groupId ?? ""}` : source?.type === "room" ? `room:${source.roomId ?? ""}` : `user:${source?.userId ?? ""}`;
	return {
		key: `${accountId}|${event.type}|${sourceId}|${eventId}`,
		eventId: `event:${eventId}`
	};
}
function getLineReplayCandidate(event, context) {
	const replay = buildLineWebhookReplayKey(event, context.account.accountId);
	const cache = context.replayCache;
	if (!replay || !cache) return null;
	return {
		key: replay.key,
		eventId: replay.eventId,
		cache
	};
}
async function claimLineReplayEvent(candidate) {
	const claim = await candidate.cache.claim(candidate.key);
	if (claim.kind === "claimed") return { skip: false };
	if (claim.kind === "inflight") {
		logVerbose(`line: skipped in-flight replayed webhook event ${candidate.eventId}`);
		return {
			skip: true,
			inFlightResult: claim.pending.then(() => void 0)
		};
	}
	logVerbose(`line: skipped replayed webhook event ${candidate.eventId}`);
	return { skip: true };
}
function resolveLineGroupConfig(params) {
	return resolveLineGroupConfigEntry(params.config.groups, {
		groupId: params.groupId,
		roomId: params.roomId
	});
}
async function sendLinePairingReply(params) {
	const { senderId, replyToken, context } = params;
	const idLabel = (() => {
		try {
			return resolvePairingIdLabel("line");
		} catch {
			return "lineUserId";
		}
	})();
	await createChannelPairingChallengeIssuer({
		channel: "line",
		upsertPairingRequest: async ({ id, meta }) => await upsertChannelPairingRequest({
			channel: "line",
			id,
			accountId: context.account.accountId,
			meta
		})
	})({
		senderId,
		senderIdLine: `Your ${idLabel}: ${senderId}`,
		onCreated: () => {
			logVerbose(`line pairing request sender=${senderId}`);
		},
		sendPairingReply: async (text) => {
			if (replyToken) try {
				await replyMessageLine(replyToken, [{
					type: "text",
					text
				}], {
					cfg: context.cfg,
					accountId: context.account.accountId,
					channelAccessToken: context.account.channelAccessToken
				});
				return;
			} catch (err) {
				logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
			}
			try {
				await pushMessageLine(`line:${senderId}`, text, {
					cfg: context.cfg,
					accountId: context.account.accountId,
					channelAccessToken: context.account.channelAccessToken
				});
			} catch (err) {
				logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
			}
		}
	});
}
async function shouldProcessLineEvent(event, context) {
	const { cfg, account } = context;
	const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
	const senderId = userId ?? "";
	const groupConfig = resolveLineGroupConfig({
		config: account.config,
		groupId,
		roomId
	});
	const rawText = resolveEventRawText(event);
	const requireMention = isGroup ? groupConfig?.requireMention !== false : false;
	const dmPolicy = account.config.dmPolicy ?? "pairing";
	const { groupPolicy: runtimeGroupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
		providerConfigPresent: cfg.channels?.line !== void 0,
		groupPolicy: account.config.groupPolicy,
		defaultGroupPolicy: resolveDefaultGroupPolicy(cfg)
	});
	const groupPolicy = runtimeGroupPolicy === "disabled" ? "disabled" : groupConfig?.allowFrom !== void 0 ? "allowlist" : runtimeGroupPolicy;
	const groupAllowFrom = normalizeStringEntries(firstDefined(groupConfig?.allowFrom, account.config.groupAllowFrom, account.config.allowFrom?.length ? account.config.allowFrom : void 0));
	const mentionFacts = (() => {
		if (!isGroup || event.type !== "message") return {
			canDetectMention: false,
			wasMentioned: false,
			hasAnyMention: false
		};
		const peerId = groupId ?? roomId ?? userId ?? "unknown";
		const { agentId } = resolveAgentRoute({
			cfg,
			channel: "line",
			accountId: account.accountId,
			peer: {
				kind: "group",
				id: peerId
			}
		});
		const mentionRegexes = buildMentionRegexes(cfg, agentId);
		const wasMentionedByNative = isLineBotMentioned(event.message);
		const wasMentionedByPattern = event.message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
		return {
			canDetectMention: event.message.type === "text",
			wasMentioned: wasMentionedByNative || wasMentionedByPattern,
			hasAnyMention: hasAnyLineMention(event.message)
		};
	})();
	const access = await resolveStableChannelMessageIngress({
		channelId: "line",
		accountId: account.accountId,
		identity: {
			key: "line-user-id",
			normalize: normalizeLineIngressEntry,
			sensitivity: "pii",
			entryIdPrefix: "line-entry"
		},
		cfg,
		readStoreAllowFrom: async () => await readChannelAllowFromStore("line", void 0, account.accountId),
		subject: { stableId: senderId },
		conversation: {
			kind: isGroup ? "group" : "direct",
			id: (groupId ?? roomId ?? senderId) || "unknown"
		},
		...isGroup && groupConfig?.enabled === false ? { route: {
			id: "line:group-config",
			enabled: false
		} } : {},
		mentionFacts: isGroup && event.type === "message" ? {
			canDetectMention: mentionFacts.canDetectMention,
			wasMentioned: mentionFacts.wasMentioned,
			hasAnyMention: mentionFacts.hasAnyMention,
			implicitMentionKinds: []
		} : void 0,
		event: { kind: event.type === "postback" ? "postback" : "message" },
		dmPolicy,
		groupPolicy,
		policy: {
			groupAllowFromFallbackToAllowFrom: false,
			activation: {
				requireMention: isGroup && event.type === "message" && requireMention,
				allowTextCommands: true
			}
		},
		allowFrom: normalizeStringEntries(account.config.allowFrom),
		groupAllowFrom,
		command: {
			hasControlCommand: shouldComputeCommandAuthorized(rawText, cfg),
			groupOwnerAllowFrom: "none"
		}
	});
	warnMissingProviderGroupPolicyFallbackOnce({
		providerMissingFallbackApplied,
		providerKey: "line",
		accountId: account.accountId,
		log: (message) => logVerbose(message)
	});
	if (access.senderAccess.decision === "allow" && (access.ingress.admission === "dispatch" || access.ingress.admission === "observe" || access.ingress.admission === "skip")) return access;
	if (access.senderAccess.decision === "allow") {
		logVerbose(`Blocked line event (${access.ingress.reasonCode})`);
		return null;
	}
	if (isGroup) {
		if (groupConfig?.enabled === false) {
			logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
			return null;
		}
		if (groupConfig?.allowFrom !== void 0) {
			if (!senderId) {
				logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
				return null;
			}
			if (access.senderAccess.reasonCode !== "group_policy_allowed") {
				logVerbose(`Blocked line group sender ${senderId} (group allowFrom override)`);
				return null;
			}
		}
		if (access.senderAccess.reasonCode === "group_policy_disabled") logVerbose("Blocked line group message (groupPolicy: disabled)");
		else if (!senderId && groupPolicy === "allowlist") logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
		else if (access.senderAccess.reasonCode === "group_policy_empty_allowlist") logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
		else logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
		return null;
	}
	if (access.senderAccess.reasonCode === "dm_policy_disabled") {
		logVerbose("Blocked line sender (dmPolicy: disabled)");
		return null;
	}
	if (access.senderAccess.decision === "pairing") {
		if (!senderId) {
			logVerbose("Blocked line sender (dmPolicy: pairing, no sender ID)");
			return null;
		}
		await sendLinePairingReply({
			senderId,
			replyToken: "replyToken" in event ? event.replyToken : void 0,
			context
		});
		return null;
	}
	logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${account.config.dmPolicy ?? "pairing"})`);
	return null;
}
function getLineMentionees(message) {
	if (message.type !== "text") return [];
	const mentionees = message.mention?.mentionees;
	return Array.isArray(mentionees) ? mentionees : [];
}
function isLineBotMentioned(message) {
	return getLineMentionees(message).some((m) => m.isSelf === true || m.type === "all");
}
function hasAnyLineMention(message) {
	return getLineMentionees(message).length > 0;
}
function resolveEventRawText(event) {
	if (event.type === "message") {
		const msg = event.message;
		if (msg.type === "text") return msg.text;
		return "";
	}
	if (event.type === "postback") return event.postback?.data?.trim() ?? "";
	return "";
}
async function handleMessageEvent(event, context) {
	const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
	const message = event.message;
	const decision = await shouldProcessLineEvent(event, context);
	if (!decision) return;
	const { isGroup, groupId, roomId } = getLineSourceInfo(event.source);
	if (isGroup && decision.activationAccess.shouldSkip) {
		const rawText = message.type === "text" ? message.text : "";
		const sourceInfo = getLineSourceInfo(event.source);
		logVerbose(`line: skipping group message (requireMention, not mentioned)`);
		const historyKey = groupId ?? roomId;
		const senderId = sourceInfo.userId ?? "unknown";
		if (historyKey && context.groupHistories) createChannelHistoryWindow({ historyMap: context.groupHistories }).record({
			historyKey,
			limit: context.historyLimit ?? 50,
			entry: {
				sender: `user:${senderId}`,
				body: rawText || `<${message.type}>`,
				timestamp: event.timestamp
			}
		});
		return;
	}
	const allMedia = [];
	if (isDownloadableLineMessageType(message.type)) try {
		const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes);
		allMedia.push({
			path: media.path,
			contentType: media.contentType
		});
	} catch (err) {
		const errMsg = String(err);
		if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
		else runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
	}
	const messageContext = await buildLineMessageContext({
		event,
		allMedia,
		cfg,
		account,
		commandAuthorized: decision.commandAccess.authorized,
		groupHistories: context.groupHistories,
		historyLimit: context.historyLimit ?? 50
	});
	if (!messageContext) {
		logVerbose("line: skipping empty message");
		return;
	}
	await processMessage(messageContext);
	if (isGroup && context.groupHistories) {
		const historyKey = groupId ?? roomId;
		if (historyKey && context.groupHistories.has(historyKey)) createChannelHistoryWindow({ historyMap: context.groupHistories }).clear({
			historyKey,
			limit: context.historyLimit ?? 50
		});
	}
}
async function handleFollowEvent(event, _context) {
	const { userId } = getLineSourceInfo(event.source);
	logVerbose(`line: user ${userId ?? "unknown"} followed`);
}
async function handleUnfollowEvent(event, _context) {
	const { userId } = getLineSourceInfo(event.source);
	logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
}
async function handleJoinEvent(event, _context) {
	const { groupId, roomId } = getLineSourceInfo(event.source);
	logVerbose(`line: bot joined ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
}
async function handleLeaveEvent(event, _context) {
	const { groupId, roomId } = getLineSourceInfo(event.source);
	logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
}
async function handlePostbackEvent(event, context) {
	const data = event.postback.data;
	logVerbose(`line: received postback: ${data}`);
	const decision = await shouldProcessLineEvent(event, context);
	if (!decision) return;
	const postbackContext = await buildLinePostbackContext({
		event,
		cfg: context.cfg,
		account: context.account,
		commandAuthorized: decision.commandAccess.authorized
	});
	if (!postbackContext) return;
	await context.processMessage(postbackContext);
}
async function handleLineWebhookEvents(events, context) {
	let firstError;
	for (const event of events) {
		const replayCandidate = getLineReplayCandidate(event, context);
		const replaySkip = replayCandidate ? await claimLineReplayEvent(replayCandidate) : null;
		if (replaySkip?.skip) {
			if (replaySkip.inFlightResult) try {
				await replaySkip.inFlightResult;
			} catch (err) {
				context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
				firstError ??= err;
			}
			continue;
		}
		try {
			switch (event.type) {
				case "message":
					await handleMessageEvent(event, context);
					break;
				case "follow":
					await handleFollowEvent(event, context);
					break;
				case "unfollow":
					await handleUnfollowEvent(event, context);
					break;
				case "join":
					await handleJoinEvent(event, context);
					break;
				case "leave":
					await handleLeaveEvent(event, context);
					break;
				case "postback":
					await handlePostbackEvent(event, context);
					break;
				default: logVerbose(`line: unhandled event type: ${event.type}`);
			}
			if (replayCandidate) await replayCandidate.cache.commit(replayCandidate.key);
		} catch (err) {
			if (replayCandidate) if (err instanceof LineRetryableWebhookError) replayCandidate.cache.release(replayCandidate.key, { error: err });
			else await replayCandidate.cache.commit(replayCandidate.key);
			context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
			firstError ??= err;
		}
	}
	if (firstError) throw toLintErrorObject(firstError, "Non-Error thrown");
}
function toLintErrorObject(value, fallbackMessage) {
	if (value instanceof Error) return value;
	if (typeof value === "string") return new Error(value);
	const error = new Error(fallbackMessage, { cause: value });
	if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
	return error;
}
//#endregion
//#region extensions/line/src/bot.ts
function createLineBot(opts) {
	const runtime = opts.runtime ?? createNonExitingRuntime();
	const cfg = opts.config ?? getRuntimeConfig();
	const account = resolveLineAccount({
		cfg,
		accountId: opts.accountId
	});
	const mediaMaxBytes = (opts.mediaMaxMb ?? account.config.mediaMaxMb ?? 10) * 1024 * 1024;
	const processMessage = opts.onMessage ?? (async () => {
		logVerbose("line: no message handler configured");
	});
	const replayCache = createLineWebhookReplayCache();
	const groupHistories = /* @__PURE__ */ new Map();
	const handleWebhook = async (body) => {
		if (!body.events || body.events.length === 0) return;
		await handleLineWebhookEvents(body.events, {
			cfg,
			account,
			runtime,
			mediaMaxBytes,
			processMessage,
			replayCache,
			groupHistories,
			historyLimit: cfg.messages?.groupChat?.historyLimit ?? 50
		});
	};
	return {
		handleWebhook,
		account
	};
}
//#endregion
//#region extensions/line/src/monitor-durable.ts
function hasLineChannelData(payload) {
	const lineData = payload.channelData?.line;
	return Boolean(lineData && Object.keys(lineData).length > 0);
}
function resolveLineDurableReplyOptions(params) {
	if (params.infoKind !== "final") return false;
	if (params.replyToken && !params.replyTokenUsed) return false;
	if (hasLineChannelData(params.payload)) return false;
	const reply = resolveSendableOutboundReplyParts(params.payload);
	if (reply.hasMedia || !reply.hasText) return false;
	return { to: params.to };
}
//#endregion
//#region extensions/line/src/reply-chunks.ts
async function sendLineReplyChunks(params) {
	const hasQuickReplies = Boolean(params.quickReplies?.length);
	let replyTokenUsed = Boolean(params.replyTokenUsed);
	if (params.chunks.length === 0) return { replyTokenUsed };
	if (params.replyToken && !replyTokenUsed) try {
		const replyBatch = params.chunks.slice(0, 5);
		const remaining = params.chunks.slice(replyBatch.length);
		const replyMessages = replyBatch.map((chunk) => ({
			type: "text",
			text: chunk
		}));
		if (hasQuickReplies && remaining.length === 0 && replyMessages.length > 0) {
			const lastIndex = replyMessages.length - 1;
			replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(replyBatch[lastIndex], params.quickReplies);
		}
		await params.replyMessageLine(params.replyToken, replyMessages, {
			cfg: params.cfg,
			accountId: params.accountId
		});
		replyTokenUsed = true;
		for (let i = 0; i < remaining.length; i += 1) if (i === remaining.length - 1 && hasQuickReplies) await params.pushTextMessageWithQuickReplies(params.to, remaining[i], params.quickReplies, {
			cfg: params.cfg,
			accountId: params.accountId
		});
		else await params.pushMessageLine(params.to, remaining[i], {
			cfg: params.cfg,
			accountId: params.accountId
		});
		return { replyTokenUsed };
	} catch (err) {
		params.onReplyError?.(err);
		replyTokenUsed = true;
	}
	for (let i = 0; i < params.chunks.length; i += 1) if (i === params.chunks.length - 1 && hasQuickReplies) await params.pushTextMessageWithQuickReplies(params.to, params.chunks[i], params.quickReplies, {
		cfg: params.cfg,
		accountId: params.accountId
	});
	else await params.pushMessageLine(params.to, params.chunks[i], {
		cfg: params.cfg,
		accountId: params.accountId
	});
	return { replyTokenUsed };
}
//#endregion
//#region extensions/line/src/signature.ts
function validateLineSignature(body, signature, channelSecret) {
	const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
	const hashBuffer = Buffer.from(hash);
	const signatureBuffer = Buffer.from(signature);
	const maxLen = Math.max(hashBuffer.length, signatureBuffer.length);
	const paddedHash = Buffer.alloc(maxLen);
	const paddedSig = Buffer.alloc(maxLen);
	hashBuffer.copy(paddedHash);
	signatureBuffer.copy(paddedSig);
	const timingResult = crypto.timingSafeEqual(paddedHash, paddedSig);
	return hashBuffer.length === signatureBuffer.length && timingResult;
}
//#endregion
//#region extensions/line/src/webhook-utils.ts
function parseLineWebhookBody(rawBody) {
	try {
		return JSON.parse(rawBody);
	} catch {
		return null;
	}
}
//#endregion
//#region extensions/line/src/webhook-node.ts
const LINE_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 64 * 1024;
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
	return await readRequestBodyWithLimit(req, {
		maxBytes,
		timeoutMs
	});
}
function logLineWebhookDispatchError(runtime, err) {
	runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
}
function createLineNodeWebhookHandler(params) {
	const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
	const readBody = params.readBody ?? readLineWebhookRequestBody;
	return async (req, res) => {
		if (req.method === "GET" || req.method === "HEAD") {
			if (req.method === "HEAD") {
				res.statusCode = 204;
				res.end();
				return;
			}
			res.statusCode = 200;
			res.setHeader("Content-Type", "text/plain");
			res.end("OK");
			return;
		}
		if (req.method !== "POST") {
			res.statusCode = 405;
			res.setHeader("Allow", "GET, HEAD, POST");
			res.setHeader("Content-Type", "application/json");
			res.end(JSON.stringify({ error: "Method Not Allowed" }));
			return;
		}
		let receiveContext;
		try {
			const signatureHeader = req.headers["x-line-signature"];
			const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
			if (!signature) {
				logVerbose("line: webhook missing X-Line-Signature header");
				res.statusCode = 400;
				res.setHeader("Content-Type", "application/json");
				res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
				return;
			}
			const rawBody = await readBody(req, Math.min(maxBodyBytes, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1), LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1);
			if (!validateLineSignature(rawBody, signature, params.channelSecret)) {
				logVerbose("line: webhook signature validation failed");
				res.statusCode = 401;
				res.setHeader("Content-Type", "application/json");
				res.end(JSON.stringify({ error: "Invalid signature" }));
				return;
			}
			const body = parseLineWebhookBody(rawBody);
			if (!body) {
				res.statusCode = 400;
				res.setHeader("Content-Type", "application/json");
				res.end(JSON.stringify({ error: "Invalid webhook payload" }));
				return;
			}
			params.onRequestAuthenticated?.();
			receiveContext = createMessageReceiveContext({
				id: `${Date.now()}:line:webhook`,
				channel: "line",
				message: body,
				ackPolicy: "after_receive_record",
				onAck: () => {
					res.statusCode = 200;
					res.setHeader("Content-Type", "application/json");
					res.end(JSON.stringify({ status: "ok" }));
				}
			});
			if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
			if (body.events && body.events.length > 0) {
				logVerbose(`line: received ${body.events.length} webhook events`);
				Promise.resolve().then(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
			}
		} catch (err) {
			await receiveContext?.nack(err);
			if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
				res.statusCode = 413;
				res.setHeader("Content-Type", "application/json");
				res.end(JSON.stringify({ error: "Payload too large" }));
				return;
			}
			if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
				res.statusCode = 408;
				res.setHeader("Content-Type", "application/json");
				res.end(JSON.stringify({ error: requestBodyErrorToText("REQUEST_BODY_TIMEOUT") }));
				return;
			}
			params.runtime.error?.(danger(`line webhook error: ${String(err)}`));
			if (!res.headersSent) {
				res.statusCode = 500;
				res.setHeader("Content-Type", "application/json");
				res.end(JSON.stringify({ error: "Internal server error" }));
			}
		}
	};
}
//#endregion
//#region extensions/line/src/monitor.ts
const runtimeState = /* @__PURE__ */ new Map();
const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 64 * 1024;
const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
const lineWebhookTargets = /* @__PURE__ */ new Map();
function recordChannelRuntimeState(params) {
	const key = `${params.channel}:${params.accountId}`;
	const existing = runtimeState.get(key) ?? {
		running: false,
		lastStartAt: null,
		lastStopAt: null,
		lastError: null
	};
	runtimeState.set(key, {
		...existing,
		...params.state
	});
}
function startLineLoadingKeepalive(params) {
	const intervalMs = params.intervalMs ?? 18e3;
	const loadingSeconds = params.loadingSeconds ?? 20;
	let stopped = false;
	const trigger = () => {
		if (stopped) return;
		showLoadingAnimation(params.userId, {
			cfg: params.cfg,
			accountId: params.accountId,
			loadingSeconds
		}).catch(() => {});
	};
	trigger();
	const timer = setInterval(trigger, intervalMs);
	return () => {
		if (stopped) return;
		stopped = true;
		clearInterval(timer);
	};
}
async function monitorLineProvider(opts) {
	const { channelAccessToken, channelSecret, accountId, config, runtime, abortSignal, webhookPath } = opts;
	const resolvedAccountId = accountId ?? resolveDefaultLineAccountId(config);
	const token = channelAccessToken.trim();
	const secret = channelSecret.trim();
	if (!token) throw new Error("LINE webhook mode requires a non-empty channel access token.");
	if (!secret) throw new Error("LINE webhook mode requires a non-empty channel secret.");
	recordChannelRuntimeState({
		channel: "line",
		accountId: resolvedAccountId,
		state: {
			running: true,
			lastStartAt: Date.now()
		}
	});
	const bot = createLineBot({
		channelAccessToken: token,
		channelSecret: secret,
		accountId,
		runtime,
		config,
		onMessage: async (ctx) => {
			if (!ctx) return;
			const { ctxPayload, replyToken, route } = ctx;
			recordChannelRuntimeState({
				channel: "line",
				accountId: resolvedAccountId,
				state: { lastInboundAt: Date.now() }
			});
			const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
			const displayNamePromise = ctx.userId ? getUserDisplayName(ctx.userId, {
				cfg: config,
				accountId: ctx.accountId
			}) : Promise.resolve(ctxPayload.From);
			const stopLoading = shouldShowLoading ? startLineLoadingKeepalive({
				cfg: config,
				userId: ctx.userId,
				accountId: ctx.accountId
			}) : null;
			logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
			try {
				const textLimit = 5e3;
				let replyTokenUsed = false;
				const core = getLineRuntime();
				const turnResult = await core.channel.inbound.run({
					channel: "line",
					accountId: route.accountId,
					raw: ctx,
					adapter: {
						ingest: () => ({
							id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
							rawText: ctxPayload.RawBody ?? ctxPayload.BodyForAgent ?? ""
						}),
						resolveTurn: () => ({
							cfg: config,
							channel: "line",
							accountId: route.accountId,
							agentId: route.agentId,
							routeSessionKey: route.sessionKey,
							storePath: ctx.turn.storePath,
							ctxPayload,
							recordInboundSession: core.channel.session.recordInboundSession,
							dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
							record: ctx.turn.record,
							replyPipeline: {},
							delivery: {
								durable: (payload, info) => resolveLineDurableReplyOptions({
									payload,
									infoKind: info.kind,
									to: ctxPayload.From,
									replyToken,
									replyTokenUsed
								}),
								deliver: async (payload) => {
									const lineData = payload.channelData?.line ?? {};
									if (ctx.userId && !ctx.isGroup) showLoadingAnimation(ctx.userId, {
										cfg: config,
										accountId: ctx.accountId
									}).catch(() => {});
									const { replyTokenUsed: nextReplyTokenUsed } = await deliverLineAutoReply({
										payload,
										lineData,
										to: ctxPayload.From,
										replyToken,
										replyTokenUsed,
										accountId: ctx.accountId,
										cfg: config,
										textLimit,
										deps: {
											buildTemplateMessageFromPayload,
											processLineMessage,
											chunkMarkdownText,
											sendLineReplyChunks,
											replyMessageLine,
											pushMessageLine,
											pushTextMessageWithQuickReplies,
											createQuickReplyItems,
											createTextMessageWithQuickReplies,
											pushMessagesLine,
											createFlexMessage,
											createImageMessage,
											createLocationMessage,
											onReplyError: (replyErr) => {
												logVerbose(`line: reply token failed, falling back to push: ${String(replyErr)}`);
											}
										}
									});
									replyTokenUsed = nextReplyTokenUsed;
									recordChannelRuntimeState({
										channel: "line",
										accountId: resolvedAccountId,
										state: { lastOutboundAt: Date.now() }
									});
								},
								onError: (err, info) => {
									runtime.error?.(danger(`line ${info.kind} reply failed: ${String(err)}`));
								}
							}
						})
					}
				});
				if (!hasFinalChannelTurnDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
			} catch (err) {
				runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
				if (replyToken) try {
					await replyMessageLine(replyToken, [{
						type: "text",
						text: "Sorry, I encountered an error processing your message."
					}], {
						cfg: config,
						accountId: ctx.accountId
					});
				} catch (replyErr) {
					runtime.error?.(danger(`line: error reply failed: ${String(replyErr)}`));
				}
			} finally {
				stopLoading?.();
			}
		}
	});
	const normalizedPath = normalizeWebhookPath(normalizePluginHttpPath(webhookPath, "/line/webhook") ?? "/line/webhook");
	const createScopedLineWebhookHandler = (target) => createLineNodeWebhookHandler({
		channelSecret: target.channelSecret,
		bot: target.bot,
		runtime: target.runtime
	});
	const { unregister: unregisterHttp } = registerWebhookTargetWithPluginRoute({
		targetsByPath: lineWebhookTargets,
		target: {
			accountId: resolvedAccountId,
			bot,
			channelSecret: secret,
			path: normalizedPath,
			runtime
		},
		route: {
			auth: "plugin",
			pluginId: "line",
			accountId: resolvedAccountId,
			log: (msg) => logVerbose(msg),
			handler: async (req, res) => {
				const targets = lineWebhookTargets.get(normalizedPath) ?? [];
				const firstTarget = targets[0];
				if (req.method !== "POST") {
					if (!firstTarget) {
						res.statusCode = 404;
						res.end("Not Found");
						return;
					}
					await createScopedLineWebhookHandler(firstTarget)(req, res);
					return;
				}
				const requestLifecycle = beginWebhookRequestPipelineOrReject({
					req,
					res,
					inFlightLimiter: lineWebhookInFlightLimiter,
					inFlightKey: `line:${normalizedPath}`
				});
				if (!requestLifecycle.ok) return;
				try {
					const signatureHeader = req.headers["x-line-signature"];
					const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
					if (!signature) {
						logVerbose("line: webhook missing X-Line-Signature header");
						res.statusCode = 400;
						res.setHeader("Content-Type", "application/json");
						res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
						return;
					}
					const rawBody = await readLineWebhookRequestBody(req, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES, LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS);
					const match = resolveSingleWebhookTarget(targets, (target) => validateLineSignature(rawBody, signature, target.channelSecret));
					if (match.kind === "none") {
						logVerbose("line: webhook signature validation failed");
						res.statusCode = 401;
						res.setHeader("Content-Type", "application/json");
						res.end(JSON.stringify({ error: "Invalid signature" }));
						return;
					}
					if (match.kind === "ambiguous") {
						logVerbose("line: webhook signature matched multiple accounts");
						res.statusCode = 401;
						res.setHeader("Content-Type", "application/json");
						res.end(JSON.stringify({ error: "Ambiguous webhook target" }));
						return;
					}
					const body = parseLineWebhookBody(rawBody);
					if (!body) {
						res.statusCode = 400;
						res.setHeader("Content-Type", "application/json");
						res.end(JSON.stringify({ error: "Invalid webhook payload" }));
						return;
					}
					requestLifecycle.release();
					res.statusCode = 200;
					res.setHeader("Content-Type", "application/json");
					res.end(JSON.stringify({ status: "ok" }));
					if (body.events && body.events.length > 0) {
						logVerbose(`line: received ${body.events.length} webhook events`);
						Promise.resolve().then(() => match.target.bot.handleWebhook(body)).catch((err) => {
							match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
						});
					}
				} catch (err) {
					if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
						res.statusCode = 413;
						res.setHeader("Content-Type", "application/json");
						res.end(JSON.stringify({ error: "Payload too large" }));
						return;
					}
					if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
						res.statusCode = 408;
						res.setHeader("Content-Type", "application/json");
						res.end(JSON.stringify({ error: requestBodyErrorToText("REQUEST_BODY_TIMEOUT") }));
						return;
					}
					runtime.error?.(danger(`line webhook error: ${String(err)}`));
					if (!res.headersSent) {
						res.statusCode = 500;
						res.setHeader("Content-Type", "application/json");
						res.end(JSON.stringify({ error: "Internal server error" }));
					}
				} finally {
					requestLifecycle.release();
				}
			}
		}
	});
	logVerbose(`line: registered webhook handler at ${normalizedPath}`);
	let stopped = false;
	const stopHandler = () => {
		if (stopped) return;
		stopped = true;
		logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
		unregisterHttp();
		recordChannelRuntimeState({
			channel: "line",
			accountId: resolvedAccountId,
			state: {
				running: false,
				lastStopAt: Date.now()
			}
		});
	};
	if (abortSignal?.aborted) stopHandler();
	else if (abortSignal) {
		abortSignal.addEventListener("abort", stopHandler, { once: true });
		await waitForAbortSignal(abortSignal);
	}
	return {
		account: bot.account,
		handleWebhook: bot.handleWebhook,
		stop: () => {
			stopHandler();
			abortSignal?.removeEventListener("abort", stopHandler);
		}
	};
}
//#endregion
export { validateLineSignature as a, normalizeAllowFrom as c, parseLineWebhookBody as i, createLineNodeWebhookHandler as n, downloadLineMedia as o, readLineWebhookRequestBody as r, MessagingApiBlobClient as s, monitorLineProvider as t };