@@ -300,14 +300,102 @@ describe('renderChatStructured', () => {
300300 expect ( controlBearing . text ) . not . toContain ( ESC )
301301 } )
302302
303- it ( 'renders credential links as a plain action without exposing the destination' , ( ) => {
303+ it ( 'renders only same-origin Sim credential links as visible destinations' , ( ) => {
304+ const callbackUrl = encodeURIComponent ( 'https://sim.example/workspace/ws-1' )
305+ const trusted = renderChatStructured (
306+ `<credential>{"type":"link","provider":"Slack","value":"https://sim.example/api/auth/oauth2/authorize?providerId=slack&workspaceId=ws-1&callbackURL=${ callbackUrl } "}</credential>` ,
307+ { simEndpoint : 'https://sim.example' , workspaceId : 'ws-1' }
308+ )
309+ const lookalike = renderChatStructured (
310+ '<credential>{"type":"link","provider":"Slack","value":"https://sim.example.evil.test/connect"}</credential>' ,
311+ { simEndpoint : 'https://sim.example' , workspaceId : 'ws-1' }
312+ )
313+
314+ expect ( trusted . text ) . toBe (
315+ `Connect Slack in Sim:\nhttps://sim.example/api/auth/oauth2/authorize?providerId=slack&workspaceId=ws-1&callbackURL=${ callbackUrl } `
316+ )
317+ expect ( lookalike . text ) . toBe ( 'Open Sim to connect Slack.' )
318+ expect ( lookalike . text ) . not . toContain ( 'sim.example.evil.test' )
319+ expect ( trusted . text ) . not . toContain ( ESC )
320+ expect ( lookalike . text ) . not . toContain ( ESC )
321+ } )
322+
323+ it ( 'rejects same-origin non-auth links, remote HTTP, and links for another workspace' , ( ) => {
324+ const options = { simEndpoint : 'https://sim.example' , workspaceId : 'ws-1' }
325+ const wrongPath = renderChatStructured (
326+ '<credential>{"type":"link","provider":"Slack","value":"https://sim.example/workspace/ws-1/settings"}</credential>' ,
327+ options
328+ )
329+ const remoteHttp = renderChatStructured (
330+ '<credential>{"type":"link","provider":"Slack","value":"http://sim.example/api/auth/trello/authorize"}</credential>' ,
331+ { simEndpoint : 'http://sim.example' , workspaceId : 'ws-1' }
332+ )
333+ const wrongWorkspace = renderChatStructured (
334+ '<credential>{"type":"link","provider":"Slack","value":"https://sim.example/api/auth/oauth2/authorize?providerId=slack&workspaceId=ws-2&callbackURL=https%3A%2F%2Fsim.example%2Fworkspace%2Fws-2"}</credential>' ,
335+ options
336+ )
337+
338+ expect ( wrongPath . text ) . toBe ( 'Open Sim to connect Slack.' )
339+ expect ( remoteHttp . text ) . toBe ( 'Open Sim to connect Slack.' )
340+ expect ( wrongWorkspace . text ) . toBe ( 'Open Sim to connect Slack.' )
341+ } )
342+
343+ it ( 'accepts exact browser-auth routes, including HTTP on localhost only' , ( ) => {
344+ const trello = renderChatStructured (
345+ '<credential>{"type":"link","provider":"Trello","value":"http://localhost:3000/api/auth/trello/authorize"}</credential>' ,
346+ { simEndpoint : 'http://localhost:3000' , workspaceId : 'ws-1' }
347+ )
348+ const instagram = renderChatStructured (
349+ '<credential>{"type":"link","provider":"Instagram","value":"https://sim.example/api/auth/instagram/authorize?returnUrl=https%3A%2F%2Fsim.example%2Fworkspace%2Fws-1%2Fchat%2Fchat-1&workspaceId=ws-1"}</credential>' ,
350+ { simEndpoint : 'https://sim.example' , workspaceId : 'ws-1' }
351+ )
352+
353+ expect ( trello . text ) . toContain ( 'http://localhost:3000/api/auth/trello/authorize' )
354+ expect ( instagram . text ) . toContain ( 'https://sim.example/api/auth/instagram/authorize?' )
355+ } )
356+
357+ it ( 'turns secret inputs into interactive controls without putting a value in chat text' , ( ) => {
304358 const content =
305- '<credential>{"type":"link","provider":"Slack","value":"https://sim.example.evil.test/connect"}</credential>'
306- const result = renderChatStructured ( content )
359+ '<credential>{"type":"secret_input","name":"OPENAI_API_KEY","scope":"personal"}</credential>'
360+ const interactive = renderChatStructured ( content , { printMode : false } )
361+ const readOnly = renderChatStructured ( content , {
362+ printMode : false ,
363+ allowSecretInput : false ,
364+ } )
365+ const printable = renderChatStructured ( content )
366+
367+ expect ( interactive . text ) . toBe ( '' )
368+ expect ( interactive . interactions ) . toEqual ( [
369+ {
370+ kind : 'credential' ,
371+ credential : {
372+ type : 'secret_input' ,
373+ name : 'OPENAI_API_KEY' ,
374+ scope : 'personal' ,
375+ provider : undefined ,
376+ } ,
377+ } ,
378+ ] )
379+ expect ( printable . text ) . toBe (
380+ 'Provide OPENAI_API_KEY securely with: sim secrets set OPENAI_API_KEY --scope personal'
381+ )
382+ expect ( readOnly . interactions ) . toEqual ( [ ] )
383+ expect ( readOnly . text ) . toBe ( printable . text )
384+ } )
307385
308- expect ( result . text ) . toBe ( 'Open Sim to connect Slack.' )
309- expect ( result . text ) . not . toContain ( 'sim.example.evil.test' )
310- expect ( result . text ) . not . toContain ( ESC )
386+ it ( 'links service-account setup to the active workspace without exposing tag values' , ( ) => {
387+ const result = renderChatStructured (
388+ '<credential>{"type":"service_account","provider":"Slack","credentialId":"cred-1"}</credential>' ,
389+ {
390+ simEndpoint : 'https://sim.example/api' ,
391+ workspaceId : 'workspace / 1' ,
392+ }
393+ )
394+
395+ expect ( result . text ) . toBe (
396+ 'Connect Slack with a service account in Sim:\nhttps://sim.example/workspace/workspace%20%2F%201/integrations'
397+ )
398+ expect ( result . text ) . not . toContain ( 'cred-1' )
311399 } )
312400
313401 it ( 'sanitizes workspace titles before rendering a plain resource name' , ( ) => {
0 commit comments