okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petcli: gofmt the colab sign-in types and test132f576

main
7.4 KiB196 linesraw
1package daemon
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"io"
8	"net/http"
9	"net/url"
10
11	"albedo/cli/internal/config"
12	"albedo/cli/internal/daemon/protocol"
13)
14
15type SignIn struct {
16	Provider, Label, Detail, Protocol, Kind string
17	Flows                                   []string
18	Fields                                  []FormField
19}
20type Account struct {
21	Provider, ID, Label, Detail, Kind string
22	Selected                          bool
23	SelectedByProfiles                []string
24}
25type SignIns struct {
26	Logins   []SignIn
27	Accounts []Account
28}
29type StartedSignIn struct{ ID, URL, ETag string }
30type SignInStatus struct {
31	State, Message, ETag, Instructions, URL, Provider string
32	Accounts                                          []Account
33}
34
35func accountValue(w protocol.Account) Account {
36	return Account{Provider: w.Provider, ID: w.ID, Label: w.Label, Detail: w.Detail, Kind: w.Kind, Selected: len(w.SelectedByProfiles) > 0, SelectedByProfiles: w.SelectedByProfiles}
37}
38func SignInList(ctx context.Context, conn *Connection) (SignIns, error) {
39	var result SignIns
40	err := executeRead(ctx, conn, operation{Capability: "provider_auth", Name: "list provider accounts", BuildRequest: func(base string, body io.Reader) (*http.Request, error) { return protocol.NewGetAuthRequest(base) }, Policy: readRecovery}, func(data []byte) error {
41		var w protocol.Auth
42		if err := decodeRequired(data, &w); err != nil {
43			return err
44		}
45		if w.Providers == nil || w.Accounts == nil {
46			return fieldError("auth")
47		}
48		result = SignIns{Logins: []SignIn{}, Accounts: []Account{}}
49		for _, p := range w.Providers {
50			result.Logins = append(result.Logins, SignIn{Provider: p.ID, Label: p.Label, Detail: p.Detail, Kind: p.Kind, Flows: p.Flows, Fields: p.Fields})
51		}
52		for _, a := range w.Accounts {
53			result.Accounts = append(result.Accounts, accountValue(a))
54		}
55		return nil
56	})
57	return result, err
58}
59func loginValue(w protocol.Login, etag string) (SignInStatus, error) {
60	if w.ID == "" || etag == "" || w.Accounts == nil {
61		return SignInStatus{}, fieldError("login")
62	}
63	switch w.State {
64	case "waiting", "exchanging", "complete", "failed", "cancelled", "expired":
65	default:
66		return SignInStatus{}, fieldError("login state")
67	}
68	if urlText := value(w.URL); urlText != "" {
69		parsed, err := url.Parse(urlText)
70		if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
71			return SignInStatus{}, fieldError("login URL")
72		}
73	}
74	r := SignInStatus{State: w.State, Message: w.Progress, ETag: etag, Instructions: value(w.Instructions), URL: value(w.URL), Provider: w.Provider, Accounts: []Account{}}
75	if w.Failure != nil {
76		r.Message = w.Failure.Detail
77	}
78	for _, a := range w.Accounts {
79		r.Accounts = append(r.Accounts, accountValue(a))
80	}
81	return r, nil
82}
83
84// StartSignInWithID retains the same admitted intent when a transport failure is retried.
85func StartSignInWithID(ctx context.Context, conn *Connection, id, provider, flow string, values map[string]json.RawMessage) (StartedSignIn, error) {
86	result := StartedSignIn{ID: id}
87	body := protocol.LoginRequest{Provider: provider, Flow: optionalText(flow), Values: nonNilMap(values)}
88	frozen, err := json.Marshal(body)
89	if err != nil {
90		return result, err
91	}
92	op := operation{Capability: "provider_auth", Name: "start provider login", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
93		return protocol.NewCreateLoginRequestWithBody(base, id, "application/json", body)
94	}, Body: json.RawMessage(frozen), Validator: &result.ETag, Policy: noRecovery}
95	for attempt := range 2 {
96		err = executeMutation(ctx, conn, op, []int{201, 200}, func(data []byte, _ int) error {
97			var w protocol.Login
98			if err := decodeRequired(data, &w); err != nil {
99				return err
100			}
101			if w.ID != id || w.Provider != provider {
102				return fieldError("login identity")
103			}
104			status, err := loginValue(w, result.ETag)
105			if err != nil {
106				return err
107			}
108			result.URL = status.URL
109			return nil
110		})
111		if err == nil {
112			return result, nil
113		}
114		if _, uncertain := errors.AsType[*UncertainOutcomeError](err); !uncertain {
115			return result, err
116		}
117		if ctx != nil && ctx.Err() != nil {
118			return result, err
119		}
120		status, lookupErr := PollSignIn(ctx, conn, id)
121		if lookupErr == nil {
122			if status.Provider != provider {
123				return result, fieldError("login provider")
124			}
125			result.URL, result.ETag = status.URL, status.ETag
126			return result, nil
127		}
128		if api, ok := errors.AsType[*APIError](lookupErr); ok && api.StatusCode == 410 {
129			return result, lookupErr
130		}
131		if attempt == 1 {
132			return result, err
133		}
134	}
135	panic("unreachable login retry budget")
136}
137func PollSignIn(ctx context.Context, conn *Connection, id string) (SignInStatus, error) {
138	var result SignInStatus
139	var etag string
140	err := executeRead(ctx, conn, operation{Capability: "provider_auth", Name: "read provider login", BuildRequest: func(base string, body io.Reader) (*http.Request, error) { return protocol.NewGetLoginRequest(base, id) }, Validator: &etag, Policy: readRecovery}, func(data []byte) error {
141		var w protocol.Login
142		if err := decodeRequired(data, &w); err != nil {
143			return err
144		}
145		if w.ID != id {
146			return fieldError("login identity")
147		}
148		var err error
149		result, err = loginValue(w, etag)
150		return err
151	})
152	return result, err
153}
154func SignInInput(ctx context.Context, conn *Connection, id, input, etag string) error {
155	headers, err := observedHeaders(etag)
156	if err != nil {
157		return err
158	}
159	var next string
160	return executeMutation(ctx, conn, operation{Capability: "provider_auth", Name: "answer provider login", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
161		return protocol.NewAnswerLoginRequestWithBody(base, id, &protocol.AnswerLoginParams{IfMatch: etag}, "application/merge-patch+json", body)
162	}, Body: protocol.LoginPatch{Response: &input}, Headers: headers, Validator: &next, Policy: noRecovery}, []int{200}, func(data []byte, _ int) error {
163		var w protocol.Login
164		if err := decodeRequired(data, &w); err != nil {
165			return err
166		}
167		_, err := loginValue(w, next)
168		return err
169	})
170}
171func CancelSignIn(ctx context.Context, conn *Connection, id string) error {
172	return executeMutation(ctx, conn, operation{Capability: "provider_auth", Name: "cancel provider login", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
173		return protocol.NewCancelLoginRequest(base, id)
174	}, Policy: noRecovery}, []int{204}, func([]byte, int) error { return nil })
175}
176func SelectProfileAccount(ctx context.Context, conn *Connection, profile string, account Account, profiles config.Profiles) error {
177	saved, ok := profiles.Providers[profile]
178	if !ok || saved.Extension != account.Provider {
179		return errors.New("select a profile for this account provider")
180	}
181	saved.AccountID = &account.ID
182	return SaveProvider(ctx, conn, profile, saved, profiles.ETag)
183}
184func RemoveAccount(ctx context.Context, conn *Connection, id string) error {
185	return executeMutation(ctx, conn, operation{Capability: "provider_auth", Name: "remove provider account", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
186		return protocol.NewDeleteAccountRequest(base, id)
187	}, Policy: noRecovery}, []int{204}, func([]byte, int) error { return nil })
188}
189
190func StartProviderLogin(ctx context.Context, conn *Connection, provider, flow string, values map[string]json.RawMessage) (StartedSignIn, error) {
191	id, err := operationID()
192	if err != nil {
193		return StartedSignIn{}, err
194	}
195	return StartSignInWithID(ctx, conn, id, provider, flow, values)
196}