···1package auth
23import (
4+ "context"
5 "encoding/json"
6+ "fmt"
78+ "github.com/bluesky-social/indigo/atproto/auth/oauth"
9+ "github.com/bluesky-social/indigo/atproto/syntax"
10 "github.com/zalando/go-keyring"
11)
1213const (
14+ keyringService = "blup"
15+ currentSessionKey = "current-session"
16+ sessionKeyPrefix = "session:"
17+ authRequestPrefix = "auth-request:"
18+ pendingAuthStateKey = "pending-auth-state"
19+ legacyTokensKey = "oauth-tokens"
20+ legacyJWKSKey = "oauth-jwks"
21)
2223+// KeyringAuthStore implements oauth.ClientAuthStore using the system keyring
24+type KeyringAuthStore struct{}
25+26+func NewKeyringAuthStore() *KeyringAuthStore {
27+ return &KeyringAuthStore{}
00028}
2930+// sessionKey creates the keyring key for a session
31+func sessionKey(did syntax.DID, sessionID string) string {
32+ return fmt.Sprintf("%s%s:%s", sessionKeyPrefix, did.String(), sessionID)
00000033}
3435+// GetSession retrieves a session from the keyring
36+func (s *KeyringAuthStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) {
37+ data, err := keyring.Get(keyringService, sessionKey(did, sessionID))
38 if err != nil {
39 return nil, err
40 }
4142+ var sess oauth.ClientSessionData
43+ if err := json.Unmarshal([]byte(data), &sess); err != nil {
044 return nil, err
45 }
4647+ return &sess, nil
48}
4950+// SaveSession stores a session in the keyring
51+func (s *KeyringAuthStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error {
52+ data, err := json.Marshal(sess)
53+ if err != nil {
54+ return err
000055 }
5657+ return keyring.Set(keyringService, sessionKey(sess.AccountDID, sess.SessionID), string(data))
58+}
59+60+// DeleteSession removes a session from the keyring
61+func (s *KeyringAuthStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error {
62+ return keyring.Delete(keyringService, sessionKey(did, sessionID))
63+}
64+65+// authRequestKey creates the keyring key for an auth request
66+func authRequestKey(state string) string {
67+ return fmt.Sprintf("%s%s", authRequestPrefix, state)
68+}
69+70+// GetAuthRequestInfo retrieves pending auth request info
71+func (s *KeyringAuthStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) {
72+ data, err := keyring.Get(keyringService, authRequestKey(state))
73 if err != nil {
74 return nil, err
75 }
7677+ var info oauth.AuthRequestData
78+ if err := json.Unmarshal([]byte(data), &info); err != nil {
79 return nil, err
80 }
8182+ return &info, nil
83}
8485+// SaveAuthRequestInfo stores pending auth request info
86+func (s *KeyringAuthStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error {
87+ data, err := json.Marshal(info)
88 if err != nil {
89+ return err
90 }
9192+ // Save the auth request data
93+ if err := keyring.Set(keyringService, authRequestKey(info.State), string(data)); err != nil {
94+ return err
95 }
9697+ // Also save the state as the current pending auth (for SSE correlation)
98+ return keyring.Set(keyringService, pendingAuthStateKey, info.State)
99+}
100+101+// GetPendingAuthState returns the state of the current pending auth request
102+func (s *KeyringAuthStore) GetPendingAuthState() (string, error) {
103+ return keyring.Get(keyringService, pendingAuthStateKey)
104}
105106+// ClearPendingAuthState removes the pending auth state
107+func (s *KeyringAuthStore) ClearPendingAuthState() error {
108+ return keyring.Delete(keyringService, pendingAuthStateKey)
109+}
110+111+// DeleteAuthRequestInfo removes pending auth request info
112+func (s *KeyringAuthStore) DeleteAuthRequestInfo(ctx context.Context, state string) error {
113+ return keyring.Delete(keyringService, authRequestKey(state))
114+}
115+116+// CurrentSessionRef stores reference to the current active session
117+type CurrentSessionRef struct {
118+ DID string `json:"did"`
119+ SessionID string `json:"session_id"`
120+}
121+122+// GetCurrentSession retrieves the current active session for the CLI
123+func (s *KeyringAuthStore) GetCurrentSession(ctx context.Context) (*oauth.ClientSessionData, error) {
124+ refData, err := keyring.Get(keyringService, currentSessionKey)
125 if err != nil {
126 return nil, err
127 }
128129+ var ref CurrentSessionRef
130+ if err := json.Unmarshal([]byte(refData), &ref); err != nil {
131+ return nil, err
132+ }
133+134+ did, err := syntax.ParseDID(ref.DID)
135+ if err != nil {
136 return nil, err
137 }
138139+ return s.GetSession(ctx, did, ref.SessionID)
140}
141142+// SetCurrentSession sets the current active session reference
143+func (s *KeyringAuthStore) SetCurrentSession(ctx context.Context, sess *oauth.ClientSessionData) error {
144+ ref := CurrentSessionRef{
145+ DID: sess.AccountDID.String(),
146+ SessionID: sess.SessionID,
147+ }
148+149+ data, err := json.Marshal(ref)
150+ if err != nil {
151+ return err
152+ }
153+154+ return keyring.Set(keyringService, currentSessionKey, string(data))
155}
156157+// ClearCurrentSession removes the current session reference
158+func (s *KeyringAuthStore) ClearCurrentSession() error {
159+ return keyring.Delete(keyringService, currentSessionKey)
160}
161162+// HasLegacyTokens checks if old-format tokens exist (for migration)
163+func HasLegacyTokens() bool {
164+ _, err := keyring.Get(keyringService, legacyTokensKey)
165+ return err == nil
00166}
167168+// DeleteLegacyTokens removes old-format tokens and JWKS
169+func DeleteLegacyTokens() error {
170+ // Ignore errors - keys might not exist
171+ keyring.Delete(keyringService, legacyTokensKey)
172+ keyring.Delete(keyringService, legacyJWKSKey)
173+ return nil
174}
+12
internal/clipboard/clipboard.go
···000000000000
···1+// Package clipboard provides native clipboard access for Wayland.
2+package clipboard
3+4+// CopyText copies the given text to the system clipboard.
5+// On Wayland, this uses the wlr-data-control protocol directly,
6+// without requiring external tools like wl-copy.
7+//
8+// The implementation forks a background process to serve paste
9+// requests until another application takes clipboard ownership.
10+func CopyText(text string) error {
11+ return copyTextPlatform(text)
12+}