From f3f2c784c4fc3ef9030e499d8f555ebb4c58b9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A8=B1=E5=85=83=E8=B1=AA?= <146086744+edenfunf@users.noreply.github.com> Date: Thu, 14 May 2026 14:29:08 +0800 Subject: [PATCH] fix(line): reject lowercased LINE-shaped recipients before push (#81628) Defense-in-depth safety net for #81628: even with the cron-tool fix in place, any other code path that ever produces a 33-char LINE-shaped recipient missing its leading capital (C/U/R) would otherwise hit the LINE API and return HTTP 400 with no permanent-error signal, causing delivery-recovery to retry five times before moving the entry to failed/. normalizeTarget now throws "Recipient is not a valid LINE id ..." when the post-strip value looks like a LINE id but the case was lost. The message matches the existing /recipient is not a valid/i pattern in delivery-queue-recovery's PERMANENT_ERROR_PATTERNS, so recovery moves the entry to failed/ on the first attempt instead of silently retrying. Short fixtures (length < 33) are left alone so existing tests using "U123", "line:user:1", etc. keep working. --- extensions/line/src/send.test.ts | 27 +++++++++++++++++++++++++++ extensions/line/src/send.ts | 12 ++++++++++++ 2 files changed, 39 insertions(+) diff --git a/extensions/line/src/send.test.ts b/extensions/line/src/send.test.ts index e809489dbb1..9e056aa34b3 100644 --- a/extensions/line/src/send.test.ts +++ b/extensions/line/src/send.test.ts @@ -358,6 +358,33 @@ describe("LINE send helpers", () => { ); }); + it("rejects lowercased LINE-shaped recipients (#81628 safety net)", async () => { + // 33-char value with lowercase leading char — what an upstream session-key + // fragment looked like before the cron-tool fix. LINE rejects with HTTP 400 + // anyway; throwing locally keeps the failure permanent so delivery-recovery + // moves the entry to failed/ immediately instead of silently retrying 5×. + await expect( + sendModule.pushMessagesLine( + "cabcdef0123456789abcdef0123456789", + [{ type: "text", text: "hello" }], + { cfg: LINE_TEST_CFG }, + ), + ).rejects.toThrow(/Recipient is not a valid LINE id/); + expect(pushMessageMock).not.toHaveBeenCalled(); + }); + + it("accepts case-exact LINE recipients with the leading capital preserved", async () => { + await sendModule.pushMessagesLine( + "Cabcdef0123456789abcdef0123456789", + [{ type: "text", text: "hello" }], + { cfg: LINE_TEST_CFG }, + ); + expect(pushMessageMock).toHaveBeenCalledWith({ + to: "Cabcdef0123456789abcdef0123456789", + messages: [{ type: "text", text: "hello" }], + }); + }); + it("logs HTTP body when push fails", async () => { const err = new Error("LINE push failed") as Error & { status: number; diff --git a/extensions/line/src/send.ts b/extensions/line/src/send.ts index 636427f5a6b..fde40a23e30 100644 --- a/extensions/line/src/send.ts +++ b/extensions/line/src/send.ts @@ -68,6 +68,18 @@ function normalizeTarget(to: string): string { throw new Error("Recipient is required for LINE sends"); } + // Real LINE chat ids are a capital C/U/R followed by 32 lowercase hex chars + // (33 chars total) and are case-sensitive — push returns HTTP 400 otherwise. + // Reject values that match the LINE id shape but lost their leading capital + // so the failure is surfaced as a permanent error (recovery moves the entry + // to failed/ immediately instead of silently retrying 5 times). Short test + // fixtures (e.g. "U123") are left alone. openclaw/openclaw#81628 + if (normalized.length >= 33 && !/^[CUR]/.test(normalized)) { + throw new Error( + `Recipient is not a valid LINE id (case-sensitive; expected leading capital C/U/R): ${normalized.slice(0, 4)}…`, + ); + } + return normalized; }