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; }