@@ -9,7 +9,7 @@ import { getBotsChatRuntime } from "./runtime.js";
99import type { BotsChatChannelConfig , CloudInbound , ResolvedBotsChatAccount } from "./types.js" ;
1010import { BotsChatCloudClient } from "./ws-client.js" ;
1111import crypto from "crypto" ;
12- import { encryptText , decryptText , decryptBytes , toBase64 , fromBase64 } from "./e2e-crypto.js" ;
12+ import { encryptText , encryptBytes , decryptText , decryptBytes , toBase64 , fromBase64 } from "./e2e-crypto.js" ;
1313
1414// ---------------------------------------------------------------------------
1515// A2UI message-tool hints — injected via agentPrompt.messageToolHints so
@@ -51,6 +51,8 @@ function readAgentModel(_agentId: string): string | undefined {
5151const cloudClients = new Map < string , BotsChatCloudClient > ( ) ;
5252/** Maps accountId → cloudUrl so handleCloudMessage can resolve relative URLs */
5353const cloudUrls = new Map < string , string > ( ) ;
54+ /** Maps accountId → pairingToken for plugin HTTP uploads */
55+ const pairingTokens = new Map < string , string > ( ) ;
5456
5557function getCloudClient ( accountId : string ) : BotsChatCloudClient | undefined {
5658 return cloudClients . get ( accountId ) ;
@@ -176,17 +178,18 @@ export const botschatPlugin = {
176178 mediaUrl ?: string ;
177179 accountId ?: string | null ;
178180 } ) => {
179- const client = getCloudClient ( ctx . accountId ?? "default" ) ;
181+ const accountId = ctx . accountId ?? "default" ;
182+ const client = getCloudClient ( accountId ) ;
180183 if ( ! client ?. connected ) {
181184 return { ok : false , error : new Error ( "Not connected to BotsChat cloud" ) } ;
182185 }
183186 const messageId = crypto . randomUUID ( ) ;
184187 let text = ctx . text ;
185188 let encrypted = false ;
189+ let mediaEncrypted = false ;
186190
187- if ( client . e2eKey && text ) { // Only encrypt checksum if present
191+ if ( client . e2eKey && text ) {
188192 try {
189- // Encrypt caption using messageId as contextId
190193 const ciphertext = await encryptText ( client . e2eKey , text , messageId ) ;
191194 text = toBase64 ( ciphertext ) ;
192195 encrypted = true ;
@@ -195,17 +198,63 @@ export const botschatPlugin = {
195198 }
196199 }
197200
201+ let finalMediaUrl = ctx . mediaUrl ;
202+
203+ if ( client . e2eKey && ctx . mediaUrl && ! ctx . mediaUrl . startsWith ( "/api/media/" ) ) {
204+ try {
205+ const baseUrl = cloudUrls . get ( accountId ) ;
206+ const token = pairingTokens . get ( accountId ) ;
207+ if ( baseUrl && token ) {
208+ const resp = await fetch ( ctx . mediaUrl , { signal : AbortSignal . timeout ( 15_000 ) } ) ;
209+ if ( resp . ok ) {
210+ const rawBytes = new Uint8Array ( await resp . arrayBuffer ( ) ) ;
211+ const encBytes = await encryptBytes ( client . e2eKey , rawBytes , `${ messageId } :media` ) ;
212+
213+ const contentType = resp . headers . get ( "Content-Type" ) ?? "application/octet-stream" ;
214+ const extMap : Record < string , string > = { "image/png" : "png" , "image/jpeg" : "jpg" , "image/gif" : "gif" , "image/webp" : "webp" } ;
215+ const ext = extMap [ contentType ] ?? ( contentType . startsWith ( "image/" ) ? "png" : "bin" ) ;
216+
217+ const formData = new FormData ( ) ;
218+ const blob = new Blob ( [ encBytes as any ] , { type : contentType } ) ;
219+ formData . append ( "file" , blob , `encrypted.${ ext } ` ) ;
220+
221+ const uploadUrl = `${ baseUrl . replace ( / \/ $ / , "" ) } /api/plugin-upload` ;
222+ const uploadResp = await fetch ( uploadUrl , {
223+ method : "POST" ,
224+ headers : { "X-Pairing-Token" : token } ,
225+ body : formData as any ,
226+ signal : AbortSignal . timeout ( 30_000 ) ,
227+ } ) ;
228+
229+ if ( uploadResp . ok ) {
230+ const result = await uploadResp . json ( ) as { url : string } ;
231+ finalMediaUrl = result . url ;
232+ mediaEncrypted = true ;
233+ console . log ( `[botschat][sendMedia] E2E encrypted media uploaded (${ rawBytes . length } → ${ encBytes . length } bytes)` ) ;
234+ } else {
235+ console . error ( `[botschat][sendMedia] Plugin upload failed: HTTP ${ uploadResp . status } ` ) ;
236+ }
237+ } else {
238+ console . error ( `[botschat][sendMedia] Failed to download media: HTTP ${ resp . status } ` ) ;
239+ }
240+ }
241+ } catch ( err ) {
242+ console . error ( `[botschat][sendMedia] E2E media encryption failed, sending unencrypted:` , err ) ;
243+ }
244+ }
245+
198246 const notifyPreview = ( encrypted && client . notifyPreview && ctx . text )
199247 ? ( ctx . text . length > 100 ? ctx . text . slice ( 0 , 100 ) + "…" : ctx . text )
200248 : undefined ;
201- if ( ctx . mediaUrl ) {
249+ if ( finalMediaUrl ) {
202250 client . send ( {
203251 type : "agent.media" ,
204252 sessionKey : ctx . to ,
205- mediaUrl : ctx . mediaUrl ,
253+ mediaUrl : finalMediaUrl ,
206254 caption : text || undefined ,
207255 messageId,
208256 encrypted,
257+ mediaEncrypted,
209258 ...( notifyPreview ? { notifyPreview } : { } ) ,
210259 } ) ;
211260 } else {
@@ -241,11 +290,16 @@ export const botschatPlugin = {
241290 }
242291
243292 const existingClient = cloudClients . get ( accountId ) ;
293+ if ( existingClient ?. connected ) {
294+ log ?. info ( `[${ accountId } ] Already connected — skipping restart` ) ;
295+ return existingClient ;
296+ }
244297 if ( existingClient ) {
245- log ?. info ( `[${ accountId } ] Disconnecting previous client before reconnect` ) ;
298+ log ?. info ( `[${ accountId } ] Disconnecting stale client before reconnect` ) ;
246299 existingClient . disconnect ( ) ;
247300 cloudClients . delete ( accountId ) ;
248301 cloudUrls . delete ( accountId ) ;
302+ pairingTokens . delete ( accountId ) ;
249303 }
250304
251305 ctx . setStatus ( {
@@ -281,12 +335,14 @@ export const botschatPlugin = {
281335
282336 cloudClients . set ( accountId , client ) ;
283337 cloudUrls . set ( accountId , account . cloudUrl ) ;
338+ pairingTokens . set ( accountId , account . pairingToken ) ;
284339 client . connect ( ) ;
285340
286341 ctx . abortSignal . addEventListener ( "abort" , ( ) => {
287342 client . disconnect ( ) ;
288343 cloudClients . delete ( accountId ) ;
289344 cloudUrls . delete ( accountId ) ;
345+ pairingTokens . delete ( accountId ) ;
290346 } ) ;
291347
292348 return client ;
0 commit comments