@@ -32,80 +32,86 @@ const getCIAuthToken = (): string | undefined => {
3232 return ( window as CliWindow ) . __ABLY_CLI_CI_AUTH_TOKEN__ ;
3333} ;
3434
35- // Get credentials from various sources
35+ // Get signed credentials from various sources
3636const getInitialCredentials = ( ) => {
3737 const urlParams = new URLSearchParams ( window . location . search ) ;
38-
38+
3939 // Get the domain from the WebSocket URL for scoping
4040 const wsUrl = getWebSocketUrl ( ) ;
4141 const wsDomain = new URL ( wsUrl ) . host ;
42-
42+
4343 // Check if we should clear credentials (for testing)
4444 if ( urlParams . get ( 'clearCredentials' ) === 'true' ) {
45+ // Clear new signed format
46+ localStorage . removeItem ( `ably.web-cli.signedConfig.${ wsDomain } ` ) ;
47+ localStorage . removeItem ( `ably.web-cli.signature.${ wsDomain } ` ) ;
48+ localStorage . removeItem ( `ably.web-cli.rememberCredentials.${ wsDomain } ` ) ;
49+ sessionStorage . removeItem ( `ably.web-cli.signedConfig.${ wsDomain } ` ) ;
50+ sessionStorage . removeItem ( `ably.web-cli.signature.${ wsDomain } ` ) ;
51+ // Also clear old format (migration)
4552 localStorage . removeItem ( `ably.web-cli.apiKey.${ wsDomain } ` ) ;
4653 localStorage . removeItem ( `ably.web-cli.accessToken.${ wsDomain } ` ) ;
47- localStorage . removeItem ( `ably.web-cli.rememberCredentials.${ wsDomain } ` ) ;
48- // Also clear from sessionStorage
4954 sessionStorage . removeItem ( `ably.web-cli.apiKey.${ wsDomain } ` ) ;
5055 sessionStorage . removeItem ( `ably.web-cli.accessToken.${ wsDomain } ` ) ;
5156 // Remove the clearCredentials param from URL
5257 const cleanUrl = new URL ( window . location . href ) ;
5358 cleanUrl . searchParams . delete ( 'clearCredentials' ) ;
5459 window . history . replaceState ( null , '' , cleanUrl . toString ( ) ) ;
5560 }
56-
57- // Check localStorage for persisted credentials (if user chose to remember)
61+
62+ // Check query parameters FIRST (for test environment signed configs)
63+ const qsSignedConfig = urlParams . get ( 'signedConfig' ) ;
64+ const qsSignature = urlParams . get ( 'signature' ) ;
65+
66+ if ( qsSignedConfig && qsSignature ) {
67+ console . log ( '[App] Using signed config from query parameters' ) ;
68+ return {
69+ signedConfig : qsSignedConfig ,
70+ signature : qsSignature ,
71+ source : 'query' as const
72+ } ;
73+ }
74+
75+ // Check localStorage for persisted signed credentials (if user chose to remember)
5876 const rememberCredentials = localStorage . getItem ( `ably.web-cli.rememberCredentials.${ wsDomain } ` ) === 'true' ;
5977 if ( rememberCredentials ) {
60- const storedApiKey = localStorage . getItem ( `ably.web-cli.apiKey.${ wsDomain } ` ) ;
61- const storedAccessToken = localStorage . getItem ( `ably.web-cli.accessToken.${ wsDomain } ` ) ;
62- if ( storedApiKey ) {
63- return {
64- apiKey : storedApiKey ,
65- accessToken : storedAccessToken || undefined ,
78+ const storedSignedConfig = localStorage . getItem ( `ably.web-cli.signedConfig.${ wsDomain } ` ) ;
79+ const storedSignature = localStorage . getItem ( `ably.web-cli.signature.${ wsDomain } ` ) ;
80+ if ( storedSignedConfig && storedSignature ) {
81+ console . log ( '[App] Using signed config from localStorage' ) ;
82+ return {
83+ signedConfig : storedSignedConfig ,
84+ signature : storedSignature ,
6685 source : 'localStorage' as const
6786 } ;
6887 }
6988 }
70-
71- // Check sessionStorage for session-only credentials
72- const sessionApiKey = sessionStorage . getItem ( `ably.web-cli.apiKey.${ wsDomain } ` ) ;
73- const sessionAccessToken = sessionStorage . getItem ( `ably.web-cli.accessToken.${ wsDomain } ` ) ;
74- if ( sessionApiKey ) {
75- return {
76- apiKey : sessionApiKey ,
77- accessToken : sessionAccessToken || undefined ,
89+
90+ // Check sessionStorage for session-only signed credentials
91+ const sessionSignedConfig = sessionStorage . getItem ( `ably.web-cli.signedConfig.${ wsDomain } ` ) ;
92+ const sessionSignature = sessionStorage . getItem ( `ably.web-cli.signature.${ wsDomain } ` ) ;
93+ if ( sessionSignedConfig && sessionSignature ) {
94+ console . log ( '[App] Using signed config from sessionStorage' ) ;
95+ return {
96+ signedConfig : sessionSignedConfig ,
97+ signature : sessionSignature ,
7898 source : 'session' as const
7999 } ;
80100 }
81101
82- // Then check query parameters (only in non-production environments)
83- const qsApiKey = urlParams . get ( 'apikey' ) || urlParams . get ( 'apiKey' ) ;
84- const qsAccessToken = urlParams . get ( 'accessToken' ) || urlParams . get ( 'accesstoken' ) ;
85-
86- // Security check: only allow query param auth in development/test environments
87- const isProduction = import . meta. env . PROD && ! window . location . hostname . includes ( 'localhost' ) && ! window . location . hostname . includes ( '127.0.0.1' ) ;
88-
89- if ( qsApiKey ) {
90- if ( isProduction ) {
91- console . error ( 'Security Warning: API keys in query parameters are not allowed in production environments.' ) ;
92- // Clear the sensitive query parameters from the URL
93- const cleanUrl = new URL ( window . location . href ) ;
94- cleanUrl . searchParams . delete ( 'apikey' ) ;
95- cleanUrl . searchParams . delete ( 'apiKey' ) ;
96- cleanUrl . searchParams . delete ( 'accessToken' ) ;
97- cleanUrl . searchParams . delete ( 'accesstoken' ) ;
98- window . history . replaceState ( null , '' , cleanUrl . toString ( ) ) ;
99- } else {
100- return {
101- apiKey : qsApiKey ,
102- accessToken : qsAccessToken || undefined ,
103- source : 'query' as const
104- } ;
105- }
102+ // Check for old format credentials (migration)
103+ const oldApiKey = localStorage . getItem ( `ably.web-cli.apiKey.${ wsDomain } ` ) ||
104+ sessionStorage . getItem ( `ably.web-cli.apiKey.${ wsDomain } ` ) ;
105+ if ( oldApiKey ) {
106+ console . warn ( '[App] Found old credential format. Please re-authenticate with signed credentials.' ) ;
107+ // Clear old format
108+ localStorage . removeItem ( `ably.web-cli.apiKey.${ wsDomain } ` ) ;
109+ localStorage . removeItem ( `ably.web-cli.accessToken.${ wsDomain } ` ) ;
110+ sessionStorage . removeItem ( `ably.web-cli.apiKey.${ wsDomain } ` ) ;
111+ sessionStorage . removeItem ( `ably.web-cli.accessToken.${ wsDomain } ` ) ;
106112 }
107113
108- return { apiKey : undefined , accessToken : undefined , source : 'none' as const } ;
114+ return { signedConfig : undefined , signature : undefined , source : 'none' as const } ;
109115} ;
110116
111117function App ( ) {
@@ -117,11 +123,11 @@ function App() {
117123 const [ displayMode , setDisplayMode ] = useState < "fullscreen" | "drawer" > ( initialMode ) ;
118124 const [ showAuthSettings , setShowAuthSettings ] = useState ( false ) ;
119125
120- // Initialize credentials
126+ // Initialize signed credentials
121127 const initialCreds = getInitialCredentials ( ) ;
122- const [ apiKey , setApiKey ] = useState < string | undefined > ( initialCreds . apiKey ) ;
123- const [ accessToken , setAccessToken ] = useState < string | undefined > ( initialCreds . accessToken ) ;
124- const [ isAuthenticated , setIsAuthenticated ] = useState ( Boolean ( initialCreds . apiKey && initialCreds . apiKey . trim ( ) ) ) ;
128+ const [ signedConfig , setSignedConfig ] = useState < string | undefined > ( initialCreds . signedConfig ) ;
129+ const [ signature , setSignature ] = useState < string | undefined > ( initialCreds . signature ) ;
130+ const [ isAuthenticated , setIsAuthenticated ] = useState ( Boolean ( initialCreds . signedConfig && initialCreds . signature ) ) ;
125131 const [ authSource , setAuthSource ] = useState ( initialCreds . source ) ;
126132 // Get the URL and domain early for use in state initialization
127133 const currentWebsocketUrl = getWebSocketUrl ( ) ;
@@ -144,64 +150,79 @@ function App() {
144150 } , [ ] ) ;
145151
146152 // Handle authentication
147- const handleAuthenticate = useCallback ( ( newApiKey : string , newAccessToken : string , remember ?: boolean ) => {
148- // Clear any existing session data when credentials change (domain-scoped)
149- sessionStorage . removeItem ( `ably.cli.sessionId.${ wsDomain } ` ) ;
150- sessionStorage . removeItem ( `ably.cli.secondarySessionId.${ wsDomain } ` ) ;
151- sessionStorage . removeItem ( `ably.cli.isSplit.${ wsDomain } ` ) ;
152-
153- setApiKey ( newApiKey ) ;
154- setAccessToken ( newAccessToken ) ;
155- setIsAuthenticated ( true ) ;
156- setShowAuthSettings ( false ) ;
157-
158- // Determine if we should remember based on parameter or current state
159- const shouldRemember = remember !== undefined ? remember : rememberCredentials ;
160-
161- if ( shouldRemember ) {
162- // Store in localStorage for persistence (domain-scoped)
163- localStorage . setItem ( `ably.web-cli.apiKey.${ wsDomain } ` , newApiKey ) ;
164- localStorage . setItem ( `ably.web-cli.rememberCredentials.${ wsDomain } ` , 'true' ) ;
165- if ( newAccessToken ) {
166- localStorage . setItem ( `ably.web-cli.accessToken.${ wsDomain } ` , newAccessToken ) ;
167- } else {
168- localStorage . removeItem ( `ably.web-cli.accessToken.${ wsDomain } ` ) ;
153+ const handleAuthenticate = useCallback ( async ( newApiKey : string , remember ?: boolean ) => {
154+ try {
155+ // Call /api/sign endpoint to get signed config
156+ const response = await fetch ( '/api/sign' , {
157+ method : 'POST' ,
158+ headers : { 'Content-Type' : 'application/json' } ,
159+ body : JSON . stringify ( {
160+ apiKey : newApiKey ,
161+ bypassRateLimit : false
162+ } )
163+ } ) ;
164+
165+ if ( ! response . ok ) {
166+ const error = await response . json ( ) ;
167+ console . error ( '[App] Failed to sign credentials:' , error ) ;
168+ throw new Error ( error . error || 'Failed to sign credentials' ) ;
169169 }
170- setAuthSource ( 'localStorage' ) ;
171- } else {
172- // Store only in sessionStorage (domain-scoped)
173- sessionStorage . setItem ( `ably.web-cli.apiKey.${ wsDomain } ` , newApiKey ) ;
174- if ( newAccessToken ) {
175- sessionStorage . setItem ( `ably.web-cli.accessToken.${ wsDomain } ` , newAccessToken ) ;
170+
171+ const { signedConfig : newSignedConfig , signature : newSignature } = await response . json ( ) ;
172+
173+ // Clear any existing session data when credentials change (domain-scoped)
174+ sessionStorage . removeItem ( `ably.cli.sessionId.${ wsDomain } ` ) ;
175+ sessionStorage . removeItem ( `ably.cli.secondarySessionId.${ wsDomain } ` ) ;
176+ sessionStorage . removeItem ( `ably.cli.isSplit.${ wsDomain } ` ) ;
177+
178+ setSignedConfig ( newSignedConfig ) ;
179+ setSignature ( newSignature ) ;
180+ setIsAuthenticated ( true ) ;
181+ setShowAuthSettings ( false ) ;
182+
183+ // Determine if we should remember based on parameter or current state
184+ const shouldRemember = remember !== undefined ? remember : rememberCredentials ;
185+
186+ if ( shouldRemember ) {
187+ // Store in localStorage for persistence (domain-scoped)
188+ localStorage . setItem ( `ably.web-cli.signedConfig.${ wsDomain } ` , newSignedConfig ) ;
189+ localStorage . setItem ( `ably.web-cli.signature.${ wsDomain } ` , newSignature ) ;
190+ localStorage . setItem ( `ably.web-cli.rememberCredentials.${ wsDomain } ` , 'true' ) ;
191+ setAuthSource ( 'localStorage' ) ;
176192 } else {
177- sessionStorage . removeItem ( `ably.web-cli.accessToken.${ wsDomain } ` ) ;
193+ // Store only in sessionStorage (domain-scoped)
194+ sessionStorage . setItem ( `ably.web-cli.signedConfig.${ wsDomain } ` , newSignedConfig ) ;
195+ sessionStorage . setItem ( `ably.web-cli.signature.${ wsDomain } ` , newSignature ) ;
196+ // Clear from localStorage if it was there (domain-scoped)
197+ localStorage . removeItem ( `ably.web-cli.signedConfig.${ wsDomain } ` ) ;
198+ localStorage . removeItem ( `ably.web-cli.signature.${ wsDomain } ` ) ;
199+ localStorage . removeItem ( `ably.web-cli.rememberCredentials.${ wsDomain } ` ) ;
200+ setAuthSource ( 'session' ) ;
178201 }
179- // Clear from localStorage if it was there (domain-scoped)
180- localStorage . removeItem ( `ably.web-cli.apiKey. ${ wsDomain } ` ) ;
181- localStorage . removeItem ( `ably.web-cli.accessToken. ${ wsDomain } ` ) ;
182- localStorage . removeItem ( `ably.web-cli.rememberCredentials. ${ wsDomain } ` ) ;
183- setAuthSource ( 'session' ) ;
202+
203+ setRememberCredentials ( shouldRemember ) ;
204+ } catch ( error ) {
205+ console . error ( '[App] Authentication error:' , error ) ;
206+ throw error ;
184207 }
185-
186- setRememberCredentials ( shouldRemember ) ;
187208 } , [ rememberCredentials , wsDomain ] ) ;
188209
189210 // Handle auth settings save
190- const handleAuthSettingsSave = useCallback ( ( newApiKey : string , newAccessToken : string , remember : boolean ) => {
211+ const handleAuthSettingsSave = useCallback ( async ( newApiKey : string , remember : boolean ) => {
191212 if ( newApiKey ) {
192- handleAuthenticate ( newApiKey , newAccessToken , remember ) ;
213+ await handleAuthenticate ( newApiKey , remember ) ;
193214 } else {
194215 // Clear all credentials - go back to auth screen (domain-scoped)
195216 sessionStorage . removeItem ( `ably.cli.sessionId.${ wsDomain } ` ) ;
196217 sessionStorage . removeItem ( `ably.cli.secondarySessionId.${ wsDomain } ` ) ;
197218 sessionStorage . removeItem ( `ably.cli.isSplit.${ wsDomain } ` ) ;
198- sessionStorage . removeItem ( `ably.web-cli.apiKey .${ wsDomain } ` ) ;
199- sessionStorage . removeItem ( `ably.web-cli.accessToken .${ wsDomain } ` ) ;
200- localStorage . removeItem ( `ably.web-cli.apiKey .${ wsDomain } ` ) ;
201- localStorage . removeItem ( `ably.web-cli.accessToken .${ wsDomain } ` ) ;
219+ sessionStorage . removeItem ( `ably.web-cli.signedConfig .${ wsDomain } ` ) ;
220+ sessionStorage . removeItem ( `ably.web-cli.signature .${ wsDomain } ` ) ;
221+ localStorage . removeItem ( `ably.web-cli.signedConfig .${ wsDomain } ` ) ;
222+ localStorage . removeItem ( `ably.web-cli.signature .${ wsDomain } ` ) ;
202223 localStorage . removeItem ( `ably.web-cli.rememberCredentials.${ wsDomain } ` ) ;
203- setApiKey ( undefined ) ;
204- setAccessToken ( undefined ) ;
224+ setSignedConfig ( undefined ) ;
225+ setSignature ( undefined ) ;
205226 setIsAuthenticated ( false ) ;
206227 setShowAuthSettings ( false ) ;
207228 setRememberCredentials ( false ) ;
@@ -220,11 +241,11 @@ function App() {
220241 // Prepare the terminal component instance to pass it down
221242 const termRef = useRef < AblyCliTerminalHandle > ( null ) ;
222243 const TerminalInstance = useCallback ( ( ) => (
223- isAuthenticated && apiKey && apiKey . trim ( ) ? (
244+ isAuthenticated && signedConfig && signature ? (
224245 < AblyCliTerminal
225246 ref = { termRef }
226- ablyAccessToken = { accessToken }
227- ablyApiKey = { apiKey }
247+ signedConfig = { signedConfig }
248+ signature = { signature }
228249 onConnectionStatusChange = { handleConnectionChange }
229250 onSessionEnd = { handleSessionEnd }
230251 onSessionId = { handleSessionId }
@@ -233,10 +254,9 @@ function App() {
233254 enableSplitScreen = { true }
234255 showSplitControl = { true }
235256 maxReconnectAttempts = { 5 } /* In the example, limit reconnection attempts for testing, default is 15 */
236- ciAuthToken = { getCIAuthToken ( ) }
237257 />
238258 ) : null
239- ) , [ isAuthenticated , apiKey , accessToken , handleConnectionChange , handleSessionEnd , handleSessionId , currentWebsocketUrl ] ) ;
259+ ) , [ isAuthenticated , signedConfig , signature , handleConnectionChange , handleSessionEnd , handleSessionId , currentWebsocketUrl ] ) ;
240260
241261 // Show auth screen if not authenticated
242262 if ( ! isAuthenticated ) {
@@ -323,8 +343,7 @@ function App() {
323343 isOpen = { showAuthSettings }
324344 onClose = { ( ) => setShowAuthSettings ( false ) }
325345 onSave = { handleAuthSettingsSave }
326- currentApiKey = { apiKey }
327- currentAccessToken = { accessToken }
346+ currentSignedConfig = { signedConfig }
328347 rememberCredentials = { rememberCredentials }
329348 />
330349 </ div >
0 commit comments