feat(cli): add sales report downloads
Add App Store Connect sales report downloads to the CLI, including account-level sales report retrieval, gzip TSV parsing, and JSON output. Co-Authored-By: siGit Code <sigit@sigit.si>
Seto Elkahfi committed
Sep 12, 2026 at 19:05 UTC
4f38474d44229527d04b54d483e21795dfd6858e
8 files changed
+505
-71
Cargo.lock
+166
-69
@@ -2,6 +2,12 @@
2
# It is not intended for manual editing.
3
version = 4
4
5
+[[package]]
6
+name = "adler2"
7
+version = "2.0.1"
8
+source = "registry+https://github.com/rust-lang/crates.io-index"
9
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10
+
11
[[package]]
12
name = "android_system_properties"
13
version = "0.1.6"
@@ -114,7 +120,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
120
dependencies = [
121
"proc-macro2",
122
"quote",
117
- "syn 3.0.4",
123
+ "syn 3.0.5",
124
]
125
126
[[package]]
@@ -131,9 +137,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
137
138
[[package]]
139
name = "aws-lc-rs"
134
-version = "1.18.0"
140
+version = "1.18.1"
141
source = "registry+https://github.com/rust-lang/crates.io-index"
136
-checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
142
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
143
dependencies = [
144
"aws-lc-sys",
145
"untrusted 0.7.1",
@@ -142,9 +148,9 @@ dependencies = [
148
149
[[package]]
150
name = "aws-lc-sys"
145
-version = "0.44.0"
151
+version = "0.45.0"
152
source = "registry+https://github.com/rust-lang/crates.io-index"
147
-checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
153
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
154
dependencies = [
155
"cc",
156
"cmake",
@@ -159,6 +165,12 @@ version = "0.22.1"
165
source = "registry+https://github.com/rust-lang/crates.io-index"
166
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
167
168
+[[package]]
169
+name = "base64"
170
+version = "0.23.1"
171
+source = "registry+https://github.com/rust-lang/crates.io-index"
172
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
173
+
174
[[package]]
175
name = "base64ct"
176
version = "1.8.3"
@@ -176,9 +188,9 @@ dependencies = [
188
189
[[package]]
190
name = "bitflags"
179
-version = "2.13.1"
191
+version = "2.13.2"
192
source = "registry+https://github.com/rust-lang/crates.io-index"
181
-checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
193
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
194
195
[[package]]
196
name = "bumpalo"
@@ -194,9 +206,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
206
207
[[package]]
208
name = "cc"
197
-version = "1.4.4"
209
+version = "1.4.5"
210
source = "registry+https://github.com/rust-lang/crates.io-index"
199
-checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
211
+checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
212
dependencies = [
213
"find-msvc-tools",
214
"jobserver",
@@ -270,7 +282,7 @@ dependencies = [
282
"heck",
283
"proc-macro2",
284
"quote",
273
- "syn 3.0.4",
285
+ "syn 3.0.5",
286
]
287
288
[[package]]
@@ -318,13 +330,22 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
330
331
[[package]]
332
name = "cpufeatures"
321
-version = "0.3.0"
333
+version = "0.3.1"
334
source = "registry+https://github.com/rust-lang/crates.io-index"
323
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
335
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
336
dependencies = [
337
"libc",
338
]
339
340
+[[package]]
341
+name = "crc32fast"
342
+version = "1.5.1"
343
+source = "registry+https://github.com/rust-lang/crates.io-index"
344
+checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
345
+dependencies = [
346
+ "cfg-if",
347
+]
348
+
349
[[package]]
350
name = "crypto-common"
351
version = "0.1.7"
@@ -335,6 +356,27 @@ dependencies = [
356
"typenum",
357
]
358
359
+[[package]]
360
+name = "csv"
361
+version = "1.4.0"
362
+source = "registry+https://github.com/rust-lang/crates.io-index"
363
+checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
364
+dependencies = [
365
+ "csv-core",
366
+ "itoa",
367
+ "ryu",
368
+ "serde_core",
369
+]
370
+
371
+[[package]]
372
+name = "csv-core"
373
+version = "0.1.13"
374
+source = "registry+https://github.com/rust-lang/crates.io-index"
375
+checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
376
+dependencies = [
377
+ "memchr",
378
+]
379
+
380
[[package]]
381
name = "darling"
382
version = "0.23.0"
@@ -424,7 +466,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
466
dependencies = [
467
"proc-macro2",
468
"quote",
427
- "syn 3.0.4",
469
+ "syn 3.0.5",
470
]
471
472
[[package]]
@@ -441,9 +483,20 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
483
484
[[package]]
485
name = "find-msvc-tools"
444
-version = "0.1.11"
486
+version = "0.1.12"
487
+source = "registry+https://github.com/rust-lang/crates.io-index"
488
+checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
489
+
490
+[[package]]
491
+name = "flate2"
492
+version = "1.1.10"
493
source = "registry+https://github.com/rust-lang/crates.io-index"
446
-checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
494
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
495
+dependencies = [
496
+ "crc32fast",
497
+ "miniz_oxide",
498
+ "zlib-rs",
499
+]
500
501
[[package]]
502
name = "form_urlencoded"
@@ -516,7 +569,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
569
dependencies = [
570
"proc-macro2",
571
"quote",
519
- "syn 3.0.4",
572
+ "syn 3.0.5",
573
]
574
575
[[package]]
@@ -632,9 +685,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
685
686
[[package]]
687
name = "hyper"
635
-version = "1.11.0"
688
+version = "1.11.1"
689
source = "registry+https://github.com/rust-lang/crates.io-index"
637
-checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
690
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
691
dependencies = [
692
"atomic-waker",
693
"bytes",
@@ -672,7 +725,7 @@ version = "0.1.20"
725
source = "registry+https://github.com/rust-lang/crates.io-index"
726
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
727
dependencies = [
675
- "base64",
728
+ "base64 0.22.1",
729
"bytes",
730
"futures-channel",
731
"futures-util",
@@ -825,9 +878,9 @@ dependencies = [
878
879
[[package]]
880
name = "ipnet"
828
-version = "2.12.1"
881
+version = "2.12.2"
882
source = "registry+https://github.com/rust-lang/crates.io-index"
830
-checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
883
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
884
885
[[package]]
886
name = "is_terminal_polyfill"
@@ -853,9 +906,9 @@ dependencies = [
906
907
[[package]]
908
name = "js-sys"
856
-version = "0.3.104"
909
+version = "0.3.105"
910
source = "registry+https://github.com/rust-lang/crates.io-index"
858
-checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
911
+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
912
dependencies = [
913
"cfg-if",
914
"futures-util",
@@ -868,9 +921,9 @@ version = "9.3.1"
921
source = "registry+https://github.com/rust-lang/crates.io-index"
922
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
923
dependencies = [
871
- "base64",
924
+ "base64 0.22.1",
925
"js-sys",
873
- "pem",
926
+ "pem 3.0.6",
927
"ring",
928
"serde",
929
"serde_json",
@@ -934,11 +987,21 @@ version = "0.2.1"
987
source = "registry+https://github.com/rust-lang/crates.io-index"
988
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
989
990
+[[package]]
991
+name = "miniz_oxide"
992
+version = "0.9.1"
993
+source = "registry+https://github.com/rust-lang/crates.io-index"
994
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
995
+dependencies = [
996
+ "adler2",
997
+ "simd-adler32",
998
+]
999
+
1000
[[package]]
1001
name = "mio"
939
-version = "1.2.2"
1002
+version = "1.2.3"
1003
source = "registry+https://github.com/rust-lang/crates.io-index"
941
-checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
1004
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
1005
dependencies = [
1006
"libc",
1007
"wasi",
@@ -1055,7 +1118,17 @@ version = "3.0.6"
1118
source = "registry+https://github.com/rust-lang/crates.io-index"
1119
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
1120
dependencies = [
1058
- "base64",
1121
+ "base64 0.22.1",
1122
+ "serde_core",
1123
+]
1124
+
1125
+[[package]]
1126
+name = "pem"
1127
+version = "4.0.0"
1128
+source = "registry+https://github.com/rust-lang/crates.io-index"
1129
+checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120"
1130
+dependencies = [
1131
+ "base64 0.23.1",
1132
"serde_core",
1133
]
1134
@@ -1269,12 +1342,12 @@ dependencies = [
1342
1343
[[package]]
1344
name = "rcgen"
1272
-version = "0.14.9"
1345
+version = "0.14.10"
1346
source = "registry+https://github.com/rust-lang/crates.io-index"
1274
-checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4"
1347
+checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8"
1348
dependencies = [
1349
"aws-lc-rs",
1277
- "pem",
1350
+ "pem 4.0.0",
1351
"rustls-pki-types",
1352
"time",
1353
"x509-parser",
@@ -1298,7 +1371,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
1371
dependencies = [
1372
"proc-macro2",
1373
"quote",
1301
- "syn 3.0.4",
1374
+ "syn 3.0.5",
1375
]
1376
1377
[[package]]
@@ -1307,7 +1380,7 @@ version = "0.12.28"
1380
source = "registry+https://github.com/rust-lang/crates.io-index"
1381
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
1382
dependencies = [
1310
- "base64",
1383
+ "base64 0.22.1",
1384
"bytes",
1385
"futures-core",
1386
"http",
@@ -1360,7 +1433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1433
checksum = "14db48ee17a9ba61810ab1a9c1beb7d06d8136ae39ac25a1137f10d357af01af"
1434
dependencies = [
1435
"async-trait",
1363
- "base64",
1436
+ "base64 0.22.1",
1437
"chrono",
1438
"futures",
1439
"pastey",
@@ -1425,9 +1498,9 @@ dependencies = [
1498
1499
[[package]]
1500
name = "rustls"
1428
-version = "0.23.43"
1501
+version = "0.23.44"
1502
source = "registry+https://github.com/rust-lang/crates.io-index"
1430
-checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
1503
+checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"
1504
dependencies = [
1505
"once_cell",
1506
"ring",
@@ -1514,7 +1587,7 @@ dependencies = [
1587
"proc-macro2",
1588
"quote",
1589
"serde_derive_internals",
1517
- "syn 3.0.4",
1590
+ "syn 3.0.5",
1591
]
1592
1593
[[package]]
@@ -1567,7 +1640,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
1640
dependencies = [
1641
"proc-macro2",
1642
"quote",
1570
- "syn 3.0.4",
1643
+ "syn 3.0.5",
1644
]
1645
1646
[[package]]
@@ -1578,7 +1651,7 @@ checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
1651
dependencies = [
1652
"proc-macro2",
1653
"quote",
1581
- "syn 3.0.4",
1654
+ "syn 3.0.5",
1655
]
1656
1657
[[package]]
@@ -1622,6 +1695,12 @@ dependencies = [
1695
"rand_core 0.6.4",
1696
]
1697
1698
+[[package]]
1699
+name = "simd-adler32"
1700
+version = "0.3.10"
1701
+source = "registry+https://github.com/rust-lang/crates.io-index"
1702
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
1703
+
1704
[[package]]
1705
name = "simple_asn1"
1706
version = "0.6.4"
@@ -1642,9 +1721,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1721
1722
[[package]]
1723
name = "smallvec"
1645
-version = "1.15.2"
1724
+version = "1.16.1"
1725
source = "registry+https://github.com/rust-lang/crates.io-index"
1647
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
1726
+checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
1727
1728
[[package]]
1729
name = "smbcloud-ascapi-aso"
@@ -1663,8 +1742,10 @@ name = "smbcloud-ascapi-cli"
1742
version = "0.1.1"
1743
dependencies = [
1744
"anyhow",
1666
- "base64",
1745
+ "base64 0.22.1",
1746
"clap",
1747
+ "csv",
1748
+ "flate2",
1749
"serde",
1750
"serde_json",
1751
"smbcloud-ascapi-aso",
@@ -1672,6 +1753,7 @@ dependencies = [
1753
"smbcloud-ascapi-frontend",
1754
"smbcloud-ascapi-mcp",
1755
"smbcloud-ascapi-pricing",
1756
+ "smbcloud-ascapi-reports",
1757
"smbcloud-ascapi-signing",
1758
"tokio",
1759
]
@@ -1691,7 +1773,7 @@ dependencies = [
1773
name = "smbcloud-ascapi-frontend"
1774
version = "0.1.1"
1775
dependencies = [
1694
- "base64",
1776
+ "base64 0.22.1",
1777
"schemars",
1778
"serde",
1779
"serde_json",
@@ -1727,6 +1809,15 @@ dependencies = [
1809
"smbcloud-ascapi-core",
1810
]
1811
1812
+[[package]]
1813
+name = "smbcloud-ascapi-reports"
1814
+version = "0.1.1"
1815
+dependencies = [
1816
+ "async-trait",
1817
+ "reqwest",
1818
+ "smbcloud-ascapi-core",
1819
+]
1820
+
1821
[[package]]
1822
name = "smbcloud-ascapi-signing"
1823
version = "0.1.1"
@@ -1799,9 +1890,9 @@ dependencies = [
1890
1891
[[package]]
1892
name = "syn"
1802
-version = "3.0.4"
1893
+version = "3.0.5"
1894
source = "registry+https://github.com/rust-lang/crates.io-index"
1804
-checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
1895
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
1896
dependencies = [
1897
"proc-macro2",
1898
"quote",
@@ -1845,7 +1936,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
1936
dependencies = [
1937
"proc-macro2",
1938
"quote",
1848
- "syn 3.0.4",
1939
+ "syn 3.0.5",
1940
]
1941
1942
[[package]]
@@ -1890,9 +1981,9 @@ dependencies = [
1981
1982
[[package]]
1983
name = "tinyvec"
1893
-version = "1.12.0"
1984
+version = "1.13.2"
1985
source = "registry+https://github.com/rust-lang/crates.io-index"
1895
-checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
1986
+checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"
1987
dependencies = [
1988
"tinyvec_macros",
1989
]
@@ -1926,14 +2017,14 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
2017
dependencies = [
2018
"proc-macro2",
2019
"quote",
1929
- "syn 3.0.4",
2020
+ "syn 3.0.5",
2021
]
2022
2023
[[package]]
2024
name = "tokio-rustls"
1934
-version = "0.26.4"
2025
+version = "0.26.5"
2026
source = "registry+https://github.com/rust-lang/crates.io-index"
1936
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
2027
+checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
2028
dependencies = [
2029
"rustls",
2030
"tokio",
@@ -2106,9 +2197,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
2197
2198
[[package]]
2199
name = "wasm-bindgen"
2109
-version = "0.2.127"
2200
+version = "0.2.128"
2201
source = "registry+https://github.com/rust-lang/crates.io-index"
2111
-checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
2202
+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
2203
dependencies = [
2204
"cfg-if",
2205
"once_cell",
@@ -2119,9 +2210,9 @@ dependencies = [
2210
2211
[[package]]
2212
name = "wasm-bindgen-futures"
2122
-version = "0.4.77"
2213
+version = "0.4.78"
2214
source = "registry+https://github.com/rust-lang/crates.io-index"
2124
-checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
2215
+checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
2216
dependencies = [
2217
"js-sys",
2218
"wasm-bindgen",
@@ -2129,9 +2220,9 @@ dependencies = [
2220
2221
[[package]]
2222
name = "wasm-bindgen-macro"
2132
-version = "0.2.127"
2223
+version = "0.2.128"
2224
source = "registry+https://github.com/rust-lang/crates.io-index"
2134
-checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
2225
+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
2226
dependencies = [
2227
"quote",
2228
"wasm-bindgen-macro-support",
@@ -2139,31 +2230,31 @@ dependencies = [
2230
2231
[[package]]
2232
name = "wasm-bindgen-macro-support"
2142
-version = "0.2.127"
2233
+version = "0.2.128"
2234
source = "registry+https://github.com/rust-lang/crates.io-index"
2144
-checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
2235
+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
2236
dependencies = [
2237
"bumpalo",
2238
"proc-macro2",
2239
"quote",
2149
- "syn 2.0.119",
2240
+ "syn 3.0.5",
2241
"wasm-bindgen-shared",
2242
]
2243
2244
[[package]]
2245
name = "wasm-bindgen-shared"
2155
-version = "0.2.127"
2246
+version = "0.2.128"
2247
source = "registry+https://github.com/rust-lang/crates.io-index"
2157
-checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
2248
+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
2249
dependencies = [
2250
"unicode-ident",
2251
]
2252
2253
[[package]]
2254
name = "web-sys"
2164
-version = "0.3.104"
2255
+version = "0.3.105"
2256
source = "registry+https://github.com/rust-lang/crates.io-index"
2166
-checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
2257
+checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
2258
dependencies = [
2259
"js-sys",
2260
"wasm-bindgen",
@@ -2379,18 +2470,18 @@ dependencies = [
2470
2471
[[package]]
2472
name = "zerocopy"
2382
-version = "0.8.56"
2473
+version = "0.8.57"
2474
source = "registry+https://github.com/rust-lang/crates.io-index"
2384
-checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
2475
+checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
2476
dependencies = [
2477
"zerocopy-derive",
2478
]
2479
2480
[[package]]
2481
name = "zerocopy-derive"
2391
-version = "0.8.56"
2482
+version = "0.8.57"
2483
source = "registry+https://github.com/rust-lang/crates.io-index"
2393
-checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
2484
+checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
2485
dependencies = [
2486
"proc-macro2",
2487
"quote",
@@ -2454,9 +2545,15 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
2545
dependencies = [
2546
"proc-macro2",
2547
"quote",
2457
- "syn 3.0.4",
2548
+ "syn 3.0.5",
2549
]
2550
2551
+[[package]]
2552
+name = "zlib-rs"
2553
+version = "0.6.7"
2554
+source = "registry+https://github.com/rust-lang/crates.io-index"
2555
+checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
2556
+
2557
[[package]]
2558
name = "zmij"
2559
version = "1.0.23"
Cargo.toml
+4
@@ -5,6 +5,7 @@ members = [
5
"crates/aso",
6
"crates/signing",
7
"crates/pricing",
8
+ "crates/reports",
9
"crates/frontend",
10
"crates/mcp",
11
"crates/cli",
@@ -22,6 +23,7 @@ smbcloud-ascapi-core = { version = "0.1.0", path = "crates/core" }
23
smbcloud-ascapi-aso = { version = "0.1.0", path = "crates/aso" }
24
smbcloud-ascapi-signing = { version = "0.1.0", path = "crates/signing" }
25
smbcloud-ascapi-pricing = { version = "0.1.0", path = "crates/pricing" }
26
+smbcloud-ascapi-reports = { version = "0.1.0", path = "crates/reports" }
27
smbcloud-ascapi-frontend = { version = "0.1.0", path = "crates/frontend" }
28
smbcloud-ascapi-mcp = { version = "0.1.0", path = "crates/mcp" }
29
anyhow = "1"
@@ -34,6 +36,8 @@ thiserror = "2.0"
36
tokio = "1"
37
async-trait = "0.1"
38
base64 = "0.22"
39
+csv = "1"
40
+flate2 = "1"
41
rcgen = { version = "0.14", default-features = false, features = ["pem", "crypto", "aws_lc_rs"] }
42
rsa = "0.9"
43
rand = "0.8"
crates/cli/Cargo.toml
+3
@@ -16,12 +16,15 @@ path = "src/main.rs"
16
anyhow = { workspace = true }
17
base64 = { workspace = true }
18
clap = { workspace = true, features = ["derive", "env"] }
19
+csv = { workspace = true }
20
+flate2 = { workspace = true }
21
serde = { workspace = true }
22
serde_json = { workspace = true }
23
smbcloud-ascapi-aso = { workspace = true }
24
smbcloud-ascapi-core = { workspace = true }
25
smbcloud-ascapi-pricing = { workspace = true }
26
smbcloud-ascapi-signing = { workspace = true }
27
+smbcloud-ascapi-reports = { workspace = true }
28
smbcloud-ascapi-frontend = { workspace = true }
29
smbcloud-ascapi-mcp = { workspace = true }
30
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
crates/cli/README.md
+29
@@ -25,6 +25,35 @@ export ASC_ISSUER_ID=<issuer id>
25
export ASC_PRIVATE_KEY_PATH=/path/to/AuthKey_XXXXXXXXXX.p8
26
```
27
28
+## Sales reports
29
+
30
+`sales-reports download` retrieves account-level App Store sales data. It is
31
+for paid-app and in-app purchase reporting across the team; it is not the
32
+per-customer transaction-history API. Reports are returned as a JSON array
33
+whose keys are Apple's original TSV column names and whose values are strings.
34
+
35
+Set the vendor number shown in App Store Connect → Payments and Financial
36
+Reports, then request a report date and frequency:
37
+
38
+```bash
39
+export ASC_VENDOR_NUMBER=12345678
40
+
41
+# Yesterday's paid-app and in-app purchase sales.
42
+ascapi sales-reports download \
43
+ --frequency DAILY \
44
+ --report-date 2026-09-11
45
+
46
+# Save a monthly report rather than printing it to stdout.
47
+ascapi sales-reports download \
48
+ --frequency MONTHLY \
49
+ --report-date 2026-08 \
50
+ --output sales-2026-08.json
51
+```
52
+
53
+`--report-type` and `--report-subtype` default to `SALES` and `SUMMARY`.
54
+Pass the exact Apple values to request another supported sales-report layout;
55
+use `--version` where that layout requires a report format version.
56
+
57
## Signing certificates
58
59
```bash
crates/cli/src/main.rs
+171
@@ -16,12 +16,15 @@ use smbcloud_ascapi_aso::prelude::*;
16
use smbcloud_ascapi_core::{ApiKey, Client};
17
use smbcloud_ascapi_pricing::app_price::PriceKind;
18
use smbcloud_ascapi_pricing::prelude::*;
19
+use smbcloud_ascapi_reports::prelude::*;
20
+use smbcloud_ascapi_reports::SalesReportRequest;
21
use smbcloud_ascapi_signing::certificate::{CertificateCreateAttributes, CertificateType};
22
use smbcloud_ascapi_signing::csr::generate_certificate_request;
23
use smbcloud_ascapi_signing::prelude::*;
24
use smbcloud_ascapi_signing::profile::{
25
ProfileCreateAttributes, ProfileCreateRelationships, ProfileType,
26
};
27
+use std::collections::HashSet;
28
use std::path::PathBuf;
29
30
/// Add/update App Store Connect app metadata (apps, app infos, app store
@@ -121,6 +124,40 @@ enum Command {
124
#[command(subcommand)]
125
command: ProfilesCommand,
126
},
127
+ /// Download account-level App Store sales reports as JSON.
128
+ SalesReports {
129
+ #[command(subcommand)]
130
+ command: SalesReportsCommand,
131
+ },
132
+}
133
+
134
+#[derive(Subcommand)]
135
+enum SalesReportsCommand {
136
+ /// Download, decompress, and convert one Apple sales report to JSON.
137
+ Download {
138
+ /// App Store Connect vendor number, from Payments and Financial
139
+ /// Reports. May also be set through ASC_VENDOR_NUMBER.
140
+ #[arg(long, env = "ASC_VENDOR_NUMBER")]
141
+ vendor_number: String,
142
+ /// Apple report frequency, such as DAILY, WEEKLY, MONTHLY, or YEARLY.
143
+ #[arg(long)]
144
+ frequency: String,
145
+ /// Report date in Apple's required format for the selected frequency.
146
+ #[arg(long)]
147
+ report_date: String,
148
+ /// Apple report type. Defaults to SALES for paid-app and IAP sales.
149
+ #[arg(long, default_value = "SALES")]
150
+ report_type: String,
151
+ /// Apple report subtype. Defaults to SUMMARY.
152
+ #[arg(long, default_value = "SUMMARY")]
153
+ report_subtype: String,
154
+ /// Optional Apple report format version, for report types that use one.
155
+ #[arg(long)]
156
+ version: Option<String>,
157
+ /// Write the JSON array to this file instead of stdout.
158
+ #[arg(long)]
159
+ output: Option<PathBuf>,
160
+ },
161
}
162
163
#[derive(Subcommand)]
@@ -610,6 +647,54 @@ fn dry_run_guard(dry_run: bool, value: &impl serde::Serialize) -> Result<bool> {
647
Ok(dry_run)
648
}
649
650
+/// Convert Apple's gzip-compressed TSV report to JSON without normalizing
651
+/// columns: report layouts vary by report type and Apple can add columns.
652
+fn sales_report_as_json(gzip: &[u8]) -> Result<Vec<serde_json::Value>> {
653
+ use flate2::read::GzDecoder;
654
+ use serde_json::{Map, Value};
655
+
656
+ let decoder = GzDecoder::new(gzip);
657
+ let mut reader = csv::ReaderBuilder::new()
658
+ .delimiter(b'\t')
659
+ .has_headers(true)
660
+ .flexible(false)
661
+ .from_reader(decoder);
662
+
663
+ let headers = reader
664
+ .headers()
665
+ .context("reading the TSV header from Apple's sales report")?
666
+ .iter()
667
+ .map(str::to_owned)
668
+ .collect::<Vec<_>>();
669
+ if headers.is_empty() || headers.iter().any(|header| header.is_empty()) {
670
+ anyhow::bail!("Apple's sales report has an empty TSV header")
671
+ }
672
+ let unique_headers = headers.iter().collect::<HashSet<_>>();
673
+ if unique_headers.len() != headers.len() {
674
+ anyhow::bail!("Apple's sales report has duplicate TSV headers")
675
+ }
676
+
677
+ let mut rows = Vec::new();
678
+ for (index, record) in reader.records().enumerate() {
679
+ let record = record.with_context(|| format!("parsing TSV row {}", index + 2))?;
680
+ if record.len() != headers.len() {
681
+ anyhow::bail!(
682
+ "TSV row {} has {} columns; expected {}",
683
+ index + 2,
684
+ record.len(),
685
+ headers.len()
686
+ )
687
+ }
688
+ let object = headers
689
+ .iter()
690
+ .zip(record.iter())
691
+ .map(|(header, value)| (header.clone(), Value::String(value.to_owned())))
692
+ .collect::<Map<_, _>>();
693
+ rows.push(Value::Object(object));
694
+ }
695
+ Ok(rows)
696
+}
697
+
698
#[tokio::main]
699
async fn main() -> Result<()> {
700
let cli = Cli::parse();
@@ -666,6 +751,7 @@ async fn main() -> Result<()> {
751
Command::AppPrices { command } => run_app_prices(&client, command).await,
752
Command::Certificates { command } => run_certificates(&client, command, cli.dry_run).await,
753
Command::Profiles { command } => run_profiles(&client, command, cli.dry_run).await,
754
+ Command::SalesReports { command } => run_sales_reports(&client, command).await,
755
}
756
}
757
@@ -677,6 +763,40 @@ fn dirs_home() -> PathBuf {
763
.unwrap_or_else(|| PathBuf::from("."))
764
}
765
766
+async fn run_sales_reports(client: &Client, command: SalesReportsCommand) -> Result<()> {
767
+ match command {
768
+ SalesReportsCommand::Download {
769
+ vendor_number,
770
+ frequency,
771
+ report_date,
772
+ report_type,
773
+ report_subtype,
774
+ version,
775
+ output,
776
+ } => {
777
+ let report = client
778
+ .download_sales_report(&SalesReportRequest {
779
+ vendor_number,
780
+ frequency,
781
+ report_date,
782
+ report_type,
783
+ report_subtype,
784
+ version,
785
+ })
786
+ .await?;
787
+ let rows = sales_report_as_json(&report)?;
788
+ if let Some(path) = output {
789
+ let json = serde_json::to_vec_pretty(&rows)?;
790
+ std::fs::write(&path, json)
791
+ .with_context(|| format!("writing sales report JSON to {}", path.display()))?;
792
+ } else {
793
+ print_json(&rows)?;
794
+ }
795
+ Ok(())
796
+ }
797
+ }
798
+}
799
+
800
async fn run_apps(client: &Client, command: AppsCommand, dry_run: bool) -> Result<()> {
801
match command {
802
AppsCommand::List { bundle_id } => {
@@ -1313,3 +1433,54 @@ fn base64_decode(input: &str) -> Result<Vec<u8>> {
1433
use base64::Engine;
1434
Ok(base64::engine::general_purpose::STANDARD.decode(input.trim())?)
1435
}
1436
+
1437
+#[cfg(test)]
1438
+mod tests {
1439
+ use super::sales_report_as_json;
1440
+ use flate2::write::GzEncoder;
1441
+ use flate2::Compression;
1442
+ use std::io::Write;
1443
+
1444
+ fn gzip(tsv: &str) -> Vec<u8> {
1445
+ let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1446
+ encoder.write_all(tsv.as_bytes()).unwrap();
1447
+ encoder.finish().unwrap()
1448
+ }
1449
+
1450
+ #[test]
1451
+ fn sales_report_preserves_headers_and_string_values() {
1452
+ let rows = sales_report_as_json(&gzip(
1453
+ "Provider Country\tUnits\tTitle\nUS\t12\t\"An app\twith tab\"\n",
1454
+ ))
1455
+ .unwrap();
1456
+
1457
+ assert_eq!(
1458
+ rows,
1459
+ vec![serde_json::json!({
1460
+ "Provider Country": "US",
1461
+ "Units": "12",
1462
+ "Title": "An app\twith tab",
1463
+ })]
1464
+ );
1465
+ }
1466
+
1467
+ #[test]
1468
+ fn sales_report_rejects_mismatched_rows() {
1469
+ let error = sales_report_as_json(&gzip("Country\tUnits\nUS\n")).unwrap_err();
1470
+ assert!(error.to_string().contains("parsing TSV row 2"));
1471
+ }
1472
+
1473
+ #[test]
1474
+ fn sales_report_rejects_invalid_gzip() {
1475
+ let error = sales_report_as_json(b"not a gzip file").unwrap_err();
1476
+ assert!(error
1477
+ .to_string()
1478
+ .contains("reading the TSV header from Apple's sales report"));
1479
+ }
1480
+
1481
+ #[test]
1482
+ fn sales_report_rejects_duplicate_headers() {
1483
+ let error = sales_report_as_json(&gzip("Country\tCountry\nUS\tCA\n")).unwrap_err();
1484
+ assert!(error.to_string().contains("duplicate TSV headers"));
1485
+ }
1486
+}
crates/core/src/client.rs
+24
-2
@@ -83,7 +83,7 @@ impl Client {
83
query: &[(&str, &str)],
84
body: Option<&B>,
85
) -> Result<T> {
86
- let (status, bytes) = self.send(method, path, query, body).await?;
86
+ let (status, bytes) = self.send(method, path, query, body, &[]).await?;
87
if !status.is_success() {
88
return Err(api_error(status, &bytes));
89
}
@@ -98,19 +98,38 @@ impl Client {
98
query: &[(&str, &str)],
99
body: Option<&B>,
100
) -> Result<()> {
101
- let (status, bytes) = self.send(method, path, query, body).await?;
101
+ let (status, bytes) = self.send(method, path, query, body, &[]).await?;
102
if !status.is_success() {
103
return Err(api_error(status, &bytes));
104
}
105
Ok(())
106
}
107
108
+ /// Send an authenticated request whose successful response is raw bytes.
109
+ /// Domain APIs use this for endpoints such as sales reports that return a
110
+ /// compressed file instead of a JSON:API document.
111
+ pub async fn request_bytes<B: Serialize>(
112
+ &self,
113
+ method: Method,
114
+ path: &str,
115
+ query: &[(&str, &str)],
116
+ body: Option<&B>,
117
+ headers: &[(&str, &str)],
118
+ ) -> Result<Vec<u8>> {
119
+ let (status, bytes) = self.send(method, path, query, body, headers).await?;
120
+ if !status.is_success() {
121
+ return Err(api_error(status, &bytes));
122
+ }
123
+ Ok(bytes)
124
+ }
125
+
126
async fn send<B: Serialize>(
127
&self,
128
method: Method,
129
path: &str,
130
query: &[(&str, &str)],
131
body: Option<&B>,
132
+ headers: &[(&str, &str)],
133
) -> Result<(StatusCode, Vec<u8>)> {
134
let url = format!("{}{}", self.base_url, path);
135
let token = self.bearer_token()?;
@@ -121,6 +140,9 @@ impl Client {
140
if let Some(b) = body {
141
req = req.json(b);
142
}
143
+ for (name, value) in headers {
144
+ req = req.header(*name, *value);
145
+ }
146
147
let response = req.send().await?;
148
let status = response.status();
crates/reports/Cargo.toml
new
+16
@@ -0,0 +1,16 @@
1
+[package]
2
+name = "smbcloud-ascapi-reports"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+repository.workspace = true
8
+description = "Sales report downloads through the App Store Connect API."
9
+
10
+[lib]
11
+path = "src/lib.rs"
12
+
13
+[dependencies]
14
+async-trait = { workspace = true }
15
+reqwest = { workspace = true }
16
+smbcloud-ascapi-core = { workspace = true }
crates/reports/src/lib.rs
new
+92
@@ -0,0 +1,92 @@
1
+//! Sales report downloads from App Store Connect.
2
+//!
3
+//! Apple returns these reports as gzip-compressed TSV rather than JSON:API.
4
+//! This crate owns the request shape; consumers decide how to persist or
5
+//! transform the downloaded file.
6
+
7
+use async_trait::async_trait;
8
+use reqwest::Method;
9
+use smbcloud_ascapi_core::{Client, Result};
10
+
11
+/// Arguments accepted by `GET /v1/salesReports`.
12
+#[derive(Debug, Clone)]
13
+pub struct SalesReportRequest {
14
+ pub vendor_number: String,
15
+ pub frequency: String,
16
+ pub report_date: String,
17
+ pub report_type: String,
18
+ pub report_subtype: String,
19
+ pub version: Option<String>,
20
+}
21
+
22
+impl SalesReportRequest {
23
+ /// Build the documented App Store Connect query filters.
24
+ pub fn query(&self) -> Vec<(&str, &str)> {
25
+ let mut query = vec![
26
+ ("filter[frequency]", self.frequency.as_str()),
27
+ ("filter[reportDate]", self.report_date.as_str()),
28
+ ("filter[reportSubType]", self.report_subtype.as_str()),
29
+ ("filter[reportType]", self.report_type.as_str()),
30
+ ("filter[vendorNumber]", self.vendor_number.as_str()),
31
+ ];
32
+ if let Some(version) = self.version.as_deref() {
33
+ query.push(("filter[version]", version));
34
+ }
35
+ query
36
+ }
37
+}
38
+
39
+/// Download account-level sales data. The successful body is Apple’s
40
+/// gzip-compressed TSV report.
41
+#[async_trait]
42
+pub trait SalesReportsApi {
43
+ async fn download_sales_report(&self, request: &SalesReportRequest) -> Result<Vec<u8>>;
44
+}
45
+
46
+#[async_trait]
47
+impl SalesReportsApi for Client {
48
+ async fn download_sales_report(&self, request: &SalesReportRequest) -> Result<Vec<u8>> {
49
+ self.request_bytes(
50
+ Method::GET,
51
+ "/v1/salesReports",
52
+ &request.query(),
53
+ None::<&()>,
54
+ &[("Accept", "application/a-gzip")],
55
+ )
56
+ .await
57
+ }
58
+}
59
+
60
+/// Traits to import when using this crate.
61
+pub mod prelude {
62
+ pub use crate::SalesReportsApi;
63
+}
64
+
65
+#[cfg(test)]
66
+mod tests {
67
+ use super::SalesReportRequest;
68
+
69
+ #[test]
70
+ fn query_contains_defaults_and_optional_version() {
71
+ let request = SalesReportRequest {
72
+ vendor_number: "12345678".into(),
73
+ frequency: "DAILY".into(),
74
+ report_date: "2026-09-11".into(),
75
+ report_type: "SALES".into(),
76
+ report_subtype: "SUMMARY".into(),
77
+ version: Some("1_0".into()),
78
+ };
79
+
80
+ assert_eq!(
81
+ request.query(),
82
+ vec![
83
+ ("filter[frequency]", "DAILY"),
84
+ ("filter[reportDate]", "2026-09-11"),
85
+ ("filter[reportSubType]", "SUMMARY"),
86
+ ("filter[reportType]", "SALES"),
87
+ ("filter[vendorNumber]", "12345678"),
88
+ ("filter[version]", "1_0"),
89
+ ]
90
+ );
91
+ }
92
+}