orch-code 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ package auth
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strings"
7
+ "time"
8
+ )
9
+
10
+ type AccountSession struct {
11
+ repoRoot string
12
+ provider string
13
+ currentID string
14
+ excluded map[string]struct{}
15
+ }
16
+
17
+ func NewAccountSession(repoRoot, provider string) *AccountSession {
18
+ return &AccountSession{
19
+ repoRoot: strings.TrimSpace(repoRoot),
20
+ provider: strings.ToLower(strings.TrimSpace(provider)),
21
+ excluded: map[string]struct{}{},
22
+ }
23
+ }
24
+
25
+ func (s *AccountSession) ResolveToken(ctx context.Context) (string, error) {
26
+ _ = ctx
27
+ cred, err := s.pickCredential()
28
+ if err != nil {
29
+ return "", err
30
+ }
31
+ if cred == nil {
32
+ return "", fmt.Errorf("no active oauth credential available for provider %s", s.provider)
33
+ }
34
+ s.currentID = cred.ID
35
+ return strings.TrimSpace(cred.AccessToken), nil
36
+ }
37
+
38
+ func (s *AccountSession) Failover(ctx context.Context, cooldown time.Duration, reason string) (string, bool, error) {
39
+ _ = ctx
40
+ if strings.TrimSpace(s.currentID) == "" {
41
+ return "", false, nil
42
+ }
43
+ if err := mutateCredential(s.repoRoot, s.provider, s.currentID, func(cred *Credential) error {
44
+ if cooldown > 0 {
45
+ cred.CooldownUntil = time.Now().UTC().Add(cooldown)
46
+ }
47
+ cred.LastError = strings.TrimSpace(reason)
48
+ cred.UpdatedAt = time.Now().UTC()
49
+ return nil
50
+ }); err != nil {
51
+ return "", false, err
52
+ }
53
+ s.excluded[s.currentID] = struct{}{}
54
+
55
+ cred, err := s.pickCredential()
56
+ if err != nil {
57
+ return "", false, err
58
+ }
59
+ if cred == nil {
60
+ return "", false, nil
61
+ }
62
+ s.currentID = cred.ID
63
+ return strings.TrimSpace(cred.AccessToken), true, nil
64
+ }
65
+
66
+ func (s *AccountSession) MarkSuccess(ctx context.Context) {
67
+ _ = ctx
68
+ if strings.TrimSpace(s.currentID) == "" {
69
+ return
70
+ }
71
+ _ = mutateCredential(s.repoRoot, s.provider, s.currentID, func(cred *Credential) error {
72
+ cred.LastError = ""
73
+ cred.CooldownUntil = time.Time{}
74
+ cred.LastUsedAt = time.Now().UTC()
75
+ cred.UpdatedAt = time.Now().UTC()
76
+ return nil
77
+ })
78
+ s.excluded = map[string]struct{}{}
79
+ }
80
+
81
+ func (s *AccountSession) pickCredential() (*Credential, error) {
82
+ credentials, activeID, err := List(s.repoRoot, s.provider)
83
+ if err != nil {
84
+ return nil, err
85
+ }
86
+ if len(credentials) == 0 {
87
+ return nil, nil
88
+ }
89
+
90
+ now := time.Now().UTC()
91
+ ordered := orderCredentials(credentials, activeID, s.currentID)
92
+ for _, candidate := range ordered {
93
+ if candidate.Type != "oauth" {
94
+ continue
95
+ }
96
+ if _, skip := s.excluded[candidate.ID]; skip {
97
+ continue
98
+ }
99
+ if !candidate.CooldownUntil.IsZero() && candidate.CooldownUntil.After(now) {
100
+ continue
101
+ }
102
+ if candidate.ID != activeID {
103
+ if err := SetActive(s.repoRoot, s.provider, candidate.ID); err != nil {
104
+ return nil, err
105
+ }
106
+ }
107
+ resolved, err := resolveAccountCredentialByID(s.repoRoot, s.provider, candidate.ID)
108
+ if err == nil {
109
+ return resolved, nil
110
+ }
111
+ _ = mutateCredential(s.repoRoot, s.provider, candidate.ID, func(cred *Credential) error {
112
+ cred.LastError = strings.TrimSpace(err.Error())
113
+ cred.CooldownUntil = now.Add(5 * time.Minute)
114
+ cred.UpdatedAt = now
115
+ return nil
116
+ })
117
+ s.excluded[candidate.ID] = struct{}{}
118
+ }
119
+
120
+ return nil, nil
121
+ }
122
+
123
+ func orderCredentials(credentials []Credential, activeID, currentID string) []Credential {
124
+ ordered := make([]Credential, 0, len(credentials))
125
+ appendByID := func(id string) {
126
+ if strings.TrimSpace(id) == "" {
127
+ return
128
+ }
129
+ for _, cred := range credentials {
130
+ if cred.ID == id && !containsCredential(ordered, id) {
131
+ ordered = append(ordered, cred)
132
+ return
133
+ }
134
+ }
135
+ }
136
+ appendByID(currentID)
137
+ appendByID(activeID)
138
+ for _, cred := range credentials {
139
+ if containsCredential(ordered, cred.ID) {
140
+ continue
141
+ }
142
+ ordered = append(ordered, cred)
143
+ }
144
+ return ordered
145
+ }
146
+
147
+ func containsCredential(credentials []Credential, credentialID string) bool {
148
+ for _, cred := range credentials {
149
+ if cred.ID == credentialID {
150
+ return true
151
+ }
152
+ }
153
+ return false
154
+ }
@@ -0,0 +1,71 @@
1
+ package auth
2
+
3
+ import (
4
+ "context"
5
+ "encoding/base64"
6
+ "fmt"
7
+ "testing"
8
+ "time"
9
+ )
10
+
11
+ func TestAccountSessionFailsOverToNextCredential(t *testing.T) {
12
+ repoRoot := t.TempDir()
13
+ if err := Set(repoRoot, "openai", Credential{Type: "oauth", AccessToken: testSessionAccountToken("acc-1"), RefreshToken: "refresh-1", AccountID: "acc-1", Email: "one@example.com"}); err != nil {
14
+ t.Fatalf("set first account: %v", err)
15
+ }
16
+ if err := Set(repoRoot, "openai", Credential{Type: "oauth", AccessToken: testSessionAccountToken("acc-2"), RefreshToken: "refresh-2", AccountID: "acc-2", Email: "two@example.com"}); err != nil {
17
+ t.Fatalf("set second account: %v", err)
18
+ }
19
+ if err := SetActive(repoRoot, "openai", "acc-1"); err != nil {
20
+ t.Fatalf("set active: %v", err)
21
+ }
22
+
23
+ session := NewAccountSession(repoRoot, "openai")
24
+ token, err := session.ResolveToken(context.Background())
25
+ if err != nil {
26
+ t.Fatalf("resolve token: %v", err)
27
+ }
28
+ if token != testSessionAccountToken("acc-1") {
29
+ t.Fatalf("expected first token, got %q", token)
30
+ }
31
+
32
+ nextToken, switched, err := session.Failover(context.Background(), time.Minute, "rate limited")
33
+ if err != nil {
34
+ t.Fatalf("failover: %v", err)
35
+ }
36
+ if !switched {
37
+ t.Fatalf("expected failover switch")
38
+ }
39
+ if nextToken != testSessionAccountToken("acc-2") {
40
+ t.Fatalf("expected second token, got %q", nextToken)
41
+ }
42
+
43
+ active, err := Get(repoRoot, "openai")
44
+ if err != nil {
45
+ t.Fatalf("get active credential: %v", err)
46
+ }
47
+ if active == nil || active.ID != "acc-2" {
48
+ t.Fatalf("expected acc-2 active after failover, got %#v", active)
49
+ }
50
+ credentials, _, err := List(repoRoot, "openai")
51
+ if err != nil {
52
+ t.Fatalf("list credentials: %v", err)
53
+ }
54
+ var first *Credential
55
+ for i := range credentials {
56
+ if credentials[i].ID == "acc-1" {
57
+ first = &credentials[i]
58
+ break
59
+ }
60
+ }
61
+ if first == nil || first.CooldownUntil.IsZero() {
62
+ t.Fatalf("expected first credential to have cooldown set, got %#v", first)
63
+ }
64
+ }
65
+
66
+ func testSessionAccountToken(accountID string) string {
67
+ header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`))
68
+ payload := fmt.Sprintf(`{"https://api.openai.com/auth":{"chatgpt_account_id":"%s"}}`, accountID)
69
+ body := base64.RawURLEncoding.EncodeToString([]byte(payload))
70
+ return header + "." + body + ".sig"
71
+ }