@openclaw/line 2026.5.2-beta.2 → 2026.5.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/accounts-UXAvdBo_.js +105 -0
  2. package/dist/api.js +11 -0
  3. package/dist/basic-cards-7DTG6E-A.js +307 -0
  4. package/dist/card-command-UlPc6Vuc.js +240 -0
  5. package/dist/channel-Rls9s60K.js +601 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/channel.runtime-bXoj8_Sl.js +4 -0
  8. package/dist/contract-api.js +2 -0
  9. package/dist/index.js +45 -0
  10. package/dist/markdown-to-line-CCWK6PJh.js +789 -0
  11. package/dist/monitor-B4_RxHc0.js +1523 -0
  12. package/dist/monitor.runtime-DyxYlrM9.js +2 -0
  13. package/dist/outbound-media-BfhBmWq5.js +55 -0
  14. package/dist/outbound.runtime-B-luYwEB.js +2 -0
  15. package/dist/probe-D9BPzb2n.js +30 -0
  16. package/dist/probe.runtime-N91OWW1J.js +2 -0
  17. package/dist/quick-reply-fallback-HskUUWsn.js +55 -0
  18. package/dist/reply-payload-transform-CFGTs9ln.js +713 -0
  19. package/dist/runtime-api.js +277 -0
  20. package/dist/schedule-cards-BwQChTdM.js +359 -0
  21. package/dist/secret-contract-api.js +5 -0
  22. package/dist/setup-api.js +2 -0
  23. package/dist/setup-entry.js +11 -0
  24. package/dist/setup-surface-DCH7CE-y.js +281 -0
  25. package/package.json +14 -6
  26. package/api.ts +0 -11
  27. package/channel-plugin-api.ts +0 -1
  28. package/contract-api.ts +0 -5
  29. package/index.ts +0 -54
  30. package/runtime-api.ts +0 -187
  31. package/secret-contract-api.ts +0 -4
  32. package/setup-api.ts +0 -2
  33. package/setup-entry.ts +0 -9
  34. package/src/account-helpers.ts +0 -16
  35. package/src/accounts.test.ts +0 -290
  36. package/src/accounts.ts +0 -187
  37. package/src/actions.ts +0 -61
  38. package/src/auto-reply-delivery.test.ts +0 -248
  39. package/src/auto-reply-delivery.ts +0 -200
  40. package/src/bindings.ts +0 -65
  41. package/src/bot-access.ts +0 -48
  42. package/src/bot-handlers.test.ts +0 -1089
  43. package/src/bot-handlers.ts +0 -642
  44. package/src/bot-message-context.test.ts +0 -405
  45. package/src/bot-message-context.ts +0 -581
  46. package/src/bot.ts +0 -70
  47. package/src/card-command.ts +0 -347
  48. package/src/channel-access-token.ts +0 -14
  49. package/src/channel-api.ts +0 -17
  50. package/src/channel-setup-status.contract.test.ts +0 -70
  51. package/src/channel-shared.ts +0 -48
  52. package/src/channel.logout.test.ts +0 -145
  53. package/src/channel.runtime.ts +0 -3
  54. package/src/channel.sendPayload.test.ts +0 -514
  55. package/src/channel.setup.ts +0 -11
  56. package/src/channel.status.test.ts +0 -63
  57. package/src/channel.ts +0 -154
  58. package/src/config-adapter.ts +0 -29
  59. package/src/config-schema.ts +0 -57
  60. package/src/download.test.ts +0 -133
  61. package/src/download.ts +0 -87
  62. package/src/flex-templates/basic-cards.ts +0 -395
  63. package/src/flex-templates/common.ts +0 -20
  64. package/src/flex-templates/media-control-cards.ts +0 -555
  65. package/src/flex-templates/message.ts +0 -13
  66. package/src/flex-templates/schedule-cards.ts +0 -467
  67. package/src/flex-templates/types.ts +0 -22
  68. package/src/flex-templates.ts +0 -32
  69. package/src/gateway.ts +0 -129
  70. package/src/group-keys.test.ts +0 -123
  71. package/src/group-keys.ts +0 -65
  72. package/src/group-policy.ts +0 -22
  73. package/src/markdown-to-line.test.ts +0 -348
  74. package/src/markdown-to-line.ts +0 -416
  75. package/src/message-cards.test.ts +0 -204
  76. package/src/monitor.lifecycle.test.ts +0 -421
  77. package/src/monitor.runtime.ts +0 -1
  78. package/src/monitor.ts +0 -506
  79. package/src/outbound-media.test.ts +0 -189
  80. package/src/outbound-media.ts +0 -120
  81. package/src/outbound.runtime.ts +0 -12
  82. package/src/outbound.ts +0 -356
  83. package/src/probe.contract.test.ts +0 -9
  84. package/src/probe.runtime.ts +0 -1
  85. package/src/probe.ts +0 -34
  86. package/src/quick-reply-fallback.ts +0 -10
  87. package/src/reply-chunks.test.ts +0 -179
  88. package/src/reply-chunks.ts +0 -110
  89. package/src/reply-payload-transform.test.ts +0 -387
  90. package/src/reply-payload-transform.ts +0 -317
  91. package/src/rich-menu.test.ts +0 -310
  92. package/src/rich-menu.ts +0 -326
  93. package/src/runtime.ts +0 -32
  94. package/src/send.test.ts +0 -346
  95. package/src/send.ts +0 -489
  96. package/src/setup-core.ts +0 -149
  97. package/src/setup-runtime-api.ts +0 -9
  98. package/src/setup-surface.test.ts +0 -474
  99. package/src/setup-surface.ts +0 -227
  100. package/src/signature.test.ts +0 -34
  101. package/src/signature.ts +0 -24
  102. package/src/status.ts +0 -37
  103. package/src/template-messages.ts +0 -333
  104. package/src/types.ts +0 -128
  105. package/src/webhook-node.test.ts +0 -513
  106. package/src/webhook-node.ts +0 -131
  107. package/src/webhook-utils.ts +0 -10
  108. package/src/webhook.ts +0 -111
  109. package/tsconfig.json +0 -16
@@ -0,0 +1,1523 @@
1
+ import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-UXAvdBo_.js";
2
+ import { a as resolveLineGroupConfigEntry, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-HskUUWsn.js";
3
+ import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, f as createImageMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-CCWK6PJh.js";
4
+ import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
5
+ import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
6
+ import { lowercasePreservingWhitespace, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
7
+ import { firstDefined, isSenderIdAllowed, mergeDmAllowFromSources } from "openclaw/plugin-sdk/allow-from";
8
+ import { messagingApi } from "@line/bot-sdk";
9
+ import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
10
+ import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
11
+ import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
12
+ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
13
+ import { hasFinalInboundReplyDispatch } from "openclaw/plugin-sdk/inbound-reply-dispatch";
14
+ import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
15
+ import { isRequestBodyLimitError, normalizePluginHttpPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
16
+ import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1 } from "openclaw/plugin-sdk/webhook-request-guards";
17
+ import { DEFAULT_GROUP_HISTORY_LIMIT, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
18
+ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
19
+ import { buildMentionRegexes, formatInboundEnvelope, formatLocationText, matchesMentionPatterns, resolveInboundMentionDecision, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
20
+ import { hasControlCommand, resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
21
+ import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
22
+ import { evaluateMatchedGroupAccessForPolicy } from "openclaw/plugin-sdk/group-access";
23
+ import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
24
+ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
25
+ import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
26
+ import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
27
+ import crypto from "node:crypto";
28
+ //#region extensions/line/src/bot-access.ts
29
+ function normalizeAllowEntry(value) {
30
+ const trimmed = String(value).trim();
31
+ if (!trimmed) return "";
32
+ if (trimmed === "*") return "*";
33
+ return trimmed.replace(/^line:(?:user:)?/i, "");
34
+ }
35
+ const normalizeAllowFrom = (list) => {
36
+ const entries = (list ?? []).map((value) => normalizeAllowEntry(value)).filter(Boolean);
37
+ return {
38
+ entries,
39
+ hasWildcard: entries.includes("*"),
40
+ hasEntries: entries.length > 0
41
+ };
42
+ };
43
+ const normalizeDmAllowFromWithStore = (params) => normalizeAllowFrom(mergeDmAllowFromSources(params));
44
+ const isSenderAllowed = (params) => {
45
+ const { allow, senderId } = params;
46
+ return isSenderIdAllowed(allow, senderId, false);
47
+ };
48
+ //#endregion
49
+ //#region extensions/line/src/download.ts
50
+ const AUDIO_BRANDS = new Set([
51
+ "m4a ",
52
+ "m4b ",
53
+ "m4p ",
54
+ "m4r ",
55
+ "f4a ",
56
+ "f4b "
57
+ ]);
58
+ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024) {
59
+ const response = await new messagingApi.MessagingApiBlobClient({ channelAccessToken }).getMessageContent(messageId);
60
+ const chunks = [];
61
+ let totalSize = 0;
62
+ for await (const chunk of response) {
63
+ totalSize += chunk.length;
64
+ if (totalSize > maxBytes) throw new Error(`Media exceeds ${Math.round(maxBytes / (1024 * 1024))}MB limit`);
65
+ chunks.push(chunk);
66
+ }
67
+ const buffer = Buffer.concat(chunks);
68
+ const saved = await saveMediaBuffer(buffer, detectContentType(buffer), "inbound", maxBytes);
69
+ logVerbose(`line: persisted media ${messageId} to ${saved.path} (${buffer.length} bytes)`);
70
+ return {
71
+ path: saved.path,
72
+ contentType: saved.contentType,
73
+ size: buffer.length
74
+ };
75
+ }
76
+ function detectContentType(buffer) {
77
+ const hasFtypBox = buffer.length >= 12 && buffer[4] === 102 && buffer[5] === 116 && buffer[6] === 121 && buffer[7] === 112;
78
+ if (buffer.length >= 2) {
79
+ if (buffer[0] === 255 && buffer[1] === 216) return "image/jpeg";
80
+ if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) return "image/png";
81
+ if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70) return "image/gif";
82
+ if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70 && buffer[8] === 87 && buffer[9] === 69 && buffer[10] === 66 && buffer[11] === 80) return "image/webp";
83
+ if (hasFtypBox) {
84
+ const majorBrand = lowercasePreservingWhitespace(buffer.toString("ascii", 8, 12));
85
+ if (AUDIO_BRANDS.has(majorBrand)) return "audio/mp4";
86
+ return "video/mp4";
87
+ }
88
+ }
89
+ return "application/octet-stream";
90
+ }
91
+ //#endregion
92
+ //#region extensions/line/src/auto-reply-delivery.ts
93
+ async function deliverLineAutoReply(params) {
94
+ const { payload, lineData, replyToken, accountId, to, textLimit, deps } = params;
95
+ let replyTokenUsed = params.replyTokenUsed;
96
+ const pushLineMessages = async (messages) => {
97
+ if (messages.length === 0) return;
98
+ for (let i = 0; i < messages.length; i += 5) await deps.pushMessagesLine(to, messages.slice(i, i + 5), {
99
+ cfg: params.cfg,
100
+ accountId
101
+ });
102
+ };
103
+ const sendLineMessages = async (messages, allowReplyToken) => {
104
+ if (messages.length === 0) return;
105
+ let remaining = messages;
106
+ if (allowReplyToken && replyToken && !replyTokenUsed) {
107
+ const replyBatch = remaining.slice(0, 5);
108
+ try {
109
+ await deps.replyMessageLine(replyToken, replyBatch, {
110
+ cfg: params.cfg,
111
+ accountId
112
+ });
113
+ } catch (err) {
114
+ deps.onReplyError?.(err);
115
+ await pushLineMessages(replyBatch);
116
+ }
117
+ replyTokenUsed = true;
118
+ remaining = remaining.slice(replyBatch.length);
119
+ }
120
+ if (remaining.length > 0) await pushLineMessages(remaining);
121
+ };
122
+ const richMessages = [];
123
+ const hasQuickReplies = Boolean(lineData.quickReplies?.length);
124
+ if (lineData.flexMessage) richMessages.push(deps.createFlexMessage(lineData.flexMessage.altText.slice(0, 400), lineData.flexMessage.contents));
125
+ if (lineData.templateMessage) {
126
+ const templateMsg = deps.buildTemplateMessageFromPayload(lineData.templateMessage);
127
+ if (templateMsg) richMessages.push(templateMsg);
128
+ }
129
+ if (lineData.location) richMessages.push(deps.createLocationMessage(lineData.location));
130
+ const processed = payload.text ? deps.processLineMessage(payload.text) : {
131
+ text: "",
132
+ flexMessages: []
133
+ };
134
+ for (const flexMsg of processed.flexMessages) richMessages.push(deps.createFlexMessage(flexMsg.altText.slice(0, 400), flexMsg.contents));
135
+ const chunks = processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : [];
136
+ const mediaMessages = resolveSendableOutboundReplyParts(payload).mediaUrls.map((url) => url?.trim()).filter((url) => Boolean(url)).map((url) => deps.createImageMessage(url));
137
+ if (chunks.length > 0) {
138
+ const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0;
139
+ if (hasQuickReplies && hasRichOrMedia) try {
140
+ await sendLineMessages([...richMessages, ...mediaMessages], false);
141
+ } catch (err) {
142
+ deps.onReplyError?.(err);
143
+ }
144
+ const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
145
+ to,
146
+ chunks,
147
+ quickReplies: lineData.quickReplies,
148
+ replyToken,
149
+ replyTokenUsed,
150
+ cfg: params.cfg,
151
+ accountId,
152
+ replyMessageLine: deps.replyMessageLine,
153
+ pushMessageLine: deps.pushMessageLine,
154
+ pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies,
155
+ createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies
156
+ });
157
+ replyTokenUsed = nextReplyTokenUsed;
158
+ if (!hasQuickReplies || !hasRichOrMedia) {
159
+ await sendLineMessages(richMessages, false);
160
+ if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
161
+ }
162
+ } else {
163
+ const combined = [...richMessages, ...mediaMessages];
164
+ if (hasQuickReplies && combined.length === 0) {
165
+ const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
166
+ to,
167
+ chunks: [buildLineQuickReplyFallbackText(lineData.quickReplies)],
168
+ quickReplies: lineData.quickReplies,
169
+ replyToken,
170
+ replyTokenUsed,
171
+ cfg: params.cfg,
172
+ accountId,
173
+ replyMessageLine: deps.replyMessageLine,
174
+ pushMessageLine: deps.pushMessageLine,
175
+ pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies,
176
+ createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
177
+ onReplyError: deps.onReplyError
178
+ });
179
+ replyTokenUsed = nextReplyTokenUsed;
180
+ } else {
181
+ if (hasQuickReplies && combined.length > 0) {
182
+ const quickReply = deps.createQuickReplyItems(lineData.quickReplies);
183
+ const targetIndex = replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1;
184
+ combined[targetIndex] = {
185
+ ...combined[targetIndex],
186
+ quickReply
187
+ };
188
+ }
189
+ await sendLineMessages(combined, true);
190
+ }
191
+ }
192
+ return { replyTokenUsed };
193
+ }
194
+ //#endregion
195
+ //#region extensions/line/src/bot-message-context.ts
196
+ function getLineSourceInfo(source) {
197
+ if (!source) return {
198
+ userId: void 0,
199
+ groupId: void 0,
200
+ roomId: void 0,
201
+ isGroup: false
202
+ };
203
+ return {
204
+ userId: source.type === "user" ? source.userId : source.type === "group" ? source.userId : source.type === "room" ? source.userId : void 0,
205
+ groupId: source.type === "group" ? source.groupId : void 0,
206
+ roomId: source.type === "room" ? source.roomId : void 0,
207
+ isGroup: source.type === "group" || source.type === "room"
208
+ };
209
+ }
210
+ function buildPeerId(source) {
211
+ if (!source) return "unknown";
212
+ const groupKey = normalizeOptionalString(source.type === "group" ? source.groupId : void 0) ?? normalizeOptionalString(source.type === "room" ? source.roomId : void 0);
213
+ if (groupKey) return groupKey;
214
+ if (source.type === "user" && source.userId) return source.userId;
215
+ return "unknown";
216
+ }
217
+ async function resolveLineInboundRoute(params) {
218
+ recordChannelActivity({
219
+ channel: "line",
220
+ accountId: params.account.accountId,
221
+ direction: "inbound"
222
+ });
223
+ const { userId, groupId, roomId, isGroup } = getLineSourceInfo(params.source);
224
+ const peerId = buildPeerId(params.source);
225
+ let route = resolveAgentRoute({
226
+ cfg: params.cfg,
227
+ channel: "line",
228
+ accountId: params.account.accountId,
229
+ peer: {
230
+ kind: isGroup ? "group" : "direct",
231
+ id: peerId
232
+ }
233
+ });
234
+ const configuredRoute = resolveConfiguredBindingRoute({
235
+ cfg: params.cfg,
236
+ route,
237
+ conversation: {
238
+ channel: "line",
239
+ accountId: params.account.accountId,
240
+ conversationId: peerId
241
+ }
242
+ });
243
+ let configuredBinding = configuredRoute.bindingResolution;
244
+ const configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
245
+ route = configuredRoute.route;
246
+ const runtimeRoute = resolveRuntimeConversationBindingRoute({
247
+ route,
248
+ conversation: {
249
+ channel: "line",
250
+ accountId: params.account.accountId,
251
+ conversationId: peerId
252
+ }
253
+ });
254
+ route = runtimeRoute.route;
255
+ if (runtimeRoute.bindingRecord) {
256
+ configuredBinding = null;
257
+ logVerbose(runtimeRoute.boundSessionKey ? `line: routed via bound conversation ${peerId} -> ${runtimeRoute.boundSessionKey}` : `line: plugin-bound conversation ${peerId}`);
258
+ }
259
+ if (configuredBinding) {
260
+ const ensured = await ensureConfiguredBindingRouteReady({
261
+ cfg: params.cfg,
262
+ bindingResolution: configuredBinding
263
+ });
264
+ if (!ensured.ok) {
265
+ logVerbose(`line: configured ACP binding unavailable for ${peerId} -> ${configuredBindingSessionKey}: ${ensured.error}`);
266
+ throw new Error(`Configured ACP binding unavailable: ${ensured.error}`);
267
+ }
268
+ logVerbose(`line: using configured ACP binding for ${peerId} -> ${configuredBindingSessionKey}`);
269
+ }
270
+ return {
271
+ userId,
272
+ groupId,
273
+ roomId,
274
+ isGroup,
275
+ peerId,
276
+ route
277
+ };
278
+ }
279
+ const STICKER_PACKAGES = {
280
+ "1": "Moon & James",
281
+ "2": "Cony & Brown",
282
+ "3": "Brown & Friends",
283
+ "4": "Moon Special",
284
+ "789": "LINE Characters",
285
+ "6136": "Cony's Happy Life",
286
+ "6325": "Brown's Life",
287
+ "6359": "Choco",
288
+ "6362": "Sally",
289
+ "6370": "Edward",
290
+ "11537": "Cony",
291
+ "11538": "Brown",
292
+ "11539": "Moon"
293
+ };
294
+ function describeStickerKeywords(sticker) {
295
+ const keywords = sticker.keywords;
296
+ if (keywords && keywords.length > 0) return keywords.slice(0, 3).join(", ");
297
+ const stickerText = sticker.text;
298
+ if (stickerText) return stickerText;
299
+ return "";
300
+ }
301
+ function extractMessageText(message) {
302
+ if (message.type === "text") return message.text;
303
+ if (message.type === "location") {
304
+ const loc = message;
305
+ return formatLocationText({
306
+ latitude: loc.latitude,
307
+ longitude: loc.longitude,
308
+ name: loc.title,
309
+ address: loc.address
310
+ }) ?? "";
311
+ }
312
+ if (message.type === "sticker") {
313
+ const sticker = message;
314
+ const packageName = STICKER_PACKAGES[sticker.packageId] ?? "sticker";
315
+ const keywords = describeStickerKeywords(sticker);
316
+ if (keywords) return `[Sent a ${packageName} sticker: ${keywords}]`;
317
+ return `[Sent a ${packageName} sticker]`;
318
+ }
319
+ return "";
320
+ }
321
+ function extractMediaPlaceholder(message) {
322
+ switch (message.type) {
323
+ case "image": return "<media:image>";
324
+ case "video": return "<media:video>";
325
+ case "audio": return "<media:audio>";
326
+ case "file": return "<media:document>";
327
+ default: return "";
328
+ }
329
+ }
330
+ function resolveLineConversationLabel(params) {
331
+ return params.isGroup ? params.groupId ? `group:${params.groupId}` : params.roomId ? `room:${params.roomId}` : "unknown-group" : params.senderLabel;
332
+ }
333
+ function resolveLineAddresses(params) {
334
+ const fromAddress = params.isGroup ? params.groupId ? `line:group:${params.groupId}` : params.roomId ? `line:room:${params.roomId}` : `line:${params.peerId}` : `line:${params.userId ?? params.peerId}`;
335
+ return {
336
+ fromAddress,
337
+ toAddress: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`,
338
+ originatingTo: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`
339
+ };
340
+ }
341
+ async function finalizeLineInboundContext(params) {
342
+ const { fromAddress, toAddress, originatingTo } = resolveLineAddresses({
343
+ isGroup: params.source.isGroup,
344
+ groupId: params.source.groupId,
345
+ roomId: params.source.roomId,
346
+ userId: params.source.userId,
347
+ peerId: params.source.peerId
348
+ });
349
+ const senderId = params.source.userId ?? "unknown";
350
+ const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
351
+ const conversationLabel = resolveLineConversationLabel({
352
+ isGroup: params.source.isGroup,
353
+ groupId: params.source.groupId,
354
+ roomId: params.source.roomId,
355
+ senderLabel
356
+ });
357
+ const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
358
+ cfg: params.cfg,
359
+ agentId: params.route.agentId,
360
+ sessionKey: params.route.sessionKey
361
+ });
362
+ const body = formatInboundEnvelope({
363
+ channel: "LINE",
364
+ from: conversationLabel,
365
+ timestamp: params.timestamp,
366
+ body: params.rawBody,
367
+ chatType: params.source.isGroup ? "group" : "direct",
368
+ sender: { id: senderId },
369
+ previousTimestamp,
370
+ envelope: envelopeOptions
371
+ });
372
+ const ctxPayload = finalizeInboundContext({
373
+ Body: body,
374
+ BodyForAgent: params.rawBody,
375
+ RawBody: params.rawBody,
376
+ CommandBody: params.rawBody,
377
+ From: fromAddress,
378
+ To: toAddress,
379
+ SessionKey: params.route.sessionKey,
380
+ AccountId: params.route.accountId,
381
+ ChatType: params.source.isGroup ? "group" : "direct",
382
+ ConversationLabel: conversationLabel,
383
+ GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
384
+ SenderId: senderId,
385
+ Provider: "line",
386
+ Surface: "line",
387
+ MessageSid: params.messageSid,
388
+ Timestamp: params.timestamp,
389
+ MediaPath: params.media.firstPath,
390
+ MediaType: params.media.firstContentType,
391
+ MediaUrl: params.media.firstPath,
392
+ MediaPaths: params.media.paths,
393
+ MediaUrls: params.media.paths,
394
+ MediaTypes: params.media.types,
395
+ ...params.locationContext,
396
+ CommandAuthorized: params.commandAuthorized,
397
+ OriginatingChannel: "line",
398
+ OriginatingTo: originatingTo,
399
+ GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
400
+ groupId: params.source.groupId,
401
+ roomId: params.source.roomId
402
+ })?.systemPrompt) : void 0,
403
+ InboundHistory: params.inboundHistory
404
+ });
405
+ const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
406
+ dmScope: params.cfg.session?.dmScope,
407
+ allowFrom: params.account.config.allowFrom,
408
+ normalizeEntry: (entry) => normalizeAllowFrom([entry]).entries[0]
409
+ }) : null;
410
+ if (shouldLogVerbose()) {
411
+ const preview = body.slice(0, 200).replace(/\n/g, "\\n");
412
+ const mediaInfo = params.verboseLog.kind === "inbound" && (params.verboseLog.mediaCount ?? 0) > 1 ? ` mediaCount=${params.verboseLog.mediaCount}` : "";
413
+ logVerbose(`${params.verboseLog.kind === "inbound" ? "line inbound" : "line postback"}: from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`);
414
+ }
415
+ return {
416
+ ctxPayload,
417
+ replyToken: params.event.replyToken,
418
+ turn: {
419
+ storePath,
420
+ record: {
421
+ updateLastRoute: !params.source.isGroup ? {
422
+ sessionKey: params.route.mainSessionKey,
423
+ channel: "line",
424
+ to: params.source.userId ?? params.source.peerId,
425
+ accountId: params.route.accountId,
426
+ mainDmOwnerPin: pinnedMainDmOwner && params.source.userId ? {
427
+ ownerRecipient: pinnedMainDmOwner,
428
+ senderRecipient: params.source.userId,
429
+ onSkip: ({ ownerRecipient, senderRecipient }) => {
430
+ logVerbose(`line: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`);
431
+ }
432
+ } : void 0
433
+ } : void 0,
434
+ onRecordError: (err) => {
435
+ logVerbose(`line: failed updating session meta: ${String(err)}`);
436
+ }
437
+ }
438
+ }
439
+ };
440
+ }
441
+ async function buildLineMessageContext(params) {
442
+ const { event, allMedia, cfg, account, commandAuthorized, groupHistories, historyLimit } = params;
443
+ const source = event.source;
444
+ const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
445
+ source,
446
+ cfg,
447
+ account
448
+ });
449
+ const message = event.message;
450
+ const messageId = message.id;
451
+ const timestamp = event.timestamp;
452
+ const textContent = extractMessageText(message);
453
+ const placeholder = extractMediaPlaceholder(message);
454
+ let rawBody = textContent || placeholder;
455
+ if (!rawBody && allMedia.length > 0) rawBody = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
456
+ if (!rawBody && allMedia.length === 0) return null;
457
+ let locationContext;
458
+ if (message.type === "location") {
459
+ const loc = message;
460
+ locationContext = toLocationContext({
461
+ latitude: loc.latitude,
462
+ longitude: loc.longitude,
463
+ name: loc.title,
464
+ address: loc.address
465
+ });
466
+ }
467
+ const historyKey = isGroup ? peerId : void 0;
468
+ const inboundHistory = historyKey && groupHistories && (historyLimit ?? 0) > 0 ? (groupHistories.get(historyKey) ?? []).map((entry) => ({
469
+ sender: entry.sender,
470
+ body: entry.body,
471
+ timestamp: entry.timestamp
472
+ })) : void 0;
473
+ const finalized = await finalizeLineInboundContext({
474
+ cfg,
475
+ account,
476
+ event,
477
+ route,
478
+ source: {
479
+ userId,
480
+ groupId,
481
+ roomId,
482
+ isGroup,
483
+ peerId
484
+ },
485
+ rawBody,
486
+ timestamp,
487
+ messageSid: messageId,
488
+ commandAuthorized,
489
+ media: {
490
+ firstPath: allMedia[0]?.path,
491
+ firstContentType: allMedia[0]?.contentType,
492
+ paths: allMedia.length > 0 ? allMedia.map((m) => m.path) : void 0,
493
+ types: allMedia.length > 0 ? allMedia.map((m) => m.contentType).filter(Boolean) : void 0
494
+ },
495
+ locationContext,
496
+ verboseLog: {
497
+ kind: "inbound",
498
+ mediaCount: allMedia.length
499
+ },
500
+ inboundHistory
501
+ });
502
+ return {
503
+ ctxPayload: finalized.ctxPayload,
504
+ turn: finalized.turn,
505
+ event,
506
+ userId,
507
+ groupId,
508
+ roomId,
509
+ isGroup,
510
+ route,
511
+ replyToken: event.replyToken,
512
+ accountId: account.accountId
513
+ };
514
+ }
515
+ async function buildLinePostbackContext(params) {
516
+ const { event, cfg, account, commandAuthorized } = params;
517
+ const source = event.source;
518
+ const { userId, groupId, roomId, isGroup, peerId, route } = await resolveLineInboundRoute({
519
+ source,
520
+ cfg,
521
+ account
522
+ });
523
+ const timestamp = event.timestamp;
524
+ const rawData = event.postback?.data?.trim() ?? "";
525
+ if (!rawData) return null;
526
+ let rawBody = rawData;
527
+ if (rawData.includes("line.action=")) {
528
+ const searchParams = new URLSearchParams(rawData);
529
+ const action = searchParams.get("line.action") ?? "";
530
+ const device = searchParams.get("line.device");
531
+ rawBody = device ? `line action ${action} device ${device}` : `line action ${action}`;
532
+ }
533
+ const messageSid = event.replyToken ? `postback:${event.replyToken}` : `postback:${timestamp}`;
534
+ const finalized = await finalizeLineInboundContext({
535
+ cfg,
536
+ account,
537
+ event,
538
+ route,
539
+ source: {
540
+ userId,
541
+ groupId,
542
+ roomId,
543
+ isGroup,
544
+ peerId
545
+ },
546
+ rawBody,
547
+ timestamp,
548
+ messageSid,
549
+ commandAuthorized,
550
+ media: {
551
+ firstPath: "",
552
+ firstContentType: void 0,
553
+ paths: void 0,
554
+ types: void 0
555
+ },
556
+ verboseLog: { kind: "postback" }
557
+ });
558
+ return {
559
+ ctxPayload: finalized.ctxPayload,
560
+ turn: finalized.turn,
561
+ event,
562
+ userId,
563
+ groupId,
564
+ roomId,
565
+ isGroup,
566
+ route,
567
+ replyToken: event.replyToken,
568
+ accountId: account.accountId
569
+ };
570
+ }
571
+ //#endregion
572
+ //#region extensions/line/src/bot-handlers.ts
573
+ const LINE_DOWNLOADABLE_MESSAGE_TYPES = new Set([
574
+ "image",
575
+ "video",
576
+ "audio",
577
+ "file"
578
+ ]);
579
+ function isDownloadableLineMessageType(messageType) {
580
+ return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
581
+ }
582
+ const LINE_WEBHOOK_REPLAY_WINDOW_MS = 600 * 1e3;
583
+ const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
584
+ var LineRetryableWebhookError = class extends Error {
585
+ constructor(message, options) {
586
+ super(message, options);
587
+ this.name = "LineRetryableWebhookError";
588
+ }
589
+ };
590
+ function createLineWebhookReplayCache() {
591
+ return createClaimableDedupe({
592
+ ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
593
+ memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
594
+ });
595
+ }
596
+ function buildLineWebhookReplayKey(event, accountId) {
597
+ if (event.type === "message") {
598
+ const messageId = event.message?.id?.trim();
599
+ if (messageId) return {
600
+ key: `${accountId}|message:${messageId}`,
601
+ eventId: `message:${messageId}`
602
+ };
603
+ }
604
+ const eventId = event.webhookEventId?.trim();
605
+ if (!eventId) return null;
606
+ const source = event.source;
607
+ const sourceId = source?.type === "group" ? `group:${source.groupId ?? ""}` : source?.type === "room" ? `room:${source.roomId ?? ""}` : `user:${source?.userId ?? ""}`;
608
+ return {
609
+ key: `${accountId}|${event.type}|${sourceId}|${eventId}`,
610
+ eventId: `event:${eventId}`
611
+ };
612
+ }
613
+ function getLineReplayCandidate(event, context) {
614
+ const replay = buildLineWebhookReplayKey(event, context.account.accountId);
615
+ const cache = context.replayCache;
616
+ if (!replay || !cache) return null;
617
+ return {
618
+ key: replay.key,
619
+ eventId: replay.eventId,
620
+ cache
621
+ };
622
+ }
623
+ async function claimLineReplayEvent(candidate) {
624
+ const claim = await candidate.cache.claim(candidate.key);
625
+ if (claim.kind === "claimed") return { skip: false };
626
+ if (claim.kind === "inflight") {
627
+ logVerbose(`line: skipped in-flight replayed webhook event ${candidate.eventId}`);
628
+ return {
629
+ skip: true,
630
+ inFlightResult: claim.pending.then(() => void 0)
631
+ };
632
+ }
633
+ logVerbose(`line: skipped replayed webhook event ${candidate.eventId}`);
634
+ return { skip: true };
635
+ }
636
+ function resolveLineGroupConfig(params) {
637
+ return resolveLineGroupConfigEntry(params.config.groups, {
638
+ groupId: params.groupId,
639
+ roomId: params.roomId
640
+ });
641
+ }
642
+ async function sendLinePairingReply(params) {
643
+ const { senderId, replyToken, context } = params;
644
+ const idLabel = (() => {
645
+ try {
646
+ return resolvePairingIdLabel("line");
647
+ } catch {
648
+ return "lineUserId";
649
+ }
650
+ })();
651
+ await createChannelPairingChallengeIssuer({
652
+ channel: "line",
653
+ upsertPairingRequest: async ({ id, meta }) => await upsertChannelPairingRequest({
654
+ channel: "line",
655
+ id,
656
+ accountId: context.account.accountId,
657
+ meta
658
+ })
659
+ })({
660
+ senderId,
661
+ senderIdLine: `Your ${idLabel}: ${senderId}`,
662
+ onCreated: () => {
663
+ logVerbose(`line pairing request sender=${senderId}`);
664
+ },
665
+ sendPairingReply: async (text) => {
666
+ if (replyToken) try {
667
+ await replyMessageLine(replyToken, [{
668
+ type: "text",
669
+ text
670
+ }], {
671
+ cfg: context.cfg,
672
+ accountId: context.account.accountId,
673
+ channelAccessToken: context.account.channelAccessToken
674
+ });
675
+ return;
676
+ } catch (err) {
677
+ logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
678
+ }
679
+ try {
680
+ await pushMessageLine(`line:${senderId}`, text, {
681
+ cfg: context.cfg,
682
+ accountId: context.account.accountId,
683
+ channelAccessToken: context.account.channelAccessToken
684
+ });
685
+ } catch (err) {
686
+ logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
687
+ }
688
+ }
689
+ });
690
+ }
691
+ async function shouldProcessLineEvent(event, context) {
692
+ const denied = {
693
+ allowed: false,
694
+ commandAuthorized: false
695
+ };
696
+ const { cfg, account } = context;
697
+ const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
698
+ const senderId = userId ?? "";
699
+ const dmPolicy = account.config.dmPolicy ?? "pairing";
700
+ const storeAllowFrom = await readChannelAllowFromStore("line", void 0, account.accountId).catch(() => []);
701
+ const effectiveDmAllow = normalizeDmAllowFromWithStore({
702
+ allowFrom: account.config.allowFrom,
703
+ storeAllowFrom,
704
+ dmPolicy
705
+ });
706
+ const groupConfig = resolveLineGroupConfig({
707
+ config: account.config,
708
+ groupId,
709
+ roomId
710
+ });
711
+ const groupAllowOverride = groupConfig?.allowFrom;
712
+ const fallbackGroupAllowFrom = account.config.allowFrom?.length ? account.config.allowFrom : void 0;
713
+ const effectiveGroupAllow = normalizeAllowFrom(firstDefined(groupAllowOverride, account.config.groupAllowFrom, fallbackGroupAllowFrom));
714
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
715
+ const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
716
+ providerConfigPresent: cfg.channels?.line !== void 0,
717
+ groupPolicy: account.config.groupPolicy,
718
+ defaultGroupPolicy
719
+ });
720
+ warnMissingProviderGroupPolicyFallbackOnce({
721
+ providerMissingFallbackApplied,
722
+ providerKey: "line",
723
+ accountId: account.accountId,
724
+ log: (message) => logVerbose(message)
725
+ });
726
+ if (isGroup) {
727
+ if (groupConfig?.enabled === false) {
728
+ logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
729
+ return denied;
730
+ }
731
+ if (groupAllowOverride !== void 0) {
732
+ if (!senderId) {
733
+ logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
734
+ return denied;
735
+ }
736
+ if (!isSenderAllowed({
737
+ allow: effectiveGroupAllow,
738
+ senderId
739
+ })) {
740
+ logVerbose(`Blocked line group sender ${senderId} (group allowFrom override)`);
741
+ return denied;
742
+ }
743
+ }
744
+ const senderGroupAccess = evaluateMatchedGroupAccessForPolicy({
745
+ groupPolicy,
746
+ requireMatchInput: true,
747
+ hasMatchInput: Boolean(senderId),
748
+ allowlistConfigured: effectiveGroupAllow.entries.length > 0,
749
+ allowlistMatched: Boolean(senderId) && isSenderAllowed({
750
+ allow: effectiveGroupAllow,
751
+ senderId
752
+ })
753
+ });
754
+ if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
755
+ logVerbose("Blocked line group message (groupPolicy: disabled)");
756
+ return denied;
757
+ }
758
+ if (!senderGroupAccess.allowed && senderGroupAccess.reason === "missing_match_input") {
759
+ logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
760
+ return denied;
761
+ }
762
+ if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
763
+ logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
764
+ return denied;
765
+ }
766
+ if (!senderGroupAccess.allowed && senderGroupAccess.reason === "not_allowlisted") {
767
+ logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
768
+ return denied;
769
+ }
770
+ return {
771
+ allowed: true,
772
+ commandAuthorized: resolveLineCommandAuthorized({
773
+ cfg,
774
+ event,
775
+ senderId,
776
+ allow: effectiveGroupAllow
777
+ })
778
+ };
779
+ }
780
+ if (dmPolicy === "disabled") {
781
+ logVerbose("Blocked line sender (dmPolicy: disabled)");
782
+ return denied;
783
+ }
784
+ if (!isSenderAllowed({
785
+ allow: effectiveDmAllow,
786
+ senderId
787
+ })) {
788
+ if (dmPolicy === "pairing") {
789
+ if (!senderId) {
790
+ logVerbose("Blocked line sender (dmPolicy: pairing, no sender ID)");
791
+ return denied;
792
+ }
793
+ await sendLinePairingReply({
794
+ senderId,
795
+ replyToken: "replyToken" in event ? event.replyToken : void 0,
796
+ context
797
+ });
798
+ } else logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${dmPolicy})`);
799
+ return denied;
800
+ }
801
+ return {
802
+ allowed: true,
803
+ commandAuthorized: resolveLineCommandAuthorized({
804
+ cfg,
805
+ event,
806
+ senderId,
807
+ allow: effectiveDmAllow
808
+ })
809
+ };
810
+ }
811
+ function getLineMentionees(message) {
812
+ if (message.type !== "text") return [];
813
+ const mentionees = message.mention?.mentionees;
814
+ return Array.isArray(mentionees) ? mentionees : [];
815
+ }
816
+ function isLineBotMentioned(message) {
817
+ return getLineMentionees(message).some((m) => m.isSelf === true || m.type === "all");
818
+ }
819
+ function hasAnyLineMention(message) {
820
+ return getLineMentionees(message).length > 0;
821
+ }
822
+ function resolveEventRawText(event) {
823
+ if (event.type === "message") {
824
+ const msg = event.message;
825
+ if (msg.type === "text") return msg.text;
826
+ return "";
827
+ }
828
+ if (event.type === "postback") return event.postback?.data?.trim() ?? "";
829
+ return "";
830
+ }
831
+ function resolveLineCommandAuthorized(params) {
832
+ const senderAllowedForCommands = isSenderAllowed({
833
+ allow: params.allow,
834
+ senderId: params.senderId
835
+ });
836
+ const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
837
+ const rawText = resolveEventRawText(params.event);
838
+ return resolveControlCommandGate({
839
+ useAccessGroups,
840
+ authorizers: [{
841
+ configured: params.allow.hasEntries,
842
+ allowed: senderAllowedForCommands
843
+ }],
844
+ allowTextCommands: true,
845
+ hasControlCommand: hasControlCommand(rawText, params.cfg)
846
+ }).commandAuthorized;
847
+ }
848
+ async function handleMessageEvent(event, context) {
849
+ const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
850
+ const message = event.message;
851
+ const decision = await shouldProcessLineEvent(event, context);
852
+ if (!decision.allowed) return;
853
+ const { isGroup, groupId, roomId } = getLineSourceInfo(event.source);
854
+ if (isGroup) {
855
+ const requireMention = resolveLineGroupConfig({
856
+ config: account.config,
857
+ groupId,
858
+ roomId
859
+ })?.requireMention !== false;
860
+ const rawText = message.type === "text" ? message.text : "";
861
+ const sourceInfo = getLineSourceInfo(event.source);
862
+ const peerId = groupId ?? roomId ?? sourceInfo.userId ?? "unknown";
863
+ const { agentId } = resolveAgentRoute({
864
+ cfg,
865
+ channel: "line",
866
+ accountId: account.accountId,
867
+ peer: {
868
+ kind: "group",
869
+ id: peerId
870
+ }
871
+ });
872
+ const mentionRegexes = buildMentionRegexes(cfg, agentId);
873
+ const wasMentionedByNative = isLineBotMentioned(message);
874
+ const wasMentionedByPattern = message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
875
+ const wasMentioned = wasMentionedByNative || wasMentionedByPattern;
876
+ if (resolveInboundMentionDecision({
877
+ facts: {
878
+ canDetectMention: message.type === "text",
879
+ wasMentioned,
880
+ hasAnyMention: hasAnyLineMention(message),
881
+ implicitMentionKinds: []
882
+ },
883
+ policy: {
884
+ isGroup: true,
885
+ requireMention,
886
+ allowTextCommands: true,
887
+ hasControlCommand: hasControlCommand(rawText, cfg),
888
+ commandAuthorized: decision.commandAuthorized
889
+ }
890
+ }).shouldSkip) {
891
+ logVerbose(`line: skipping group message (requireMention, not mentioned)`);
892
+ const historyKey = groupId ?? roomId;
893
+ const senderId = sourceInfo.userId ?? "unknown";
894
+ if (historyKey && context.groupHistories) recordPendingHistoryEntryIfEnabled({
895
+ historyMap: context.groupHistories,
896
+ historyKey,
897
+ limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
898
+ entry: {
899
+ sender: `user:${senderId}`,
900
+ body: rawText || `<${message.type}>`,
901
+ timestamp: event.timestamp
902
+ }
903
+ });
904
+ return;
905
+ }
906
+ }
907
+ const allMedia = [];
908
+ if (isDownloadableLineMessageType(message.type)) try {
909
+ const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes);
910
+ allMedia.push({
911
+ path: media.path,
912
+ contentType: media.contentType
913
+ });
914
+ } catch (err) {
915
+ const errMsg = String(err);
916
+ if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
917
+ else runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
918
+ }
919
+ const messageContext = await buildLineMessageContext({
920
+ event,
921
+ allMedia,
922
+ cfg,
923
+ account,
924
+ commandAuthorized: decision.commandAuthorized,
925
+ groupHistories: context.groupHistories,
926
+ historyLimit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
927
+ });
928
+ if (!messageContext) {
929
+ logVerbose("line: skipping empty message");
930
+ return;
931
+ }
932
+ await processMessage(messageContext);
933
+ if (isGroup && context.groupHistories) {
934
+ const historyKey = groupId ?? roomId;
935
+ if (historyKey && context.groupHistories.has(historyKey)) clearHistoryEntriesIfEnabled({
936
+ historyMap: context.groupHistories,
937
+ historyKey,
938
+ limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
939
+ });
940
+ }
941
+ }
942
+ async function handleFollowEvent(event, _context) {
943
+ const { userId } = getLineSourceInfo(event.source);
944
+ logVerbose(`line: user ${userId ?? "unknown"} followed`);
945
+ }
946
+ async function handleUnfollowEvent(event, _context) {
947
+ const { userId } = getLineSourceInfo(event.source);
948
+ logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
949
+ }
950
+ async function handleJoinEvent(event, _context) {
951
+ const { groupId, roomId } = getLineSourceInfo(event.source);
952
+ logVerbose(`line: bot joined ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
953
+ }
954
+ async function handleLeaveEvent(event, _context) {
955
+ const { groupId, roomId } = getLineSourceInfo(event.source);
956
+ logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
957
+ }
958
+ async function handlePostbackEvent(event, context) {
959
+ const data = event.postback.data;
960
+ logVerbose(`line: received postback: ${data}`);
961
+ const decision = await shouldProcessLineEvent(event, context);
962
+ if (!decision.allowed) return;
963
+ const postbackContext = await buildLinePostbackContext({
964
+ event,
965
+ cfg: context.cfg,
966
+ account: context.account,
967
+ commandAuthorized: decision.commandAuthorized
968
+ });
969
+ if (!postbackContext) return;
970
+ await context.processMessage(postbackContext);
971
+ }
972
+ async function handleLineWebhookEvents(events, context) {
973
+ let firstError;
974
+ for (const event of events) {
975
+ const replayCandidate = getLineReplayCandidate(event, context);
976
+ const replaySkip = replayCandidate ? await claimLineReplayEvent(replayCandidate) : null;
977
+ if (replaySkip?.skip) {
978
+ if (replaySkip.inFlightResult) try {
979
+ await replaySkip.inFlightResult;
980
+ } catch (err) {
981
+ context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
982
+ firstError ??= err;
983
+ }
984
+ continue;
985
+ }
986
+ try {
987
+ switch (event.type) {
988
+ case "message":
989
+ await handleMessageEvent(event, context);
990
+ break;
991
+ case "follow":
992
+ await handleFollowEvent(event, context);
993
+ break;
994
+ case "unfollow":
995
+ await handleUnfollowEvent(event, context);
996
+ break;
997
+ case "join":
998
+ await handleJoinEvent(event, context);
999
+ break;
1000
+ case "leave":
1001
+ await handleLeaveEvent(event, context);
1002
+ break;
1003
+ case "postback":
1004
+ await handlePostbackEvent(event, context);
1005
+ break;
1006
+ default: logVerbose(`line: unhandled event type: ${event.type}`);
1007
+ }
1008
+ if (replayCandidate) await replayCandidate.cache.commit(replayCandidate.key);
1009
+ } catch (err) {
1010
+ if (replayCandidate) if (err instanceof LineRetryableWebhookError) replayCandidate.cache.release(replayCandidate.key, { error: err });
1011
+ else await replayCandidate.cache.commit(replayCandidate.key);
1012
+ context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
1013
+ firstError ??= err;
1014
+ }
1015
+ }
1016
+ if (firstError) throw firstError;
1017
+ }
1018
+ //#endregion
1019
+ //#region extensions/line/src/bot.ts
1020
+ function createLineBot(opts) {
1021
+ const runtime = opts.runtime ?? createNonExitingRuntime();
1022
+ const cfg = opts.config ?? getRuntimeConfig();
1023
+ const account = resolveLineAccount({
1024
+ cfg,
1025
+ accountId: opts.accountId
1026
+ });
1027
+ const mediaMaxBytes = (opts.mediaMaxMb ?? account.config.mediaMaxMb ?? 10) * 1024 * 1024;
1028
+ const processMessage = opts.onMessage ?? (async () => {
1029
+ logVerbose("line: no message handler configured");
1030
+ });
1031
+ const replayCache = createLineWebhookReplayCache();
1032
+ const groupHistories = /* @__PURE__ */ new Map();
1033
+ const handleWebhook = async (body) => {
1034
+ if (!body.events || body.events.length === 0) return;
1035
+ await handleLineWebhookEvents(body.events, {
1036
+ cfg,
1037
+ account,
1038
+ runtime,
1039
+ mediaMaxBytes,
1040
+ processMessage,
1041
+ replayCache,
1042
+ groupHistories,
1043
+ historyLimit: cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
1044
+ });
1045
+ };
1046
+ return {
1047
+ handleWebhook,
1048
+ account
1049
+ };
1050
+ }
1051
+ //#endregion
1052
+ //#region extensions/line/src/reply-chunks.ts
1053
+ async function sendLineReplyChunks(params) {
1054
+ const hasQuickReplies = Boolean(params.quickReplies?.length);
1055
+ let replyTokenUsed = Boolean(params.replyTokenUsed);
1056
+ if (params.chunks.length === 0) return { replyTokenUsed };
1057
+ if (params.replyToken && !replyTokenUsed) try {
1058
+ const replyBatch = params.chunks.slice(0, 5);
1059
+ const remaining = params.chunks.slice(replyBatch.length);
1060
+ const replyMessages = replyBatch.map((chunk) => ({
1061
+ type: "text",
1062
+ text: chunk
1063
+ }));
1064
+ if (hasQuickReplies && remaining.length === 0 && replyMessages.length > 0) {
1065
+ const lastIndex = replyMessages.length - 1;
1066
+ replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(replyBatch[lastIndex], params.quickReplies);
1067
+ }
1068
+ await params.replyMessageLine(params.replyToken, replyMessages, {
1069
+ cfg: params.cfg,
1070
+ accountId: params.accountId
1071
+ });
1072
+ replyTokenUsed = true;
1073
+ for (let i = 0; i < remaining.length; i += 1) if (i === remaining.length - 1 && hasQuickReplies) await params.pushTextMessageWithQuickReplies(params.to, remaining[i], params.quickReplies, {
1074
+ cfg: params.cfg,
1075
+ accountId: params.accountId
1076
+ });
1077
+ else await params.pushMessageLine(params.to, remaining[i], {
1078
+ cfg: params.cfg,
1079
+ accountId: params.accountId
1080
+ });
1081
+ return { replyTokenUsed };
1082
+ } catch (err) {
1083
+ params.onReplyError?.(err);
1084
+ replyTokenUsed = true;
1085
+ }
1086
+ 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, {
1087
+ cfg: params.cfg,
1088
+ accountId: params.accountId
1089
+ });
1090
+ else await params.pushMessageLine(params.to, params.chunks[i], {
1091
+ cfg: params.cfg,
1092
+ accountId: params.accountId
1093
+ });
1094
+ return { replyTokenUsed };
1095
+ }
1096
+ //#endregion
1097
+ //#region extensions/line/src/signature.ts
1098
+ function validateLineSignature(body, signature, channelSecret) {
1099
+ const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
1100
+ const hashBuffer = Buffer.from(hash);
1101
+ const signatureBuffer = Buffer.from(signature);
1102
+ const maxLen = Math.max(hashBuffer.length, signatureBuffer.length);
1103
+ const paddedHash = Buffer.alloc(maxLen);
1104
+ const paddedSig = Buffer.alloc(maxLen);
1105
+ hashBuffer.copy(paddedHash);
1106
+ signatureBuffer.copy(paddedSig);
1107
+ const timingResult = crypto.timingSafeEqual(paddedHash, paddedSig);
1108
+ return hashBuffer.length === signatureBuffer.length && timingResult;
1109
+ }
1110
+ //#endregion
1111
+ //#region extensions/line/src/webhook-utils.ts
1112
+ function parseLineWebhookBody(rawBody) {
1113
+ try {
1114
+ return JSON.parse(rawBody);
1115
+ } catch {
1116
+ return null;
1117
+ }
1118
+ }
1119
+ //#endregion
1120
+ //#region extensions/line/src/webhook-node.ts
1121
+ const LINE_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1122
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1 = 64 * 1024;
1123
+ const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1 = 5e3;
1124
+ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES, timeoutMs = LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1) {
1125
+ return await readRequestBodyWithLimit(req, {
1126
+ maxBytes,
1127
+ timeoutMs
1128
+ });
1129
+ }
1130
+ function createLineNodeWebhookHandler(params) {
1131
+ const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
1132
+ const readBody = params.readBody ?? readLineWebhookRequestBody;
1133
+ return async (req, res) => {
1134
+ if (req.method === "GET" || req.method === "HEAD") {
1135
+ if (req.method === "HEAD") {
1136
+ res.statusCode = 204;
1137
+ res.end();
1138
+ return;
1139
+ }
1140
+ res.statusCode = 200;
1141
+ res.setHeader("Content-Type", "text/plain");
1142
+ res.end("OK");
1143
+ return;
1144
+ }
1145
+ if (req.method !== "POST") {
1146
+ res.statusCode = 405;
1147
+ res.setHeader("Allow", "GET, HEAD, POST");
1148
+ res.setHeader("Content-Type", "application/json");
1149
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
1150
+ return;
1151
+ }
1152
+ try {
1153
+ const signatureHeader = req.headers["x-line-signature"];
1154
+ const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
1155
+ if (!signature) {
1156
+ logVerbose("line: webhook missing X-Line-Signature header");
1157
+ res.statusCode = 400;
1158
+ res.setHeader("Content-Type", "application/json");
1159
+ res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
1160
+ return;
1161
+ }
1162
+ const rawBody = await readBody(req, Math.min(maxBodyBytes, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES$1), LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS$1);
1163
+ if (!validateLineSignature(rawBody, signature, params.channelSecret)) {
1164
+ logVerbose("line: webhook signature validation failed");
1165
+ res.statusCode = 401;
1166
+ res.setHeader("Content-Type", "application/json");
1167
+ res.end(JSON.stringify({ error: "Invalid signature" }));
1168
+ return;
1169
+ }
1170
+ const body = parseLineWebhookBody(rawBody);
1171
+ if (!body) {
1172
+ res.statusCode = 400;
1173
+ res.setHeader("Content-Type", "application/json");
1174
+ res.end(JSON.stringify({ error: "Invalid webhook payload" }));
1175
+ return;
1176
+ }
1177
+ params.onRequestAuthenticated?.();
1178
+ if (body.events && body.events.length > 0) {
1179
+ logVerbose(`line: received ${body.events.length} webhook events`);
1180
+ await params.bot.handleWebhook(body);
1181
+ }
1182
+ res.statusCode = 200;
1183
+ res.setHeader("Content-Type", "application/json");
1184
+ res.end(JSON.stringify({ status: "ok" }));
1185
+ } catch (err) {
1186
+ if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
1187
+ res.statusCode = 413;
1188
+ res.setHeader("Content-Type", "application/json");
1189
+ res.end(JSON.stringify({ error: "Payload too large" }));
1190
+ return;
1191
+ }
1192
+ if (isRequestBodyLimitError$1(err, "REQUEST_BODY_TIMEOUT")) {
1193
+ res.statusCode = 408;
1194
+ res.setHeader("Content-Type", "application/json");
1195
+ res.end(JSON.stringify({ error: requestBodyErrorToText$1("REQUEST_BODY_TIMEOUT") }));
1196
+ return;
1197
+ }
1198
+ params.runtime.error?.(danger(`line webhook error: ${String(err)}`));
1199
+ if (!res.headersSent) {
1200
+ res.statusCode = 500;
1201
+ res.setHeader("Content-Type", "application/json");
1202
+ res.end(JSON.stringify({ error: "Internal server error" }));
1203
+ }
1204
+ }
1205
+ };
1206
+ }
1207
+ //#endregion
1208
+ //#region extensions/line/src/monitor.ts
1209
+ const runtimeState = /* @__PURE__ */ new Map();
1210
+ const lineWebhookInFlightLimiter = createWebhookInFlightLimiter();
1211
+ const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 64 * 1024;
1212
+ const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5e3;
1213
+ const lineWebhookTargets = /* @__PURE__ */ new Map();
1214
+ function recordChannelRuntimeState(params) {
1215
+ const key = `${params.channel}:${params.accountId}`;
1216
+ const existing = runtimeState.get(key) ?? {
1217
+ running: false,
1218
+ lastStartAt: null,
1219
+ lastStopAt: null,
1220
+ lastError: null
1221
+ };
1222
+ runtimeState.set(key, {
1223
+ ...existing,
1224
+ ...params.state
1225
+ });
1226
+ }
1227
+ function startLineLoadingKeepalive(params) {
1228
+ const intervalMs = params.intervalMs ?? 18e3;
1229
+ const loadingSeconds = params.loadingSeconds ?? 20;
1230
+ let stopped = false;
1231
+ const trigger = () => {
1232
+ if (stopped) return;
1233
+ showLoadingAnimation(params.userId, {
1234
+ cfg: params.cfg,
1235
+ accountId: params.accountId,
1236
+ loadingSeconds
1237
+ }).catch(() => {});
1238
+ };
1239
+ trigger();
1240
+ const timer = setInterval(trigger, intervalMs);
1241
+ return () => {
1242
+ if (stopped) return;
1243
+ stopped = true;
1244
+ clearInterval(timer);
1245
+ };
1246
+ }
1247
+ async function monitorLineProvider(opts) {
1248
+ const { channelAccessToken, channelSecret, accountId, config, runtime, abortSignal, webhookPath } = opts;
1249
+ const resolvedAccountId = accountId ?? resolveDefaultLineAccountId(config);
1250
+ const token = channelAccessToken.trim();
1251
+ const secret = channelSecret.trim();
1252
+ if (!token) throw new Error("LINE webhook mode requires a non-empty channel access token.");
1253
+ if (!secret) throw new Error("LINE webhook mode requires a non-empty channel secret.");
1254
+ recordChannelRuntimeState({
1255
+ channel: "line",
1256
+ accountId: resolvedAccountId,
1257
+ state: {
1258
+ running: true,
1259
+ lastStartAt: Date.now()
1260
+ }
1261
+ });
1262
+ const bot = createLineBot({
1263
+ channelAccessToken: token,
1264
+ channelSecret: secret,
1265
+ accountId,
1266
+ runtime,
1267
+ config,
1268
+ onMessage: async (ctx) => {
1269
+ if (!ctx) return;
1270
+ const { ctxPayload, replyToken, route } = ctx;
1271
+ recordChannelRuntimeState({
1272
+ channel: "line",
1273
+ accountId: resolvedAccountId,
1274
+ state: { lastInboundAt: Date.now() }
1275
+ });
1276
+ const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
1277
+ const displayNamePromise = ctx.userId ? getUserDisplayName(ctx.userId, {
1278
+ cfg: config,
1279
+ accountId: ctx.accountId
1280
+ }) : Promise.resolve(ctxPayload.From);
1281
+ const stopLoading = shouldShowLoading ? startLineLoadingKeepalive({
1282
+ cfg: config,
1283
+ userId: ctx.userId,
1284
+ accountId: ctx.accountId
1285
+ }) : null;
1286
+ logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
1287
+ try {
1288
+ const textLimit = 5e3;
1289
+ let replyTokenUsed = false;
1290
+ const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
1291
+ cfg: config,
1292
+ agentId: route.agentId,
1293
+ channel: "line",
1294
+ accountId: route.accountId
1295
+ });
1296
+ const core = getLineRuntime();
1297
+ const turnResult = await core.channel.turn.run({
1298
+ channel: "line",
1299
+ accountId: route.accountId,
1300
+ raw: ctx,
1301
+ adapter: {
1302
+ ingest: () => ({
1303
+ id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
1304
+ rawText: ctxPayload.RawBody ?? ctxPayload.BodyForAgent ?? ""
1305
+ }),
1306
+ resolveTurn: () => ({
1307
+ cfg: config,
1308
+ channel: "line",
1309
+ accountId: route.accountId,
1310
+ agentId: route.agentId,
1311
+ routeSessionKey: route.sessionKey,
1312
+ storePath: ctx.turn.storePath,
1313
+ ctxPayload,
1314
+ recordInboundSession: core.channel.session.recordInboundSession,
1315
+ dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
1316
+ record: ctx.turn.record,
1317
+ dispatcherOptions: { ...replyPipeline },
1318
+ replyOptions: { onModelSelected },
1319
+ delivery: {
1320
+ deliver: async (payload) => {
1321
+ const lineData = payload.channelData?.line ?? {};
1322
+ if (ctx.userId && !ctx.isGroup) showLoadingAnimation(ctx.userId, {
1323
+ cfg: config,
1324
+ accountId: ctx.accountId
1325
+ }).catch(() => {});
1326
+ const { replyTokenUsed: nextReplyTokenUsed } = await deliverLineAutoReply({
1327
+ payload,
1328
+ lineData,
1329
+ to: ctxPayload.From,
1330
+ replyToken,
1331
+ replyTokenUsed,
1332
+ accountId: ctx.accountId,
1333
+ cfg: config,
1334
+ textLimit,
1335
+ deps: {
1336
+ buildTemplateMessageFromPayload,
1337
+ processLineMessage,
1338
+ chunkMarkdownText,
1339
+ sendLineReplyChunks,
1340
+ replyMessageLine,
1341
+ pushMessageLine,
1342
+ pushTextMessageWithQuickReplies,
1343
+ createQuickReplyItems,
1344
+ createTextMessageWithQuickReplies,
1345
+ pushMessagesLine,
1346
+ createFlexMessage,
1347
+ createImageMessage,
1348
+ createLocationMessage,
1349
+ onReplyError: (replyErr) => {
1350
+ logVerbose(`line: reply token failed, falling back to push: ${String(replyErr)}`);
1351
+ }
1352
+ }
1353
+ });
1354
+ replyTokenUsed = nextReplyTokenUsed;
1355
+ recordChannelRuntimeState({
1356
+ channel: "line",
1357
+ accountId: resolvedAccountId,
1358
+ state: { lastOutboundAt: Date.now() }
1359
+ });
1360
+ },
1361
+ onError: (err, info) => {
1362
+ runtime.error?.(danger(`line ${info.kind} reply failed: ${String(err)}`));
1363
+ }
1364
+ }
1365
+ })
1366
+ }
1367
+ });
1368
+ if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1369
+ } catch (err) {
1370
+ runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
1371
+ if (replyToken) try {
1372
+ await replyMessageLine(replyToken, [{
1373
+ type: "text",
1374
+ text: "Sorry, I encountered an error processing your message."
1375
+ }], {
1376
+ cfg: config,
1377
+ accountId: ctx.accountId
1378
+ });
1379
+ } catch (replyErr) {
1380
+ runtime.error?.(danger(`line: error reply failed: ${String(replyErr)}`));
1381
+ }
1382
+ } finally {
1383
+ stopLoading?.();
1384
+ }
1385
+ }
1386
+ });
1387
+ const normalizedPath = normalizePluginHttpPath(webhookPath, "/line/webhook") ?? "/line/webhook";
1388
+ const createScopedLineWebhookHandler = (target) => createLineNodeWebhookHandler({
1389
+ channelSecret: target.channelSecret,
1390
+ bot: target.bot,
1391
+ runtime: target.runtime
1392
+ });
1393
+ const { unregister: unregisterHttp } = registerWebhookTargetWithPluginRoute({
1394
+ targetsByPath: lineWebhookTargets,
1395
+ target: {
1396
+ accountId: resolvedAccountId,
1397
+ bot,
1398
+ channelSecret: secret,
1399
+ path: normalizedPath,
1400
+ runtime
1401
+ },
1402
+ route: {
1403
+ auth: "plugin",
1404
+ pluginId: "line",
1405
+ accountId: resolvedAccountId,
1406
+ log: (msg) => logVerbose(msg),
1407
+ handler: async (req, res) => {
1408
+ const targets = lineWebhookTargets.get(normalizedPath) ?? [];
1409
+ const firstTarget = targets[0];
1410
+ if (req.method !== "POST") {
1411
+ if (!firstTarget) {
1412
+ res.statusCode = 404;
1413
+ res.end("Not Found");
1414
+ return;
1415
+ }
1416
+ await createScopedLineWebhookHandler(firstTarget)(req, res);
1417
+ return;
1418
+ }
1419
+ const requestLifecycle = beginWebhookRequestPipelineOrReject({
1420
+ req,
1421
+ res,
1422
+ inFlightLimiter: lineWebhookInFlightLimiter,
1423
+ inFlightKey: `line:${normalizedPath}`
1424
+ });
1425
+ if (!requestLifecycle.ok) return;
1426
+ try {
1427
+ const signatureHeader = req.headers["x-line-signature"];
1428
+ const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
1429
+ if (!signature) {
1430
+ logVerbose("line: webhook missing X-Line-Signature header");
1431
+ res.statusCode = 400;
1432
+ res.setHeader("Content-Type", "application/json");
1433
+ res.end(JSON.stringify({ error: "Missing X-Line-Signature header" }));
1434
+ return;
1435
+ }
1436
+ const rawBody = await readLineWebhookRequestBody(req, LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES, LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS);
1437
+ const match = resolveSingleWebhookTarget(targets, (target) => validateLineSignature(rawBody, signature, target.channelSecret));
1438
+ if (match.kind === "none") {
1439
+ logVerbose("line: webhook signature validation failed");
1440
+ res.statusCode = 401;
1441
+ res.setHeader("Content-Type", "application/json");
1442
+ res.end(JSON.stringify({ error: "Invalid signature" }));
1443
+ return;
1444
+ }
1445
+ if (match.kind === "ambiguous") {
1446
+ logVerbose("line: webhook signature matched multiple accounts");
1447
+ res.statusCode = 401;
1448
+ res.setHeader("Content-Type", "application/json");
1449
+ res.end(JSON.stringify({ error: "Ambiguous webhook target" }));
1450
+ return;
1451
+ }
1452
+ const body = parseLineWebhookBody(rawBody);
1453
+ if (!body) {
1454
+ res.statusCode = 400;
1455
+ res.setHeader("Content-Type", "application/json");
1456
+ res.end(JSON.stringify({ error: "Invalid webhook payload" }));
1457
+ return;
1458
+ }
1459
+ requestLifecycle.release();
1460
+ if (body.events && body.events.length > 0) {
1461
+ logVerbose(`line: received ${body.events.length} webhook events`);
1462
+ await match.target.bot.handleWebhook(body);
1463
+ }
1464
+ res.statusCode = 200;
1465
+ res.setHeader("Content-Type", "application/json");
1466
+ res.end(JSON.stringify({ status: "ok" }));
1467
+ } catch (err) {
1468
+ if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1469
+ res.statusCode = 413;
1470
+ res.setHeader("Content-Type", "application/json");
1471
+ res.end(JSON.stringify({ error: "Payload too large" }));
1472
+ return;
1473
+ }
1474
+ if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
1475
+ res.statusCode = 408;
1476
+ res.setHeader("Content-Type", "application/json");
1477
+ res.end(JSON.stringify({ error: requestBodyErrorToText("REQUEST_BODY_TIMEOUT") }));
1478
+ return;
1479
+ }
1480
+ runtime.error?.(danger(`line webhook error: ${String(err)}`));
1481
+ if (!res.headersSent) {
1482
+ res.statusCode = 500;
1483
+ res.setHeader("Content-Type", "application/json");
1484
+ res.end(JSON.stringify({ error: "Internal server error" }));
1485
+ }
1486
+ } finally {
1487
+ requestLifecycle.release();
1488
+ }
1489
+ }
1490
+ }
1491
+ });
1492
+ logVerbose(`line: registered webhook handler at ${normalizedPath}`);
1493
+ let stopped = false;
1494
+ const stopHandler = () => {
1495
+ if (stopped) return;
1496
+ stopped = true;
1497
+ logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
1498
+ unregisterHttp();
1499
+ recordChannelRuntimeState({
1500
+ channel: "line",
1501
+ accountId: resolvedAccountId,
1502
+ state: {
1503
+ running: false,
1504
+ lastStopAt: Date.now()
1505
+ }
1506
+ });
1507
+ };
1508
+ if (abortSignal?.aborted) stopHandler();
1509
+ else if (abortSignal) {
1510
+ abortSignal.addEventListener("abort", stopHandler, { once: true });
1511
+ await waitForAbortSignal(abortSignal);
1512
+ }
1513
+ return {
1514
+ account: bot.account,
1515
+ handleWebhook: bot.handleWebhook,
1516
+ stop: () => {
1517
+ stopHandler();
1518
+ abortSignal?.removeEventListener("abort", stopHandler);
1519
+ }
1520
+ };
1521
+ }
1522
+ //#endregion
1523
+ export { validateLineSignature as a, isSenderAllowed as c, parseLineWebhookBody as i, normalizeAllowFrom as l, createLineNodeWebhookHandler as n, downloadLineMedia as o, readLineWebhookRequestBody as r, firstDefined as s, monitorLineProvider as t, normalizeDmAllowFromWithStore as u };