1 import Base 2 import ../json.bend as Json 3 import ../fs.bend as Fs 4 import ../act.bend as Act 5 import ../store.bend as Store 6 import ../store/base.bend as SBase 7 import ../store/io.bend as SIo 8 import ../store/ops.bend as SOps 9 import ../store/seq.bend as SSeq 10 import ../usage.bend as Usage 11 import ../swlive.bend as L 12 import ../swbase.bend as SwB 13 import ../policy/types.bend as T 14 import ./rows.bend as Rows 15 import ./state.bend as St 16 17 # What a tick looks at: the roster, the live login, the autoswitch state, the 18 # cached usage rows, and per managed account its credential and the policy's 19 # view of it (kind, disabled, usage row). 20 21 # One managed account: its identity, its stored credential, and the policy's 22 # `Account` (a quarantined, disabled or unswitchable one is passed as disabled). 23 24 type Ent is Data: 25 Ent{slot: String, email: String, org: String, creds: String, acct: T.Account} 26 27 type Snap is Data: 28 Snap{seq: Json.Json, live: L.Live, active: Maybe<&2, String>, state: Json.Json, rows: Json.Json, ents: List<&2, Ent>} 29 30 def ActM(-A: Type) -> Type: 31 Act.of(A) 32 33 def ActM.pure(-A: Type, x: A) -> Act.of(A): 34 Act.pure(A, x) 35 36 def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B): 37 Act.bind(A, B, m, f) 38 39 def is_true(+m: Maybe<&2, Json.Json>) -> Bool: 40 match m: 41 case Some{Json.JBool{b}}: 42 b 43 case _: 44 False{} 45 46 def refresh_of(+creds: String) -> String: 47 SSeq.field_str(SSeq.get_or("claudeAiOauth", Json.unwrap(Json.parse(creds)), Json.JNull{}), "refreshToken") 48 49 def kind2(+api: Bool, +refresh: Bool) -> T.AcctKind: 50 match api refresh: 51 case True{} _: 52 T.ApiKey{} 53 case False{} True{}: 54 T.FullLogin{} 55 case False{} False{}: 56 T.SetupToken{} 57 58 def kind_of(+api_rec: Bool, +creds: String) -> T.AcctKind: 59 kind2(api_rec || SBase.looks_like_api_key(creds), SBase.nonempty(refresh_of(creds))) 60 61 def ent_of(+slot: String, +rec: Json.Json, +creds: String, +cfg_ok: Bool, +quarantined: Bool) -> Ent: 62 Ent{slot, SSeq.field_str(rec, "email"), SSeq.field_str(rec, "organizationUuid"), creds, 63 T.Account{U32.to_nat(SSeq.str_u32(slot)), SSeq.field_str(rec, "email"), kind_of(SSeq.is_api_key_rec(rec), creds), 64 is_true(Json.get("disabled", rec)) || quarantined || Bool.not(SBase.nonempty(creds) && cfg_ok)}} 65 66 def mem_str(+x: String, +xs: List<&2, String>) -> Bool: 67 match xs: 68 case Nil{}: 69 False{} 70 case Con{h, t}: 71 String.eq(h, x) || mem_str(x, t) 72 73 def has_config(+root: String, +seq: Json.Json, +slot: String) -> Act.of(Bool): 74 do ActM: 75 cfg : Maybe<&2, String> <- L.read_text_opt(SBase.config_path(root, slot, SIo.slot_email(seq, slot))) 76 return SBase.nonempty(SwB.text_or(cfg)) 77 78 def cred_of(+root: String, +seq: Json.Json, +slot: String) -> Act.of(String): 79 SIo.read_cred(root, slot, SIo.slot_email(seq, slot)) 80 81 def ent_one(+root: String, +seq: Json.Json, +slot: String, +st: Json.Json) -> Act.of(Ent): 82 do ActM: 83 creds : String <- cred_of(root, seq, slot) 84 ok : Bool <- has_config(root, seq, slot) 85 return ent_of(slot, SSeq.get_or(slot, SSeq.accounts_of(seq), Json.JNull{}), creds, ok, mem_str(slot, St.quarantined_slots(st))) 86 87 def ents_of(+root: String, +seq: Json.Json, +slots: List<&2, String>, +st: Json.Json) -> Act.of(List<&2, Ent>): 88 match slots: 89 case Nil{}: 90 Act.pure(List<&2, Ent>, []) 91 case Con{s, t}: 92 do ActM>: 93 e : Ent <- ent_one(root, seq, s, st) 94 rest : List<&2, Ent> <- ents_of(root, seq, t, st) 95 return e <> rest 96 97 def rows3(+v: String, +j: Json.Json) -> Json.Json: 98 Store.sel.j(String.eq(v, "2"), SSeq.accounts_of(j), Json.JObj{Json.JNil{}}) 99 100 def rows2(r: Result<&2, &2, String, Json.Json>) -> Json.Json: 101 match r: 102 case Fail{e}: 103 Json.JObj{Json.JNil{}} 104 case Done{+j}: 105 rows3(SOps.field_json(j, "schemaVersion"), j) 106 107 # The cached rows (accounts object); {} for a missing, torn or other-schema file. 108 def rows_of(r: Fs.Res(String)) -> Json.Json: 109 match r: 110 case Fail{e}: 111 SSeq.accounts_of(Json.JNull{}) 112 case Done{t}: 113 rows2(Json.parse(t)) 114 115 def read_rows(+root: String) -> IO(Json.Json): 116 IO.bind(Fs.Res(String), Json.Json, Fs.read_file(root ++ "/cache/usage.json"), r => IO.pure(Json.Json, rows_of(r))) 117 118 def ents_for(+ctx: SBase.Ctx, +seq: Json.Json, +st: Json.Json) -> Act.of(List<&2, Ent>): 119 ents_of(SBase.root_of(ctx), seq, SwB.slots_of(seq), st) 120 121 def state_of(+ctx: SBase.Ctx) -> Act.of(Json.Json): 122 Act.lift(Json.Json, St.read_state(SBase.root_of(ctx))) 123 124 def load(+ctx: SBase.Ctx, +seq: Json.Json) -> Act.of(Snap): 125 do ActM: 126 +live : L.Live <- L.read_live(ctx) 127 +st : Json.Json <- state_of(ctx) 128 rows : Json.Json <- Act.lift(Json.Json, read_rows(SBase.root_of(ctx))) 129 ents : List<&2, Ent> <- ents_for(ctx, seq, st) 130 return Snap{seq, live, L.lv_slot(live, seq), st, rows, ents} 131 132 def some_snap(s: Snap) -> Act.of(Maybe<&2, Snap>): 133 Act.pure(Maybe<&2, Snap>, Some{s}) 134 135 def snapshot2(+ctx: SBase.Ctx, +m: Maybe<&2, Json.Json>) -> Act.of(Maybe<&2, Snap>): 136 match m: 137 case None{}: 138 Act.pure(Maybe<&2, Snap>, None{}) 139 case Some{seq}: 140 Act.bind(Snap, Maybe<&2, Snap>, load(ctx, seq), s => some_snap(s)) 141 142 # Look the whole picture up. 143 def snapshot(+ctx: SBase.Ctx) -> Act.of(Maybe<&2, Snap>): 144 do ActM>: 145 m : Maybe<&2, Json.Json> <- SIo.read_seq(SBase.root_of(ctx)) 146 snapshot2(ctx, m) 147 148 def snap_ents(+s: Snap) -> List<&2, Ent>: 149 match s: 150 case Snap{seq, live, active, st, rows, ents}: 151 ents 152 153 def snap_active(+s: Snap) -> Maybe<&2, String>: 154 match s: 155 case Snap{seq, live, active, st, rows, ents}: 156 active 157 158 def snap_state(+s: Snap) -> Json.Json: 159 match s: 160 case Snap{seq, live, active, st, rows, ents}: 161 st 162 163 def snap_rows(+s: Snap) -> Json.Json: 164 match s: 165 case Snap{seq, live, active, st, rows, ents}: 166 rows 167 168 def in_skip(+slot: String, +skip: List<&2, String>) -> Bool: 169 mem_str(slot, skip) 170 171 def disable_ent(e: Ent, +skip: List<&2, String>) -> Ent: 172 match e: 173 case Ent{+slot, email, org, creds, T.Account{n, ae, k, d}}: 174 Ent{slot, email, org, creds, T.Account{n, ae, k, d || in_skip(slot, skip)}} 175 176 def disable_ents(+es: List<&2, Ent>, +skip: List<&2, String>) -> List<&2, Ent>: 177 match es: 178 case Nil{}: 179 [] 180 case Con{e, t}: 181 disable_ent(e, skip) <> disable_ents(t, skip) 182 183 def skip_one(s: Snap, +skip: List<&2, String>) -> Snap: 184 match s: 185 case Snap{seq, live, active, st, rows, ents}: 186 Snap{seq, live, active, st, rows, disable_ents(ents, skip)} 187 188 # The snapshot with the given slots held out of the candidates. 189 def skip_snap(+m: Maybe<&2, Snap>, +skip: List<&2, String>) -> Maybe<&2, Snap>: 190 match m: 191 case None{}: 192 None{} 193 case Some{s}: 194 Some{skip_one(s, skip)}