# storelib_rs
A Rust port of [StoreLib](https://github.com/query-store-links/StoreLib) — a library for interacting with Microsoft Store endpoints to query product information and resolve package download URLs.
Supports native (tokio), **WebAssembly**, and **C / FFI** targets.
## Features
- Query the Microsoft Store Display Catalog by Product ID, Package Family Name, Xbox Title ID, and more
- **Batch lookup** — fetch many products in one HTTP round-trip via `bigIds`
- Resolve direct `.appx` / `.msix` / `.eappx` download URLs via the FE3 delivery service
- **Typed product accessors** — `handler.title()`, `.price()`, `.packages()`, `.images_with_purpose()` etc. walk the catalog tree without `display_sku_availabilities.as_deref()?.first()?...` chains
- Search the catalog by query string and device family
- 7 endpoints (Production, Int, Xbox, XboxInt, Dev, OneP, OnePInt)
- 259 markets + 185 ISO 639-1 languages + 350 Microsoft Store BCP-47 language tags (sourced from the IANA registry + `learn.microsoft.com`'s supported-languages table)
- `Locale::from_tag("en-US")` / `Locale.fromTag("zh-Hant-TW")` builds a locale directly from a BCP-47 tag
- Optional MSA / XBL3.0 authentication token support for sandboxed and flighted listings
- **Real-time progress reporting** via a callback on every platform (native closure / JS function / C function pointer)
- **Cancellation** via `AbortSignal` (WASM), `CancellationToken` (native), or `StorelibCancellation` (FFI). Cancel from any thread.
- **Configurable retry + timeout** — `ClientConfig` exposes `timeout`, `max_retries`, `initial_backoff`, `max_backoff`, `retry_on_status`. Cancel-aware backoff returns `Cancelled` instantly.
- **Structured errors** in JS — thrown `Error` objects carry a `kind` discriminant (`"http" | "json" | "xml" | "notFound" | "timedOut" | "cancelled" | "other"`)
- **First-class TypeScript types** — every WASM binding is typed (`Promise<DisplayCatalogModel | null>`, `Promise<PackageInstance[]>`, …); no more `any` returns
- camelCase JSON wire format across all bindings; PascalCase from the upstream MS Store API is accepted on the way in
- Structured logging via the `log` facade (`env_logger` on native, pluggable on WASM)
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
storelib_rs = { git = "https://github.com/query-store-links/storelib_rs" }
```
For WebAssembly, enable the `wasm` feature:
```toml
[dependencies]
storelib_rs = { git = "https://github.com/query-store-links/storelib_rs", features = ["wasm"] }
```
## Library usage
### Query a product
```rust
use storelib_rs::{DisplayCatalogHandler, IdentifierType};
let mut handler = DisplayCatalogHandler::production();
handler.query_dcat("9wzdncrfj3tj", IdentifierType::ProductId, None).await?;
if handler.is_found {
// Typed accessors walk the catalog tree for you — no manual
// `display_sku_availabilities.as_deref()?.first()?.sku.as_ref()?...` chains.
println!("Title: {:?}", handler.title());
println!("Publisher: {:?}", handler.publisher_name());
println!("Price: {:?}", handler.price().map(|p| (&p.currency_code, p.list_price)));
println!("Packages: {}", handler.packages().len());
// The raw model is still available if you need a field the accessors
// don't expose.
let _listing = handler.product_listing.as_ref().unwrap();
}
```
### Batch query (many products in one round-trip)
```rust
let ids = ["9WZDNCRFJ3TJ", "9NBLGGH4R315", "9P3JFPWWDZRC"];
handler.query_dcat_batch(&ids, None).await?;
for product in handler.products() {
let title = product
.localized_properties.as_deref()
.and_then(|v| v.first())
.and_then(|lp| lp.product_title.as_deref())
.unwrap_or("<no title>");
println!("{title}");
}
```
`query_dcat_batch` accepts Microsoft Store Product IDs only — the `bigIds` endpoint doesn't support alternate identifiers.
### Resolve package download URLs
```rust
let packages = handler.get_packages_for_product(None).await?;
for pkg in &packages {
println!("{} [{:?}]", pkg.package_moniker, pkg.package_type);
if let Some(url) = &pkg.package_uri {
println!(" {url}");
}
}
```
### Search the catalog
```rust
use storelib_rs::DeviceFamily;
let results = handler.search_dcat("netflix", DeviceFamily::Desktop).await?;
println!("Total: {}", results.total_result_count.unwrap_or(0));
```
### Custom locale and endpoint
```rust
use storelib_rs::{DCatEndpoint, DisplayCatalogHandler, Lang, Locale, Market};
let locale = Locale::new(Market::Gb, Lang::En, true);
let mut handler = DisplayCatalogHandler::new(DCatEndpoint::Production, locale);
```
### Building a locale from a BCP-47 tag
```rust
use storelib_rs::{LanguageTag, Locale};
use std::str::FromStr;
// "zh-Hant-TW" → Locale { market: TW, language: zh, include_neutral: true }
let tag = LanguageTag::from_str("zh-Hant-TW")?;
let locale = Locale::from_tag(tag, true)?;
```
### Authentication (sandboxed / flighted listings)
```rust
// MSA token or XBL3.0 token
handler.query_dcat("9wzdncrfj3tj", IdentifierType::ProductId, Some("your-token")).await?;
// Authenticated package resolution
let packages = handler.get_packages_for_product(Some("your-token")).await?;
```
### Progress reporting
```rust
use storelib_rs::ProgressEvent;
}));
```
Stages emitted during `query_dcat` / `get_packages_for_product` / `search_dcat`:
| `query_dcat` | `dcat.request` → `dcat.response` → `dcat.parse` → `dcat.done` (or `dcat.notFound`) |
| `get_packages_for_product` | `fe3.start` → `fe3.getCookie` → `fe3.syncUpdates` → `fe3.parseUpdateIds`[`.done`] → `fe3.parsePackages`[`.done`] → `fe3.resolveUrls`[`.done`] → `fe3.done` |
| `search_dcat` | `search.request` → `search.response` → `search.parse` → `search.done` |
`.done` counter stages populate `current`/`total` with `N` of `N` items processed.
## JavaScript / WebAssembly usage
```js
import {
DisplayCatalogHandler,
Locale,
parseLanguageTag,
listLanguageTags,
} from 'storelib_rs';
// Build a locale directly from a BCP-47 tag.
const locale = Locale.fromTag('en-US', /* includeNeutral */ false);
const handler = new DisplayCatalogHandler('production', locale);
// Subscribe to per-stage progress updates.
handler.onProgress(({stage, message, current, total}) => {
console.log(`[${stage}] ${message}` + (total != null ? ` (${current}/${total})` : ''));
});
// Cancel a stalled call after 5s.
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);
try {
await handler.queryDcat('9WZDNCRFJ3TJ', 'ProductId', null, ctrl.signal);
// Typed accessors on the handler — read fields without hand-walking the model.
console.log(handler.title, handler.publisherName, handler.price);
const pkgs = await handler.getPackagesForProduct(null, ctrl.signal);
// pkgs: Array<{ packageMoniker, packageUri, packageType, applicabilityBlob, updateId, packageSize }>
} catch (e) {
if (e.kind === 'cancelled') return;
if (e.kind === 'notFound') showNotFound();
else console.error(e.kind, e.message);
}
// Batch lookup — single round-trip for many products.
const products = await handler.queryDcatBatch(
['9WZDNCRFJ3TJ', '9NBLGGH4R315', '9P3JFPWWDZRC'],
null,
null,
);
for (const p of products) {
console.log(p.localizedProperties?.[0]?.productTitle);
}
// Enumerate the full Microsoft Store BCP-47 tag list.
for (const {code, englishName} of listLanguageTags()) {
console.log(code, englishName);
}
```
The `idType` argument to `queryDcat` is tolerant — `"ProductId"`, `"productId"`, `"product-id"`, and `"PRODUCT_ID"` all resolve to the same enum.
The handler exposes typed JS getters for common fields: `title`, `description`, `publisherName`, `price`, `prices`, `packages`, `availabilities`, `products`, `wuCategoryId`, `lastModifiedDate`, plus `imagesWithPurpose("Logo" | "Tile" | "Screenshot" | …)`.
## CLI usage
```
storelib_rs [--log-level <LEVEL>] <COMMAND>
Commands:
packages Fetch direct download URLs for a product's packages
query Query detailed product information
search Search the store catalog
Options:
### Examples
```sh
# Resolve all package URLs for an app
storelib_rs packages 9wzdncrfj3tj
# Query by Package Family Name
storelib_rs query NETFLIX.APP_mcm4njqhnhss8 --type pfn
# Search for Xbox games
storelib_rs search halo --family xbox
# Authenticated query (flighted / sandbox)
storelib_rs packages 9wzdncrfj3tj --token "XBL3.0 x=..."
# Debug logging (shows HTTP requests, FE3 update IDs, URL resolution)
storelib_rs packages 9wzdncrfj3tj --log-level debug
# Trace logging (also dumps raw SOAP response bodies)
storelib_rs packages 9wzdncrfj3tj --log-level trace
```
`RUST_LOG` overrides `--log-level`:
```sh
RUST_LOG=storelib_rs=debug storelib_rs packages 9wzdncrfj3tj
```
## Identifier types
| `product-id` *(default)* | Microsoft Store Product ID (e.g. `9wzdncrfj3tj`) |
| `pfn` | Package Family Name |
| `content-id` | Content ID |
| `xbox-title-id` | Xbox Title ID |
| `legacy-phone` | Legacy Windows Phone Product ID |
| `legacy-store` | Legacy Windows Store Product ID |
| `legacy-xbox` | Legacy Xbox Product ID |
## C / FFI usage
Build the shared library with `cargo build --release --features ffi`. The C header is at `include/storelib_rs.h`. See the header for the full API; minimal example:
```c
#include "storelib_rs.h"
#include <stdio.h>
static void on_progress(
const char* stage,
const char* message,
int32_t has_current, uint32_t current,
int32_t has_total, uint32_t total,
void* user_data
) {
(void)user_data;
if (has_total) {
printf("[%s] %s (%u/%u)\n", stage, message, current, total);
} else {
printf("[%s] %s\n", stage, message);
}
}
int main(void) {
StorelibHandle* h = storelib_new();
storelib_set_progress_callback(h, on_progress, NULL);
int32_t rc = storelib_query(h, "9wzdncrfj3tj", STORELIB_ID_PRODUCT_ID, NULL);
if (rc == STORELIB_OK && storelib_is_found(h)) {
char* title = storelib_product_title(h);
if (title) { printf("Title: %s\n", title); storelib_free_string(title); }
char* price = storelib_price_json(h);
if (price) { printf("Price: %s\n", price); storelib_free_string(price); }
char* json = storelib_packages_json(h, NULL);
if (json) { puts(json); storelib_free_string(json); }
} else {
fprintf(stderr, "query failed (%d): %s\n", rc, storelib_last_error(h));
}
// Batch query — one HTTP round-trip for three products.
const char* ids[] = { "9WZDNCRFJ3TJ", "9NBLGGH4R315", "9P3JFPWWDZRC" };
char* batch = storelib_query_batch_json(h, ids, 3, NULL);
if (batch) { puts(batch); storelib_free_string(batch); }
storelib_free(h);
return 0;
}
```
## Building
```sh
# Native
cargo build --release
# WebAssembly
cargo build --target wasm32-unknown-unknown --features wasm
# C / FFI shared library + header
cargo build --release --features ffi
# Header is at include/storelib_rs.h
```
## License
This program is free software: you can redistribute it and/or modify it under the terms of the
[GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.html) as published by the
Free Software Foundation.
See [LICENSE](LICENSE) for the full text.