+ return nil
+}
+
+// post posts body to the path of the login service and decodes a 200 answer
+// into result. It returns the HTTP status, or 0 when no answer came. The error
+// holds the status, but not the body, which can hold a token.
+func (a *ChatGPTAuth) post(ctx context.Context, path, contentType string, body []byte, result any) (status int, err error) {
+ request, err := http.NewRequestWithContext(ctx, http.MethodPost, a.issuer+path, bytes.NewReader(body))
+ if err != nil {
+ return 0, fmt.Errorf("create request: %w", err)
+ }
+
+ request.Header.Set("Content-Type", contentType)
+
+ response, err := a.http.Do(request)
+ if err != nil {
+ return 0, fmt.Errorf("send request: %w", err)
+ }
+
+ defer func() {
+ err = errors.Join(err, response.Body.Close())
+ }()
+
+ if response.StatusCode != http.StatusOK {
+ return response.StatusCode, fmt.Errorf("HTTP %d", response.StatusCode)
+ }
+
+ err = json.NewDecoder(response.Body).Decode(result)
+ if err != nil {
+ return response.StatusCode, fmt.Errorf("decode answer: %w", err)
+ }
+
+ return response.StatusCode, nil
+}
+
+// Login runs the device code login. It writes the link and the code for the
+// user to out, waits until the user approves the code, and writes the tokens to
+// the token file.
+func (a *ChatGPTAuth) Login(ctx context.Context, out io.Writer) error {
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ var code ChatGPTDeviceCode
+
+ err := a.postJSON(ctx, "/api/accounts/deviceauth/usercode", map[string]string{
+ "client_id": chatgptClientID,
+ }, &code)
+ if err != nil {
+ return fmt.Errorf("request device code (device code login must be allowed in the ChatGPT security settings): %w", err)
+ }
+
+ userCode := cmp.Or(code.UserCode, code.UserCodeAlt)
+ if code.DeviceAuthID == "" || userCode == "" {
+ return errors.New("device code response has no device ID or user code")
+ }
+
+ _, err = fmt.Fprintf(out, "Open %s/codex/device on any device and enter this code:\n\n %s\n\nWaiting for the approval...\n", a.issuer, userCode)
+ if err != nil {
+ return fmt.Errorf("write device code: %w", err)
+ }
+
+ grant, err := a.poll(ctx, code.DeviceAuthID, userCode, chatgptPollInterval(code.Interval))
+ if err != nil {
+ return fmt.Errorf("wait for device grant: %w", err)
+ }
+
+ tokens, err := a.exchange(ctx, grant)
+ if err != nil {
+ return fmt.Errorf("exchange device grant: %w", err)
+ }
+
+ err = a.write(tokens)
+ if err != nil {
+ return fmt.Errorf("save login tokens: %w", err)
+ }
+
+ _, err = fmt.Fprintln(out, "Logged in.")
+ if err != nil {
+ return fmt.Errorf("write login result: %w", err)
+ }
+
+ return nil