@kodelyth/line 2026.5.39 → 2026.5.42

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