Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/core",
"crates/aso",
"crates/signing",
"crates/pricing",
"crates/frontend",
"crates/mcp",
"crates/cli",
Expand All @@ -20,6 +21,7 @@ repository = "https://github.com/smbcloudXYZ/smbcloud-ascapi"
smbcloud-ascapi-core = { version = "0.1.0", path = "crates/core" }
smbcloud-ascapi-aso = { version = "0.1.0", path = "crates/aso" }
smbcloud-ascapi-signing = { version = "0.1.0", path = "crates/signing" }
smbcloud-ascapi-pricing = { version = "0.1.0", path = "crates/pricing" }
smbcloud-ascapi-frontend = { version = "0.1.0", path = "crates/frontend" }
smbcloud-ascapi-mcp = { version = "0.1.0", path = "crates/mcp" }
anyhow = "1"
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@

`smbcloud-ascapi` is the app stores coolest API.

Six crates, split by domain and layered so neither front end can drift
Seven crates, split by domain and layered so neither front end can drift
from the other:

| Crate | What it is |
| --- | --- |
| `smbcloud-ascapi-core` | Shared transport: JWT auth, the HTTP client, JSON:API envelopes, error types |
| `smbcloud-ascapi-aso` | App Metadata: apps, app infos, versions, bundle IDs, localizations, screenshots |
| `smbcloud-ascapi-pricing` | Pricing: app price schedules, and the per-territory prices joined with their price points and currencies |
| `smbcloud-ascapi-signing` | Code signing: certificates and provisioning profiles, plus local RSA key pair and CSR generation |
| `smbcloud-ascapi-frontend` | Operations both surfaces share, so the CLI and the MCP server agree by construction |
| `smbcloud-ascapi-mcp` | The MCP contract and stdio server |
| `smbcloud-ascapi-cli` | The `ascapi` binary: clap command tree, plus `--mcp` |

`aso` and `signing` know nothing about each other, and neither knows
anything about the front ends. Both add their calls to
`aso`, `pricing` and `signing` know nothing about each other, and neither knows
anything about the front ends. Each adds its calls to
`smbcloud_ascapi_core::Client` as **extension traits**, since Rust only
allows inherent impls in the crate that defines a type:

```rust
use smbcloud_ascapi_core::{ApiKey, Client};
use smbcloud_ascapi_aso::prelude::*; // or signing::prelude
use smbcloud_ascapi_aso::prelude::*; // or pricing::prelude, signing::prelude
```

MCP Registry name: `mcp-name: io.github.smbcloudXYZ/ascapi`
Expand Down
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
smbcloud-ascapi-aso = { workspace = true }
smbcloud-ascapi-core = { workspace = true }
smbcloud-ascapi-pricing = { workspace = true }
smbcloud-ascapi-signing = { workspace = true }
smbcloud-ascapi-frontend = { workspace = true }
smbcloud-ascapi-mcp = { workspace = true }
Expand Down
60 changes: 60 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use smbcloud_ascapi_aso::app_store_version_localization::{
use smbcloud_ascapi_aso::bundle_id::{BundleIdCreateAttributes, BundleIdPlatform};
use smbcloud_ascapi_aso::prelude::*;
use smbcloud_ascapi_core::{ApiKey, Client};
use smbcloud_ascapi_pricing::app_price::PriceKind;
use smbcloud_ascapi_pricing::prelude::*;
use smbcloud_ascapi_signing::certificate::{CertificateCreateAttributes, CertificateType};
use smbcloud_ascapi_signing::csr::generate_certificate_request;
use smbcloud_ascapi_signing::prelude::*;
Expand Down Expand Up @@ -100,6 +102,12 @@ enum Command {
#[command(subcommand)]
command: AppScreenshotsCommand,
},
/// What an app costs, per territory — the manual price a human set
/// and the automatic ones Apple converted it into.
AppPrices {
#[command(subcommand)]
command: AppPricesCommand,
},
/// Signing certificates: list what the team holds, issue a new one, or
/// revoke an old one.
Certificates {
Expand Down Expand Up @@ -300,6 +308,32 @@ impl From<CliCertificateType> for CertificateType {
}
}

#[derive(Subcommand)]
enum AppPricesCommand {
/// `GET /v1/appPriceSchedules/{app_id}` — the base territory Apple
/// converts every other storefront from.
Schedule { app_id: String },
/// `GET /v1/appPriceSchedules/{app_id}/manualPrices`, joined against
/// the price points and territories so the money is actually visible.
///
/// Defaults to the manual prices — what a human set, usually one row
/// in the base territory. `--automatic` instead lists what Apple
/// converted that into for all ~178 storefronts, which is where a
/// scheduled conversion shows up as a near-future `endDate`.
List {
app_id: String,
/// List Apple's converted per-storefront prices rather than the
/// manually set ones.
#[arg(long)]
automatic: bool,
/// Narrow to one territory code, e.g. IDN. Applied by Apple as
/// `filter[territory]`, so it is much cheaper than fetching every
/// storefront.
#[arg(long)]
territory: Option<String>,
},
}

#[derive(Subcommand)]
enum AppsCommand {
/// `GET /v1/apps`.
Expand Down Expand Up @@ -629,6 +663,7 @@ async fn main() -> Result<()> {
Command::AppScreenshots { command } => {
run_app_screenshots(&client, command, cli.dry_run).await
}
Command::AppPrices { command } => run_app_prices(&client, command).await,
Command::Certificates { command } => run_certificates(&client, command, cli.dry_run).await,
Command::Profiles { command } => run_profiles(&client, command, cli.dry_run).await,
}
Expand Down Expand Up @@ -1038,6 +1073,31 @@ fn now_iso8601() -> String {
)
}

/// Read-only throughout, so unlike its siblings it takes no `dry_run`:
/// there is no request body to preview.
async fn run_app_prices(client: &Client, command: AppPricesCommand) -> Result<()> {
match command {
AppPricesCommand::Schedule { app_id } => {
print_json(&client.get_app_price_schedule(&app_id).await?)
}
AppPricesCommand::List {
app_id,
automatic,
territory,
} => {
let kind = if automatic {
PriceKind::Automatic
} else {
PriceKind::Manual
};
let prices = client
.list_app_prices(&app_id, kind, territory.as_deref())
.await?;
print_json(&prices)
}
}
}

async fn run_certificates(
client: &Client,
command: CertificatesCommand,
Expand Down
20 changes: 20 additions & 0 deletions crates/pricing/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "smbcloud-ascapi-pricing"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "App Store Connect pricing resources: app price schedules, and the per-territory prices and price points hanging off them."

[lib]
path = "src/lib.rs"

[dependencies]
async-trait = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true, features = ["derive"] }
smbcloud-ascapi-core = { workspace = true }

[dev-dependencies]
serde_json = { workspace = true }
Loading