From 8db25576e1ed1b4cb41799c1a56176871a37a69f Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Wed, 15 Jul 2026 21:10:03 +0530 Subject: [PATCH 1/2] fix(sdk): fail fast when agent messaging has no encryption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Agent E2E facade (sendMessage) silently sent the body as plaintext when the client was built without `encryption: { store }`. The relay rejects a plaintext JSON body with a cryptic `HTTP 400: body must be encrypted ciphertext` — surfacing far from the real cause (a client/daemon constructed without a signer or session store). Guard the facade on `client.encryptionEnabled` and throw a clear, actionable error instead of leaking plaintext. The low-level transparent `client.messages.send` path is untouched, so intentional plain relay transport still works. Co-Authored-By: Claude --- sdk/typescript/src/agent/messaging.ts | 19 +++++++++++++++++-- sdk/typescript/tests/agent-messaging.test.ts | 12 ++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/agent/messaging.ts b/sdk/typescript/src/agent/messaging.ts index 040a55ac..08adcf1f 100644 --- a/sdk/typescript/src/agent/messaging.ts +++ b/sdk/typescript/src/agent/messaging.ts @@ -90,8 +90,16 @@ export interface SendMessageResult { /** * Sends a message to `recipient` (a @handle, cryptoId, or base64 key). The body - * is Signal-encrypted by the client before it leaves the process when encryption - * is configured (the recommended setup); otherwise it is sent as plaintext. + * is Signal-encrypted by the client before it leaves the process. + * + * This is the E2E facade: it REQUIRES the client to have encryption configured + * (`encryption: { store }` + a signer). A client without encryption would send + * the body as plaintext, which the relay rejects — a plaintext JSON body trips + * its `looksLikeJSON` guard with `HTTP 400: body must be encrypted ciphertext`, + * surfacing far from the misconfiguration (a client built without a signer/store, + * e.g. an under-provisioned daemon). Fail fast here with a clear cause instead of + * leaking plaintext and getting a cryptic relay rejection. Callers that genuinely + * want plain relay transport use `client.messages.send` directly. */ export async function sendMessage( client: TinyPlaceClient, @@ -99,6 +107,13 @@ export async function sendMessage( recipient: string, text: string, ): Promise { + if (!client.encryptionEnabled) { + throw new Error( + "agent messaging requires encryption: construct the client with " + + "`encryption: { store }` and a signer. Sending a plaintext body over the " + + 'E2E channel is rejected by the relay ("body must be encrypted ciphertext").', + ); + } const to = await resolveRecipientKey(client, recipient); const envelope: MessageEnvelope = { id: messageId(), diff --git a/sdk/typescript/tests/agent-messaging.test.ts b/sdk/typescript/tests/agent-messaging.test.ts index 600e3c5e..9d3f8e41 100644 --- a/sdk/typescript/tests/agent-messaging.test.ts +++ b/sdk/typescript/tests/agent-messaging.test.ts @@ -162,4 +162,16 @@ describe("sendMessage / readMessages round-trip", () => { // Consumed on read. expect(await readMessages(bob.client, bob.signer)).toHaveLength(0); }); + + it("refuses to send when the client has no encryption configured", async () => { + // A client built without `encryption: { store }` would relay the body as + // plaintext, which the backend rejects with `400: body must be encrypted + // ciphertext`. The facade must fail fast at the misconfiguration instead. + const signer = await LocalSigner.generate(); + const plain = new TinyPlaceClient({ baseUrl: "https://relay.test", signer }); + expect(plain.encryptionEnabled).toBe(false); + await expect( + sendMessage(plain, signer, signer.agentId, "hi"), + ).rejects.toThrow(/requires encryption/); + }); }); From 1561dc69079808e4b7c5d2c85699e0f22bb3c982 Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Wed, 15 Jul 2026 21:17:41 +0530 Subject: [PATCH 2/2] refactor(sdk): extract sendMessage encryption guard into a helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeRabbit: keep sendMessage focused/short by moving the encryption assertion into assertEncryptionEnabled(). Declined the paired try/catch suggestion — no sibling in this module wraps its async directory/transport calls, the file's idiom is to let errors propagate. Co-Authored-By: Claude --- sdk/typescript/src/agent/messaging.ts | 39 ++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/sdk/typescript/src/agent/messaging.ts b/sdk/typescript/src/agent/messaging.ts index 08adcf1f..039639ef 100644 --- a/sdk/typescript/src/agent/messaging.ts +++ b/sdk/typescript/src/agent/messaging.ts @@ -89,17 +89,26 @@ export interface SendMessageResult { } /** - * Sends a message to `recipient` (a @handle, cryptoId, or base64 key). The body - * is Signal-encrypted by the client before it leaves the process. - * - * This is the E2E facade: it REQUIRES the client to have encryption configured - * (`encryption: { store }` + a signer). A client without encryption would send - * the body as plaintext, which the relay rejects — a plaintext JSON body trips - * its `looksLikeJSON` guard with `HTTP 400: body must be encrypted ciphertext`, - * surfacing far from the misconfiguration (a client built without a signer/store, - * e.g. an under-provisioned daemon). Fail fast here with a clear cause instead of - * leaking plaintext and getting a cryptic relay rejection. Callers that genuinely - * want plain relay transport use `client.messages.send` directly. + * The E2E facade REQUIRES a client with encryption configured (`encryption: { store }` + * + a signer). A client without it would relay the body as plaintext, which the backend + * rejects — a plaintext JSON body trips its `looksLikeJSON` guard with `HTTP 400: body + * must be encrypted ciphertext`, surfacing far from the misconfiguration (a client built + * without a signer/store, e.g. an under-provisioned daemon). Fail fast with a clear cause + * instead of leaking plaintext. Callers that want plain transport use `client.messages.send`. + */ +function assertEncryptionEnabled(client: TinyPlaceClient): void { + if (client.encryptionEnabled) return; + throw new Error( + "agent messaging requires encryption: construct the client with " + + "`encryption: { store }` and a signer. Sending a plaintext body over the " + + 'E2E channel is rejected by the relay ("body must be encrypted ciphertext").', + ); +} + +/** + * Sends a message to `recipient` (a @handle, cryptoId, or base64 key). The body is + * Signal-encrypted by the client before it leaves the process; encryption must be + * configured (see {@link assertEncryptionEnabled}). */ export async function sendMessage( client: TinyPlaceClient, @@ -107,13 +116,7 @@ export async function sendMessage( recipient: string, text: string, ): Promise { - if (!client.encryptionEnabled) { - throw new Error( - "agent messaging requires encryption: construct the client with " + - "`encryption: { store }` and a signer. Sending a plaintext body over the " + - 'E2E channel is rejected by the relay ("body must be encrypted ciphertext").', - ); - } + assertEncryptionEnabled(client); const to = await resolveRecipientKey(client, recipient); const envelope: MessageEnvelope = { id: messageId(),