@kodelyth/line 2026.5.42 → 2026.6.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 (90) hide show
  1. package/klaw.plugin.json +329 -2
  2. package/package.json +16 -4
  3. package/api.ts +0 -11
  4. package/channel-plugin-api.ts +0 -1
  5. package/contract-api.ts +0 -5
  6. package/index.ts +0 -53
  7. package/runtime-api.ts +0 -179
  8. package/secret-contract-api.ts +0 -4
  9. package/setup-api.ts +0 -2
  10. package/setup-entry.ts +0 -9
  11. package/src/account-helpers.ts +0 -16
  12. package/src/accounts.test.ts +0 -288
  13. package/src/accounts.ts +0 -187
  14. package/src/actions.ts +0 -61
  15. package/src/auto-reply-delivery.test.ts +0 -253
  16. package/src/auto-reply-delivery.ts +0 -200
  17. package/src/bindings.ts +0 -65
  18. package/src/bot-access.ts +0 -30
  19. package/src/bot-handlers.test.ts +0 -1094
  20. package/src/bot-handlers.ts +0 -620
  21. package/src/bot-message-context.test.ts +0 -420
  22. package/src/bot-message-context.ts +0 -586
  23. package/src/bot.ts +0 -66
  24. package/src/card-command.ts +0 -347
  25. package/src/channel-access-token.ts +0 -14
  26. package/src/channel-api.ts +0 -17
  27. package/src/channel-setup-status.contract.test.ts +0 -70
  28. package/src/channel-shared.ts +0 -48
  29. package/src/channel.logout.test.ts +0 -145
  30. package/src/channel.runtime.ts +0 -3
  31. package/src/channel.sendPayload.test.ts +0 -659
  32. package/src/channel.setup.ts +0 -11
  33. package/src/channel.status.test.ts +0 -63
  34. package/src/channel.ts +0 -155
  35. package/src/config-adapter.ts +0 -29
  36. package/src/config-schema.test.ts +0 -53
  37. package/src/config-schema.ts +0 -81
  38. package/src/download.test.ts +0 -164
  39. package/src/download.ts +0 -34
  40. package/src/flex-templates/basic-cards.ts +0 -395
  41. package/src/flex-templates/common.ts +0 -20
  42. package/src/flex-templates/media-control-cards.ts +0 -555
  43. package/src/flex-templates/message.ts +0 -13
  44. package/src/flex-templates/schedule-cards.ts +0 -467
  45. package/src/flex-templates/types.ts +0 -22
  46. package/src/flex-templates.ts +0 -32
  47. package/src/gateway.ts +0 -129
  48. package/src/group-keys.test.ts +0 -123
  49. package/src/group-keys.ts +0 -65
  50. package/src/group-policy.ts +0 -22
  51. package/src/markdown-to-line.test.ts +0 -348
  52. package/src/markdown-to-line.ts +0 -416
  53. package/src/message-cards.test.ts +0 -204
  54. package/src/monitor-durable.test.ts +0 -57
  55. package/src/monitor-durable.ts +0 -37
  56. package/src/monitor.lifecycle.test.ts +0 -499
  57. package/src/monitor.runtime.ts +0 -1
  58. package/src/monitor.ts +0 -507
  59. package/src/outbound-media.test.ts +0 -194
  60. package/src/outbound-media.ts +0 -120
  61. package/src/outbound.runtime.ts +0 -12
  62. package/src/outbound.ts +0 -427
  63. package/src/probe.contract.test.ts +0 -9
  64. package/src/probe.runtime.ts +0 -1
  65. package/src/probe.ts +0 -34
  66. package/src/quick-reply-fallback.ts +0 -10
  67. package/src/reply-chunks.test.ts +0 -180
  68. package/src/reply-chunks.ts +0 -110
  69. package/src/reply-payload-transform.test.ts +0 -392
  70. package/src/reply-payload-transform.ts +0 -317
  71. package/src/rich-menu.test.ts +0 -315
  72. package/src/rich-menu.ts +0 -326
  73. package/src/runtime.ts +0 -32
  74. package/src/send-receipt.ts +0 -32
  75. package/src/send.test.ts +0 -453
  76. package/src/send.ts +0 -531
  77. package/src/setup-core.ts +0 -149
  78. package/src/setup-runtime-api.ts +0 -9
  79. package/src/setup-surface.test.ts +0 -481
  80. package/src/setup-surface.ts +0 -229
  81. package/src/signature.test.ts +0 -34
  82. package/src/signature.ts +0 -24
  83. package/src/status.ts +0 -37
  84. package/src/template-messages.ts +0 -333
  85. package/src/types.ts +0 -130
  86. package/src/webhook-node.test.ts +0 -598
  87. package/src/webhook-node.ts +0 -155
  88. package/src/webhook-utils.ts +0 -10
  89. package/src/webhook.ts +0 -135
  90. package/tsconfig.json +0 -16
package/src/send.test.ts DELETED
@@ -1,453 +0,0 @@
1
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const {
4
- pushMessageMock,
5
- replyMessageMock,
6
- showLoadingAnimationMock,
7
- getProfileMock,
8
- MessagingApiClientMock,
9
- requireRuntimeConfigMock,
10
- resolveLineAccountMock,
11
- resolveLineChannelAccessTokenMock,
12
- recordChannelActivityMock,
13
- logVerboseMock,
14
- resolvePinnedHostnameWithPolicyMock,
15
- } = vi.hoisted(() => {
16
- const pushMessageMock = vi.fn();
17
- const replyMessageMock = vi.fn();
18
- const showLoadingAnimationMock = vi.fn();
19
- const getProfileMock = vi.fn();
20
- const MessagingApiClientMock = vi.fn(function () {
21
- return {
22
- pushMessage: pushMessageMock,
23
- replyMessage: replyMessageMock,
24
- showLoadingAnimation: showLoadingAnimationMock,
25
- getProfile: getProfileMock,
26
- };
27
- });
28
- const requireRuntimeConfigMock = vi.fn((cfg: unknown) => cfg ?? {});
29
- const resolveLineAccountMock = vi.fn(() => ({ accountId: "default" }));
30
- const resolveLineChannelAccessTokenMock = vi.fn(() => "line-token");
31
- const recordChannelActivityMock = vi.fn();
32
- const logVerboseMock = vi.fn();
33
- const resolvePinnedHostnameWithPolicyMock = vi.fn();
34
- return {
35
- pushMessageMock,
36
- replyMessageMock,
37
- showLoadingAnimationMock,
38
- getProfileMock,
39
- MessagingApiClientMock,
40
- requireRuntimeConfigMock,
41
- resolveLineAccountMock,
42
- resolveLineChannelAccessTokenMock,
43
- recordChannelActivityMock,
44
- logVerboseMock,
45
- resolvePinnedHostnameWithPolicyMock,
46
- };
47
- });
48
-
49
- vi.mock("@line/bot-sdk", () => ({
50
- messagingApi: { MessagingApiClient: MessagingApiClientMock },
51
- }));
52
-
53
- vi.mock("klaw/plugin-sdk/plugin-config-runtime", () => ({
54
- requireRuntimeConfig: requireRuntimeConfigMock,
55
- }));
56
-
57
- vi.mock("./accounts.js", () => ({
58
- resolveLineAccount: resolveLineAccountMock,
59
- }));
60
-
61
- vi.mock("./channel-access-token.js", () => ({
62
- resolveLineChannelAccessToken: resolveLineChannelAccessTokenMock,
63
- }));
64
-
65
- vi.mock("klaw/plugin-sdk/channel-activity-runtime", () => ({
66
- recordChannelActivity: recordChannelActivityMock,
67
- }));
68
-
69
- vi.mock("klaw/plugin-sdk/runtime-env", async () => {
70
- const actual = await vi.importActual<typeof import("klaw/plugin-sdk/runtime-env")>(
71
- "klaw/plugin-sdk/runtime-env",
72
- );
73
- return {
74
- ...actual,
75
- logVerbose: logVerboseMock,
76
- };
77
- });
78
-
79
- vi.mock("klaw/plugin-sdk/ssrf-runtime", () => ({
80
- resolvePinnedHostnameWithPolicy: resolvePinnedHostnameWithPolicyMock,
81
- }));
82
-
83
- let sendModule: typeof import("./send.js");
84
-
85
- const LINE_TEST_CFG = {
86
- channels: {
87
- line: {
88
- accounts: {
89
- default: {},
90
- },
91
- },
92
- },
93
- };
94
-
95
- describe("LINE send helpers", () => {
96
- const fixedSentAt = 1_800_000_000_000;
97
-
98
- beforeAll(async () => {
99
- sendModule = await import("./send.js");
100
- });
101
-
102
- afterAll(() => {
103
- vi.doUnmock("@line/bot-sdk");
104
- vi.doUnmock("klaw/plugin-sdk/plugin-config-runtime");
105
- vi.doUnmock("./accounts.js");
106
- vi.doUnmock("./channel-access-token.js");
107
- vi.doUnmock("klaw/plugin-sdk/channel-activity-runtime");
108
- vi.doUnmock("klaw/plugin-sdk/runtime-env");
109
- vi.doUnmock("klaw/plugin-sdk/ssrf-runtime");
110
- vi.resetModules();
111
- });
112
-
113
- beforeEach(() => {
114
- vi.setSystemTime(fixedSentAt);
115
- pushMessageMock.mockReset();
116
- replyMessageMock.mockReset();
117
- showLoadingAnimationMock.mockReset();
118
- getProfileMock.mockReset();
119
- MessagingApiClientMock.mockReset();
120
- requireRuntimeConfigMock.mockClear();
121
- resolveLineAccountMock.mockReset();
122
- resolveLineChannelAccessTokenMock.mockReset();
123
- recordChannelActivityMock.mockReset();
124
- logVerboseMock.mockReset();
125
- resolvePinnedHostnameWithPolicyMock.mockReset();
126
-
127
- MessagingApiClientMock.mockImplementation(function () {
128
- return {
129
- pushMessage: pushMessageMock,
130
- replyMessage: replyMessageMock,
131
- showLoadingAnimation: showLoadingAnimationMock,
132
- getProfile: getProfileMock,
133
- };
134
- });
135
- requireRuntimeConfigMock.mockImplementation((cfg: unknown) => cfg ?? LINE_TEST_CFG);
136
- resolveLineAccountMock.mockReturnValue({ accountId: "default" });
137
- resolveLineChannelAccessTokenMock.mockReturnValue("line-token");
138
- resolvePinnedHostnameWithPolicyMock.mockResolvedValue({
139
- hostname: "example.com",
140
- addresses: ["93.184.216.34"],
141
- });
142
- pushMessageMock.mockResolvedValue({});
143
- replyMessageMock.mockResolvedValue({});
144
- showLoadingAnimationMock.mockResolvedValue({});
145
- });
146
-
147
- afterEach(() => {
148
- vi.useRealTimers();
149
- });
150
-
151
- it("limits quick reply items to 13", () => {
152
- const labels = Array.from({ length: 20 }, (_, index) => `Option ${index + 1}`);
153
- const quickReply = sendModule.createQuickReplyItems(labels);
154
-
155
- expect(quickReply.items).toHaveLength(13);
156
- });
157
-
158
- it("pushes images via normalized LINE target", async () => {
159
- const result = await sendModule.pushImageMessage(
160
- "line:user:U123",
161
- "https://example.com/original.jpg",
162
- undefined,
163
- { cfg: LINE_TEST_CFG, verbose: true },
164
- );
165
-
166
- expect(pushMessageMock).toHaveBeenCalledWith({
167
- to: "U123",
168
- messages: [
169
- {
170
- type: "image",
171
- originalContentUrl: "https://example.com/original.jpg",
172
- previewImageUrl: "https://example.com/original.jpg",
173
- },
174
- ],
175
- });
176
- expect(recordChannelActivityMock).toHaveBeenCalledWith({
177
- channel: "line",
178
- accountId: "default",
179
- direction: "outbound",
180
- });
181
- expect(logVerboseMock).toHaveBeenCalledWith("line: pushed image to U123");
182
- expect(result).toEqual({
183
- chatId: "U123",
184
- messageId: "push",
185
- receipt: {
186
- parts: [
187
- {
188
- index: 0,
189
- kind: "media",
190
- platformMessageId: "push",
191
- raw: {
192
- channel: "line",
193
- chatId: "U123",
194
- conversationId: "U123",
195
- messageId: "push",
196
- meta: { messageCount: 1 },
197
- },
198
- threadId: "U123",
199
- },
200
- ],
201
- platformMessageIds: ["push"],
202
- primaryPlatformMessageId: "push",
203
- raw: [
204
- {
205
- channel: "line",
206
- chatId: "U123",
207
- conversationId: "U123",
208
- messageId: "push",
209
- meta: { messageCount: 1 },
210
- },
211
- ],
212
- sentAt: fixedSentAt,
213
- threadId: "U123",
214
- },
215
- });
216
- });
217
-
218
- it("replies when reply token is provided", async () => {
219
- const result = await sendModule.sendMessageLine("line:group:C1", "Hello", {
220
- cfg: LINE_TEST_CFG,
221
- replyToken: "reply-token",
222
- mediaUrl: "https://example.com/media.jpg",
223
- verbose: true,
224
- });
225
-
226
- expect(replyMessageMock).toHaveBeenCalledTimes(1);
227
- expect(pushMessageMock).not.toHaveBeenCalled();
228
- expect(replyMessageMock).toHaveBeenCalledWith({
229
- replyToken: "reply-token",
230
- messages: [
231
- {
232
- type: "image",
233
- originalContentUrl: "https://example.com/media.jpg",
234
- previewImageUrl: "https://example.com/media.jpg",
235
- },
236
- {
237
- type: "text",
238
- text: "Hello",
239
- },
240
- ],
241
- });
242
- expect(logVerboseMock).toHaveBeenCalledWith("line: replied to C1");
243
- expect(result).toEqual({
244
- chatId: "C1",
245
- messageId: "reply",
246
- receipt: {
247
- parts: [
248
- {
249
- index: 0,
250
- kind: "media",
251
- platformMessageId: "reply",
252
- raw: {
253
- channel: "line",
254
- chatId: "C1",
255
- conversationId: "C1",
256
- messageId: "reply",
257
- meta: { messageCount: 2 },
258
- },
259
- threadId: "C1",
260
- },
261
- ],
262
- platformMessageIds: ["reply"],
263
- primaryPlatformMessageId: "reply",
264
- raw: [
265
- {
266
- channel: "line",
267
- chatId: "C1",
268
- conversationId: "C1",
269
- messageId: "reply",
270
- meta: { messageCount: 2 },
271
- },
272
- ],
273
- sentAt: fixedSentAt,
274
- threadId: "C1",
275
- },
276
- });
277
- });
278
-
279
- it("sends video with explicit image preview URL", async () => {
280
- await sendModule.sendMessageLine("line:user:U100", "Video", {
281
- cfg: LINE_TEST_CFG,
282
- mediaUrl: "https://example.com/video.mp4",
283
- mediaKind: "video",
284
- previewImageUrl: "https://example.com/preview.jpg",
285
- trackingId: "track-1",
286
- });
287
-
288
- expect(pushMessageMock).toHaveBeenCalledWith({
289
- to: "U100",
290
- messages: [
291
- {
292
- type: "video",
293
- originalContentUrl: "https://example.com/video.mp4",
294
- previewImageUrl: "https://example.com/preview.jpg",
295
- trackingId: "track-1",
296
- },
297
- {
298
- type: "text",
299
- text: "Video",
300
- },
301
- ],
302
- });
303
- });
304
-
305
- it("throws when video preview URL is missing", async () => {
306
- await expect(
307
- sendModule.sendMessageLine("line:user:U200", "Video", {
308
- cfg: LINE_TEST_CFG,
309
- mediaUrl: "https://example.com/video.mp4",
310
- mediaKind: "video",
311
- }),
312
- ).rejects.toThrow(/require previewimageurl/i);
313
- });
314
-
315
- it("blocks private-network media URLs before calling LINE", async () => {
316
- resolvePinnedHostnameWithPolicyMock.mockRejectedValueOnce(
317
- new Error("SSRF blocked private network target"),
318
- );
319
-
320
- await expect(
321
- sendModule.sendMessageLine("line:user:U200", "Image", {
322
- cfg: LINE_TEST_CFG,
323
- mediaUrl: "https://127.0.0.1/image.jpg",
324
- }),
325
- ).rejects.toThrow(/private network/i);
326
-
327
- expect(pushMessageMock).not.toHaveBeenCalled();
328
- });
329
-
330
- it("omits trackingId for non-user destinations", async () => {
331
- await sendModule.sendMessageLine("line:group:C100", "Video", {
332
- cfg: LINE_TEST_CFG,
333
- mediaUrl: "https://example.com/video.mp4",
334
- mediaKind: "video",
335
- previewImageUrl: "https://example.com/preview.jpg",
336
- trackingId: "track-group",
337
- });
338
-
339
- expect(pushMessageMock).toHaveBeenCalledWith({
340
- to: "C100",
341
- messages: [
342
- {
343
- type: "video",
344
- originalContentUrl: "https://example.com/video.mp4",
345
- previewImageUrl: "https://example.com/preview.jpg",
346
- },
347
- {
348
- type: "text",
349
- text: "Video",
350
- },
351
- ],
352
- });
353
- });
354
-
355
- it("throws when push messages are empty", async () => {
356
- await expect(sendModule.pushMessagesLine("U123", [], { cfg: LINE_TEST_CFG })).rejects.toThrow(
357
- "Message must be non-empty for LINE sends",
358
- );
359
- });
360
-
361
- it("rejects lowercased LINE-shaped recipients (#81628 safety net)", async () => {
362
- // 33-char value with lowercase leading char — what an upstream session-key
363
- // fragment looked like before the cron-tool fix. LINE rejects with HTTP 400
364
- // anyway; throwing locally keeps the failure permanent so delivery-recovery
365
- // moves the entry to failed/ immediately instead of silently retrying 5×.
366
- await expect(
367
- sendModule.pushMessagesLine(
368
- "cabcdef0123456789abcdef0123456789",
369
- [{ type: "text", text: "hello" }],
370
- { cfg: LINE_TEST_CFG },
371
- ),
372
- ).rejects.toThrow(/Recipient is not a valid LINE id/);
373
- expect(pushMessageMock).not.toHaveBeenCalled();
374
- });
375
-
376
- it("accepts case-exact LINE recipients with the leading capital preserved", async () => {
377
- await sendModule.pushMessagesLine(
378
- "Cabcdef0123456789abcdef0123456789",
379
- [{ type: "text", text: "hello" }],
380
- { cfg: LINE_TEST_CFG },
381
- );
382
- expect(pushMessageMock).toHaveBeenCalledWith({
383
- to: "Cabcdef0123456789abcdef0123456789",
384
- messages: [{ type: "text", text: "hello" }],
385
- });
386
- });
387
-
388
- it("logs HTTP body when push fails", async () => {
389
- const err = new Error("LINE push failed") as Error & {
390
- status: number;
391
- statusText: string;
392
- body: string;
393
- };
394
- err.status = 400;
395
- err.statusText = "Bad Request";
396
- err.body = "invalid flex payload";
397
- pushMessageMock.mockRejectedValueOnce(err);
398
-
399
- await expect(
400
- sendModule.pushMessagesLine("U999", [{ type: "text", text: "hello" }], {
401
- cfg: LINE_TEST_CFG,
402
- }),
403
- ).rejects.toThrow("LINE push failed");
404
-
405
- expect(logVerboseMock).toHaveBeenCalledWith(
406
- "line: push message failed (400 Bad Request): invalid flex payload",
407
- );
408
- });
409
-
410
- it("caches profile results by default", async () => {
411
- getProfileMock.mockResolvedValue({
412
- displayName: "Peter",
413
- pictureUrl: "https://example.com/peter.jpg",
414
- });
415
-
416
- const first = await sendModule.getUserProfile("U-cache", { cfg: LINE_TEST_CFG });
417
- const second = await sendModule.getUserProfile("U-cache", { cfg: LINE_TEST_CFG });
418
-
419
- expect(first).toEqual({
420
- displayName: "Peter",
421
- pictureUrl: "https://example.com/peter.jpg",
422
- });
423
- expect(second).toEqual(first);
424
- expect(getProfileMock).toHaveBeenCalledTimes(1);
425
- });
426
-
427
- it("continues when loading animation is unsupported", async () => {
428
- showLoadingAnimationMock.mockRejectedValueOnce(new Error("unsupported"));
429
-
430
- await expect(
431
- sendModule.showLoadingAnimation("line:room:R1", { cfg: LINE_TEST_CFG }),
432
- ).resolves.toBeUndefined();
433
-
434
- expect(logVerboseMock).toHaveBeenCalledWith(
435
- "line: loading animation failed (non-fatal): Error: unsupported",
436
- );
437
- });
438
-
439
- it("pushes quick-reply text and caps to 13 buttons", async () => {
440
- await sendModule.pushTextMessageWithQuickReplies(
441
- "U-quick",
442
- "Pick one",
443
- Array.from({ length: 20 }, (_, index) => `Choice ${index + 1}`),
444
- { cfg: LINE_TEST_CFG },
445
- );
446
-
447
- expect(pushMessageMock).toHaveBeenCalledTimes(1);
448
- const firstCall = pushMessageMock.mock.calls.at(0) as [
449
- { messages: Array<{ quickReply?: { items: unknown[] } }> },
450
- ];
451
- expect(firstCall[0].messages[0].quickReply?.items).toHaveLength(13);
452
- });
453
- });