diff --git a/internal/clerk/clerk.go b/internal/clerk/clerk.go index ba0f68124..d095f043f 100644 --- a/internal/clerk/clerk.go +++ b/internal/clerk/clerk.go @@ -5,66 +5,16 @@ package clerk import ( - "context" - "encoding/json" - "net/http" - "net/url" - "strings" - "sync" - "time" - "github.com/spf13/pflag" - "namespacelabs.dev/foundation/internal/fnerrors" -) - -var ( - ErrUnauthorized = fnerrors.Newf("Unauthorized") - - devClerk = false ) -const ( - prodAddress = "https://clerk.namespace.so/" - devAddress = "https://clerk.firm.hare-7.lcl.dev" - - prodTokenPrefix = "ct_" - devTokenPrefix = "devct_" -) +var devClerk = false func SetupFlags(flags *pflag.FlagSet) { flags.BoolVar(&devClerk, "dev_clerk", devClerk, "Use DEV Clerk instance.") _ = flags.MarkHidden("dev_clerk") } -type devBrowserResponse struct { - ID string `json:"id"` - InstanceID string `json:"instance_id"` - Token string `json:"token"` - ClientID string `json:"client_id"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - HomeOrigin string `json:"home_origin"` -} - -type clerkResponse struct { - Response struct { - Identifier string `json:"identifier"` - } `json:"response"` - Client struct { - Sessions []struct { - User struct { - ExternalAccounts []struct { - Provider string `json:"provider"` - Username string `json:"username"` - Verification struct { - Status string `json:"status"` - } - } `json:"external_accounts"` - } `json:"user"` - } `json:"sessions"` - } `json:"client"` -} - type State struct { Email string `json:"email,omitempty"` Name string `json:"name,omitempty"` @@ -72,168 +22,3 @@ type State struct { GithubUsername string `json:"github_username,omitempty"` DevSession string `json:"dev_session,omitempty"` // Only for DEV instance } - -func Login(ctx context.Context, ticket string) (*State, error) { - form := url.Values{} - form.Add("strategy", "ticket") - form.Add("ticket", ticket) - - devSession := "" - signinsURL := prodAddress + "/v1/client/sign_ins" - if devClerk { - req, err := http.NewRequestWithContext(ctx, "POST", devAddress+"/v1/dev_browser", nil) - if err != nil { - return nil, err - } - req.Header.Add("Origin", "https://accounts.namespace.so") - req.Header.Add("User-Agent", "NamespaceCLI/1.0") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fnerrors.InvocationError("login", "failed to authenticate client: %v", resp.Status) - } - - var x devBrowserResponse - if err := json.NewDecoder(resp.Body).Decode(&x); err != nil { - return nil, fnerrors.InvocationError("login", "bad response for client authentication: %w", err) - } - devSession = x.Token - signinsURL = devAddress + "/v1/client/sign_ins?__dev_session=" + devSession - } - - req, err := http.NewRequestWithContext(ctx, "POST", signinsURL, strings.NewReader(form.Encode())) - if err != nil { - return nil, err - } - - req.Header.Add("Origin", "https://accounts.namespace.so") - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("User-Agent", "NamespaceCLI/1.0") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fnerrors.InvocationError("login", "failed to authenticate: %v", resp.Status) - } - - var client string - for _, k := range resp.Cookies() { - if k.Name == "__client" { - client = k.Value - } - } - if k := resp.Header.Get("Clerk-Cookie"); devClerk && k != "" { - client = k - } - - if client == "" { - return nil, fnerrors.InvocationError("login", "missing client cookie") - } - - var x clerkResponse - if err := json.NewDecoder(resp.Body).Decode(&x); err != nil { - return nil, fnerrors.InvocationError("login", "bad response: %w", err) - } - - state := &State{ - Email: x.Response.Identifier, - ClerkClient: client, - DevSession: devSession, - } - - for _, session := range x.Client.Sessions { - for _, external := range session.User.ExternalAccounts { - if external.Provider == "oauth_github" && external.Verification.Status == "verified" { - state.GithubUsername = external.Username - } - } - } - - return state, nil -} - -type jwtResponse struct { - JWT string `json:"jwt"` -} - -type cachedJWT struct { - JWT string - CreatedAt time.Time -} - -var ( - tokenCache = map[string]cachedJWT{} - tokenCacheMu sync.Mutex -) - -func JWT(ctx context.Context, st *State) (string, error) { - tokenCacheMu.Lock() - defer tokenCacheMu.Unlock() - - if cached, ok := tokenCache[st.ClerkClient]; ok { - if time.Now().Before(cached.CreatedAt.Add(10 * time.Second)) { - return cached.JWT, nil - } else { - delete(tokenCache, st.ClerkClient) - } - } - - tokenPrefix := prodTokenPrefix - url := prodAddress + "/v1/me/tokens" - if devClerk { - url = devAddress + "/v1/me/tokens?__dev_session=" + st.DevSession - tokenPrefix = devTokenPrefix - } - req, err := http.NewRequestWithContext(ctx, "POST", url, nil) - if err != nil { - return "", err - } - - req.AddCookie(&http.Cookie{ - Name: "__client", - Value: st.ClerkClient, - }) - - req.Header.Add("Origin", "https://cli.namespace.so") - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("User-Agent", "NamespaceCLI/1.0") - - now := time.Now() - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", fnerrors.InvocationError("jwt", "failed to obtain token: %w", err) - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - if resp.StatusCode == http.StatusUnauthorized { - return "", ErrUnauthorized - } - - return "", fnerrors.InvocationError("jwt", "failed to obtain token: %v", resp.Status) - } - - var x jwtResponse - if err := json.NewDecoder(resp.Body).Decode(&x); err != nil { - return "", fnerrors.InvocationError("jwt", "bad response: %w", err) - } - - token := tokenPrefix + x.JWT - tokenCache[st.ClerkClient] = cachedJWT{ - CreatedAt: now, - JWT: token, - } - - return token, nil -}