Initial commit
Seto Elkahfi committed
Jul 16, 2026 at 20:48 UTC
f34c8067f24de05b873258fc11624d36be5b6c47
21 files changed
+3851
.gitignore
new
+1
@@ -0,0 +1 @@
1
+/target
Cargo.lock
new
+1592
@@ -0,0 +1,1592 @@
1
+# This file is automatically @generated by Cargo.
2
+# It is not intended for manual editing.
3
+version = 4
4
+
5
+[[package]]
6
+name = "anstream"
7
+version = "1.0.0"
8
+source = "registry+https://github.com/rust-lang/crates.io-index"
9
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
10
+dependencies = [
11
+ "anstyle",
12
+ "anstyle-parse",
13
+ "anstyle-query",
14
+ "anstyle-wincon",
15
+ "colorchoice",
16
+ "is_terminal_polyfill",
17
+ "utf8parse",
18
+]
19
+
20
+[[package]]
21
+name = "anstyle"
22
+version = "1.0.14"
23
+source = "registry+https://github.com/rust-lang/crates.io-index"
24
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
25
+
26
+[[package]]
27
+name = "anstyle-parse"
28
+version = "1.0.0"
29
+source = "registry+https://github.com/rust-lang/crates.io-index"
30
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
31
+dependencies = [
32
+ "utf8parse",
33
+]
34
+
35
+[[package]]
36
+name = "anstyle-query"
37
+version = "1.1.5"
38
+source = "registry+https://github.com/rust-lang/crates.io-index"
39
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
40
+dependencies = [
41
+ "windows-sys 0.61.2",
42
+]
43
+
44
+[[package]]
45
+name = "anstyle-wincon"
46
+version = "3.0.11"
47
+source = "registry+https://github.com/rust-lang/crates.io-index"
48
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
49
+dependencies = [
50
+ "anstyle",
51
+ "once_cell_polyfill",
52
+ "windows-sys 0.61.2",
53
+]
54
+
55
+[[package]]
56
+name = "anyhow"
57
+version = "1.0.103"
58
+source = "registry+https://github.com/rust-lang/crates.io-index"
59
+checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
60
+
61
+[[package]]
62
+name = "atomic-waker"
63
+version = "1.1.2"
64
+source = "registry+https://github.com/rust-lang/crates.io-index"
65
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
66
+
67
+[[package]]
68
+name = "autocfg"
69
+version = "1.5.1"
70
+source = "registry+https://github.com/rust-lang/crates.io-index"
71
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
72
+
73
+[[package]]
74
+name = "base64"
75
+version = "0.22.1"
76
+source = "registry+https://github.com/rust-lang/crates.io-index"
77
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
78
+
79
+[[package]]
80
+name = "bitflags"
81
+version = "2.13.1"
82
+source = "registry+https://github.com/rust-lang/crates.io-index"
83
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
84
+
85
+[[package]]
86
+name = "bumpalo"
87
+version = "3.20.3"
88
+source = "registry+https://github.com/rust-lang/crates.io-index"
89
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
90
+
91
+[[package]]
92
+name = "bytes"
93
+version = "1.12.1"
94
+source = "registry+https://github.com/rust-lang/crates.io-index"
95
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
96
+
97
+[[package]]
98
+name = "cc"
99
+version = "1.2.67"
100
+source = "registry+https://github.com/rust-lang/crates.io-index"
101
+checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
102
+dependencies = [
103
+ "find-msvc-tools",
104
+ "shlex",
105
+]
106
+
107
+[[package]]
108
+name = "cfg-if"
109
+version = "1.0.4"
110
+source = "registry+https://github.com/rust-lang/crates.io-index"
111
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
112
+
113
+[[package]]
114
+name = "cfg_aliases"
115
+version = "0.2.1"
116
+source = "registry+https://github.com/rust-lang/crates.io-index"
117
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
118
+
119
+[[package]]
120
+name = "chacha20"
121
+version = "0.10.1"
122
+source = "registry+https://github.com/rust-lang/crates.io-index"
123
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
124
+dependencies = [
125
+ "cfg-if",
126
+ "cpufeatures",
127
+ "rand_core",
128
+]
129
+
130
+[[package]]
131
+name = "clap"
132
+version = "4.6.2"
133
+source = "registry+https://github.com/rust-lang/crates.io-index"
134
+checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
135
+dependencies = [
136
+ "clap_builder",
137
+ "clap_derive",
138
+]
139
+
140
+[[package]]
141
+name = "clap_builder"
142
+version = "4.6.2"
143
+source = "registry+https://github.com/rust-lang/crates.io-index"
144
+checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
145
+dependencies = [
146
+ "anstream",
147
+ "anstyle",
148
+ "clap_lex",
149
+ "strsim",
150
+]
151
+
152
+[[package]]
153
+name = "clap_derive"
154
+version = "4.6.1"
155
+source = "registry+https://github.com/rust-lang/crates.io-index"
156
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
157
+dependencies = [
158
+ "heck",
159
+ "proc-macro2",
160
+ "quote",
161
+ "syn",
162
+]
163
+
164
+[[package]]
165
+name = "clap_lex"
166
+version = "1.1.0"
167
+source = "registry+https://github.com/rust-lang/crates.io-index"
168
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
169
+
170
+[[package]]
171
+name = "colorchoice"
172
+version = "1.0.5"
173
+source = "registry+https://github.com/rust-lang/crates.io-index"
174
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
175
+
176
+[[package]]
177
+name = "core-foundation"
178
+version = "0.10.1"
179
+source = "registry+https://github.com/rust-lang/crates.io-index"
180
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
181
+dependencies = [
182
+ "core-foundation-sys",
183
+ "libc",
184
+]
185
+
186
+[[package]]
187
+name = "core-foundation-sys"
188
+version = "0.8.7"
189
+source = "registry+https://github.com/rust-lang/crates.io-index"
190
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
191
+
192
+[[package]]
193
+name = "cpufeatures"
194
+version = "0.3.0"
195
+source = "registry+https://github.com/rust-lang/crates.io-index"
196
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
197
+dependencies = [
198
+ "libc",
199
+]
200
+
201
+[[package]]
202
+name = "deranged"
203
+version = "0.5.8"
204
+source = "registry+https://github.com/rust-lang/crates.io-index"
205
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
206
+
207
+[[package]]
208
+name = "displaydoc"
209
+version = "0.2.6"
210
+source = "registry+https://github.com/rust-lang/crates.io-index"
211
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
212
+dependencies = [
213
+ "proc-macro2",
214
+ "quote",
215
+ "syn",
216
+]
217
+
218
+[[package]]
219
+name = "find-msvc-tools"
220
+version = "0.1.9"
221
+source = "registry+https://github.com/rust-lang/crates.io-index"
222
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
223
+
224
+[[package]]
225
+name = "form_urlencoded"
226
+version = "1.2.2"
227
+source = "registry+https://github.com/rust-lang/crates.io-index"
228
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
229
+dependencies = [
230
+ "percent-encoding",
231
+]
232
+
233
+[[package]]
234
+name = "futures-channel"
235
+version = "0.3.32"
236
+source = "registry+https://github.com/rust-lang/crates.io-index"
237
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
238
+dependencies = [
239
+ "futures-core",
240
+]
241
+
242
+[[package]]
243
+name = "futures-core"
244
+version = "0.3.32"
245
+source = "registry+https://github.com/rust-lang/crates.io-index"
246
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
247
+
248
+[[package]]
249
+name = "futures-task"
250
+version = "0.3.32"
251
+source = "registry+https://github.com/rust-lang/crates.io-index"
252
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
253
+
254
+[[package]]
255
+name = "futures-util"
256
+version = "0.3.32"
257
+source = "registry+https://github.com/rust-lang/crates.io-index"
258
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
259
+dependencies = [
260
+ "futures-core",
261
+ "futures-task",
262
+ "pin-project-lite",
263
+ "slab",
264
+]
265
+
266
+[[package]]
267
+name = "getrandom"
268
+version = "0.2.17"
269
+source = "registry+https://github.com/rust-lang/crates.io-index"
270
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
271
+dependencies = [
272
+ "cfg-if",
273
+ "js-sys",
274
+ "libc",
275
+ "wasi",
276
+ "wasm-bindgen",
277
+]
278
+
279
+[[package]]
280
+name = "getrandom"
281
+version = "0.4.3"
282
+source = "registry+https://github.com/rust-lang/crates.io-index"
283
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
284
+dependencies = [
285
+ "cfg-if",
286
+ "js-sys",
287
+ "libc",
288
+ "r-efi",
289
+ "rand_core",
290
+ "wasm-bindgen",
291
+]
292
+
293
+[[package]]
294
+name = "heck"
295
+version = "0.5.0"
296
+source = "registry+https://github.com/rust-lang/crates.io-index"
297
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
298
+
299
+[[package]]
300
+name = "http"
301
+version = "1.4.2"
302
+source = "registry+https://github.com/rust-lang/crates.io-index"
303
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
304
+dependencies = [
305
+ "bytes",
306
+ "itoa",
307
+]
308
+
309
+[[package]]
310
+name = "http-body"
311
+version = "1.1.0"
312
+source = "registry+https://github.com/rust-lang/crates.io-index"
313
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
314
+dependencies = [
315
+ "bytes",
316
+ "http",
317
+]
318
+
319
+[[package]]
320
+name = "http-body-util"
321
+version = "0.1.4"
322
+source = "registry+https://github.com/rust-lang/crates.io-index"
323
+checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
324
+dependencies = [
325
+ "bytes",
326
+ "futures-core",
327
+ "http",
328
+ "http-body",
329
+ "pin-project-lite",
330
+]
331
+
332
+[[package]]
333
+name = "httparse"
334
+version = "1.10.1"
335
+source = "registry+https://github.com/rust-lang/crates.io-index"
336
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
337
+
338
+[[package]]
339
+name = "hyper"
340
+version = "1.10.1"
341
+source = "registry+https://github.com/rust-lang/crates.io-index"
342
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
343
+dependencies = [
344
+ "atomic-waker",
345
+ "bytes",
346
+ "futures-channel",
347
+ "futures-core",
348
+ "http",
349
+ "http-body",
350
+ "httparse",
351
+ "itoa",
352
+ "pin-project-lite",
353
+ "smallvec",
354
+ "tokio",
355
+ "want",
356
+]
357
+
358
+[[package]]
359
+name = "hyper-rustls"
360
+version = "0.27.9"
361
+source = "registry+https://github.com/rust-lang/crates.io-index"
362
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
363
+dependencies = [
364
+ "http",
365
+ "hyper",
366
+ "hyper-util",
367
+ "rustls",
368
+ "rustls-native-certs",
369
+ "tokio",
370
+ "tokio-rustls",
371
+ "tower-service",
372
+]
373
+
374
+[[package]]
375
+name = "hyper-util"
376
+version = "0.1.20"
377
+source = "registry+https://github.com/rust-lang/crates.io-index"
378
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
379
+dependencies = [
380
+ "base64",
381
+ "bytes",
382
+ "futures-channel",
383
+ "futures-util",
384
+ "http",
385
+ "http-body",
386
+ "hyper",
387
+ "ipnet",
388
+ "libc",
389
+ "percent-encoding",
390
+ "pin-project-lite",
391
+ "socket2",
392
+ "tokio",
393
+ "tower-service",
394
+ "tracing",
395
+]
396
+
397
+[[package]]
398
+name = "icu_collections"
399
+version = "2.2.0"
400
+source = "registry+https://github.com/rust-lang/crates.io-index"
401
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
402
+dependencies = [
403
+ "displaydoc",
404
+ "potential_utf",
405
+ "utf8_iter",
406
+ "yoke",
407
+ "zerofrom",
408
+ "zerovec",
409
+]
410
+
411
+[[package]]
412
+name = "icu_locale_core"
413
+version = "2.2.0"
414
+source = "registry+https://github.com/rust-lang/crates.io-index"
415
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
416
+dependencies = [
417
+ "displaydoc",
418
+ "litemap",
419
+ "tinystr",
420
+ "writeable",
421
+ "zerovec",
422
+]
423
+
424
+[[package]]
425
+name = "icu_normalizer"
426
+version = "2.2.0"
427
+source = "registry+https://github.com/rust-lang/crates.io-index"
428
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
429
+dependencies = [
430
+ "icu_collections",
431
+ "icu_normalizer_data",
432
+ "icu_properties",
433
+ "icu_provider",
434
+ "smallvec",
435
+ "zerovec",
436
+]
437
+
438
+[[package]]
439
+name = "icu_normalizer_data"
440
+version = "2.2.0"
441
+source = "registry+https://github.com/rust-lang/crates.io-index"
442
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
443
+
444
+[[package]]
445
+name = "icu_properties"
446
+version = "2.2.0"
447
+source = "registry+https://github.com/rust-lang/crates.io-index"
448
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
449
+dependencies = [
450
+ "icu_collections",
451
+ "icu_locale_core",
452
+ "icu_properties_data",
453
+ "icu_provider",
454
+ "zerotrie",
455
+ "zerovec",
456
+]
457
+
458
+[[package]]
459
+name = "icu_properties_data"
460
+version = "2.2.0"
461
+source = "registry+https://github.com/rust-lang/crates.io-index"
462
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
463
+
464
+[[package]]
465
+name = "icu_provider"
466
+version = "2.2.0"
467
+source = "registry+https://github.com/rust-lang/crates.io-index"
468
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
469
+dependencies = [
470
+ "displaydoc",
471
+ "icu_locale_core",
472
+ "writeable",
473
+ "yoke",
474
+ "zerofrom",
475
+ "zerotrie",
476
+ "zerovec",
477
+]
478
+
479
+[[package]]
480
+name = "idna"
481
+version = "1.1.0"
482
+source = "registry+https://github.com/rust-lang/crates.io-index"
483
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
484
+dependencies = [
485
+ "idna_adapter",
486
+ "smallvec",
487
+ "utf8_iter",
488
+]
489
+
490
+[[package]]
491
+name = "idna_adapter"
492
+version = "1.2.2"
493
+source = "registry+https://github.com/rust-lang/crates.io-index"
494
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
495
+dependencies = [
496
+ "icu_normalizer",
497
+ "icu_properties",
498
+]
499
+
500
+[[package]]
501
+name = "ipnet"
502
+version = "2.12.0"
503
+source = "registry+https://github.com/rust-lang/crates.io-index"
504
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
505
+
506
+[[package]]
507
+name = "is_terminal_polyfill"
508
+version = "1.70.2"
509
+source = "registry+https://github.com/rust-lang/crates.io-index"
510
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
511
+
512
+[[package]]
513
+name = "itoa"
514
+version = "1.0.18"
515
+source = "registry+https://github.com/rust-lang/crates.io-index"
516
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
517
+
518
+[[package]]
519
+name = "js-sys"
520
+version = "0.3.103"
521
+source = "registry+https://github.com/rust-lang/crates.io-index"
522
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
523
+dependencies = [
524
+ "cfg-if",
525
+ "futures-util",
526
+ "wasm-bindgen",
527
+]
528
+
529
+[[package]]
530
+name = "jsonwebtoken"
531
+version = "9.3.1"
532
+source = "registry+https://github.com/rust-lang/crates.io-index"
533
+checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
534
+dependencies = [
535
+ "base64",
536
+ "js-sys",
537
+ "pem",
538
+ "ring",
539
+ "serde",
540
+ "serde_json",
541
+ "simple_asn1",
542
+]
543
+
544
+[[package]]
545
+name = "libc"
546
+version = "0.2.186"
547
+source = "registry+https://github.com/rust-lang/crates.io-index"
548
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
549
+
550
+[[package]]
551
+name = "litemap"
552
+version = "0.8.2"
553
+source = "registry+https://github.com/rust-lang/crates.io-index"
554
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
555
+
556
+[[package]]
557
+name = "log"
558
+version = "0.4.33"
559
+source = "registry+https://github.com/rust-lang/crates.io-index"
560
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
561
+
562
+[[package]]
563
+name = "lru-slab"
564
+version = "0.1.2"
565
+source = "registry+https://github.com/rust-lang/crates.io-index"
566
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
567
+
568
+[[package]]
569
+name = "md5"
570
+version = "0.7.0"
571
+source = "registry+https://github.com/rust-lang/crates.io-index"
572
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
573
+
574
+[[package]]
575
+name = "memchr"
576
+version = "2.8.3"
577
+source = "registry+https://github.com/rust-lang/crates.io-index"
578
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
579
+
580
+[[package]]
581
+name = "mio"
582
+version = "1.2.2"
583
+source = "registry+https://github.com/rust-lang/crates.io-index"
584
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
585
+dependencies = [
586
+ "libc",
587
+ "wasi",
588
+ "windows-sys 0.61.2",
589
+]
590
+
591
+[[package]]
592
+name = "num-bigint"
593
+version = "0.4.8"
594
+source = "registry+https://github.com/rust-lang/crates.io-index"
595
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
596
+dependencies = [
597
+ "num-integer",
598
+ "num-traits",
599
+]
600
+
601
+[[package]]
602
+name = "num-conv"
603
+version = "0.2.2"
604
+source = "registry+https://github.com/rust-lang/crates.io-index"
605
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
606
+
607
+[[package]]
608
+name = "num-integer"
609
+version = "0.1.46"
610
+source = "registry+https://github.com/rust-lang/crates.io-index"
611
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
612
+dependencies = [
613
+ "num-traits",
614
+]
615
+
616
+[[package]]
617
+name = "num-traits"
618
+version = "0.2.19"
619
+source = "registry+https://github.com/rust-lang/crates.io-index"
620
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
621
+dependencies = [
622
+ "autocfg",
623
+]
624
+
625
+[[package]]
626
+name = "once_cell"
627
+version = "1.21.4"
628
+source = "registry+https://github.com/rust-lang/crates.io-index"
629
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
630
+
631
+[[package]]
632
+name = "once_cell_polyfill"
633
+version = "1.70.2"
634
+source = "registry+https://github.com/rust-lang/crates.io-index"
635
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
636
+
637
+[[package]]
638
+name = "openssl-probe"
639
+version = "0.2.1"
640
+source = "registry+https://github.com/rust-lang/crates.io-index"
641
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
642
+
643
+[[package]]
644
+name = "pem"
645
+version = "3.0.6"
646
+source = "registry+https://github.com/rust-lang/crates.io-index"
647
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
648
+dependencies = [
649
+ "base64",
650
+ "serde_core",
651
+]
652
+
653
+[[package]]
654
+name = "percent-encoding"
655
+version = "2.3.2"
656
+source = "registry+https://github.com/rust-lang/crates.io-index"
657
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
658
+
659
+[[package]]
660
+name = "pin-project-lite"
661
+version = "0.2.17"
662
+source = "registry+https://github.com/rust-lang/crates.io-index"
663
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
664
+
665
+[[package]]
666
+name = "potential_utf"
667
+version = "0.1.5"
668
+source = "registry+https://github.com/rust-lang/crates.io-index"
669
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
670
+dependencies = [
671
+ "zerovec",
672
+]
673
+
674
+[[package]]
675
+name = "powerfmt"
676
+version = "0.2.0"
677
+source = "registry+https://github.com/rust-lang/crates.io-index"
678
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
679
+
680
+[[package]]
681
+name = "proc-macro2"
682
+version = "1.0.106"
683
+source = "registry+https://github.com/rust-lang/crates.io-index"
684
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
685
+dependencies = [
686
+ "unicode-ident",
687
+]
688
+
689
+[[package]]
690
+name = "quinn"
691
+version = "0.11.11"
692
+source = "registry+https://github.com/rust-lang/crates.io-index"
693
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
694
+dependencies = [
695
+ "bytes",
696
+ "cfg_aliases",
697
+ "pin-project-lite",
698
+ "quinn-proto",
699
+ "quinn-udp",
700
+ "rustc-hash",
701
+ "rustls",
702
+ "socket2",
703
+ "thiserror",
704
+ "tokio",
705
+ "tracing",
706
+ "web-time",
707
+]
708
+
709
+[[package]]
710
+name = "quinn-proto"
711
+version = "0.11.16"
712
+source = "registry+https://github.com/rust-lang/crates.io-index"
713
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
714
+dependencies = [
715
+ "bytes",
716
+ "getrandom 0.4.3",
717
+ "lru-slab",
718
+ "rand",
719
+ "rand_pcg",
720
+ "ring",
721
+ "rustc-hash",
722
+ "rustls",
723
+ "rustls-pki-types",
724
+ "slab",
725
+ "thiserror",
726
+ "tinyvec",
727
+ "tracing",
728
+ "web-time",
729
+]
730
+
731
+[[package]]
732
+name = "quinn-udp"
733
+version = "0.5.15"
734
+source = "registry+https://github.com/rust-lang/crates.io-index"
735
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
736
+dependencies = [
737
+ "cfg_aliases",
738
+ "libc",
739
+ "once_cell",
740
+ "socket2",
741
+ "tracing",
742
+ "windows-sys 0.61.2",
743
+]
744
+
745
+[[package]]
746
+name = "quote"
747
+version = "1.0.46"
748
+source = "registry+https://github.com/rust-lang/crates.io-index"
749
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
750
+dependencies = [
751
+ "proc-macro2",
752
+]
753
+
754
+[[package]]
755
+name = "r-efi"
756
+version = "6.0.0"
757
+source = "registry+https://github.com/rust-lang/crates.io-index"
758
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
759
+
760
+[[package]]
761
+name = "rand"
762
+version = "0.10.2"
763
+source = "registry+https://github.com/rust-lang/crates.io-index"
764
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
765
+dependencies = [
766
+ "chacha20",
767
+ "getrandom 0.4.3",
768
+ "rand_core",
769
+]
770
+
771
+[[package]]
772
+name = "rand_core"
773
+version = "0.10.1"
774
+source = "registry+https://github.com/rust-lang/crates.io-index"
775
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
776
+
777
+[[package]]
778
+name = "rand_pcg"
779
+version = "0.10.2"
780
+source = "registry+https://github.com/rust-lang/crates.io-index"
781
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
782
+dependencies = [
783
+ "rand_core",
784
+]
785
+
786
+[[package]]
787
+name = "reqwest"
788
+version = "0.12.28"
789
+source = "registry+https://github.com/rust-lang/crates.io-index"
790
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
791
+dependencies = [
792
+ "base64",
793
+ "bytes",
794
+ "futures-core",
795
+ "http",
796
+ "http-body",
797
+ "http-body-util",
798
+ "hyper",
799
+ "hyper-rustls",
800
+ "hyper-util",
801
+ "js-sys",
802
+ "log",
803
+ "percent-encoding",
804
+ "pin-project-lite",
805
+ "quinn",
806
+ "rustls",
807
+ "rustls-native-certs",
808
+ "rustls-pki-types",
809
+ "serde",
810
+ "serde_json",
811
+ "serde_urlencoded",
812
+ "sync_wrapper",
813
+ "tokio",
814
+ "tokio-rustls",
815
+ "tower",
816
+ "tower-http",
817
+ "tower-service",
818
+ "url",
819
+ "wasm-bindgen",
820
+ "wasm-bindgen-futures",
821
+ "web-sys",
822
+]
823
+
824
+[[package]]
825
+name = "ring"
826
+version = "0.17.14"
827
+source = "registry+https://github.com/rust-lang/crates.io-index"
828
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
829
+dependencies = [
830
+ "cc",
831
+ "cfg-if",
832
+ "getrandom 0.2.17",
833
+ "libc",
834
+ "untrusted",
835
+ "windows-sys 0.52.0",
836
+]
837
+
838
+[[package]]
839
+name = "rustc-hash"
840
+version = "2.1.3"
841
+source = "registry+https://github.com/rust-lang/crates.io-index"
842
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
843
+
844
+[[package]]
845
+name = "rustls"
846
+version = "0.23.42"
847
+source = "registry+https://github.com/rust-lang/crates.io-index"
848
+checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
849
+dependencies = [
850
+ "once_cell",
851
+ "ring",
852
+ "rustls-pki-types",
853
+ "rustls-webpki",
854
+ "subtle",
855
+ "zeroize",
856
+]
857
+
858
+[[package]]
859
+name = "rustls-native-certs"
860
+version = "0.8.4"
861
+source = "registry+https://github.com/rust-lang/crates.io-index"
862
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
863
+dependencies = [
864
+ "openssl-probe",
865
+ "rustls-pki-types",
866
+ "schannel",
867
+ "security-framework",
868
+]
869
+
870
+[[package]]
871
+name = "rustls-pki-types"
872
+version = "1.15.0"
873
+source = "registry+https://github.com/rust-lang/crates.io-index"
874
+checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
875
+dependencies = [
876
+ "web-time",
877
+ "zeroize",
878
+]
879
+
880
+[[package]]
881
+name = "rustls-webpki"
882
+version = "0.103.13"
883
+source = "registry+https://github.com/rust-lang/crates.io-index"
884
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
885
+dependencies = [
886
+ "ring",
887
+ "rustls-pki-types",
888
+ "untrusted",
889
+]
890
+
891
+[[package]]
892
+name = "rustversion"
893
+version = "1.0.23"
894
+source = "registry+https://github.com/rust-lang/crates.io-index"
895
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
896
+
897
+[[package]]
898
+name = "ryu"
899
+version = "1.0.23"
900
+source = "registry+https://github.com/rust-lang/crates.io-index"
901
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
902
+
903
+[[package]]
904
+name = "schannel"
905
+version = "0.1.29"
906
+source = "registry+https://github.com/rust-lang/crates.io-index"
907
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
908
+dependencies = [
909
+ "windows-sys 0.61.2",
910
+]
911
+
912
+[[package]]
913
+name = "security-framework"
914
+version = "3.7.0"
915
+source = "registry+https://github.com/rust-lang/crates.io-index"
916
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
917
+dependencies = [
918
+ "bitflags",
919
+ "core-foundation",
920
+ "core-foundation-sys",
921
+ "libc",
922
+ "security-framework-sys",
923
+]
924
+
925
+[[package]]
926
+name = "security-framework-sys"
927
+version = "2.17.0"
928
+source = "registry+https://github.com/rust-lang/crates.io-index"
929
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
930
+dependencies = [
931
+ "core-foundation-sys",
932
+ "libc",
933
+]
934
+
935
+[[package]]
936
+name = "serde"
937
+version = "1.0.228"
938
+source = "registry+https://github.com/rust-lang/crates.io-index"
939
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
940
+dependencies = [
941
+ "serde_core",
942
+ "serde_derive",
943
+]
944
+
945
+[[package]]
946
+name = "serde_core"
947
+version = "1.0.228"
948
+source = "registry+https://github.com/rust-lang/crates.io-index"
949
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
950
+dependencies = [
951
+ "serde_derive",
952
+]
953
+
954
+[[package]]
955
+name = "serde_derive"
956
+version = "1.0.228"
957
+source = "registry+https://github.com/rust-lang/crates.io-index"
958
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
959
+dependencies = [
960
+ "proc-macro2",
961
+ "quote",
962
+ "syn",
963
+]
964
+
965
+[[package]]
966
+name = "serde_json"
967
+version = "1.0.150"
968
+source = "registry+https://github.com/rust-lang/crates.io-index"
969
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
970
+dependencies = [
971
+ "itoa",
972
+ "memchr",
973
+ "serde",
974
+ "serde_core",
975
+ "zmij",
976
+]
977
+
978
+[[package]]
979
+name = "serde_urlencoded"
980
+version = "0.7.1"
981
+source = "registry+https://github.com/rust-lang/crates.io-index"
982
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
983
+dependencies = [
984
+ "form_urlencoded",
985
+ "itoa",
986
+ "ryu",
987
+ "serde",
988
+]
989
+
990
+[[package]]
991
+name = "shlex"
992
+version = "2.0.1"
993
+source = "registry+https://github.com/rust-lang/crates.io-index"
994
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
995
+
996
+[[package]]
997
+name = "simple_asn1"
998
+version = "0.6.4"
999
+source = "registry+https://github.com/rust-lang/crates.io-index"
1000
+checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
1001
+dependencies = [
1002
+ "num-bigint",
1003
+ "num-traits",
1004
+ "thiserror",
1005
+ "time",
1006
+]
1007
+
1008
+[[package]]
1009
+name = "slab"
1010
+version = "0.4.12"
1011
+source = "registry+https://github.com/rust-lang/crates.io-index"
1012
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1013
+
1014
+[[package]]
1015
+name = "smallvec"
1016
+version = "1.15.2"
1017
+source = "registry+https://github.com/rust-lang/crates.io-index"
1018
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
1019
+
1020
+[[package]]
1021
+name = "smbcloud-ascapi"
1022
+version = "0.1.0"
1023
+dependencies = [
1024
+ "jsonwebtoken",
1025
+ "md5",
1026
+ "reqwest",
1027
+ "serde",
1028
+ "serde_json",
1029
+ "thiserror",
1030
+ "tokio",
1031
+]
1032
+
1033
+[[package]]
1034
+name = "smbcloud-ascapi-cli"
1035
+version = "0.1.0"
1036
+dependencies = [
1037
+ "anyhow",
1038
+ "clap",
1039
+ "serde",
1040
+ "serde_json",
1041
+ "smbcloud-ascapi",
1042
+ "tokio",
1043
+]
1044
+
1045
+[[package]]
1046
+name = "socket2"
1047
+version = "0.6.5"
1048
+source = "registry+https://github.com/rust-lang/crates.io-index"
1049
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
1050
+dependencies = [
1051
+ "libc",
1052
+ "windows-sys 0.61.2",
1053
+]
1054
+
1055
+[[package]]
1056
+name = "stable_deref_trait"
1057
+version = "1.2.1"
1058
+source = "registry+https://github.com/rust-lang/crates.io-index"
1059
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1060
+
1061
+[[package]]
1062
+name = "strsim"
1063
+version = "0.11.1"
1064
+source = "registry+https://github.com/rust-lang/crates.io-index"
1065
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1066
+
1067
+[[package]]
1068
+name = "subtle"
1069
+version = "2.6.1"
1070
+source = "registry+https://github.com/rust-lang/crates.io-index"
1071
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
1072
+
1073
+[[package]]
1074
+name = "syn"
1075
+version = "2.0.119"
1076
+source = "registry+https://github.com/rust-lang/crates.io-index"
1077
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
1078
+dependencies = [
1079
+ "proc-macro2",
1080
+ "quote",
1081
+ "unicode-ident",
1082
+]
1083
+
1084
+[[package]]
1085
+name = "sync_wrapper"
1086
+version = "1.0.2"
1087
+source = "registry+https://github.com/rust-lang/crates.io-index"
1088
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
1089
+dependencies = [
1090
+ "futures-core",
1091
+]
1092
+
1093
+[[package]]
1094
+name = "synstructure"
1095
+version = "0.13.2"
1096
+source = "registry+https://github.com/rust-lang/crates.io-index"
1097
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
1098
+dependencies = [
1099
+ "proc-macro2",
1100
+ "quote",
1101
+ "syn",
1102
+]
1103
+
1104
+[[package]]
1105
+name = "thiserror"
1106
+version = "2.0.18"
1107
+source = "registry+https://github.com/rust-lang/crates.io-index"
1108
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
1109
+dependencies = [
1110
+ "thiserror-impl",
1111
+]
1112
+
1113
+[[package]]
1114
+name = "thiserror-impl"
1115
+version = "2.0.18"
1116
+source = "registry+https://github.com/rust-lang/crates.io-index"
1117
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
1118
+dependencies = [
1119
+ "proc-macro2",
1120
+ "quote",
1121
+ "syn",
1122
+]
1123
+
1124
+[[package]]
1125
+name = "time"
1126
+version = "0.3.53"
1127
+source = "registry+https://github.com/rust-lang/crates.io-index"
1128
+checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
1129
+dependencies = [
1130
+ "deranged",
1131
+ "num-conv",
1132
+ "powerfmt",
1133
+ "serde_core",
1134
+ "time-core",
1135
+ "time-macros",
1136
+]
1137
+
1138
+[[package]]
1139
+name = "time-core"
1140
+version = "0.1.9"
1141
+source = "registry+https://github.com/rust-lang/crates.io-index"
1142
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
1143
+
1144
+[[package]]
1145
+name = "time-macros"
1146
+version = "0.2.31"
1147
+source = "registry+https://github.com/rust-lang/crates.io-index"
1148
+checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
1149
+dependencies = [
1150
+ "num-conv",
1151
+ "time-core",
1152
+]
1153
+
1154
+[[package]]
1155
+name = "tinystr"
1156
+version = "0.8.3"
1157
+source = "registry+https://github.com/rust-lang/crates.io-index"
1158
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
1159
+dependencies = [
1160
+ "displaydoc",
1161
+ "zerovec",
1162
+]
1163
+
1164
+[[package]]
1165
+name = "tinyvec"
1166
+version = "1.12.0"
1167
+source = "registry+https://github.com/rust-lang/crates.io-index"
1168
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
1169
+dependencies = [
1170
+ "tinyvec_macros",
1171
+]
1172
+
1173
+[[package]]
1174
+name = "tinyvec_macros"
1175
+version = "0.1.1"
1176
+source = "registry+https://github.com/rust-lang/crates.io-index"
1177
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1178
+
1179
+[[package]]
1180
+name = "tokio"
1181
+version = "1.52.3"
1182
+source = "registry+https://github.com/rust-lang/crates.io-index"
1183
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
1184
+dependencies = [
1185
+ "bytes",
1186
+ "libc",
1187
+ "mio",
1188
+ "pin-project-lite",
1189
+ "socket2",
1190
+ "tokio-macros",
1191
+ "windows-sys 0.61.2",
1192
+]
1193
+
1194
+[[package]]
1195
+name = "tokio-macros"
1196
+version = "2.7.0"
1197
+source = "registry+https://github.com/rust-lang/crates.io-index"
1198
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
1199
+dependencies = [
1200
+ "proc-macro2",
1201
+ "quote",
1202
+ "syn",
1203
+]
1204
+
1205
+[[package]]
1206
+name = "tokio-rustls"
1207
+version = "0.26.4"
1208
+source = "registry+https://github.com/rust-lang/crates.io-index"
1209
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
1210
+dependencies = [
1211
+ "rustls",
1212
+ "tokio",
1213
+]
1214
+
1215
+[[package]]
1216
+name = "tower"
1217
+version = "0.5.3"
1218
+source = "registry+https://github.com/rust-lang/crates.io-index"
1219
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
1220
+dependencies = [
1221
+ "futures-core",
1222
+ "futures-util",
1223
+ "pin-project-lite",
1224
+ "sync_wrapper",
1225
+ "tokio",
1226
+ "tower-layer",
1227
+ "tower-service",
1228
+]
1229
+
1230
+[[package]]
1231
+name = "tower-http"
1232
+version = "0.6.11"
1233
+source = "registry+https://github.com/rust-lang/crates.io-index"
1234
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
1235
+dependencies = [
1236
+ "bitflags",
1237
+ "bytes",
1238
+ "futures-util",
1239
+ "http",
1240
+ "http-body",
1241
+ "pin-project-lite",
1242
+ "tower",
1243
+ "tower-layer",
1244
+ "tower-service",
1245
+ "url",
1246
+]
1247
+
1248
+[[package]]
1249
+name = "tower-layer"
1250
+version = "0.3.3"
1251
+source = "registry+https://github.com/rust-lang/crates.io-index"
1252
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1253
+
1254
+[[package]]
1255
+name = "tower-service"
1256
+version = "0.3.3"
1257
+source = "registry+https://github.com/rust-lang/crates.io-index"
1258
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
1259
+
1260
+[[package]]
1261
+name = "tracing"
1262
+version = "0.1.44"
1263
+source = "registry+https://github.com/rust-lang/crates.io-index"
1264
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
1265
+dependencies = [
1266
+ "pin-project-lite",
1267
+ "tracing-core",
1268
+]
1269
+
1270
+[[package]]
1271
+name = "tracing-core"
1272
+version = "0.1.36"
1273
+source = "registry+https://github.com/rust-lang/crates.io-index"
1274
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
1275
+dependencies = [
1276
+ "once_cell",
1277
+]
1278
+
1279
+[[package]]
1280
+name = "try-lock"
1281
+version = "0.2.5"
1282
+source = "registry+https://github.com/rust-lang/crates.io-index"
1283
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
1284
+
1285
+[[package]]
1286
+name = "unicode-ident"
1287
+version = "1.0.24"
1288
+source = "registry+https://github.com/rust-lang/crates.io-index"
1289
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
1290
+
1291
+[[package]]
1292
+name = "untrusted"
1293
+version = "0.9.0"
1294
+source = "registry+https://github.com/rust-lang/crates.io-index"
1295
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1296
+
1297
+[[package]]
1298
+name = "url"
1299
+version = "2.5.8"
1300
+source = "registry+https://github.com/rust-lang/crates.io-index"
1301
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
1302
+dependencies = [
1303
+ "form_urlencoded",
1304
+ "idna",
1305
+ "percent-encoding",
1306
+ "serde",
1307
+]
1308
+
1309
+[[package]]
1310
+name = "utf8_iter"
1311
+version = "1.0.4"
1312
+source = "registry+https://github.com/rust-lang/crates.io-index"
1313
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
1314
+
1315
+[[package]]
1316
+name = "utf8parse"
1317
+version = "0.2.2"
1318
+source = "registry+https://github.com/rust-lang/crates.io-index"
1319
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1320
+
1321
+[[package]]
1322
+name = "want"
1323
+version = "0.3.1"
1324
+source = "registry+https://github.com/rust-lang/crates.io-index"
1325
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
1326
+dependencies = [
1327
+ "try-lock",
1328
+]
1329
+
1330
+[[package]]
1331
+name = "wasi"
1332
+version = "0.11.1+wasi-snapshot-preview1"
1333
+source = "registry+https://github.com/rust-lang/crates.io-index"
1334
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1335
+
1336
+[[package]]
1337
+name = "wasm-bindgen"
1338
+version = "0.2.126"
1339
+source = "registry+https://github.com/rust-lang/crates.io-index"
1340
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
1341
+dependencies = [
1342
+ "cfg-if",
1343
+ "once_cell",
1344
+ "rustversion",
1345
+ "wasm-bindgen-macro",
1346
+ "wasm-bindgen-shared",
1347
+]
1348
+
1349
+[[package]]
1350
+name = "wasm-bindgen-futures"
1351
+version = "0.4.76"
1352
+source = "registry+https://github.com/rust-lang/crates.io-index"
1353
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
1354
+dependencies = [
1355
+ "js-sys",
1356
+ "wasm-bindgen",
1357
+]
1358
+
1359
+[[package]]
1360
+name = "wasm-bindgen-macro"
1361
+version = "0.2.126"
1362
+source = "registry+https://github.com/rust-lang/crates.io-index"
1363
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
1364
+dependencies = [
1365
+ "quote",
1366
+ "wasm-bindgen-macro-support",
1367
+]
1368
+
1369
+[[package]]
1370
+name = "wasm-bindgen-macro-support"
1371
+version = "0.2.126"
1372
+source = "registry+https://github.com/rust-lang/crates.io-index"
1373
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
1374
+dependencies = [
1375
+ "bumpalo",
1376
+ "proc-macro2",
1377
+ "quote",
1378
+ "syn",
1379
+ "wasm-bindgen-shared",
1380
+]
1381
+
1382
+[[package]]
1383
+name = "wasm-bindgen-shared"
1384
+version = "0.2.126"
1385
+source = "registry+https://github.com/rust-lang/crates.io-index"
1386
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
1387
+dependencies = [
1388
+ "unicode-ident",
1389
+]
1390
+
1391
+[[package]]
1392
+name = "web-sys"
1393
+version = "0.3.103"
1394
+source = "registry+https://github.com/rust-lang/crates.io-index"
1395
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
1396
+dependencies = [
1397
+ "js-sys",
1398
+ "wasm-bindgen",
1399
+]
1400
+
1401
+[[package]]
1402
+name = "web-time"
1403
+version = "1.1.0"
1404
+source = "registry+https://github.com/rust-lang/crates.io-index"
1405
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
1406
+dependencies = [
1407
+ "js-sys",
1408
+ "wasm-bindgen",
1409
+]
1410
+
1411
+[[package]]
1412
+name = "windows-link"
1413
+version = "0.2.1"
1414
+source = "registry+https://github.com/rust-lang/crates.io-index"
1415
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1416
+
1417
+[[package]]
1418
+name = "windows-sys"
1419
+version = "0.52.0"
1420
+source = "registry+https://github.com/rust-lang/crates.io-index"
1421
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
1422
+dependencies = [
1423
+ "windows-targets",
1424
+]
1425
+
1426
+[[package]]
1427
+name = "windows-sys"
1428
+version = "0.61.2"
1429
+source = "registry+https://github.com/rust-lang/crates.io-index"
1430
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1431
+dependencies = [
1432
+ "windows-link",
1433
+]
1434
+
1435
+[[package]]
1436
+name = "windows-targets"
1437
+version = "0.52.6"
1438
+source = "registry+https://github.com/rust-lang/crates.io-index"
1439
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1440
+dependencies = [
1441
+ "windows_aarch64_gnullvm",
1442
+ "windows_aarch64_msvc",
1443
+ "windows_i686_gnu",
1444
+ "windows_i686_gnullvm",
1445
+ "windows_i686_msvc",
1446
+ "windows_x86_64_gnu",
1447
+ "windows_x86_64_gnullvm",
1448
+ "windows_x86_64_msvc",
1449
+]
1450
+
1451
+[[package]]
1452
+name = "windows_aarch64_gnullvm"
1453
+version = "0.52.6"
1454
+source = "registry+https://github.com/rust-lang/crates.io-index"
1455
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1456
+
1457
+[[package]]
1458
+name = "windows_aarch64_msvc"
1459
+version = "0.52.6"
1460
+source = "registry+https://github.com/rust-lang/crates.io-index"
1461
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1462
+
1463
+[[package]]
1464
+name = "windows_i686_gnu"
1465
+version = "0.52.6"
1466
+source = "registry+https://github.com/rust-lang/crates.io-index"
1467
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1468
+
1469
+[[package]]
1470
+name = "windows_i686_gnullvm"
1471
+version = "0.52.6"
1472
+source = "registry+https://github.com/rust-lang/crates.io-index"
1473
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1474
+
1475
+[[package]]
1476
+name = "windows_i686_msvc"
1477
+version = "0.52.6"
1478
+source = "registry+https://github.com/rust-lang/crates.io-index"
1479
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1480
+
1481
+[[package]]
1482
+name = "windows_x86_64_gnu"
1483
+version = "0.52.6"
1484
+source = "registry+https://github.com/rust-lang/crates.io-index"
1485
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1486
+
1487
+[[package]]
1488
+name = "windows_x86_64_gnullvm"
1489
+version = "0.52.6"
1490
+source = "registry+https://github.com/rust-lang/crates.io-index"
1491
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1492
+
1493
+[[package]]
1494
+name = "windows_x86_64_msvc"
1495
+version = "0.52.6"
1496
+source = "registry+https://github.com/rust-lang/crates.io-index"
1497
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1498
+
1499
+[[package]]
1500
+name = "writeable"
1501
+version = "0.6.3"
1502
+source = "registry+https://github.com/rust-lang/crates.io-index"
1503
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
1504
+
1505
+[[package]]
1506
+name = "yoke"
1507
+version = "0.8.3"
1508
+source = "registry+https://github.com/rust-lang/crates.io-index"
1509
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
1510
+dependencies = [
1511
+ "stable_deref_trait",
1512
+ "yoke-derive",
1513
+ "zerofrom",
1514
+]
1515
+
1516
+[[package]]
1517
+name = "yoke-derive"
1518
+version = "0.8.2"
1519
+source = "registry+https://github.com/rust-lang/crates.io-index"
1520
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
1521
+dependencies = [
1522
+ "proc-macro2",
1523
+ "quote",
1524
+ "syn",
1525
+ "synstructure",
1526
+]
1527
+
1528
+[[package]]
1529
+name = "zerofrom"
1530
+version = "0.1.8"
1531
+source = "registry+https://github.com/rust-lang/crates.io-index"
1532
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
1533
+dependencies = [
1534
+ "zerofrom-derive",
1535
+]
1536
+
1537
+[[package]]
1538
+name = "zerofrom-derive"
1539
+version = "0.1.7"
1540
+source = "registry+https://github.com/rust-lang/crates.io-index"
1541
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
1542
+dependencies = [
1543
+ "proc-macro2",
1544
+ "quote",
1545
+ "syn",
1546
+ "synstructure",
1547
+]
1548
+
1549
+[[package]]
1550
+name = "zeroize"
1551
+version = "1.9.0"
1552
+source = "registry+https://github.com/rust-lang/crates.io-index"
1553
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
1554
+
1555
+[[package]]
1556
+name = "zerotrie"
1557
+version = "0.2.4"
1558
+source = "registry+https://github.com/rust-lang/crates.io-index"
1559
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
1560
+dependencies = [
1561
+ "displaydoc",
1562
+ "yoke",
1563
+ "zerofrom",
1564
+]
1565
+
1566
+[[package]]
1567
+name = "zerovec"
1568
+version = "0.11.6"
1569
+source = "registry+https://github.com/rust-lang/crates.io-index"
1570
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
1571
+dependencies = [
1572
+ "yoke",
1573
+ "zerofrom",
1574
+ "zerovec-derive",
1575
+]
1576
+
1577
+[[package]]
1578
+name = "zerovec-derive"
1579
+version = "0.11.3"
1580
+source = "registry+https://github.com/rust-lang/crates.io-index"
1581
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
1582
+dependencies = [
1583
+ "proc-macro2",
1584
+ "quote",
1585
+ "syn",
1586
+]
1587
+
1588
+[[package]]
1589
+name = "zmij"
1590
+version = "1.0.23"
1591
+source = "registry+https://github.com/rust-lang/crates.io-index"
1592
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
Cargo.toml
new
+21
@@ -0,0 +1,21 @@
1
+[workspace]
2
+resolver = "2"
3
+members = ["crates/ascapi", "crates/cli"]
4
+
5
+[workspace.package]
6
+version = "0.1.0"
7
+edition = "2021"
8
+authors = ["Seto Elkahfi <seto@smbcloud.xyz>"]
9
+license = "Apache-2.0"
10
+repository = "https://github.com/smbcloudXYZ/smbcloud-ascapi"
11
+
12
+[workspace.dependencies]
13
+smbcloud-ascapi = { version = "0.1.0", path = "crates/ascapi" }
14
+anyhow = "1"
15
+clap = "4"
16
+jsonwebtoken = "9"
17
+reqwest = { version = "0.12", default-features = false }
18
+serde = "1"
19
+serde_json = "1"
20
+thiserror = "2.0"
21
+tokio = "1"
README.md
new
+3
@@ -0,0 +1,3 @@
1
+# smbcloud-ascapi
2
+
3
+Ascapi bolo bolo bolo.
crates/ascapi/Cargo.toml
new
+23
@@ -0,0 +1,23 @@
1
+[package]
2
+name = "smbcloud-ascapi"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+repository.workspace = true
8
+description = "Embeddable client for the App Store Connect API's App Metadata resources (apps, app infos, app store versions, bundle IDs, and their localizations)."
9
+readme = "README.md"
10
+
11
+[lib]
12
+path = "src/ascapi.rs"
13
+
14
+[dependencies]
15
+jsonwebtoken = { workspace = true }
16
+md5 = "0.7"
17
+reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots"] }
18
+serde = { workspace = true, features = ["derive"] }
19
+serde_json = { workspace = true }
20
+thiserror = { workspace = true }
21
+
22
+[dev-dependencies]
23
+tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
crates/ascapi/src/app.rs
new
+74
@@ -0,0 +1,74 @@
1
+//! `GET /v1/apps`, `GET`/`PATCH /v1/apps/{id}`. Note there is no
2
+//! `POST /v1/apps` — App Store Connect has no "create an app" endpoint.
3
+//! An app record comes into existence the first time you register its
4
+//! bundle ID (see [`crate::bundle_id`]) and give it a first
5
+//! [`crate::app_store_version`]; the App Store Connect UI (or, per
6
+//! platform, a build upload) is what actually provisions the `App` row.
7
+//! This module only reads and updates one that already exists.
8
+
9
+use crate::client::Client;
10
+use crate::error::Result;
11
+use crate::jsonapi::{Document, ListDocument, Resource, UpdateBody, UpdateData};
12
+use reqwest::Method;
13
+use serde::{Deserialize, Serialize};
14
+
15
+pub const RESOURCE_TYPE: &str = "apps";
16
+
17
+#[derive(Debug, Clone, Serialize, Deserialize)]
18
+#[serde(rename_all = "camelCase")]
19
+pub struct AppAttributes {
20
+ pub name: Option<String>,
21
+ pub bundle_id: Option<String>,
22
+ pub sku: Option<String>,
23
+ pub primary_locale: Option<String>,
24
+ pub is_or_ever_was_made_for_kids: Option<bool>,
25
+ pub content_rights_declaration: Option<String>,
26
+}
27
+
28
+pub type App = Resource<AppAttributes>;
29
+
30
+#[derive(Debug, Clone, Default, Serialize)]
31
+#[serde(rename_all = "camelCase")]
32
+pub struct AppUpdateAttributes {
33
+ #[serde(skip_serializing_if = "Option::is_none")]
34
+ pub primary_locale: Option<String>,
35
+ #[serde(skip_serializing_if = "Option::is_none")]
36
+ pub content_rights_declaration: Option<String>,
37
+}
38
+
39
+impl Client {
40
+ /// `GET /v1/apps`, optionally narrowed with `filter[bundleId]` — the
41
+ /// usual way to resolve an app's ASC id from the bundle identifier
42
+ /// already baked into an Xcode project.
43
+ pub async fn list_apps(&self, filter_bundle_id: Option<&str>) -> Result<Vec<App>> {
44
+ let mut query = Vec::new();
45
+ if let Some(bundle_id) = filter_bundle_id {
46
+ query.push(("filter[bundleId]", bundle_id));
47
+ }
48
+ let doc: ListDocument<AppAttributes> = self
49
+ .request(Method::GET, "/v1/apps", &query, None::<&()>)
50
+ .await?;
51
+ Ok(doc.data)
52
+ }
53
+
54
+ pub async fn get_app(&self, app_id: &str) -> Result<App> {
55
+ let path = format!("/v1/apps/{app_id}");
56
+ let doc: Document<AppAttributes> =
57
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
58
+ Ok(doc.data)
59
+ }
60
+
61
+ pub async fn update_app(&self, app_id: &str, attributes: AppUpdateAttributes) -> Result<App> {
62
+ let path = format!("/v1/apps/{app_id}");
63
+ let body = UpdateBody {
64
+ data: UpdateData {
65
+ resource_type: RESOURCE_TYPE,
66
+ id: app_id.to_string(),
67
+ attributes,
68
+ },
69
+ };
70
+ let doc: Document<AppAttributes> =
71
+ self.request(Method::PATCH, &path, &[], Some(&body)).await?;
72
+ Ok(doc.data)
73
+ }
74
+}
crates/ascapi/src/app_info.rs
new
+44
@@ -0,0 +1,44 @@
1
+//! An `AppInfo` is the platform-independent, category/age-rating side of an
2
+//! app's metadata; its localized name/subtitle live on
3
+//! [`crate::app_info_localization`]. App Store Connect creates and manages
4
+//! `AppInfo` rows itself (there is no create/delete endpoint) — this module
5
+//! only reads them and updates category/age-rating relationships.
6
+
7
+use crate::client::Client;
8
+use crate::error::Result;
9
+use crate::jsonapi::{Document, ListDocument, Resource};
10
+use reqwest::Method;
11
+use serde::{Deserialize, Serialize};
12
+
13
+pub const RESOURCE_TYPE: &str = "appInfos";
14
+
15
+#[derive(Debug, Clone, Serialize, Deserialize)]
16
+#[serde(rename_all = "camelCase")]
17
+pub struct AppInfoAttributes {
18
+ pub app_store_state: Option<String>,
19
+ pub app_store_age_rating: Option<String>,
20
+ pub brazil_age_rating: Option<String>,
21
+ pub kids_age_band: Option<String>,
22
+}
23
+
24
+pub type AppInfo = Resource<AppInfoAttributes>;
25
+
26
+impl Client {
27
+ /// `GET /v1/apps/{app_id}/appInfos`. An app usually has one current
28
+ /// `AppInfo` (plus, briefly, a second pending one while an edit awaits
29
+ /// review) — this is the parent resource `AppInfoLocalization`s hang
30
+ /// off of, and the id you need for `create_app_info_localization`.
31
+ pub async fn list_app_infos(&self, app_id: &str) -> Result<Vec<AppInfo>> {
32
+ let path = format!("/v1/apps/{app_id}/appInfos");
33
+ let doc: ListDocument<AppInfoAttributes> =
34
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
35
+ Ok(doc.data)
36
+ }
37
+
38
+ pub async fn get_app_info(&self, id: &str) -> Result<AppInfo> {
39
+ let path = format!("/v1/appInfos/{id}");
40
+ let doc: Document<AppInfoAttributes> =
41
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
42
+ Ok(doc.data)
43
+ }
44
+}
crates/ascapi/src/app_info_localization.rs
new
+114
@@ -0,0 +1,114 @@
1
+use crate::client::Client;
2
+use crate::error::Result;
3
+use crate::jsonapi::{
4
+ CreateBody, CreateData, Document, ListDocument, Resource, ResourceId, ToOne, UpdateBody,
5
+ UpdateData,
6
+};
7
+use reqwest::Method;
8
+use serde::{Deserialize, Serialize};
9
+
10
+pub const RESOURCE_TYPE: &str = "appInfoLocalizations";
11
+
12
+#[derive(Debug, Clone, Serialize, Deserialize)]
13
+#[serde(rename_all = "camelCase")]
14
+pub struct AppInfoLocalizationAttributes {
15
+ pub locale: String,
16
+ pub name: Option<String>,
17
+ pub subtitle: Option<String>,
18
+ pub privacy_policy_url: Option<String>,
19
+ pub privacy_choices_url: Option<String>,
20
+ pub privacy_policy_text: Option<String>,
21
+}
22
+
23
+pub type AppInfoLocalization = Resource<AppInfoLocalizationAttributes>;
24
+
25
+#[derive(Debug, Clone, Serialize)]
26
+#[serde(rename_all = "camelCase")]
27
+pub struct AppInfoLocalizationCreateAttributes {
28
+ pub locale: String,
29
+ pub name: String,
30
+ #[serde(skip_serializing_if = "Option::is_none")]
31
+ pub subtitle: Option<String>,
32
+ #[serde(skip_serializing_if = "Option::is_none")]
33
+ pub privacy_policy_url: Option<String>,
34
+}
35
+
36
+#[derive(Debug, Clone, Default, Serialize)]
37
+#[serde(rename_all = "camelCase")]
38
+pub struct AppInfoLocalizationUpdateAttributes {
39
+ #[serde(skip_serializing_if = "Option::is_none")]
40
+ pub name: Option<String>,
41
+ #[serde(skip_serializing_if = "Option::is_none")]
42
+ pub subtitle: Option<String>,
43
+ #[serde(skip_serializing_if = "Option::is_none")]
44
+ pub privacy_policy_url: Option<String>,
45
+}
46
+
47
+#[derive(Debug, Clone, Serialize)]
48
+#[serde(rename_all = "camelCase")]
49
+pub struct AppInfoLocalizationRelationships {
50
+ pub app_info: ToOne,
51
+}
52
+
53
+impl Client {
54
+ pub async fn list_app_info_localizations(
55
+ &self,
56
+ app_info_id: &str,
57
+ ) -> Result<Vec<AppInfoLocalization>> {
58
+ let path = format!("/v1/appInfos/{app_info_id}/appInfoLocalizations");
59
+ let doc: ListDocument<AppInfoLocalizationAttributes> =
60
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
61
+ Ok(doc.data)
62
+ }
63
+
64
+ /// `POST /v1/appInfoLocalizations` — adds a locale's name/subtitle to an
65
+ /// `AppInfo`.
66
+ pub async fn create_app_info_localization(
67
+ &self,
68
+ app_info_id: &str,
69
+ attributes: AppInfoLocalizationCreateAttributes,
70
+ ) -> Result<AppInfoLocalization> {
71
+ let body = CreateBody {
72
+ data: CreateData {
73
+ resource_type: RESOURCE_TYPE,
74
+ attributes,
75
+ relationships: Some(AppInfoLocalizationRelationships {
76
+ app_info: ToOne {
77
+ data: ResourceId {
78
+ resource_type: crate::app_info::RESOURCE_TYPE,
79
+ id: app_info_id.to_string(),
80
+ },
81
+ },
82
+ }),
83
+ },
84
+ };
85
+ let doc: Document<AppInfoLocalizationAttributes> = self
86
+ .request(Method::POST, "/v1/appInfoLocalizations", &[], Some(&body))
87
+ .await?;
88
+ Ok(doc.data)
89
+ }
90
+
91
+ pub async fn update_app_info_localization(
92
+ &self,
93
+ id: &str,
94
+ attributes: AppInfoLocalizationUpdateAttributes,
95
+ ) -> Result<AppInfoLocalization> {
96
+ let path = format!("/v1/appInfoLocalizations/{id}");
97
+ let body = UpdateBody {
98
+ data: UpdateData {
99
+ resource_type: RESOURCE_TYPE,
100
+ id: id.to_string(),
101
+ attributes,
102
+ },
103
+ };
104
+ let doc: Document<AppInfoLocalizationAttributes> =
105
+ self.request(Method::PATCH, &path, &[], Some(&body)).await?;
106
+ Ok(doc.data)
107
+ }
108
+
109
+ pub async fn delete_app_info_localization(&self, id: &str) -> Result<()> {
110
+ let path = format!("/v1/appInfoLocalizations/{id}");
111
+ self.request_no_content::<()>(Method::DELETE, &path, &[], None)
112
+ .await
113
+ }
114
+}
crates/ascapi/src/app_screenshot.rs
new
+233
@@ -0,0 +1,233 @@
1
+//! An `AppScreenshot` is one image binary within an `AppScreenshotSet`.
2
+//! Uploading one is a three-step JSON:API + raw-HTTP dance (see
3
+//! <https://developer.apple.com/documentation/appstoreconnectapi/uploading-assets-to-app-store-connect>):
4
+//!
5
+//! 1. `POST /v1/appScreenshots` reserves the asset and returns pre-signed
6
+//! `uploadOperations` (one or more chunked PUT requests).
7
+//! 2. The image bytes are PUT to each operation's URL with its exact
8
+//! `requestHeaders` — these URLs are *not* App Store Connect API
9
+//! endpoints, so no bearer token is sent.
10
+//! 3. `PATCH /v1/appScreenshots/{id}` with `uploaded: true` and an MD5
11
+//! checksum of the file commits the upload.
12
+//!
13
+//! [`Client::upload_app_screenshot`] does all three steps in one call.
14
+
15
+use crate::client::Client;
16
+use crate::error::Result;
17
+use crate::jsonapi::{
18
+ CreateBody, CreateData, Document, ListDocument, Resource, ResourceId, ToOne, UpdateBody,
19
+ UpdateData,
20
+};
21
+use reqwest::Method;
22
+use serde::{Deserialize, Deserializer, Serialize};
23
+
24
+pub const RESOURCE_TYPE: &str = "appScreenshots";
25
+
26
+/// App Store Connect sends `uploadOperations: null` (not an omitted field)
27
+/// once a screenshot has been committed — plain `#[serde(default)]` only
28
+/// covers a *missing* field, not an explicit `null`, so `Vec<T>` fails to
29
+/// deserialize without this.
30
+fn null_as_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
31
+where
32
+ D: Deserializer<'de>,
33
+ T: Default + Deserialize<'de>,
34
+{
35
+ Ok(Option::deserialize(deserializer)?.unwrap_or_default())
36
+}
37
+
38
+#[derive(Debug, Clone, Serialize, Deserialize)]
39
+#[serde(rename_all = "camelCase")]
40
+pub struct RequestHeader {
41
+ pub name: String,
42
+ pub value: String,
43
+}
44
+
45
+#[derive(Debug, Clone, Serialize, Deserialize)]
46
+#[serde(rename_all = "camelCase")]
47
+pub struct UploadOperation {
48
+ pub method: String,
49
+ pub url: String,
50
+ #[serde(default)]
51
+ pub length: u64,
52
+ #[serde(default)]
53
+ pub offset: u64,
54
+ #[serde(default, deserialize_with = "null_as_default")]
55
+ pub request_headers: Vec<RequestHeader>,
56
+}
57
+
58
+#[derive(Debug, Clone, Serialize, Deserialize)]
59
+#[serde(rename_all = "camelCase")]
60
+pub struct AssetDeliveryState {
61
+ pub state: String,
62
+ #[serde(default, deserialize_with = "null_as_default")]
63
+ pub errors: Vec<serde_json::Value>,
64
+ #[serde(default, deserialize_with = "null_as_default")]
65
+ pub warnings: Vec<serde_json::Value>,
66
+}
67
+
68
+#[derive(Debug, Clone, Serialize, Deserialize)]
69
+#[serde(rename_all = "camelCase")]
70
+pub struct AppScreenshotAttributes {
71
+ pub file_name: String,
72
+ pub file_size: u64,
73
+ #[serde(default)]
74
+ pub source_file_checksum: Option<String>,
75
+ #[serde(default)]
76
+ pub image_asset: Option<serde_json::Value>,
77
+ #[serde(default)]
78
+ pub asset_delivery_state: Option<AssetDeliveryState>,
79
+ #[serde(default, deserialize_with = "null_as_default")]
80
+ pub upload_operations: Vec<UploadOperation>,
81
+ #[serde(default)]
82
+ pub uploaded: Option<bool>,
83
+}
84
+
85
+pub type AppScreenshot = Resource<AppScreenshotAttributes>;
86
+
87
+#[derive(Debug, Clone, Serialize)]
88
+#[serde(rename_all = "camelCase")]
89
+pub struct AppScreenshotCreateAttributes {
90
+ pub file_name: String,
91
+ pub file_size: u64,
92
+}
93
+
94
+#[derive(Debug, Clone, Serialize)]
95
+#[serde(rename_all = "camelCase")]
96
+pub struct AppScreenshotSetRelationship {
97
+ pub app_screenshot_set: ToOne,
98
+}
99
+
100
+#[derive(Debug, Clone, Serialize)]
101
+#[serde(rename_all = "camelCase")]
102
+pub struct AppScreenshotCommitAttributes {
103
+ pub uploaded: bool,
104
+ #[serde(skip_serializing_if = "Option::is_none")]
105
+ pub source_file_checksum: Option<String>,
106
+}
107
+
108
+impl Client {
109
+ /// `GET /v1/appScreenshotSets/{id}/appScreenshots`.
110
+ pub async fn list_app_screenshots(
111
+ &self,
112
+ app_screenshot_set_id: &str,
113
+ ) -> Result<Vec<AppScreenshot>> {
114
+ let path = format!("/v1/appScreenshotSets/{app_screenshot_set_id}/appScreenshots");
115
+ let doc: ListDocument<AppScreenshotAttributes> =
116
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
117
+ Ok(doc.data)
118
+ }
119
+
120
+ /// `POST /v1/appScreenshots` — reserves the asset and returns
121
+ /// pre-signed `uploadOperations`. Prefer [`Client::upload_app_screenshot`]
122
+ /// unless you need to drive the upload/commit steps yourself.
123
+ pub async fn create_app_screenshot(
124
+ &self,
125
+ app_screenshot_set_id: &str,
126
+ attributes: AppScreenshotCreateAttributes,
127
+ ) -> Result<AppScreenshot> {
128
+ let body = CreateBody {
129
+ data: CreateData {
130
+ resource_type: RESOURCE_TYPE,
131
+ attributes,
132
+ relationships: Some(AppScreenshotSetRelationship {
133
+ app_screenshot_set: ToOne {
134
+ data: ResourceId {
135
+ resource_type: crate::app_screenshot_set::RESOURCE_TYPE,
136
+ id: app_screenshot_set_id.to_string(),
137
+ },
138
+ },
139
+ }),
140
+ },
141
+ };
142
+ let doc: Document<AppScreenshotAttributes> = self
143
+ .request(Method::POST, "/v1/appScreenshots", &[], Some(&body))
144
+ .await?;
145
+ Ok(doc.data)
146
+ }
147
+
148
+ /// PUTs `bytes` to every one of `screenshot`'s reserved
149
+ /// `uploadOperations`, slicing the buffer per operation's `offset` /
150
+ /// `length`. Does not commit the upload — call
151
+ /// [`Client::commit_app_screenshot`] (or use
152
+ /// [`Client::upload_app_screenshot`]) after this succeeds.
153
+ pub async fn upload_app_screenshot_bytes(
154
+ &self,
155
+ screenshot: &AppScreenshot,
156
+ bytes: &[u8],
157
+ ) -> Result<()> {
158
+ for op in &screenshot.attributes.upload_operations {
159
+ let start = op.offset as usize;
160
+ let end = start + op.length as usize;
161
+ let chunk = bytes
162
+ .get(start..end)
163
+ .map(|slice| slice.to_vec())
164
+ .unwrap_or_default();
165
+ let headers: Vec<(String, String)> = op
166
+ .request_headers
167
+ .iter()
168
+ .map(|h| (h.name.clone(), h.value.clone()))
169
+ .collect();
170
+ let method: Method = op.method.parse().unwrap_or(Method::PUT);
171
+ self.upload_bytes(method, &op.url, &headers, chunk).await?;
172
+ }
173
+ Ok(())
174
+ }
175
+
176
+ /// `PATCH /v1/appScreenshots/{id}` with `uploaded: true` — tells App
177
+ /// Store Connect the binary is fully transferred so it can start
178
+ /// processing/validating the asset.
179
+ pub async fn commit_app_screenshot(
180
+ &self,
181
+ id: &str,
182
+ source_file_checksum: String,
183
+ ) -> Result<AppScreenshot> {
184
+ let path = format!("/v1/appScreenshots/{id}");
185
+ let body = UpdateBody {
186
+ data: UpdateData {
187
+ resource_type: RESOURCE_TYPE,
188
+ id: id.to_string(),
189
+ attributes: AppScreenshotCommitAttributes {
190
+ uploaded: true,
191
+ source_file_checksum: Some(source_file_checksum),
192
+ },
193
+ },
194
+ };
195
+ let doc: Document<AppScreenshotAttributes> =
196
+ self.request(Method::PATCH, &path, &[], Some(&body)).await?;
197
+ Ok(doc.data)
198
+ }
199
+
200
+ /// `DELETE /v1/appScreenshots/{id}`.
201
+ pub async fn delete_app_screenshot(&self, id: &str) -> Result<()> {
202
+ let path = format!("/v1/appScreenshots/{id}");
203
+ self.request_no_content::<()>(Method::DELETE, &path, &[], None)
204
+ .await
205
+ }
206
+
207
+ /// Reserve, upload, and commit an image in one call: `bytes` becomes an
208
+ /// `AppScreenshot` under `app_screenshot_set_id`, named `file_name`.
209
+ /// This is the entry point most callers want; the lower-level
210
+ /// `create_app_screenshot` / `upload_app_screenshot_bytes` /
211
+ /// `commit_app_screenshot` are exposed for callers that need to
212
+ /// checkpoint between steps (e.g. a CLI resuming a failed upload).
213
+ pub async fn upload_app_screenshot(
214
+ &self,
215
+ app_screenshot_set_id: &str,
216
+ file_name: String,
217
+ bytes: Vec<u8>,
218
+ ) -> Result<AppScreenshot> {
219
+ let checksum = format!("{:x}", md5::compute(&bytes));
220
+ let screenshot = self
221
+ .create_app_screenshot(
222
+ app_screenshot_set_id,
223
+ AppScreenshotCreateAttributes {
224
+ file_name,
225
+ file_size: bytes.len() as u64,
226
+ },
227
+ )
228
+ .await?;
229
+ self.upload_app_screenshot_bytes(&screenshot, &bytes)
230
+ .await?;
231
+ self.commit_app_screenshot(&screenshot.id, checksum).await
232
+ }
233
+}
crates/ascapi/src/app_screenshot_set.rs
new
+133
@@ -0,0 +1,133 @@
1
+//! An `AppScreenshotSet` groups the screenshots for one device/display
2
+//! class (e.g. Apple Vision Pro, iPhone 6.7") within one localization of an
3
+//! `AppStoreVersion`. Screenshots themselves are a separate resource
4
+//! ([`crate::app_screenshot`]) that belongs to a set.
5
+
6
+use crate::client::Client;
7
+use crate::error::Result;
8
+use crate::jsonapi::{CreateBody, CreateData, Document, ListDocument, Resource, ResourceId, ToOne};
9
+use reqwest::Method;
10
+use serde::{Deserialize, Serialize};
11
+
12
+pub const RESOURCE_TYPE: &str = "appScreenshotSets";
13
+
14
+/// The device/display class a screenshot set targets. Apple documents the
15
+/// full set at
16
+/// <https://developer.apple.com/documentation/appstoreconnectapi/screenshotdisplaytype>;
17
+/// this covers the App Store (non-iMessage) display classes in current use.
18
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19
+pub enum ScreenshotDisplayType {
20
+ #[serde(rename = "APP_APPLE_VISION_PRO")]
21
+ AppleVisionPro,
22
+ #[serde(rename = "APP_IPHONE_67")]
23
+ Iphone67,
24
+ #[serde(rename = "APP_IPHONE_65")]
25
+ Iphone65,
26
+ #[serde(rename = "APP_IPHONE_61")]
27
+ Iphone61,
28
+ #[serde(rename = "APP_IPHONE_58")]
29
+ Iphone58,
30
+ #[serde(rename = "APP_IPHONE_55")]
31
+ Iphone55,
32
+ #[serde(rename = "APP_IPHONE_47")]
33
+ Iphone47,
34
+ #[serde(rename = "APP_IPHONE_40")]
35
+ Iphone40,
36
+ #[serde(rename = "APP_IPHONE_35")]
37
+ Iphone35,
38
+ #[serde(rename = "APP_IPAD_PRO_3GEN_129")]
39
+ IpadPro3Gen129,
40
+ #[serde(rename = "APP_IPAD_PRO_3GEN_11")]
41
+ IpadPro3Gen11,
42
+ #[serde(rename = "APP_IPAD_PRO_129")]
43
+ IpadPro129,
44
+ #[serde(rename = "APP_IPAD_105")]
45
+ Ipad105,
46
+ #[serde(rename = "APP_IPAD_97")]
47
+ Ipad97,
48
+ #[serde(rename = "APP_DESKTOP")]
49
+ Desktop,
50
+ #[serde(rename = "APP_APPLE_TV")]
51
+ AppleTv,
52
+ #[serde(rename = "APP_WATCH_ULTRA")]
53
+ WatchUltra,
54
+ #[serde(rename = "APP_WATCH_SERIES_10")]
55
+ WatchSeries10,
56
+ #[serde(rename = "APP_WATCH_SERIES_7")]
57
+ WatchSeries7,
58
+ #[serde(rename = "APP_WATCH_SERIES_4")]
59
+ WatchSeries4,
60
+ #[serde(rename = "APP_WATCH_SERIES_3")]
61
+ WatchSeries3,
62
+}
63
+
64
+#[derive(Debug, Clone, Serialize, Deserialize)]
65
+#[serde(rename_all = "camelCase")]
66
+pub struct AppScreenshotSetAttributes {
67
+ pub screenshot_display_type: ScreenshotDisplayType,
68
+}
69
+
70
+pub type AppScreenshotSet = Resource<AppScreenshotSetAttributes>;
71
+
72
+#[derive(Debug, Clone, Serialize)]
73
+#[serde(rename_all = "camelCase")]
74
+pub struct AppScreenshotSetCreateAttributes {
75
+ pub screenshot_display_type: ScreenshotDisplayType,
76
+}
77
+
78
+#[derive(Debug, Clone, Serialize)]
79
+#[serde(rename_all = "camelCase")]
80
+pub struct AppScreenshotSetRelationships {
81
+ pub app_store_version_localization: ToOne,
82
+}
83
+
84
+impl Client {
85
+ /// `GET /v1/appStoreVersionLocalizations/{id}/appScreenshotSets`.
86
+ pub async fn list_app_screenshot_sets(
87
+ &self,
88
+ app_store_version_localization_id: &str,
89
+ ) -> Result<Vec<AppScreenshotSet>> {
90
+ let path = format!(
91
+ "/v1/appStoreVersionLocalizations/{app_store_version_localization_id}/appScreenshotSets"
92
+ );
93
+ let doc: ListDocument<AppScreenshotSetAttributes> =
94
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
95
+ Ok(doc.data)
96
+ }
97
+
98
+ /// `POST /v1/appScreenshotSets` — adds a device/display class's
99
+ /// screenshot bucket to a localization. Screenshots are then added to
100
+ /// the returned set with [`Client::create_app_screenshot`] /
101
+ /// [`Client::upload_app_screenshot`].
102
+ pub async fn create_app_screenshot_set(
103
+ &self,
104
+ app_store_version_localization_id: &str,
105
+ attributes: AppScreenshotSetCreateAttributes,
106
+ ) -> Result<AppScreenshotSet> {
107
+ let body = CreateBody {
108
+ data: CreateData {
109
+ resource_type: RESOURCE_TYPE,
110
+ attributes,
111
+ relationships: Some(AppScreenshotSetRelationships {
112
+ app_store_version_localization: ToOne {
113
+ data: ResourceId {
114
+ resource_type: crate::app_store_version_localization::RESOURCE_TYPE,
115
+ id: app_store_version_localization_id.to_string(),
116
+ },
117
+ },
118
+ }),
119
+ },
120
+ };
121
+ let doc: Document<AppScreenshotSetAttributes> = self
122
+ .request(Method::POST, "/v1/appScreenshotSets", &[], Some(&body))
123
+ .await?;
124
+ Ok(doc.data)
125
+ }
126
+
127
+ /// `DELETE /v1/appScreenshotSets/{id}`.
128
+ pub async fn delete_app_screenshot_set(&self, id: &str) -> Result<()> {
129
+ let path = format!("/v1/appScreenshotSets/{id}");
130
+ self.request_no_content::<()>(Method::DELETE, &path, &[], None)
131
+ .await
132
+ }
133
+}
crates/ascapi/src/app_store_version.rs
new
+166
@@ -0,0 +1,166 @@
1
+//! An `AppStoreVersion` is App Store Connect's per-platform record for an
2
+//! app: `platform` is one of `IOS` / `MAC_OS` / `TV_OS` / `VISION_OS`.
3
+//! Since there is no direct "create app for platform X" endpoint (see
4
+//! [`crate::app`]), creating an `AppStoreVersion` for a platform an app
5
+//! doesn't have one for yet — e.g. adding visionOS to an app that so far
6
+//! only ships on iOS/macOS — *is* how you add that platform.
7
+
8
+use crate::client::Client;
9
+use crate::error::Result;
10
+use crate::jsonapi::{
11
+ CreateBody, CreateData, Document, ListDocument, Resource, ResourceId, ToOne,
12
+ UpdateRelationshipsBody, UpdateRelationshipsData,
13
+};
14
+use reqwest::Method;
15
+use serde::{Deserialize, Serialize};
16
+
17
+pub const RESOURCE_TYPE: &str = "appStoreVersions";
18
+
19
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20
+pub enum Platform {
21
+ #[serde(rename = "IOS")]
22
+ Ios,
23
+ #[serde(rename = "MAC_OS")]
24
+ MacOs,
25
+ #[serde(rename = "TV_OS")]
26
+ TvOs,
27
+ #[serde(rename = "VISION_OS")]
28
+ VisionOs,
29
+}
30
+
31
+impl Platform {
32
+ fn as_query_value(self) -> &'static str {
33
+ match self {
34
+ Platform::Ios => "IOS",
35
+ Platform::MacOs => "MAC_OS",
36
+ Platform::TvOs => "TV_OS",
37
+ Platform::VisionOs => "VISION_OS",
38
+ }
39
+ }
40
+}
41
+
42
+#[derive(Debug, Clone, Serialize, Deserialize)]
43
+#[serde(rename_all = "camelCase")]
44
+pub struct AppStoreVersionAttributes {
45
+ pub platform: Platform,
46
+ pub version_string: Option<String>,
47
+ pub app_version_state: Option<String>,
48
+ pub copyright: Option<String>,
49
+ pub created_date: Option<String>,
50
+ pub downloadable: Option<bool>,
51
+}
52
+
53
+pub type AppStoreVersion = Resource<AppStoreVersionAttributes>;
54
+
55
+#[derive(Debug, Clone, Serialize)]
56
+#[serde(rename_all = "camelCase")]
57
+pub struct AppStoreVersionCreateAttributes {
58
+ pub platform: Platform,
59
+ pub version_string: String,
60
+ #[serde(skip_serializing_if = "Option::is_none")]
61
+ pub copyright: Option<String>,
62
+}
63
+
64
+#[derive(Debug, Clone, Serialize)]
65
+#[serde(rename_all = "camelCase")]
66
+pub struct AppStoreVersionRelationships {
67
+ pub app: ToOne,
68
+}
69
+
70
+impl Client {
71
+ /// `GET /v1/apps/{app_id}/appStoreVersions`, optionally narrowed to one
72
+ /// platform.
73
+ pub async fn list_app_store_versions(
74
+ &self,
75
+ app_id: &str,
76
+ filter_platform: Option<Platform>,
77
+ ) -> Result<Vec<AppStoreVersion>> {
78
+ let path = format!("/v1/apps/{app_id}/appStoreVersions");
79
+ let mut query = Vec::new();
80
+ if let Some(platform) = filter_platform {
81
+ query.push(("filter[platform]", platform.as_query_value()));
82
+ }
83
+ let doc: ListDocument<AppStoreVersionAttributes> = self
84
+ .request(Method::GET, &path, &query, None::<&()>)
85
+ .await?;
86
+ Ok(doc.data)
87
+ }
88
+
89
+ pub async fn get_app_store_version(&self, id: &str) -> Result<AppStoreVersion> {
90
+ let path = format!("/v1/appStoreVersions/{id}");
91
+ let doc: Document<AppStoreVersionAttributes> =
92
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
93
+ Ok(doc.data)
94
+ }
95
+
96
+ /// `POST /v1/appStoreVersions`.
97
+ pub async fn create_app_store_version(
98
+ &self,
99
+ app_id: &str,
100
+ attributes: AppStoreVersionCreateAttributes,
101
+ ) -> Result<AppStoreVersion> {
102
+ let body = CreateBody {
103
+ data: CreateData {
104
+ resource_type: RESOURCE_TYPE,
105
+ attributes,
106
+ relationships: Some(AppStoreVersionRelationships {
107
+ app: ToOne {
108
+ data: ResourceId {
109
+ resource_type: crate::app::RESOURCE_TYPE,
110
+ id: app_id.to_string(),
111
+ },
112
+ },
113
+ }),
114
+ },
115
+ };
116
+ let doc: Document<AppStoreVersionAttributes> = self
117
+ .request(Method::POST, "/v1/appStoreVersions", &[], Some(&body))
118
+ .await?;
119
+ Ok(doc.data)
120
+ }
121
+
122
+ /// `DELETE /v1/appStoreVersions/{id}` — only allowed while the version
123
+ /// hasn't been submitted for review.
124
+ pub async fn delete_app_store_version(&self, id: &str) -> Result<()> {
125
+ let path = format!("/v1/appStoreVersions/{id}");
126
+ self.request_no_content::<()>(Method::DELETE, &path, &[], None)
127
+ .await
128
+ }
129
+
130
+ /// `PATCH /v1/appStoreVersions/{id}` with a `build` relationship —
131
+ /// attaches (or replaces) the `Build` this version will submit. Needed
132
+ /// after a re-upload fixes an `INVALID_BINARY` version: the fixed
133
+ /// build must be attached here before the version can be resubmitted
134
+ /// for review (resubmission itself is outside this crate's scope — see
135
+ /// [`crate::build`]'s module doc).
136
+ pub async fn set_app_store_version_build(
137
+ &self,
138
+ id: &str,
139
+ build_id: &str,
140
+ ) -> Result<AppStoreVersion> {
141
+ let path = format!("/v1/appStoreVersions/{id}");
142
+ let body = UpdateRelationshipsBody {
143
+ data: UpdateRelationshipsData {
144
+ resource_type: RESOURCE_TYPE,
145
+ id: id.to_string(),
146
+ relationships: AppStoreVersionBuildRelationship {
147
+ build: ToOne {
148
+ data: ResourceId {
149
+ resource_type: crate::build::RESOURCE_TYPE,
150
+ id: build_id.to_string(),
151
+ },
152
+ },
153
+ },
154
+ },
155
+ };
156
+ let doc: Document<AppStoreVersionAttributes> =
157
+ self.request(Method::PATCH, &path, &[], Some(&body)).await?;
158
+ Ok(doc.data)
159
+ }
160
+}
161
+
162
+#[derive(Debug, Clone, Serialize)]
163
+#[serde(rename_all = "camelCase")]
164
+pub struct AppStoreVersionBuildRelationship {
165
+ pub build: ToOne,
166
+}
crates/ascapi/src/app_store_version_localization.rs
new
+124
@@ -0,0 +1,124 @@
1
+use crate::client::Client;
2
+use crate::error::Result;
3
+use crate::jsonapi::{
4
+ CreateBody, CreateData, Document, ListDocument, Resource, ResourceId, ToOne, UpdateBody,
5
+ UpdateData,
6
+};
7
+use reqwest::Method;
8
+use serde::{Deserialize, Serialize};
9
+
10
+pub const RESOURCE_TYPE: &str = "appStoreVersionLocalizations";
11
+
12
+#[derive(Debug, Clone, Serialize, Deserialize)]
13
+#[serde(rename_all = "camelCase")]
14
+pub struct AppStoreVersionLocalizationAttributes {
15
+ pub locale: String,
16
+ pub description: Option<String>,
17
+ pub keywords: Option<String>,
18
+ pub marketing_url: Option<String>,
19
+ pub promotional_text: Option<String>,
20
+ pub support_url: Option<String>,
21
+ pub whats_new: Option<String>,
22
+}
23
+
24
+pub type AppStoreVersionLocalization = Resource<AppStoreVersionLocalizationAttributes>;
25
+
26
+#[derive(Debug, Clone, Default, Serialize)]
27
+#[serde(rename_all = "camelCase")]
28
+pub struct AppStoreVersionLocalizationFields {
29
+ #[serde(skip_serializing_if = "Option::is_none")]
30
+ pub description: Option<String>,
31
+ #[serde(skip_serializing_if = "Option::is_none")]
32
+ pub keywords: Option<String>,
33
+ #[serde(skip_serializing_if = "Option::is_none")]
34
+ pub marketing_url: Option<String>,
35
+ #[serde(skip_serializing_if = "Option::is_none")]
36
+ pub promotional_text: Option<String>,
37
+ #[serde(skip_serializing_if = "Option::is_none")]
38
+ pub support_url: Option<String>,
39
+ #[serde(skip_serializing_if = "Option::is_none")]
40
+ pub whats_new: Option<String>,
41
+}
42
+
43
+#[derive(Debug, Clone, Serialize)]
44
+#[serde(rename_all = "camelCase")]
45
+pub struct AppStoreVersionLocalizationCreateAttributes {
46
+ pub locale: String,
47
+ #[serde(flatten)]
48
+ pub fields: AppStoreVersionLocalizationFields,
49
+}
50
+
51
+#[derive(Debug, Clone, Serialize)]
52
+#[serde(rename_all = "camelCase")]
53
+pub struct AppStoreVersionLocalizationRelationships {
54
+ pub app_store_version: ToOne,
55
+}
56
+
57
+impl Client {
58
+ pub async fn list_app_store_version_localizations(
59
+ &self,
60
+ app_store_version_id: &str,
61
+ ) -> Result<Vec<AppStoreVersionLocalization>> {
62
+ let path =
63
+ format!("/v1/appStoreVersions/{app_store_version_id}/appStoreVersionLocalizations");
64
+ let doc: ListDocument<AppStoreVersionLocalizationAttributes> =
65
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
66
+ Ok(doc.data)
67
+ }
68
+
69
+ /// `POST /v1/appStoreVersionLocalizations` — adds a locale's
70
+ /// description/keywords/etc to an `AppStoreVersion`.
71
+ pub async fn create_app_store_version_localization(
72
+ &self,
73
+ app_store_version_id: &str,
74
+ attributes: AppStoreVersionLocalizationCreateAttributes,
75
+ ) -> Result<AppStoreVersionLocalization> {
76
+ let body = CreateBody {
77
+ data: CreateData {
78
+ resource_type: RESOURCE_TYPE,
79
+ attributes,
80
+ relationships: Some(AppStoreVersionLocalizationRelationships {
81
+ app_store_version: ToOne {
82
+ data: ResourceId {
83
+ resource_type: crate::app_store_version::RESOURCE_TYPE,
84
+ id: app_store_version_id.to_string(),
85
+ },
86
+ },
87
+ }),
88
+ },
89
+ };
90
+ let doc: Document<AppStoreVersionLocalizationAttributes> = self
91
+ .request(
92
+ Method::POST,
93
+ "/v1/appStoreVersionLocalizations",
94
+ &[],
95
+ Some(&body),
96
+ )
97
+ .await?;
98
+ Ok(doc.data)
99
+ }
100
+
101
+ pub async fn update_app_store_version_localization(
102
+ &self,
103
+ id: &str,
104
+ fields: AppStoreVersionLocalizationFields,
105
+ ) -> Result<AppStoreVersionLocalization> {
106
+ let path = format!("/v1/appStoreVersionLocalizations/{id}");
107
+ let body = UpdateBody {
108
+ data: UpdateData {
109
+ resource_type: RESOURCE_TYPE,
110
+ id: id.to_string(),
111
+ attributes: fields,
112
+ },
113
+ };
114
+ let doc: Document<AppStoreVersionLocalizationAttributes> =
115
+ self.request(Method::PATCH, &path, &[], Some(&body)).await?;
116
+ Ok(doc.data)
117
+ }
118
+
119
+ pub async fn delete_app_store_version_localization(&self, id: &str) -> Result<()> {
120
+ let path = format!("/v1/appStoreVersionLocalizations/{id}");
121
+ self.request_no_content::<()>(Method::DELETE, &path, &[], None)
122
+ .await
123
+ }
124
+}
crates/ascapi/src/ascapi.rs
new
+56
@@ -0,0 +1,56 @@
1
+//! Embeddable Rust client (and CLI, in the sibling `smbcloud-ascapi-cli`
2
+//! crate / `ascapi` binary) for the App Store Connect API's
3
+//! [App Metadata](https://developer.apple.com/documentation/appstoreconnectapi/app-metadata)
4
+//! resources: apps, app infos and their localizations, app store versions
5
+//! and their localizations, and bundle IDs.
6
+//!
7
+//! ```no_run
8
+//! use smbcloud_ascapi::{ApiKey, Client};
9
+//! use smbcloud_ascapi::app_store_version::{AppStoreVersionCreateAttributes, Platform};
10
+//!
11
+//! # async fn example() -> smbcloud_ascapi::Result<()> {
12
+//! let api_key = ApiKey::from_p8_file(
13
+//! "L84N624YQH",
14
+//! "b4e8d369-8b7d-4538-8435-643b73237575",
15
+//! "/Users/me/private_keys/AuthKey_L84N624YQH.p8",
16
+//! )?;
17
+//! let client = Client::new(api_key);
18
+//!
19
+//! let apps = client.list_apps(Some("ai.siti.Siti")).await?;
20
+//! let app = &apps[0];
21
+//!
22
+//! client
23
+//! .create_app_store_version(
24
+//! &app.id,
25
+//! AppStoreVersionCreateAttributes {
26
+//! platform: Platform::VisionOs,
27
+//! version_string: "1.0.0".to_string(),
28
+//! copyright: None,
29
+//! },
30
+//! )
31
+//! .await?;
32
+//! # Ok(())
33
+//! # }
34
+//! ```
35
+//!
36
+//! Not covered (yet): app preview (video) binary uploads, age rating
37
+//! declarations, in-app purchases, TestFlight, and the rest of the wider
38
+//! App Store Connect API surface outside "App Metadata".
39
+
40
+pub mod app;
41
+pub mod app_info;
42
+pub mod app_info_localization;
43
+pub mod app_screenshot;
44
+pub mod app_screenshot_set;
45
+pub mod app_store_version;
46
+pub mod app_store_version_localization;
47
+pub mod auth;
48
+pub mod build;
49
+pub mod bundle_id;
50
+pub mod client;
51
+pub mod error;
52
+pub mod jsonapi;
53
+
54
+pub use auth::ApiKey;
55
+pub use client::Client;
56
+pub use error::{Error, Result};
crates/ascapi/src/auth.rs
new
+99
@@ -0,0 +1,99 @@
1
+use crate::error::{Error, Result};
2
+use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
3
+use serde::Serialize;
4
+use std::path::Path;
5
+use std::time::{SystemTime, UNIX_EPOCH};
6
+
7
+const AUDIENCE: &str = "appstoreconnect-v1";
8
+// Apple rejects tokens whose `exp` is more than 20 minutes past `iat`. See
9
+// https://developer.apple.com/documentation/appstoreconnectapi/generating-tokens-for-api-requests
10
+const MAX_TOKEN_LIFETIME_SECS: u64 = 20 * 60;
11
+
12
+#[derive(Debug, Clone, Serialize)]
13
+struct Claims {
14
+ iss: String,
15
+ iat: u64,
16
+ exp: u64,
17
+ aud: String,
18
+}
19
+
20
+/// An App Store Connect API key: the (key ID, issuer ID, private key)
21
+/// triple from App Store Connect → Users and Access → Integrations → App
22
+/// Store Connect API. Signs the short-lived ES256 JWTs the API requires as
23
+/// a bearer token on every request.
24
+#[derive(Clone)]
25
+pub struct ApiKey {
26
+ key_id: String,
27
+ issuer_id: String,
28
+ encoding_key: EncodingKey,
29
+ lifetime_secs: u64,
30
+}
31
+
32
+impl ApiKey {
33
+ /// Load a key from a PEM-encoded `.p8` private key file — the file App
34
+ /// Store Connect lets you download once, at key-creation time.
35
+ pub fn from_p8_file(
36
+ key_id: impl Into<String>,
37
+ issuer_id: impl Into<String>,
38
+ p8_path: impl AsRef<Path>,
39
+ ) -> Result<Self> {
40
+ let path = p8_path.as_ref();
41
+ let pem = std::fs::read_to_string(path).map_err(|source| Error::PrivateKeyRead {
42
+ path: path.display().to_string(),
43
+ source,
44
+ })?;
45
+ Self::from_p8_pem(key_id, issuer_id, &pem)
46
+ }
47
+
48
+ /// Load a key from an already-in-memory PEM string.
49
+ pub fn from_p8_pem(
50
+ key_id: impl Into<String>,
51
+ issuer_id: impl Into<String>,
52
+ pem: &str,
53
+ ) -> Result<Self> {
54
+ let encoding_key = EncodingKey::from_ec_pem(pem.as_bytes())?;
55
+ Ok(Self {
56
+ key_id: key_id.into(),
57
+ issuer_id: issuer_id.into(),
58
+ encoding_key,
59
+ lifetime_secs: MAX_TOKEN_LIFETIME_SECS,
60
+ })
61
+ }
62
+
63
+ /// Override the token lifetime in seconds. Values above Apple's 20
64
+ /// minute ceiling are clamped down to it.
65
+ pub fn with_lifetime_secs(mut self, secs: u64) -> Self {
66
+ self.lifetime_secs = secs.min(MAX_TOKEN_LIFETIME_SECS);
67
+ self
68
+ }
69
+
70
+ pub fn key_id(&self) -> &str {
71
+ &self.key_id
72
+ }
73
+
74
+ pub fn lifetime_secs(&self) -> u64 {
75
+ self.lifetime_secs
76
+ }
77
+
78
+ /// Mint a fresh bearer token. `Client` caches and reuses the result of
79
+ /// this until shortly before it expires rather than calling it per
80
+ /// request.
81
+ pub fn token(&self) -> Result<String> {
82
+ let now = SystemTime::now()
83
+ .duration_since(UNIX_EPOCH)
84
+ .unwrap_or_default() // a clock before 1970 isn't a real failure mode worth propagating
85
+ .as_secs();
86
+
87
+ let claims = Claims {
88
+ iss: self.issuer_id.clone(),
89
+ iat: now,
90
+ exp: now + self.lifetime_secs,
91
+ aud: AUDIENCE.to_string(),
92
+ };
93
+
94
+ let mut header = Header::new(Algorithm::ES256);
95
+ header.kid = Some(self.key_id.clone());
96
+
97
+ Ok(encode(&header, &claims, &self.encoding_key)?)
98
+ }
99
+}
crates/ascapi/src/build.rs
new
+44
@@ -0,0 +1,44 @@
1
+//! Read-only lookup for `Build` — the processed artifact behind an uploaded
2
+//! `.ipa`/`.pkg`. Uploading itself is out of this crate's scope (done via
3
+//! `xcrun altool`/Xcode, see the CLI's sibling build scripts); this module
4
+//! exists so a caller can check whether App Store Connect finished
5
+//! processing a build as `VALID` or `INVALID` after upload — the signal that
6
+//! actually confirms a binary-rejection fix worked, independent of an
7
+//! `AppStoreVersion`'s `appVersionState` (which only changes once a build is
8
+//! attached to the version and resubmitted — outside this crate's scope).
9
+
10
+use crate::client::Client;
11
+use crate::error::Result;
12
+use crate::jsonapi::{ListDocument, Resource};
13
+use reqwest::Method;
14
+use serde::{Deserialize, Serialize};
15
+
16
+pub const RESOURCE_TYPE: &str = "builds";
17
+
18
+#[derive(Debug, Clone, Serialize, Deserialize)]
19
+#[serde(rename_all = "camelCase")]
20
+pub struct BuildAttributes {
21
+ pub version: Option<String>,
22
+ pub uploaded_date: Option<String>,
23
+ pub expired: Option<bool>,
24
+ pub processing_state: Option<String>,
25
+ pub min_os_version: Option<String>,
26
+}
27
+
28
+pub type Build = Resource<BuildAttributes>;
29
+
30
+impl Client {
31
+ /// `GET /v1/apps/{app_id}/builds`, sorted newest-first by
32
+ /// `uploadedDate` (client-side — this endpoint's `sort` query parameter
33
+ /// is rejected by the API, unlike most other list endpoints), so the
34
+ /// build most recently uploaded (e.g. by a fixplist re-upload) is
35
+ /// `list_builds(app_id).await?.first()`.
36
+ pub async fn list_builds(&self, app_id: &str) -> Result<Vec<Build>> {
37
+ let path = format!("/v1/apps/{app_id}/builds");
38
+ let doc: ListDocument<BuildAttributes> =
39
+ self.request(Method::GET, &path, &[], None::<&()>).await?;
40
+ let mut builds = doc.data;
41
+ builds.sort_by(|a, b| b.attributes.uploaded_date.cmp(&a.attributes.uploaded_date));
42
+ Ok(builds)
43
+ }
44
+}
crates/ascapi/src/bundle_id.rs
new
+76
@@ -0,0 +1,76 @@
1
+use crate::client::Client;
2
+use crate::error::Result;
3
+use crate::jsonapi::{CreateBody, CreateData, Document, ListDocument, Resource};
4
+use reqwest::Method;
5
+use serde::{Deserialize, Serialize};
6
+
7
+pub const RESOURCE_TYPE: &str = "bundleIds";
8
+
9
+/// Note this is a *different* enum from
10
+/// [`crate::app_store_version::Platform`] — App Store Connect uses a
11
+/// coarser set of platforms when registering a bundle ID than when
12
+/// declaring an App Store Version's platform. A visionOS app typically
13
+/// registers its bundle ID as `Ios` (visionOS shares the iOS identifier
14
+/// namespace) rather than a dedicated visionOS value, because there isn't
15
+/// one here.
16
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17
+pub enum BundleIdPlatform {
18
+ #[serde(rename = "IOS")]
19
+ Ios,
20
+ #[serde(rename = "MAC_OS")]
21
+ MacOs,
22
+ #[serde(rename = "UNIVERSAL")]
23
+ Universal,
24
+}
25
+
26
+#[derive(Debug, Clone, Serialize, Deserialize)]
27
+#[serde(rename_all = "camelCase")]
28
+pub struct BundleIdAttributes {
29
+ pub name: Option<String>,
30
+ pub identifier: Option<String>,
31
+ pub platform: Option<BundleIdPlatform>,
32
+ pub seed_id: Option<String>,
33
+}
34
+
35
+pub type BundleId = Resource<BundleIdAttributes>;
36
+
37
+#[derive(Debug, Clone, Serialize)]
38
+#[serde(rename_all = "camelCase")]
39
+pub struct BundleIdCreateAttributes {
40
+ pub identifier: String,
41
+ pub name: String,
42
+ pub platform: BundleIdPlatform,
43
+}
44
+
45
+impl Client {
46
+ /// `GET /v1/bundleIds`, optionally filtered to an exact identifier —
47
+ /// check whether a bundle ID is already registered before trying to
48
+ /// create it, or before creating an App Store Version under an app
49
+ /// that uses it.
50
+ pub async fn list_bundle_ids(&self, filter_identifier: Option<&str>) -> Result<Vec<BundleId>> {
51
+ let mut query = Vec::new();
52
+ if let Some(identifier) = filter_identifier {
53
+ query.push(("filter[identifier]", identifier));
54
+ }
55
+ let doc: ListDocument<BundleIdAttributes> = self
56
+ .request(Method::GET, "/v1/bundleIds", &query, None::<&()>)
57
+ .await?;
58
+ Ok(doc.data)
59
+ }
60
+
61
+ /// `POST /v1/bundleIds` — registers a new bundle ID with the developer
62
+ /// account (Certificates, Identifiers & Profiles).
63
+ pub async fn create_bundle_id(&self, attributes: BundleIdCreateAttributes) -> Result<BundleId> {
64
+ let body = CreateBody {
65
+ data: CreateData {
66
+ resource_type: RESOURCE_TYPE,
67
+ attributes,
68
+ relationships: None::<()>,
69
+ },
70
+ };
71
+ let doc: Document<BundleIdAttributes> = self
72
+ .request(Method::POST, "/v1/bundleIds", &[], Some(&body))
73
+ .await?;
74
+ Ok(doc.data)
75
+ }
76
+}
crates/ascapi/src/client.rs
new
+167
@@ -0,0 +1,167 @@
1
+use crate::auth::ApiKey;
2
+use crate::error::{Error, Result};
3
+use crate::jsonapi::ErrorDocument;
4
+use reqwest::{Method, StatusCode};
5
+use serde::de::DeserializeOwned;
6
+use serde::Serialize;
7
+use std::sync::Mutex;
8
+use std::time::{Duration, Instant};
9
+
10
+const BASE_URL: &str = "https://api.appstoreconnect.apple.com";
11
+// Refresh a bit before the token's real expiry so an in-flight request never
12
+// races a signature Apple has already started rejecting.
13
+const TOKEN_REFRESH_MARGIN: Duration = Duration::from_secs(60);
14
+
15
+struct CachedToken {
16
+ value: String,
17
+ minted_at: Instant,
18
+ lifetime: Duration,
19
+}
20
+
21
+/// Thin async HTTP client for the App Store Connect API. Handles JWT minting
22
+/// (cached and refreshed automatically) and JSON:API request/response
23
+/// plumbing. Resource-specific calls (apps, app store versions, bundle IDs,
24
+/// ...) are implemented as additional `impl Client` blocks alongside each
25
+/// resource's types, in their own modules.
26
+pub struct Client {
27
+ http: reqwest::Client,
28
+ api_key: ApiKey,
29
+ base_url: String,
30
+ token: Mutex<Option<CachedToken>>,
31
+}
32
+
33
+impl Client {
34
+ pub fn new(api_key: ApiKey) -> Self {
35
+ Self {
36
+ http: reqwest::Client::new(),
37
+ api_key,
38
+ base_url: BASE_URL.to_string(),
39
+ token: Mutex::new(None),
40
+ }
41
+ }
42
+
43
+ /// Override the API host — only meaningful for pointing the client at a
44
+ /// mock server in tests.
45
+ pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
46
+ self.base_url = base_url.into();
47
+ self
48
+ }
49
+
50
+ fn bearer_token(&self) -> Result<String> {
51
+ let mut guard = self
52
+ .token
53
+ .lock()
54
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
55
+
56
+ if let Some(cached) = guard.as_ref() {
57
+ if cached.minted_at.elapsed() + TOKEN_REFRESH_MARGIN < cached.lifetime {
58
+ return Ok(cached.value.clone());
59
+ }
60
+ }
61
+
62
+ let value = self.api_key.token()?;
63
+ *guard = Some(CachedToken {
64
+ value: value.clone(),
65
+ minted_at: Instant::now(),
66
+ lifetime: Duration::from_secs(self.api_key.lifetime_secs()),
67
+ });
68
+ Ok(value)
69
+ }
70
+
71
+ /// Send a request and decode a JSON:API response body into `T`. Use
72
+ /// `request_no_content` instead for calls (typically `DELETE`) that
73
+ /// return an empty `204` body.
74
+ pub(crate) async fn request<B: Serialize, T: DeserializeOwned>(
75
+ &self,
76
+ method: Method,
77
+ path: &str,
78
+ query: &[(&str, &str)],
79
+ body: Option<&B>,
80
+ ) -> Result<T> {
81
+ let (status, bytes) = self.send(method, path, query, body).await?;
82
+ if !status.is_success() {
83
+ return Err(api_error(status, &bytes));
84
+ }
85
+ Ok(serde_json::from_slice(&bytes)?)
86
+ }
87
+
88
+ /// Send a request that returns no body on success (typically `DELETE`).
89
+ pub(crate) async fn request_no_content<B: Serialize>(
90
+ &self,
91
+ method: Method,
92
+ path: &str,
93
+ query: &[(&str, &str)],
94
+ body: Option<&B>,
95
+ ) -> Result<()> {
96
+ let (status, bytes) = self.send(method, path, query, body).await?;
97
+ if !status.is_success() {
98
+ return Err(api_error(status, &bytes));
99
+ }
100
+ Ok(())
101
+ }
102
+
103
+ async fn send<B: Serialize>(
104
+ &self,
105
+ method: Method,
106
+ path: &str,
107
+ query: &[(&str, &str)],
108
+ body: Option<&B>,
109
+ ) -> Result<(StatusCode, Vec<u8>)> {
110
+ let url = format!("{}{}", self.base_url, path);
111
+ let token = self.bearer_token()?;
112
+ let mut req = self.http.request(method, &url).bearer_auth(token);
113
+ if !query.is_empty() {
114
+ req = req.query(query);
115
+ }
116
+ if let Some(b) = body {
117
+ req = req.json(b);
118
+ }
119
+
120
+ let response = req.send().await?;
121
+ let status = response.status();
122
+ let bytes = response.bytes().await?.to_vec();
123
+ Ok((status, bytes))
124
+ }
125
+
126
+ /// PUTs a raw byte body to a pre-signed asset-upload URL (as returned by
127
+ /// e.g. `appScreenshots.uploadOperations`). Unlike [`Client::request`],
128
+ /// this does **not** prefix `base_url` or attach the ASC bearer token —
129
+ /// upload URLs are pre-signed and carry their own auth in
130
+ /// `request_headers`; sending our JWT alongside would be wrong for a
131
+ /// host that isn't `api.appstoreconnect.apple.com`.
132
+ pub(crate) async fn upload_bytes(
133
+ &self,
134
+ method: Method,
135
+ url: &str,
136
+ headers: &[(String, String)],
137
+ body: Vec<u8>,
138
+ ) -> Result<()> {
139
+ let mut req = self.http.request(method, url);
140
+ for (name, value) in headers {
141
+ req = req.header(name.as_str(), value.as_str());
142
+ }
143
+ let response = req.body(body).send().await?;
144
+ let status = response.status();
145
+ if !status.is_success() {
146
+ let bytes = response.bytes().await?.to_vec();
147
+ return Err(api_error(status, &bytes));
148
+ }
149
+ Ok(())
150
+ }
151
+}
152
+
153
+fn api_error(status: StatusCode, bytes: &[u8]) -> Error {
154
+ let detail = match serde_json::from_slice::<ErrorDocument>(bytes) {
155
+ Ok(doc) if !doc.errors.is_empty() => doc
156
+ .errors
157
+ .iter()
158
+ .map(|e| format!("{} ({}): {}", e.title, e.code, e.detail))
159
+ .collect::<Vec<_>>()
160
+ .join("; "),
161
+ _ => String::from_utf8_lossy(bytes).into_owned(),
162
+ };
163
+ Error::Api {
164
+ status: status.as_u16(),
165
+ detail,
166
+ }
167
+}
crates/ascapi/src/error.rs
new
+25
@@ -0,0 +1,25 @@
1
+use thiserror::Error;
2
+
3
+#[derive(Debug, Error)]
4
+pub enum Error {
5
+ #[error("failed to read private key at {path}: {source}")]
6
+ PrivateKeyRead {
7
+ path: String,
8
+ #[source]
9
+ source: std::io::Error,
10
+ },
11
+
12
+ #[error("failed to sign JWT: {0}")]
13
+ Jwt(#[from] jsonwebtoken::errors::Error),
14
+
15
+ #[error("http request failed: {0}")]
16
+ Http(#[from] reqwest::Error),
17
+
18
+ #[error("failed to decode JSON response: {0}")]
19
+ Json(#[from] serde_json::Error),
20
+
21
+ #[error("App Store Connect API returned {status}: {detail}")]
22
+ Api { status: u16, detail: String },
23
+}
24
+
25
+pub type Result<T> = std::result::Result<T, Error>;
crates/ascapi/src/jsonapi.rs
new
+107
@@ -0,0 +1,107 @@
1
+//! Shared JSON:API envelope types used across every App Metadata resource.
2
+//! Resource-specific attribute structs live in their own modules
3
+//! (`app`, `app_store_version`, ...) and plug into these generics.
4
+
5
+use serde::{Deserialize, Serialize};
6
+
7
+/// A single-resource JSON:API document — the body of e.g. `GET
8
+/// /v1/apps/{id}` or the response to a `POST`/`PATCH`.
9
+#[derive(Debug, Clone, Deserialize)]
10
+pub struct Document<A> {
11
+ pub data: Resource<A>,
12
+}
13
+
14
+/// A resource-collection JSON:API document — the body of e.g. `GET
15
+/// /v1/apps`.
16
+#[derive(Debug, Clone, Deserialize)]
17
+pub struct ListDocument<A> {
18
+ pub data: Vec<Resource<A>>,
19
+}
20
+
21
+/// A single JSON:API resource object: its id, type, and typed attributes.
22
+/// Relationships are intentionally left untyped (`serde_json::Value`) —
23
+/// this crate builds relationship payloads explicitly on requests rather
24
+/// than typing every relationship shape App Store Connect can return.
25
+#[derive(Debug, Clone, Serialize, Deserialize)]
26
+pub struct Resource<A> {
27
+ pub id: String,
28
+ #[serde(rename = "type")]
29
+ pub resource_type: String,
30
+ pub attributes: A,
31
+ #[serde(default, skip_serializing_if = "Option::is_none")]
32
+ pub relationships: Option<serde_json::Value>,
33
+}
34
+
35
+/// A bare `{ type, id }` resource identifier, used to point a create
36
+/// request's relationships at an existing resource.
37
+#[derive(Debug, Clone, Serialize, Deserialize)]
38
+pub struct ResourceId {
39
+ #[serde(rename = "type")]
40
+ pub resource_type: &'static str,
41
+ pub id: String,
42
+}
43
+
44
+/// A to-one relationship payload, e.g. `relationships.app` on a
45
+/// `POST /v1/appStoreVersions` body.
46
+#[derive(Debug, Clone, Serialize)]
47
+pub struct ToOne {
48
+ pub data: ResourceId,
49
+}
50
+
51
+#[derive(Debug, Clone, Serialize)]
52
+pub struct CreateBody<A, R> {
53
+ pub data: CreateData<A, R>,
54
+}
55
+
56
+#[derive(Debug, Clone, Serialize)]
57
+pub struct CreateData<A, R> {
58
+ #[serde(rename = "type")]
59
+ pub resource_type: &'static str,
60
+ pub attributes: A,
61
+ #[serde(skip_serializing_if = "Option::is_none")]
62
+ pub relationships: Option<R>,
63
+}
64
+
65
+#[derive(Debug, Clone, Serialize)]
66
+pub struct UpdateBody<A> {
67
+ pub data: UpdateData<A>,
68
+}
69
+
70
+/// A `PATCH` body that updates only relationships (no attributes) — e.g.
71
+/// attaching a `Build` to an `AppStoreVersion`.
72
+#[derive(Debug, Clone, Serialize)]
73
+pub struct UpdateRelationshipsBody<R> {
74
+ pub data: UpdateRelationshipsData<R>,
75
+}
76
+
77
+#[derive(Debug, Clone, Serialize)]
78
+pub struct UpdateRelationshipsData<R> {
79
+ #[serde(rename = "type")]
80
+ pub resource_type: &'static str,
81
+ pub id: String,
82
+ pub relationships: R,
83
+}
84
+
85
+#[derive(Debug, Clone, Serialize)]
86
+pub struct UpdateData<A> {
87
+ #[serde(rename = "type")]
88
+ pub resource_type: &'static str,
89
+ pub id: String,
90
+ pub attributes: A,
91
+}
92
+
93
+/// The `errors` envelope App Store Connect returns on non-2xx responses.
94
+#[derive(Debug, Clone, Deserialize, Default)]
95
+pub struct ErrorDocument {
96
+ #[serde(default)]
97
+ pub errors: Vec<ApiError>,
98
+}
99
+
100
+#[derive(Debug, Clone, Deserialize)]
101
+pub struct ApiError {
102
+ pub status: String,
103
+ pub code: String,
104
+ pub title: String,
105
+ #[serde(default)]
106
+ pub detail: String,
107
+}
crates/cli/Cargo.toml
new
+20
@@ -0,0 +1,20 @@
1
+[package]
2
+name = "smbcloud-ascapi-cli"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+repository.workspace = true
8
+description = "CLI for adding/updating App Store Connect app metadata, built on smbcloud-ascapi."
9
+
10
+[[bin]]
11
+name = "ascapi"
12
+path = "src/main.rs"
13
+
14
+[dependencies]
15
+anyhow = { workspace = true }
16
+clap = { workspace = true, features = ["derive", "env"] }
17
+serde = { workspace = true }
18
+serde_json = { workspace = true }
19
+smbcloud-ascapi = { workspace = true }
20
+tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
crates/cli/src/main.rs
new
+729
@@ -0,0 +1,729 @@
1
+use anyhow::{Context, Result};
2
+use clap::{Parser, Subcommand, ValueEnum};
3
+use smbcloud_ascapi::app::AppUpdateAttributes;
4
+use smbcloud_ascapi::app_info_localization::{
5
+ AppInfoLocalizationCreateAttributes, AppInfoLocalizationUpdateAttributes,
6
+};
7
+use smbcloud_ascapi::app_screenshot_set::{
8
+ AppScreenshotSetCreateAttributes, ScreenshotDisplayType,
9
+};
10
+use smbcloud_ascapi::app_store_version::{AppStoreVersionCreateAttributes, Platform};
11
+use smbcloud_ascapi::app_store_version_localization::{
12
+ AppStoreVersionLocalizationCreateAttributes, AppStoreVersionLocalizationFields,
13
+};
14
+use smbcloud_ascapi::bundle_id::{BundleIdCreateAttributes, BundleIdPlatform};
15
+use smbcloud_ascapi::{ApiKey, Client};
16
+use std::path::PathBuf;
17
+
18
+/// Add/update App Store Connect app metadata (apps, app infos, app store
19
+/// versions, and their localizations) from the terminal or a script.
20
+#[derive(Parser)]
21
+#[command(name = "ascapi", version, about)]
22
+struct Cli {
23
+ /// App Store Connect API key ID (Users and Access → Integrations → App
24
+ /// Store Connect API).
25
+ #[arg(long, env = "ASC_API_KEY")]
26
+ key_id: String,
27
+
28
+ /// App Store Connect API issuer ID (same page as the key).
29
+ #[arg(long, env = "ASC_ISSUER_ID")]
30
+ issuer_id: String,
31
+
32
+ /// Path to the key's .p8 private key file. Defaults to
33
+ /// ~/.appstoreconnect/private_keys/AuthKey_<key-id>.p8, matching
34
+ /// Xcode's own convention.
35
+ #[arg(long, env = "ASC_PRIVATE_KEY_PATH", global = true)]
36
+ private_key_path: Option<PathBuf>,
37
+
38
+ /// Print the request body that would be sent instead of sending it.
39
+ /// Only affects commands that create or modify data.
40
+ #[arg(long, global = true)]
41
+ dry_run: bool,
42
+
43
+ #[command(subcommand)]
44
+ command: Command,
45
+}
46
+
47
+#[derive(Subcommand)]
48
+enum Command {
49
+ /// Look up apps by bundle ID, or a specific app by its ASC app ID.
50
+ Apps {
51
+ #[command(subcommand)]
52
+ command: AppsCommand,
53
+ },
54
+ /// Register or look up bundle identifiers.
55
+ BundleIds {
56
+ #[command(subcommand)]
57
+ command: BundleIdsCommand,
58
+ },
59
+ /// Per-platform App Store Version records for an app — create one to
60
+ /// add a new platform (e.g. visionOS) to an existing app.
61
+ AppStoreVersions {
62
+ #[command(subcommand)]
63
+ command: AppStoreVersionsCommand,
64
+ },
65
+ /// Localized name/subtitle for an app's AppInfo.
66
+ AppInfoLocalizations {
67
+ #[command(subcommand)]
68
+ command: AppInfoLocalizationsCommand,
69
+ },
70
+ /// Localized description/keywords/etc for an App Store Version.
71
+ AppStoreVersionLocalizations {
72
+ #[command(subcommand)]
73
+ command: AppStoreVersionLocalizationsCommand,
74
+ },
75
+ /// Per-device-class screenshot buckets on an App Store Version
76
+ /// Localization — create one, then add images to it with
77
+ /// `app-screenshots upload`.
78
+ AppScreenshotSets {
79
+ #[command(subcommand)]
80
+ command: AppScreenshotSetsCommand,
81
+ },
82
+ /// Screenshot image binaries within an AppScreenshotSet.
83
+ AppScreenshots {
84
+ #[command(subcommand)]
85
+ command: AppScreenshotsCommand,
86
+ },
87
+}
88
+
89
+#[derive(Subcommand)]
90
+enum AppsCommand {
91
+ /// `GET /v1/apps`.
92
+ List {
93
+ #[arg(long)]
94
+ bundle_id: Option<String>,
95
+ },
96
+ /// `GET /v1/apps/{id}`.
97
+ Get { app_id: String },
98
+ /// `PATCH /v1/apps/{id}`.
99
+ Update {
100
+ app_id: String,
101
+ #[arg(long)]
102
+ primary_locale: Option<String>,
103
+ },
104
+ /// `GET /v1/apps/{id}/appInfos`.
105
+ Infos { app_id: String },
106
+ /// `GET /v1/apps/{id}/builds` — newest first, so the
107
+ /// most recently uploaded build (e.g. after a fixplist re-upload) is
108
+ /// first. Check `processingState` (VALID/INVALID/PROCESSING/FAILED).
109
+ Builds { app_id: String },
110
+}
111
+
112
+#[derive(Subcommand)]
113
+enum BundleIdsCommand {
114
+ /// `GET /v1/bundleIds`.
115
+ List {
116
+ #[arg(long)]
117
+ identifier: Option<String>,
118
+ },
119
+ /// `POST /v1/bundleIds`.
120
+ Create {
121
+ #[arg(long)]
122
+ identifier: String,
123
+ #[arg(long)]
124
+ name: String,
125
+ #[arg(long, value_enum, default_value_t = CliBundleIdPlatform::Ios)]
126
+ platform: CliBundleIdPlatform,
127
+ },
128
+}
129
+
130
+#[derive(Subcommand)]
131
+enum AppStoreVersionsCommand {
132
+ /// `GET /v1/apps/{app_id}/appStoreVersions`.
133
+ List {
134
+ app_id: String,
135
+ #[arg(long, value_enum)]
136
+ platform: Option<CliPlatform>,
137
+ },
138
+ /// `GET /v1/appStoreVersions/{id}`.
139
+ Get { id: String },
140
+ /// `POST /v1/appStoreVersions`.
141
+ Create {
142
+ app_id: String,
143
+ #[arg(long, value_enum)]
144
+ platform: CliPlatform,
145
+ #[arg(long)]
146
+ version_string: String,
147
+ #[arg(long)]
148
+ copyright: Option<String>,
149
+ },
150
+ /// `DELETE /v1/appStoreVersions/{id}`.
151
+ Delete { id: String },
152
+ /// `PATCH /v1/appStoreVersions/{id}` — attach a `Build` (see
153
+ /// `apps builds`) to this version, e.g. after fixing an
154
+ /// `INVALID_BINARY` version with a re-uploaded build.
155
+ SetBuild {
156
+ id: String,
157
+ #[arg(long)]
158
+ build_id: String,
159
+ },
160
+}
161
+
162
+#[derive(Subcommand)]
163
+enum AppInfoLocalizationsCommand {
164
+ List {
165
+ app_info_id: String,
166
+ },
167
+ Create {
168
+ app_info_id: String,
169
+ #[arg(long)]
170
+ locale: String,
171
+ #[arg(long)]
172
+ name: String,
173
+ #[arg(long)]
174
+ subtitle: Option<String>,
175
+ },
176
+ Update {
177
+ id: String,
178
+ #[arg(long)]
179
+ name: Option<String>,
180
+ #[arg(long)]
181
+ subtitle: Option<String>,
182
+ },
183
+ Delete {
184
+ id: String,
185
+ },
186
+}
187
+
188
+#[derive(Subcommand)]
189
+enum AppStoreVersionLocalizationsCommand {
190
+ List {
191
+ app_store_version_id: String,
192
+ },
193
+ Create {
194
+ app_store_version_id: String,
195
+ #[arg(long)]
196
+ locale: String,
197
+ #[arg(long)]
198
+ description: Option<String>,
199
+ #[arg(long)]
200
+ keywords: Option<String>,
201
+ #[arg(long)]
202
+ whats_new: Option<String>,
203
+ #[arg(long)]
204
+ promotional_text: Option<String>,
205
+ #[arg(long)]
206
+ marketing_url: Option<String>,
207
+ #[arg(long)]
208
+ support_url: Option<String>,
209
+ },
210
+ Update {
211
+ id: String,
212
+ #[arg(long)]
213
+ description: Option<String>,
214
+ #[arg(long)]
215
+ keywords: Option<String>,
216
+ #[arg(long)]
217
+ whats_new: Option<String>,
218
+ #[arg(long)]
219
+ promotional_text: Option<String>,
220
+ #[arg(long)]
221
+ marketing_url: Option<String>,
222
+ #[arg(long)]
223
+ support_url: Option<String>,
224
+ },
225
+ Delete {
226
+ id: String,
227
+ },
228
+}
229
+
230
+#[derive(Subcommand)]
231
+enum AppScreenshotSetsCommand {
232
+ /// `GET /v1/appStoreVersionLocalizations/{id}/appScreenshotSets`.
233
+ List {
234
+ app_store_version_localization_id: String,
235
+ },
236
+ /// `POST /v1/appScreenshotSets`.
237
+ Create {
238
+ app_store_version_localization_id: String,
239
+ #[arg(long, value_enum)]
240
+ display_type: CliScreenshotDisplayType,
241
+ },
242
+ /// `DELETE /v1/appScreenshotSets/{id}`.
243
+ Delete { id: String },
244
+}
245
+
246
+#[derive(Subcommand)]
247
+enum AppScreenshotsCommand {
248
+ /// `GET /v1/appScreenshotSets/{id}/appScreenshots`.
249
+ List { app_screenshot_set_id: String },
250
+ /// Reserve, PUT, and commit an image file in one call — the full
251
+ /// `POST /v1/appScreenshots` → upload → `PATCH …uploaded:true` flow.
252
+ Upload {
253
+ app_screenshot_set_id: String,
254
+ #[arg(long)]
255
+ file: PathBuf,
256
+ },
257
+ /// `DELETE /v1/appScreenshots/{id}`.
258
+ Delete { id: String },
259
+}
260
+
261
+#[derive(Debug, Clone, Copy, ValueEnum)]
262
+enum CliScreenshotDisplayType {
263
+ AppleVisionPro,
264
+ Iphone67,
265
+ Iphone65,
266
+ Iphone61,
267
+ Iphone58,
268
+ Iphone55,
269
+ Iphone47,
270
+ Iphone40,
271
+ Iphone35,
272
+ IpadPro3Gen129,
273
+ IpadPro3Gen11,
274
+ IpadPro129,
275
+ Ipad105,
276
+ Ipad97,
277
+ Desktop,
278
+ AppleTv,
279
+ WatchUltra,
280
+ WatchSeries10,
281
+ WatchSeries7,
282
+ WatchSeries4,
283
+ WatchSeries3,
284
+}
285
+
286
+impl From<CliScreenshotDisplayType> for ScreenshotDisplayType {
287
+ fn from(value: CliScreenshotDisplayType) -> Self {
288
+ match value {
289
+ CliScreenshotDisplayType::AppleVisionPro => ScreenshotDisplayType::AppleVisionPro,
290
+ CliScreenshotDisplayType::Iphone67 => ScreenshotDisplayType::Iphone67,
291
+ CliScreenshotDisplayType::Iphone65 => ScreenshotDisplayType::Iphone65,
292
+ CliScreenshotDisplayType::Iphone61 => ScreenshotDisplayType::Iphone61,
293
+ CliScreenshotDisplayType::Iphone58 => ScreenshotDisplayType::Iphone58,
294
+ CliScreenshotDisplayType::Iphone55 => ScreenshotDisplayType::Iphone55,
295
+ CliScreenshotDisplayType::Iphone47 => ScreenshotDisplayType::Iphone47,
296
+ CliScreenshotDisplayType::Iphone40 => ScreenshotDisplayType::Iphone40,
297
+ CliScreenshotDisplayType::Iphone35 => ScreenshotDisplayType::Iphone35,
298
+ CliScreenshotDisplayType::IpadPro3Gen129 => ScreenshotDisplayType::IpadPro3Gen129,
299
+ CliScreenshotDisplayType::IpadPro3Gen11 => ScreenshotDisplayType::IpadPro3Gen11,
300
+ CliScreenshotDisplayType::IpadPro129 => ScreenshotDisplayType::IpadPro129,
301
+ CliScreenshotDisplayType::Ipad105 => ScreenshotDisplayType::Ipad105,
302
+ CliScreenshotDisplayType::Ipad97 => ScreenshotDisplayType::Ipad97,
303
+ CliScreenshotDisplayType::Desktop => ScreenshotDisplayType::Desktop,
304
+ CliScreenshotDisplayType::AppleTv => ScreenshotDisplayType::AppleTv,
305
+ CliScreenshotDisplayType::WatchUltra => ScreenshotDisplayType::WatchUltra,
306
+ CliScreenshotDisplayType::WatchSeries10 => ScreenshotDisplayType::WatchSeries10,
307
+ CliScreenshotDisplayType::WatchSeries7 => ScreenshotDisplayType::WatchSeries7,
308
+ CliScreenshotDisplayType::WatchSeries4 => ScreenshotDisplayType::WatchSeries4,
309
+ CliScreenshotDisplayType::WatchSeries3 => ScreenshotDisplayType::WatchSeries3,
310
+ }
311
+ }
312
+}
313
+
314
+#[derive(Debug, Clone, Copy, ValueEnum)]
315
+enum CliPlatform {
316
+ Ios,
317
+ MacOs,
318
+ TvOs,
319
+ VisionOs,
320
+}
321
+
322
+impl From<CliPlatform> for Platform {
323
+ fn from(value: CliPlatform) -> Self {
324
+ match value {
325
+ CliPlatform::Ios => Platform::Ios,
326
+ CliPlatform::MacOs => Platform::MacOs,
327
+ CliPlatform::TvOs => Platform::TvOs,
328
+ CliPlatform::VisionOs => Platform::VisionOs,
329
+ }
330
+ }
331
+}
332
+
333
+#[derive(Debug, Clone, Copy, ValueEnum)]
334
+enum CliBundleIdPlatform {
335
+ Ios,
336
+ MacOs,
337
+ Universal,
338
+}
339
+
340
+impl From<CliBundleIdPlatform> for BundleIdPlatform {
341
+ fn from(value: CliBundleIdPlatform) -> Self {
342
+ match value {
343
+ CliBundleIdPlatform::Ios => BundleIdPlatform::Ios,
344
+ CliBundleIdPlatform::MacOs => BundleIdPlatform::MacOs,
345
+ CliBundleIdPlatform::Universal => BundleIdPlatform::Universal,
346
+ }
347
+ }
348
+}
349
+
350
+fn print_json(value: &impl serde::Serialize) -> Result<()> {
351
+ println!("{}", serde_json::to_string_pretty(value)?);
352
+ Ok(())
353
+}
354
+
355
+/// Print `value` and return without sending anything, when `--dry-run` is
356
+/// set. Every mutating command checks this before touching the network.
357
+fn dry_run_guard(dry_run: bool, value: &impl serde::Serialize) -> Result<bool> {
358
+ if dry_run {
359
+ println!("# dry run — request body that would be sent:");
360
+ print_json(value)?;
361
+ }
362
+ Ok(dry_run)
363
+}
364
+
365
+#[tokio::main]
366
+async fn main() -> Result<()> {
367
+ let cli = Cli::parse();
368
+
369
+ let private_key_path = cli.private_key_path.clone().unwrap_or_else(|| {
370
+ let mut path = dirs_home();
371
+ path.push(".appstoreconnect");
372
+ path.push("private_keys");
373
+ path.push(format!("AuthKey_{}.p8", cli.key_id));
374
+ path
375
+ });
376
+
377
+ let api_key = ApiKey::from_p8_file(&cli.key_id, &cli.issuer_id, &private_key_path)
378
+ .with_context(|| format!("loading App Store Connect API key from {private_key_path:?}"))?;
379
+ let client = Client::new(api_key);
380
+
381
+ match cli.command {
382
+ Command::Apps { command } => run_apps(&client, command, cli.dry_run).await,
383
+ Command::BundleIds { command } => run_bundle_ids(&client, command, cli.dry_run).await,
384
+ Command::AppStoreVersions { command } => {
385
+ run_app_store_versions(&client, command, cli.dry_run).await
386
+ }
387
+ Command::AppInfoLocalizations { command } => {
388
+ run_app_info_localizations(&client, command, cli.dry_run).await
389
+ }
390
+ Command::AppStoreVersionLocalizations { command } => {
391
+ run_app_store_version_localizations(&client, command, cli.dry_run).await
392
+ }
393
+ Command::AppScreenshotSets { command } => {
394
+ run_app_screenshot_sets(&client, command, cli.dry_run).await
395
+ }
396
+ Command::AppScreenshots { command } => {
397
+ run_app_screenshots(&client, command, cli.dry_run).await
398
+ }
399
+ }
400
+}
401
+
402
+fn dirs_home() -> PathBuf {
403
+ // Avoids pulling in the `dirs` crate for a single lookup; $HOME is set
404
+ // on every platform this CLI targets (macOS/Linux CI runners).
405
+ std::env::var_os("HOME")
406
+ .map(PathBuf::from)
407
+ .unwrap_or_else(|| PathBuf::from("."))
408
+}
409
+
410
+async fn run_apps(client: &Client, command: AppsCommand, dry_run: bool) -> Result<()> {
411
+ match command {
412
+ AppsCommand::List { bundle_id } => {
413
+ let apps = client.list_apps(bundle_id.as_deref()).await?;
414
+ print_json(&apps)
415
+ }
416
+ AppsCommand::Get { app_id } => print_json(&client.get_app(&app_id).await?),
417
+ AppsCommand::Update {
418
+ app_id,
419
+ primary_locale,
420
+ } => {
421
+ let attributes = AppUpdateAttributes {
422
+ primary_locale,
423
+ content_rights_declaration: None,
424
+ };
425
+ if dry_run_guard(dry_run, &attributes)? {
426
+ return Ok(());
427
+ }
428
+ print_json(&client.update_app(&app_id, attributes).await?)
429
+ }
430
+ AppsCommand::Infos { app_id } => print_json(&client.list_app_infos(&app_id).await?),
431
+ AppsCommand::Builds { app_id } => print_json(&client.list_builds(&app_id).await?),
432
+ }
433
+}
434
+
435
+async fn run_bundle_ids(client: &Client, command: BundleIdsCommand, dry_run: bool) -> Result<()> {
436
+ match command {
437
+ BundleIdsCommand::List { identifier } => {
438
+ print_json(&client.list_bundle_ids(identifier.as_deref()).await?)
439
+ }
440
+ BundleIdsCommand::Create {
441
+ identifier,
442
+ name,
443
+ platform,
444
+ } => {
445
+ let attributes = BundleIdCreateAttributes {
446
+ identifier,
447
+ name,
448
+ platform: platform.into(),
449
+ };
450
+ if dry_run_guard(dry_run, &attributes)? {
451
+ return Ok(());
452
+ }
453
+ print_json(&client.create_bundle_id(attributes).await?)
454
+ }
455
+ }
456
+}
457
+
458
+async fn run_app_store_versions(
459
+ client: &Client,
460
+ command: AppStoreVersionsCommand,
461
+ dry_run: bool,
462
+) -> Result<()> {
463
+ match command {
464
+ AppStoreVersionsCommand::List { app_id, platform } => {
465
+ let versions = client
466
+ .list_app_store_versions(&app_id, platform.map(Into::into))
467
+ .await?;
468
+ print_json(&versions)
469
+ }
470
+ AppStoreVersionsCommand::Get { id } => {
471
+ print_json(&client.get_app_store_version(&id).await?)
472
+ }
473
+ AppStoreVersionsCommand::Create {
474
+ app_id,
475
+ platform,
476
+ version_string,
477
+ copyright,
478
+ } => {
479
+ let attributes = AppStoreVersionCreateAttributes {
480
+ platform: platform.into(),
481
+ version_string,
482
+ copyright,
483
+ };
484
+ if dry_run_guard(dry_run, &attributes)? {
485
+ return Ok(());
486
+ }
487
+ print_json(&client.create_app_store_version(&app_id, attributes).await?)
488
+ }
489
+ AppStoreVersionsCommand::Delete { id } => {
490
+ if dry_run {
491
+ println!("# dry run — would DELETE /v1/appStoreVersions/{id}");
492
+ return Ok(());
493
+ }
494
+ client.delete_app_store_version(&id).await?;
495
+ println!("deleted {id}");
496
+ Ok(())
497
+ }
498
+ AppStoreVersionsCommand::SetBuild { id, build_id } => {
499
+ if dry_run {
500
+ println!(
501
+ "# dry run — would PATCH /v1/appStoreVersions/{id} relationships.build → {build_id}"
502
+ );
503
+ return Ok(());
504
+ }
505
+ print_json(&client.set_app_store_version_build(&id, &build_id).await?)
506
+ }
507
+ }
508
+}
509
+
510
+async fn run_app_info_localizations(
511
+ client: &Client,
512
+ command: AppInfoLocalizationsCommand,
513
+ dry_run: bool,
514
+) -> Result<()> {
515
+ match command {
516
+ AppInfoLocalizationsCommand::List { app_info_id } => {
517
+ print_json(&client.list_app_info_localizations(&app_info_id).await?)
518
+ }
519
+ AppInfoLocalizationsCommand::Create {
520
+ app_info_id,
521
+ locale,
522
+ name,
523
+ subtitle,
524
+ } => {
525
+ let attributes = AppInfoLocalizationCreateAttributes {
526
+ locale,
527
+ name,
528
+ subtitle,
529
+ privacy_policy_url: None,
530
+ };
531
+ if dry_run_guard(dry_run, &attributes)? {
532
+ return Ok(());
533
+ }
534
+ print_json(
535
+ &client
536
+ .create_app_info_localization(&app_info_id, attributes)
537
+ .await?,
538
+ )
539
+ }
540
+ AppInfoLocalizationsCommand::Update { id, name, subtitle } => {
541
+ let attributes = AppInfoLocalizationUpdateAttributes {
542
+ name,
543
+ subtitle,
544
+ privacy_policy_url: None,
545
+ };
546
+ if dry_run_guard(dry_run, &attributes)? {
547
+ return Ok(());
548
+ }
549
+ print_json(&client.update_app_info_localization(&id, attributes).await?)
550
+ }
551
+ AppInfoLocalizationsCommand::Delete { id } => {
552
+ if dry_run {
553
+ println!("# dry run — would DELETE /v1/appInfoLocalizations/{id}");
554
+ return Ok(());
555
+ }
556
+ client.delete_app_info_localization(&id).await?;
557
+ println!("deleted {id}");
558
+ Ok(())
559
+ }
560
+ }
561
+}
562
+
563
+async fn run_app_store_version_localizations(
564
+ client: &Client,
565
+ command: AppStoreVersionLocalizationsCommand,
566
+ dry_run: bool,
567
+) -> Result<()> {
568
+ match command {
569
+ AppStoreVersionLocalizationsCommand::List {
570
+ app_store_version_id,
571
+ } => print_json(
572
+ &client
573
+ .list_app_store_version_localizations(&app_store_version_id)
574
+ .await?,
575
+ ),
576
+ AppStoreVersionLocalizationsCommand::Create {
577
+ app_store_version_id,
578
+ locale,
579
+ description,
580
+ keywords,
581
+ whats_new,
582
+ promotional_text,
583
+ marketing_url,
584
+ support_url,
585
+ } => {
586
+ let attributes = AppStoreVersionLocalizationCreateAttributes {
587
+ locale,
588
+ fields: AppStoreVersionLocalizationFields {
589
+ description,
590
+ keywords,
591
+ whats_new,
592
+ promotional_text,
593
+ marketing_url,
594
+ support_url,
595
+ },
596
+ };
597
+ if dry_run_guard(dry_run, &attributes)? {
598
+ return Ok(());
599
+ }
600
+ print_json(
601
+ &client
602
+ .create_app_store_version_localization(&app_store_version_id, attributes)
603
+ .await?,
604
+ )
605
+ }
606
+ AppStoreVersionLocalizationsCommand::Update {
607
+ id,
608
+ description,
609
+ keywords,
610
+ whats_new,
611
+ promotional_text,
612
+ marketing_url,
613
+ support_url,
614
+ } => {
615
+ let fields = AppStoreVersionLocalizationFields {
616
+ description,
617
+ keywords,
618
+ whats_new,
619
+ promotional_text,
620
+ marketing_url,
621
+ support_url,
622
+ };
623
+ if dry_run_guard(dry_run, &fields)? {
624
+ return Ok(());
625
+ }
626
+ print_json(
627
+ &client
628
+ .update_app_store_version_localization(&id, fields)
629
+ .await?,
630
+ )
631
+ }
632
+ AppStoreVersionLocalizationsCommand::Delete { id } => {
633
+ if dry_run {
634
+ println!("# dry run — would DELETE /v1/appStoreVersionLocalizations/{id}");
635
+ return Ok(());
636
+ }
637
+ client.delete_app_store_version_localization(&id).await?;
638
+ println!("deleted {id}");
639
+ Ok(())
640
+ }
641
+ }
642
+}
643
+
644
+async fn run_app_screenshot_sets(
645
+ client: &Client,
646
+ command: AppScreenshotSetsCommand,
647
+ dry_run: bool,
648
+) -> Result<()> {
649
+ match command {
650
+ AppScreenshotSetsCommand::List {
651
+ app_store_version_localization_id,
652
+ } => print_json(
653
+ &client
654
+ .list_app_screenshot_sets(&app_store_version_localization_id)
655
+ .await?,
656
+ ),
657
+ AppScreenshotSetsCommand::Create {
658
+ app_store_version_localization_id,
659
+ display_type,
660
+ } => {
661
+ let attributes = AppScreenshotSetCreateAttributes {
662
+ screenshot_display_type: display_type.into(),
663
+ };
664
+ if dry_run_guard(dry_run, &attributes)? {
665
+ return Ok(());
666
+ }
667
+ print_json(
668
+ &client
669
+ .create_app_screenshot_set(&app_store_version_localization_id, attributes)
670
+ .await?,
671
+ )
672
+ }
673
+ AppScreenshotSetsCommand::Delete { id } => {
674
+ if dry_run {
675
+ println!("# dry run — would DELETE /v1/appScreenshotSets/{id}");
676
+ return Ok(());
677
+ }
678
+ client.delete_app_screenshot_set(&id).await?;
679
+ println!("deleted {id}");
680
+ Ok(())
681
+ }
682
+ }
683
+}
684
+
685
+async fn run_app_screenshots(
686
+ client: &Client,
687
+ command: AppScreenshotsCommand,
688
+ dry_run: bool,
689
+) -> Result<()> {
690
+ match command {
691
+ AppScreenshotsCommand::List {
692
+ app_screenshot_set_id,
693
+ } => print_json(&client.list_app_screenshots(&app_screenshot_set_id).await?),
694
+ AppScreenshotsCommand::Upload {
695
+ app_screenshot_set_id,
696
+ file,
697
+ } => {
698
+ let file_name = file
699
+ .file_name()
700
+ .map(|n| n.to_string_lossy().into_owned())
701
+ .with_context(|| format!("{file:?} has no file name"))?;
702
+ let bytes = std::fs::read(&file)
703
+ .with_context(|| format!("reading screenshot file {file:?}"))?;
704
+
705
+ if dry_run {
706
+ println!(
707
+ "# dry run — would upload {file_name} ({} bytes) to appScreenshotSet {app_screenshot_set_id}",
708
+ bytes.len()
709
+ );
710
+ return Ok(());
711
+ }
712
+
713
+ print_json(
714
+ &client
715
+ .upload_app_screenshot(&app_screenshot_set_id, file_name, bytes)
716
+ .await?,
717
+ )
718
+ }
719
+ AppScreenshotsCommand::Delete { id } => {
720
+ if dry_run {
721
+ println!("# dry run — would DELETE /v1/appScreenshots/{id}");
722
+ return Ok(());
723
+ }
724
+ client.delete_app_screenshot(&id).await?;
725
+ println!("deleted {id}");
726
+ Ok(())
727
+ }
728
+ }
729
+}