@@ -5,8 +5,8 @@ import open from "open"
55import { AltimateApi } from "../api/client"
66
77// Loopback port the CLI listens on for the browser to deliver the gateway
8- // credential after Google sign-in. Must match the redirect the web authorize
9- // page posts back to. 7317 is otherwise unused in this codebase.
8+ // credential after sign-in. Must match the redirect the web authorize page posts
9+ // back to. 7317 is otherwise unused in this codebase.
1010const CALLBACK_PORT = 7317
1111
1212// Web app that hosts the signup/login (authorize) page. Overridable for
@@ -15,14 +15,23 @@ const DEFAULT_WEB_URL = "https://app.myaltimate.com"
1515// Fallback gateway API base if the callback omits one.
1616const DEFAULT_API_URL = "https://api.myaltimate.com"
1717
18+ // Escape reflected values before interpolating them into the callback HTML — the
19+ // error text originates from the URL query string, so it must not be trusted.
20+ function escapeHtml ( s : string ) : string {
21+ return s . replace (
22+ / [ & < > " ' ] / g,
23+ ( c ) => ( { "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ , "'" : "'" } ) [ c ] as string ,
24+ )
25+ }
26+
1827const HTML_SUCCESS = `<!doctype html><meta charset="utf-8"><title>Altimate Code</title>
1928<body style="font-family:system-ui;text-align:center;padding:64px">
2029<h2>Signed in ✓</h2><p>You can return to your terminal.</p>
2130<script>setTimeout(()=>window.close(),1500)</script></body>`
2231
2332const HTML_ERROR = ( msg : string ) => `<!doctype html><meta charset="utf-8"><title>Altimate Code</title>
2433<body style="font-family:system-ui;text-align:center;padding:64px">
25- <h2>Connection failed</h2><p>${ msg } </p><p>Please return to your terminal and try again.</p></body>`
34+ <h2>Connection failed</h2><p>${ escapeHtml ( msg ) } </p><p>Please return to your terminal and try again.</p></body>`
2635
2736interface CallbackResult {
2837 api_url : string
@@ -31,13 +40,16 @@ interface CallbackResult {
3140}
3241
3342interface Pending {
34- state : string
3543 resolve : ( creds : CallbackResult ) => void
3644 reject : ( err : Error ) => void
3745}
3846
3947let server : ReturnType < typeof createServer > | undefined
40- let pending : Pending | undefined
48+ // Pending flows keyed by the unguessable `state`. Registered synchronously in
49+ // authorize() BEFORE the browser opens, so an instant redirect (an already
50+ // signed-in user) is matched instead of dropped; keying by state also lets two
51+ // concurrent /auth flows coexist without clobbering each other.
52+ const pending = new Map < string , Pending > ( )
4153
4254async function startCallbackServer ( ) : Promise < void > {
4355 if ( server ) return
@@ -54,22 +66,20 @@ async function startCallbackServer(): Promise<void> {
5466 res . end ( body )
5567 }
5668
57- const error = url . searchParams . get ( "error" )
58- if ( error ) {
59- pending ?. reject ( new Error ( error ) )
60- pending = undefined
61- html ( 200 , HTML_ERROR ( error ) )
69+ // Validate `state` FIRST — before honoring `error` — so a request without a
70+ // known state can neither cancel an in-progress flow nor deliver anything.
71+ const state = url . searchParams . get ( "state" )
72+ const entry = state ? pending . get ( state ) : undefined
73+ if ( ! state || ! entry ) {
74+ html ( 400 , HTML_ERROR ( "Invalid or unknown sign-in state" ) )
6275 return
6376 }
77+ pending . delete ( state )
6478
65- const state = url . searchParams . get ( "state" )
66- // Bind the callback to the unguessable state the CLI generated — rejects a
67- // stray/malicious local request that didn't originate from our browser tab.
68- if ( ! pending || ! state || state !== pending . state ) {
69- const msg = "Invalid state — possible CSRF"
70- pending ?. reject ( new Error ( msg ) )
71- pending = undefined
72- html ( 400 , HTML_ERROR ( msg ) )
79+ const error = url . searchParams . get ( "error" )
80+ if ( error ) {
81+ entry . reject ( new Error ( error ) )
82+ html ( 200 , HTML_ERROR ( error ) )
7383 return
7484 }
7585
@@ -78,20 +88,19 @@ async function startCallbackServer(): Promise<void> {
7888 const apiUrl = url . searchParams . get ( "url" ) || DEFAULT_API_URL
7989 if ( ! apiKey || ! instance ) {
8090 const msg = "Missing credential in callback"
81- pending . reject ( new Error ( msg ) )
82- pending = undefined
91+ entry . reject ( new Error ( msg ) )
8392 html ( 400 , HTML_ERROR ( msg ) )
8493 return
8594 }
8695
87- const current = pending
88- pending = undefined
89- current . resolve ( { api_url : apiUrl , instance, api_key : apiKey } )
96+ entry . resolve ( { api_url : apiUrl , instance, api_key : apiKey } )
9097 html ( 200 , HTML_SUCCESS )
9198 } )
9299
93100 await new Promise < void > ( ( resolve , reject ) => {
94- server ! . listen ( CALLBACK_PORT , ( ) => resolve ( ) )
101+ // Bind to loopback only — the credential/abort endpoints must not be reachable
102+ // from the LAN.
103+ server ! . listen ( CALLBACK_PORT , "127.0.0.1" , ( ) => resolve ( ) )
95104 server ! . on ( "error" , reject )
96105 } )
97106}
@@ -103,16 +112,15 @@ function stopCallbackServer() {
103112 }
104113}
105114
106- function waitForCallback ( state : string , timeoutMs = 5 * 60 * 1000 ) : Promise < CallbackResult > {
115+ // Register a pending flow keyed by `state` and return its promise. Called
116+ // synchronously in authorize() before the browser opens; the server handler
117+ // resolves/rejects it by state, so a fast redirect is never lost.
118+ function registerPending ( state : string , timeoutMs = 5 * 60 * 1000 ) : Promise < CallbackResult > {
107119 return new Promise < CallbackResult > ( ( resolve , reject ) => {
108120 const timeout = setTimeout ( ( ) => {
109- if ( pending ) {
110- pending = undefined
111- reject ( new Error ( "Timed out waiting for browser sign-in" ) )
112- }
121+ if ( pending . delete ( state ) ) reject ( new Error ( "Timed out waiting for browser sign-in" ) )
113122 } , timeoutMs )
114- pending = {
115- state,
123+ pending . set ( state , {
116124 resolve : ( creds ) => {
117125 clearTimeout ( timeout )
118126 resolve ( creds )
@@ -121,7 +129,7 @@ function waitForCallback(state: string, timeoutMs = 5 * 60 * 1000): Promise<Call
121129 clearTimeout ( timeout )
122130 reject ( err )
123131 } ,
124- }
132+ } )
125133 } )
126134}
127135
@@ -134,10 +142,11 @@ export async function AltimateAuthPlugin(_input: PluginInput): Promise<Hooks> {
134142 type : "oauth" ,
135143 label : "Altimate LLM Gateway" ,
136144 async authorize ( ) {
137- // Bind the port BEFORE opening the browser so the credential can
138- // only be delivered to this process.
139145 const state = randomBytes ( 16 ) . toString ( "hex" )
140146 await startCallbackServer ( )
147+ // Register the pending flow BEFORE opening the browser so an instant
148+ // redirect can be matched by state rather than dropped as CSRF.
149+ const result = registerPending ( state )
141150
142151 const webUrl = ( process . env . ALTIMATE_WEB_URL || DEFAULT_WEB_URL ) . replace ( / \/ + $ / , "" )
143152 const redirect = `http://localhost:${ CALLBACK_PORT } /callback`
@@ -156,7 +165,7 @@ export async function AltimateAuthPlugin(_input: PluginInput): Promise<Hooks> {
156165 method : "auto" ,
157166 async callback ( ) {
158167 try {
159- const creds = await waitForCallback ( state )
168+ const creds = await result
160169 // Persist to ~/.altimate/altimate.json — the provider loader
161170 // reads this first (it carries the instance/tenant + api_url
162171 // the generic auth.json store can't).
@@ -169,7 +178,8 @@ export async function AltimateAuthPlugin(_input: PluginInput): Promise<Hooks> {
169178 } catch {
170179 return { type : "failed" }
171180 } finally {
172- stopCallbackServer ( )
181+ // Keep the shared server up while another flow is still waiting.
182+ if ( pending . size === 0 ) stopCallbackServer ( )
173183 }
174184 } ,
175185 }
0 commit comments