usage/cred.bend
cswap-bend-cli@0.1.0.0 · 281 lines · 44 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/http.bend as Http
3import cswap-bend-cli@0.1.0.0/json.bend as Json
4import cswap-bend-cli@0.1.0.0/clock.bend as Clock
5import cswap-bend-cli@0.1.0.0/usage/text.bend as UText
6import cswap-bend-cli@0.1.0.0/usage/norm.bend as UNorm
7import cswap-bend-cli@0.1.0.0/usage/req.bend as UReq
8
9
10
11
12
13
14type Granted is Data:
15 Granted{cred: String, account: Json.Json}
16
17def parse.get(+k: String, r: Result<&2, &2, String, Json.Json>) -> Maybe<&2, Json.Json>:
18 match r:
19 case Fail{e}:
20 None{}
21 case Done{j}:
22 Json.get(k, j)
23
24def oauth.obj(+m: Maybe<&2, Json.Json>) -> Json.Json:
25 match m:
26 case Some{Json.JObj{f}}:
27 Json.JObj{f}
28 case _:
29 Json.JNull{}
30
31
32
33def oauth.of(+cred: String) -> Json.Json:
34 oauth.obj(parse.get("claudeAiOauth", Json.parse(cred)))
35
36def str.at(+k: String, +o: Json.Json) -> Maybe<&2, String>:
37 UNorm.nonempty(Json.as_string(Maybe.default(&2, Json.Json, Json.get(k, o), Json.JNull{})))
38
39def access_token(+o: Json.Json) -> Maybe<&2, String>:
40 str.at("accessToken", o)
41
42def refresh_token(+o: Json.Json) -> Maybe<&2, String>:
43 str.at("refreshToken", o)
44
45def has_refresh(+o: Json.Json) -> Bool:
46 Maybe.is_some(&2, String, refresh_token(o))
47
48def has_profile(+xs: List<&2, Json.Json>) -> Bool:
49 match xs:
50 case Nil{}:
51 False{}
52 case Con{Json.JStr{s}, t}:
53 String.eq(s, "user:profile") || has_profile(t)
54 case Con{h, t}:
55 has_profile(t)
56
57def scopes.check(+xs: List<&2, Json.Json>) -> Bool:
58 match xs:
59 case Nil{}:
60 False{}
61 case Con{h, t}:
62 Bool.not(has_profile(h <> t))
63
64
65def inference_only(+o: Json.Json) -> Bool:
66 scopes.check(Json.to_list(Maybe.default(&2, Json.Json, Json.get("scopes", o), Json.JNull{})))
67
68def exp.secs(+d: String) -> Maybe<&2, U32>:
69 U32.read(String.take(d, Nat.sub(String.length(d), 3n)))
70
71def exp.check(+ok: Bool, +d: String, +now: U32) -> Bool:
72 match ok:
73 case False{}:
74 False{}
75 case True{}:
76 U32.is_ge((now + 300 : U32), Maybe.default(&2, U32, exp.secs(d), 0))
77
78def exp.int(+ps: List<&2, String>, +now: U32) -> Bool:
79 match ps:
80 case Con{+i, t}:
81 exp.check(Clock.all_digits(i) && Bool.not(String.is_empty(i)) && Nat.is_le(String.length(i), 13n), i, now)
82 case Nil{}:
83 False{}
84
85def exp.num(+j: Json.Json, +now: U32) -> Bool:
86 match j:
87 case Json.JNum{t}:
88 exp.int(String.split(t, '.'), now)
89 case _:
90 False{}
91
92
93
94def expired(+o: Json.Json, +now: U32) -> Bool:
95 exp.num(Maybe.default(&2, Json.Json, Json.get("expiresAt", o), Json.JNull{}), now)
96
97
98
99def set.rt(+m: Maybe<&2, String>, +o: Json.Json) -> Json.Json:
100 match m:
101 case None{}:
102 o
103 case Some{r}:
104 Json.set("refreshToken", Json.JStr{r}, o)
105
106def scopes.cons(+skip: Bool, +h: String, +rest: List<&2, Json.Json>) -> List<&2, Json.Json>:
107 match skip:
108 case True{}:
109 rest
110 case False{}:
111 Json.JStr{h} <> rest
112
113def scopes.list(+ps: List<&2, String>) -> List<&2, Json.Json>:
114 match ps:
115 case Nil{}:
116 []
117 case Con{+h, t}:
118 scopes.cons(String.is_empty(h), h, scopes.list(t))
119
120def set.sc(+m: Maybe<&2, String>, +o: Json.Json) -> Json.Json:
121 match m:
122 case None{}:
123 o
124 case Some{s}:
125 Json.set("scopes", Json.arr(scopes.list(String.split(s, ' '))), o)
126
127
128
129def refresh.fields(+o: Json.Json, +at: String, +ex: U32, +rt: Maybe<&2, String>, +sc: Maybe<&2, String>, +now: U32) -> Json.Json:
130 set.sc(sc, set.rt(rt, Json.set("expiresAt", Json.JNum{U32.show((now + ex : U32)) ++ "000"}, Json.set("accessToken", Json.JStr{at}, o))))
131
132def token.org(+o: Maybe<&2, Json.Json>) -> Json.Json:
133 match o:
134 case Some{org}:
135 Maybe.default(&2, Json.Json, Json.get("uuid", org), Json.JNull{})
136 case None{}:
137 Json.JNull{}
138
139def token.acct3(+blank: Bool, +u: String, +a: Json.Json, +resp: Json.Json) -> Json.Json:
140 match blank:
141 case True{}:
142 Json.JNull{}
143 case False{}:
144 Json.JObj{Json.JField{"uuid", Json.JStr{String.trim(u)}, Json.JField{"email", Maybe.default(&2, Json.Json, Json.get("email_address", a), Json.JNull{}), Json.JField{"organizationUuid", token.org(Json.get("organization", resp)), Json.JNil{}}}}}
145
146def token.acct2(+uuid: Maybe<&2, String>, +a: Json.Json, +resp: Json.Json) -> Json.Json:
147 match uuid:
148 case None{}:
149 Json.JNull{}
150 case Some{u}:
151 token.acct3(String.is_empty(String.trim(u)), u, a, resp)
152
153def token.acct1(+a: Maybe<&2, Json.Json>, +resp: Json.Json) -> Json.Json:
154 match a:
155 case None{}:
156 Json.JNull{}
157 case Some{acct}:
158 token.acct2(str.at("uuid", acct), acct, resp)
159
160def token.account(+resp: Json.Json) -> Json.Json:
161 token.acct1(Json.get("account", resp), resp)
162
163def refresh.build(+j: Json.Json, +o: Json.Json, +resp: Json.Json, +now: U32) -> Maybe<&2, String>:
164 do Maybe<&2, String>:
165 a : Json.Json <- Json.get("access_token", resp)
166 at : String <- Json.as_string(a)
167 e : Json.Json <- Json.get("expires_in", resp)
168 ex : U32 <- Json.as_u32(e)
169 return Json.emit_py(Json.set("claudeAiOauth", refresh.fields(o, at, ex, str.at("refresh_token", resp), str.at("scope", resp), now), j))
170
171def grant.ok(+m: Maybe<&2, String>, +acct: Json.Json) -> Result<&2, &2, String, Granted>:
172 match m:
173 case Some{c}:
174 Done{Granted{c, acct}}
175 case None{}:
176 Fail{"transient"}
177
178def grant.parse(r: Result<&2, &2, String, Json.Json>, +j: Json.Json, +o: Json.Json, +now: U32) -> Result<&2, &2, String, Granted>:
179 match r:
180 case Fail{e}:
181 Fail{"transient"}
182 case Done{+resp}:
183 grant.ok(refresh.build(j, o, resp, now), token.account(resp))
184
185
186
187
188def grant.err.kind(+hit: Bool, +s: String) -> Result<&2, &2, String, Granted>:
189 match hit:
190 case True{}:
191 Fail{s}
192 case False{}:
193 Fail{"transient"}
194
195def grant.err.pick(+e: Maybe<&2, String>) -> Result<&2, &2, String, Granted>:
196 match e:
197 case Some{+s}:
198 grant.err.kind(String.eq(s, "invalid_grant") || String.eq(s, "invalid_client"), s)
199 case None{}:
200 Fail{"transient"}
201
202def grant.err.body(r: Result<&2, &2, String, Json.Json>) -> Result<&2, &2, String, Granted>:
203 match r:
204 case Fail{m}:
205 Fail{"transient"}
206 case Done{b}:
207 grant.err.pick(Json.as_string(Maybe.default(&2, Json.Json, Json.get("error", b), Json.JNull{})))
208
209def rejects(+st: U32) -> Bool:
210 U32.is_eq(st, 400) || U32.is_eq(st, 401) || U32.is_eq(st, 403)
211
212def grant.err(+rejects: Bool, +body: String) -> Result<&2, &2, String, Granted>:
213 match rejects:
214 case True{}:
215 grant.err.body(Json.parse(body))
216 case False{}:
217 Fail{"transient"}
218
219def grant.reply.pick(+ok: Bool, +rj: Bool, +body: String, +j: Json.Json, +o: Json.Json, +now: U32) -> Result<&2, &2, String, Granted>:
220 match ok:
221 case True{}:
222 grant.parse(Json.parse(body), j, o, now)
223 case False{}:
224 grant.err(rj, body)
225
226def grant.reply(+rep: Http.Reply, +j: Json.Json, +o: Json.Json, +now: U32) -> Result<&2, &2, String, Granted>:
227 match rep:
228 case Http.Reply{st, head, body}:
229 grant.reply.pick(UReq.ok2xx(st), rejects(st), body, j, o, now)
230
231def grant.result(r: Result<&2, &2, String, Http.Reply>, +j: Json.Json, +o: Json.Json, +now: U32) -> Result<&2, &2, String, Granted>:
232 match r:
233 case Fail{k}:
234 Fail{"transient"}
235 case Done{rep}:
236 grant.reply(rep, j, o, now)
237
238
239
240
241def refresh.post(+j: Json.Json, +o: Json.Json, +rt: String) -> IO(Result<&2, &2, String, Granted>):
242 do IO<Result<&2, &2, String, Granted>>:
243 now : U32 <- Clock.now_s()
244 r : Result<&2, &2, String, Http.Reply> <- Http.request("POST", UText.token_url(),
245 [("Content-Type", "application/json"), ("User-Agent", UText.user_agent())], UText.refresh.body(rt), 10)
246 return grant.result(r, j, o, now)
247
248def refresh.rt(+j: Json.Json, +o: Json.Json, +m: Maybe<&2, String>) -> IO(Result<&2, &2, String, Granted>):
249 match m:
250 case None{}:
251 IO.pure(Result<&2, &2, String, Granted>, Fail{"no_refresh_token"})
252 case Some{rt}:
253 refresh.post(j, o, rt)
254
255def refresh.top(+j: Json.Json, +o: Json.Json) -> IO(Result<&2, &2, String, Granted>):
256 match j:
257 case Json.JObj{f}:
258 refresh.rt(j, o, refresh_token(o))
259 case _:
260 IO.pure(Result<&2, &2, String, Granted>, Fail{"transient"})
261
262def refresh.parsed(r: Result<&2, &2, String, Json.Json>, +o: Json.Json) -> IO(Result<&2, &2, String, Granted>):
263 match r:
264 case Fail{e}:
265 IO.pure(Result<&2, &2, String, Granted>, Fail{"transient"})
266 case Done{j}:
267 refresh.top(j, o)
268
269
270def refresh_acct(+cred: String) -> IO(Result<&2, &2, String, Granted>):
271 refresh.parsed(Json.parse(cred), oauth.of(cred))
272
273def granted.cred(r: Result<&2, &2, String, Granted>) -> Result<&2, &2, String, String>:
274 match r:
275 case Fail{k}:
276 Fail{k}
277 case Done{Granted{c, a}}:
278 Done{c}
279
280def refresh(+cred: String) -> IO(Result<&2, &2, String, String>):
281 IO.bind(Result<&2, &2, String, Granted>, Result<&2, &2, String, String>, refresh_acct(cred), r => IO.pure(Result<&2, &2, String, String>, granted.cred(r)))