diff --git a/sdk/typescript/src/agent/messaging.ts b/sdk/typescript/src/agent/messaging.ts index 040a55ac..039639ef 100644 --- a/sdk/typescript/src/agent/messaging.ts +++ b/sdk/typescript/src/agent/messaging.ts @@ -89,9 +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 when encryption - * is configured (the recommended setup); otherwise it is sent as plaintext. + * 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, @@ -99,6 +116,7 @@ export async function sendMessage( recipient: string, text: string, ): Promise { + assertEncryptionEnabled(client); 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/); + }); });