@@ -24,7 +24,6 @@ import (
2424 "golang.org/x/term"
2525
2626 "github.com/NodeOps-app/createos-cli/internal/api"
27- "github.com/NodeOps-app/createos-cli/internal/config"
2827 "github.com/NodeOps-app/createos-cli/internal/terminal"
2928)
3029
@@ -259,12 +258,12 @@ func runShellPTY(c *cli.Context, id, ref string) error {
259258 if ctrlURL == "" {
260259 ctrlURL = api .DefaultSandboxBaseURL
261260 }
262- token , err := loadAPIToken ( )
261+ authHeader , token , err := sandboxAuth ( c )
263262 if err != nil {
264263 return err
265264 }
266265
267- conn , err := dialControlUpgrade (c .Context , ctrlURL , token , "/v1/sandboxes/" + id + "/shell" )
266+ conn , err := dialControlUpgrade (c .Context , ctrlURL , authHeader , token , "/v1/sandboxes/" + id + "/shell" )
268267 if err != nil {
269268 return err
270269 }
@@ -353,7 +352,7 @@ func writeFrame(w io.Writer, mu *sync.Mutex, typ byte, payload []byte) error {
353352// Upgrade handshake, and returns the raw connection on a 101 reply.
354353// Used by the keyless PTY path — same wire shape as the tunnel bridge
355354// but with a different target path.
356- func dialControlUpgrade (ctx context.Context , ctrlURL , token , path string ) (net.Conn , error ) {
355+ func dialControlUpgrade (ctx context.Context , ctrlURL , authHeader , token , path string ) (net.Conn , error ) {
357356 u , err := url .Parse (ctrlURL )
358357 if err != nil {
359358 return nil , fmt .Errorf ("bad sandbox URL %q: %w" , ctrlURL , err )
@@ -383,7 +382,7 @@ func dialControlUpgrade(ctx context.Context, ctrlURL, token, path string) (net.C
383382
384383 req := "POST " + path + " HTTP/1.1\r \n " +
385384 "Host: " + u .Host + "\r \n " +
386- "X-Api-Key : " + token + "\r \n " +
385+ authHeader + " : " + token + "\r \n " +
387386 "Connection: Upgrade\r \n " +
388387 "Upgrade: tcp-tunnel\r \n " +
389388 "Content-Length: 0\r \n \r \n "
@@ -458,7 +457,7 @@ func startTunnelBridge(parent context.Context, c *cli.Context, sandboxID string,
458457 if ctrlURL == "" {
459458 ctrlURL = api .DefaultSandboxBaseURL
460459 }
461- token , err := loadAPIToken ( )
460+ authHeader , token , err := sandboxAuth ( c )
462461 if err != nil {
463462 return nil , err
464463 }
@@ -479,23 +478,25 @@ func startTunnelBridge(parent context.Context, c *cli.Context, sandboxID string,
479478 if err != nil {
480479 return
481480 }
482- go bridgeOne (ctx , ctrlURL , token , sandboxID , remotePort , conn )
481+ go bridgeOne (ctx , ctrlURL , authHeader , token , sandboxID , remotePort , conn )
483482 }
484483 }()
485484 return b , nil
486485}
487486
488- // loadAPIToken pulls the user's token the same way the root Before
489- // hook does — OAuth session if present, else the static api-key file.
490- // We re-read because the Resty client doesn't expose the raw value.
491- func loadAPIToken () (string , error ) {
492- if config .HasOAuthSession () {
493- sess , err := config .LoadOAuthSession ()
494- if err == nil && sess != nil && sess .AccessToken != "" {
495- return sess .AccessToken , nil
496- }
487+ // sandboxAuth returns the auth header name and current token from the
488+ // sandbox client the root Before hook already built and stored in
489+ // metadata — the single source of truth for credentials. The raw
490+ // HTTP-Upgrade paths reuse it so they authenticate exactly like every
491+ // other sandbox call: the right header for the auth type (X-Access-Token
492+ // for OAuth, X-Api-Key for an api key) and the already-refreshed token.
493+ func sandboxAuth (c * cli.Context ) (header , token string , err error ) {
494+ sc , ok := c .App .Metadata [api .SandboxClientKey ].(* api.SandboxClient )
495+ if ! ok {
496+ return "" , "" , fmt .Errorf ("you're not signed in — run 'createos login' to get started" )
497497 }
498- return config .LoadToken ()
498+ header , token = sc .AuthHeader ()
499+ return header , token , nil
499500}
500501
501502// bridgeOne handles a single accepted local connection: it opens an
@@ -504,9 +505,9 @@ func loadAPIToken() (string, error) {
504505// not just one — because SSH negotiation interleaves writes from both
505506// peers, and closing early on a transient half-drain truncates the
506507// handshake mid-flight and looks like an auth failure.
507- func bridgeOne (ctx context.Context , ctrlURL , token , id string , port int , local net.Conn ) {
508+ func bridgeOne (ctx context.Context , ctrlURL , authHeader , token , id string , port int , local net.Conn ) {
508509 defer func () { _ = local .Close () }() //nolint:errcheck
509- remote , err := dialControlTunnel (ctx , ctrlURL , token , id , port )
510+ remote , err := dialControlTunnel (ctx , ctrlURL , authHeader , token , id , port )
510511 if err != nil {
511512 return
512513 }
@@ -542,7 +543,7 @@ func bridgeOne(ctx context.Context, ctrlURL, token, id string, port int, local n
542543// protocol by hand: POST `/v1/sandboxes/:id/tunnel/:port` with the
543544// Upgrade headers, watch for 101 Switching Protocols, then return a
544545// net.Conn that carries only tunnel bytes from then on.
545- func dialControlTunnel (ctx context.Context , ctrlURL , token , id string , port int ) (net.Conn , error ) {
546+ func dialControlTunnel (ctx context.Context , ctrlURL , authHeader , token , id string , port int ) (net.Conn , error ) {
546547 u , err := url .Parse (ctrlURL )
547548 if err != nil {
548549 return nil , fmt .Errorf ("bad sandbox URL %q: %w" , ctrlURL , err )
@@ -574,9 +575,9 @@ func dialControlTunnel(ctx context.Context, ctrlURL, token, id string, port int)
574575 }
575576
576577 req := fmt .Sprintf ("POST /v1/sandboxes/%s/tunnel/%d HTTP/1.1\r \n " +
577- "Host: %s\r \n X-Api-Key : %s\r \n " +
578+ "Host: %s\r \n %s : %s\r \n " +
578579 "Connection: Upgrade\r \n Upgrade: tcp-tunnel\r \n Content-Length: 0\r \n \r \n " ,
579- id , port , u .Host , token )
580+ id , port , u .Host , authHeader , token )
580581 if _ , err = conn .Write ([]byte (req )); err != nil {
581582 _ = conn .Close () //nolint:errcheck
582583 return nil , err
0 commit comments