store/ops.bend
cswap-bend-cli@0.1.0.0 · 293 lines · 49 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/act.bend as Act
4import cswap-bend-cli@0.1.0.0/lock.bend as Lock
5import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
6import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
7import cswap-bend-cli@0.1.0.0/store/plan.bend as SPlan
8import cswap-bend-cli@0.1.0.0/store/io.bend as SIo
9
10
11def ActM(-A: Type) -> Type:
12 Act.of(A)
13
14def ActM.pure(-A: Type, x: A) -> Act.of(A):
15 Act.pure(A, x)
16
17def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
18 Act.bind(A, B, m, f)
19
20
21
22
23
24type Eff is Data:
25 Eff{email: String, slot: Maybe<&2, U32>}
26
27def eff_slot(+api_key: Bool, +n: U32) -> Eff:
28 Eff{SPlan.default_email(api_key, n), Some{n}}
29
30def eff_noemail(+api_key: Bool, +slot: Maybe<&2, U32>, +seq: Json.Json) -> Eff:
31 eff_slot(api_key, Maybe.default(&2, U32, slot, SSeq.next_slot(seq)))
32
33def effective(+email0: Maybe<&2, String>, +slot0: Maybe<&2, U32>, +seq: Json.Json, +api_key: Bool) -> Eff:
34 match email0:
35 case Some{e}:
36 Eff{e, slot0}
37 case None{}:
38 eff_noemail(api_key, slot0, seq)
39
40def email_ok(+m: Maybe<&2, String>) -> Bool:
41 match m:
42 case None{}:
43 True{}
44 case Some{e}:
45 SBase.valid_email(e)
46
47def is_none(+m: Maybe<&2, String>) -> Bool:
48 match m:
49 case None{}:
50 True{}
51 case Some{x}:
52 False{}
53
54def clash3(+existing_api: Bool, +n: String, +email: String, +api_key: Bool) -> Maybe<&2, String>:
55 match existing_api api_key:
56 case True{} True{}:
57 None{}
58 case False{} False{}:
59 None{}
60 case _ _:
61 Some{"'" ++ email ++ "' already exists as an " ++ SPlan.kind_title(existing_api) ++ " account (slot " ++ n ++ "); cannot add it as an "
62 ++ SPlan.kind_title(api_key) ++ " account. Pass a distinct --email."}
63
64def clash2(+slot: Maybe<&2, String>, +seq: Json.Json, +email: String, +api_key: Bool) -> Maybe<&2, String>:
65 match slot:
66 case None{}:
67 None{}
68 case Some{n}:
69 clash3(SSeq.is_api_key_rec(SSeq.get_or(n, SSeq.accounts_of(seq), Json.JNull{})), n, email, api_key)
70
71
72def clash(+seq: Json.Json, +email: String, +api_key: Bool) -> Maybe<&2, String>:
73 clash2(SSeq.find_slot(seq, email, ""), seq, email, api_key)
74
75def clash_msg(+m: Maybe<&2, String>) -> String:
76 SSeq.str_or(m, "")
77
78def refresh_slot(+slot: Maybe<&2, U32>, +seq: Json.Json, +email: String) -> Maybe<&2, String>:
79 match slot:
80 case Some{n}:
81 None{}
82 case None{}:
83 SSeq.find_slot(seq, email, "")
84
85def moved_line(+m: Maybe<&2, String>, +from: String, +to: String) -> String:
86 match m:
87 case None{}:
88 ""
89 case Some{old}:
90 "Moved from slot " ++ old ++ " → " ++ to ++ "\n"
91
92def unverified_reason(+api_key: Bool) -> String:
93 SBase.sel.s(api_key, "an API key has no OAuth profile to look up", "a setup-token has only the user:inference scope, so GET /api/oauth/profile answers 403")
94
95def token_files(+root: String, +token: String, +api_key: Bool, +email: String, +num: String) -> Act.of(Unit):
96 do ActM<Unit>:
97 SIo.write_cred(root, num, email, SBase.sel.s(api_key, token, SPlan.setup_token_creds(token)))
98 SIo.write_config(root, num, email, SPlan.token_config(email))
99
100def token_logs(+root: String, +info: String, +api_key: Bool, +email: String, +num: String) -> Act.of(Unit):
101 do ActM<Unit>:
102 SIo.log(root, "INFO", info)
103 SIo.log(root, "WARNING", "identity unverified for slot " ++ num ++ " (" ++ email ++ "): " ++ unverified_reason(api_key) ++ "; recorded as unverified")
104
105def token_seq(+s2: Json.Json, +num: String, +email: String, +ts: String, +api_key: Bool) -> Json.Json:
106 SSeq.stamp(SSeq.put_account(s2, num, SPlan.token_record(email, ts, api_key)), ts)
107
108def token_place2(+root: String, +token: String, +api_key: Bool, +email: String, +ts: String, +seq: Json.Json, +num: String, +displace: Maybe<&2, String>, +migrate: Maybe<&2, String>) -> Act.of(String):
109 do ActM<String>:
110 s2 : Json.Json <- SIo.drop_pair(root, seq, displace, migrate, ts)
111 token_files(root, token, api_key, email, num)
112 SIo.write_seq(root, token_seq(s2, num, email, ts, api_key))
113 token_logs(root, "Added account " ++ num ++ " from " ++ SPlan.kind_label(api_key) ++ ": " ++ email, api_key, email, num)
114 return moved_line(migrate, "", num) ++ "Added Account " ++ num ++ ": " ++ email ++ " [personal] (from " ++ SPlan.kind_label(api_key) ++ ")"
115
116def token_place(+root: String, +token: String, +api_key: Bool, +email: String, +ts: String, +seq: Json.Json, +p: SPlan.SlotPlan, +migrate_slot: String) -> Act.of(String):
117 match p:
118 case SPlan.SPErr{code, msg}:
119 Act.fail(String, code, msg)
120 case SPlan.SPOk{num, displace, migrate}:
121 token_place2(root, token, api_key, email, ts, seq, num, displace, migrate)
122
123def token_refresh(+root: String, +token: String, +api_key: Bool, +email: String, +ts: String, +seq: Json.Json, +num: String) -> Act.of(String):
124 do ActM<String>:
125 token_files(root, token, api_key, email, num)
126 SIo.write_seq(root, SSeq.stamp(seq, ts))
127 token_logs(root, "Updated " ++ SPlan.kind_label(api_key) ++ " for account " ++ num ++ ": " ++ email, api_key, email, num)
128 return "Updated " ++ SPlan.kind_label(api_key) ++ " for Account " ++ num ++ " (" ++ email ++ " [personal])."
129
130def token_branch2(+refresh: Maybe<&2, String>, +root: String, +token: String, +api_key: Bool, +email: String, +slot: Maybe<&2, U32>, +assume_yes: Bool, +ts: String, +seq: Json.Json) -> Act.of(String):
131 match refresh:
132 case Some{num}:
133 token_refresh(root, token, api_key, email, ts, seq, num)
134 case None{}:
135 token_place(root, token, api_key, email, ts, seq, SPlan.plan_slot(slot, seq, email, "", assume_yes), "")
136
137def token_branch(+root: String, +token: String, +api_key: Bool, +e: Eff, +assume_yes: Bool, +ts: String, +seq: Json.Json) -> Act.of(String):
138 match e:
139 case Eff{+email, +slot}:
140 token_branch2(refresh_slot(slot, seq, email), root, token, api_key, email, slot, assume_yes, ts, seq)
141
142def token_checked2(+c: Maybe<&2, String>, +e: Eff, +root: String, +token: String, +api_key: Bool, +assume_yes: Bool, +ts: String, +seq: Json.Json) -> Act.of(String):
143 do ActM<String>:
144 Act.guard(is_none(c), 2, clash_msg(c))
145 token_branch(root, token, api_key, e, assume_yes, ts, seq)
146
147def token_checked(+e: Eff, +root: String, +token: String, +api_key: Bool, +assume_yes: Bool, +ts: String, +seq: Json.Json) -> Act.of(String):
148 match e:
149 case Eff{+email, slot}:
150 token_checked2(clash(seq, email, api_key), Eff{email, slot}, root, token, api_key, assume_yes, ts, seq)
151
152def add_token4(+root: String, +token: String, +email0: Maybe<&2, String>, +slot0: Maybe<&2, U32>, +assume_yes: Bool, +ts: String, +seq: Json.Json) -> Act.of(String):
153 token_checked(effective(email0, slot0, seq, SBase.looks_like_api_key(token)), root, token, SBase.looks_like_api_key(token), assume_yes, ts, seq)
154
155def add_token3(+root: String, +token: String, +email0: Maybe<&2, String>, +slot0: Maybe<&2, U32>, +assume_yes: Bool) -> Act.of(String):
156 do ActM<String>:
157 SIo.setup_dirs(root)
158 +ts : String <- SIo.now_iso()
159 seq : Json.Json <- SIo.ensure_seq(root, ts)
160 add_token4(root, token, email0, slot0, assume_yes, ts, seq)
161
162def add_token2(+root: String, +token: String, +email0: Maybe<&2, String>, +slot0: Maybe<&2, U32>, +assume_yes: Bool) -> Act.of(String):
163 do ActM<String>:
164 Act.guard(SBase.nonempty(token), 2, "Token cannot be empty")
165 Act.guard(email_ok(email0), 2, "Invalid email format: " ++ SSeq.str_or(email0, ""))
166 Lock.with_lock(String, SBase.lock_path(root), 10000, add_token3(root, token, email0, slot0, assume_yes))
167
168
169
170
171
172
173def add_token(+ctx: SBase.Ctx, +token: String, +email0: Maybe<&2, String>, +slot0: Maybe<&2, U32>, +assume_yes: Bool) -> Act.of(String):
174 add_token2(SBase.root_of(ctx), String.trim(token), email0, slot0, assume_yes)
175
176
177
178def is_some_str(+m: Maybe<&2, String>) -> Bool:
179 Bool.not(is_none(m))
180
181def valid_ident(+seq: Json.Json, +ident: String) -> Bool:
182 SBase.all_digits(ident) || SBase.valid_email(ident) || is_some_str(SSeq.find_alias(seq, ident))
183
184def scalar_text(+m: Maybe<&2, Json.Json>) -> String:
185 match m:
186 case Some{Json.JNum{t}}:
187 t
188 case Some{Json.JStr{s}}:
189 s
190 case _:
191 ""
192
193
194def field_json(+rec: Json.Json, +key: String) -> String:
195 scalar_text(Json.get(key, rec))
196
197def active_is(+seq: Json.Json, +num: String) -> Bool:
198 String.eq(field_json(seq, "activeAccountNumber"), num)
199
200def active_warning(+seq: Json.Json, +num: String, +email: String) -> String:
201 SBase.sel.s(active_is(seq, num), "Warning: Account-" ++ num ++ " (" ++ email ++ ") is currently active\n", "")
202
203def removal3(+root: String, +seq: Json.Json, +num: String, +ts: String, +email: String) -> Act.of(String):
204 do ActM<String>:
205 SIo.archive_slot(root, num, email, ts)
206 SIo.write_seq(root, SSeq.stamp(SSeq.drop_account(seq, num), ts))
207 SIo.log(root, "INFO", "Removed account " ++ num ++ ": " ++ email ++ "; credential kept under credentials/.removed/")
208 return active_warning(seq, num, email) ++ "Removed Account-" ++ num ++ " (" ++ email ++ ")"
209
210def removal2(+root: String, +seq: Json.Json, +num: String, +ts: String, +rec: Maybe<&2, Json.Json>) -> Act.of(String):
211 match rec:
212 case None{}:
213 Act.fail(String, 3, "Account-" ++ num ++ " does not exist")
214 case Some{r}:
215 removal3(root, seq, num, ts, SSeq.field_str(r, "email"))
216
217def removal(+root: String, +seq: Json.Json, +num: String, +ts: String) -> Act.of(String):
218 removal2(root, seq, num, ts, SSeq.slot_record(seq, num))
219
220def remove_resolved(+root: String, +seq: Json.Json, +ident: String, +ts: String, +r: SSeq.Resolved) -> Act.of(String):
221 match r:
222 case SSeq.RNone{}:
223 Act.fail(String, 3, "No account found with identifier: " ++ ident)
224 case SSeq.RAmb{msg}:
225 Act.fail(String, 1, msg)
226 case SSeq.RSlot{n}:
227 removal(root, seq, n, ts)
228
229def remove3(+root: String, +ident: String, +ts: String, +seq: Json.Json) -> Act.of(String):
230 do ActM<String>:
231 Act.guard(valid_ident(seq, ident), 2, "Invalid account identifier: " ++ ident)
232 remove_resolved(root, seq, ident, ts, SSeq.resolve(seq, ident))
233
234def remove4(+root: String, +ident: String, +m: Maybe<&2, Json.Json>) -> Act.of(String):
235 match m:
236 case None{}:
237 Act.fail(String, 1, "No accounts are managed yet")
238 case Some{seq}:
239 do ActM<String>:
240 ts : String <- SIo.now_iso()
241 remove3(root, ident, SBase.compact_ts(ts), seq)
242
243def remove2(+root: String, +ident: String) -> Act.of(String):
244 do ActM<String>:
245 SIo.setup_dirs(root)
246 m : Maybe<&2, Json.Json> <- SIo.read_seq(root)
247 remove4(root, ident, m)
248
249
250
251
252
253
254
255def remove(+ctx: SBase.Ctx, +ident: String) -> Act.of(String):
256 do ActM<String>:
257 Lock.with_lock(String, SBase.lock_path(SBase.root_of(ctx)), 10000, remove2(SBase.root_of(ctx), ident))
258
259def read_resolved(+root: String, +r: SSeq.Resolved, +seq: Json.Json, +ident: String) -> Act.of(String):
260 match r:
261 case SSeq.RNone{}:
262 Act.fail(String, 3, "No account found with identifier: " ++ ident)
263 case SSeq.RAmb{msg}:
264 Act.fail(String, 1, msg)
265 case SSeq.RSlot{+n}:
266 SIo.read_cred(root, n, SIo.slot_email(seq, n))
267
268def read_slot2(+root: String, +ident: String, +m: Maybe<&2, Json.Json>) -> Act.of(String):
269 match m:
270 case None{}:
271 Act.fail(String, 1, "No accounts are managed yet")
272 case Some{seq}:
273 read_resolved(root, SSeq.resolve(seq, ident), seq, ident)
274
275
276
277def read_slot(+ctx: SBase.Ctx, +ident: String) -> Act.of(String):
278 do ActM<String>:
279 m : Maybe<&2, Json.Json> <- SIo.read_seq(SBase.root_of(ctx))
280 read_slot2(SBase.root_of(ctx), ident, m)
281
282def write_slot2(+root: String, +creds: String, +num: String, +m: Maybe<&2, Json.Json>) -> Act.of(Unit):
283 match m:
284 case None{}:
285 Act.fail(Unit, 1, "No accounts are managed yet")
286 case Some{seq}:
287 SIo.write_cred(root, num, SIo.slot_email(seq, num), creds)
288
289
290def write_slot(+ctx: SBase.Ctx, +num: String, +creds: String) -> Act.of(Unit):
291 do ActM<Unit>:
292 m : Maybe<&2, Json.Json> <- SIo.read_seq(SBase.root_of(ctx))
293 write_slot2(SBase.root_of(ctx), creds, num, m)