The Download page has two additional tabs, Mods and Skins, which list the catalog hosted by the project's website (https://prism.am) and install an item in one click. The game does everything itself, natively (crates/downloader/src/repository.rs for networking, apps/desktop/src/app/hosted.rs for orchestration): never the mod thread, never the WebView.
API contract (site side)
GET <catalogBase>/api/catalog?kind=mod|skin responds with { "items": [ … ] }. Only approved items are listed.
| Field | Role |
|---|---|
id |
slug: 1 to 64 characters A-Za-z0-9._-, no . or - at the start, no . at the end; this is the identifier of the installed mod or skin |
kind |
mod or skin (an item of any other type is ignored) |
name, author, version |
displayed as is (version is free-form); at most 80, 80 and 32 characters |
seq |
integer that increases with each approved version; used to detect an update |
description, descriptionFr? |
at most 600 characters; French reads descriptionFr, other languages read description |
permissions |
list of strings (network, stage, skinImport…), displayed before a mod is installed |
sha256, size |
64 hexadecimal digits, bytes; verified after the download |
filename |
informational (the game names its own temporary file) |
url |
absolute or relative to the site, on the catalog host only (same scheme, same host, same port) |
updatedAt, screenshots? |
updatedAt is read; screenshots and any unknown field are tolerated and ignored |
An invalid item (slug, hash, size out of bounds — 64 MiB for a mod, 256 MiB for a skin —, URL elsewhere, different type, duplicate) is ignored without breaking the list. The list is read from at most 1 MiB, with at most 500 items.
Catalog address
https://prism.am by default. In order: the PRISM_CATALOG environment variable, then the catalogUrl setting (Settings → Library → Downloads → Catalog), then the default value.
- The setting requires
https;httpis accepted there only forlocalhostor a loopback address. - The variable alone can name an
httpserver (for example the current test server, with no DNS:PRISM_CATALOG=http://15.235.95.198); it then allowshttpand a port, only for the named host. - The
catalogUrlsetting is never exported with the settings.
What the network does
- The list is requested when the tab opens, never before. No identifier is sent (no cookie, no token, no machine or player identifier): only
Acceptand the game's user agent. - Each request carries a
HostPolicylimited to the catalog host: redirects are followed manually, a single host (a redirect to another host is refused), 3 hops at most. For a publichttpscatalog, a private or local address is refused at resolution, as for mod sources. - Timeouts and sizes are bounded; an unreachable catalog results in the calm "Catalog unavailable" state with a Retry button, without affecting the other tabs.
Installation
- The item is downloaded to a temporary file (
<data>/catalog-tmp/<n>/…), with progress and a Cancel button. The stream is bounded by the announced size. - The exact size and the SHA-256 are verified; at the slightest discrepancy (file too long, too short, different hash) the file is deleted and the error "does not match what the catalog announced" is displayed.
- The verified file is handed exactly to the existing import:
ModInstaller::install_packagefor a.pvmod(what the Mods page does),skin::Library::importfor a skin.zip(what the Skins page does). All of their validations apply (zip-slip, caps, identifiers). - The temporary file is deleted at the end; folders left behind by an interruption are deleted the next time a tab is opened (after one hour).
Collisions. A mod with the same identifier is replaced by the existing import (this is its explicit update: the confirmation states the version being replaced). A skin never overwrites a skin: the import gives it a free identifier (neon-2); a skin update is therefore added alongside the current one.
An installed mod is active immediately (this is the behavior of the .pvmod import; disabledMods is empty by default). This is why the confirmation comes before the installation. Afterwards, the interface says so ("it is active: check it, along with its permissions, in the Mods page") with a button that opens the Mods page.
Permissions
For a mod, the catalog's permissions are displayed as badges with a plain-language explanation, on the card and then in the installation confirmation. These are the permissions announced by the site; what matters is the package's manifest. Once installed, the game compares them: if the package declares a permission that the catalog did not announce, the interface says so ("the package requests permissions that the catalog did not announce: …"). The existing revocations (networkDenied, stageDenied, Mods page) apply as for any mod.
Installed / Update
The game keeps <data>/catalog-installed.json: for each item installed from the catalog, the local identifier (the one in a mod's manifest, the one the import chose for a skin), seq and version.
- With this link (and as long as the local item exists): update if the catalog's
seqis greater. - Without a link (installed by hand): versions are compared when both are dot-separated numbers (
1.2.10; a leadingvand a trailing-preor+buildare ignored); otherwise the item counts as installed. - No local item of that name: available.
Interface
CatalogTab.svelte is a generic component, with no per-theme styles: it reads the --pv-* tokens (the eight themes and the classic one) and reuses Button, Badge and IconButton. Text in en/fr/zh (catalog.*).
Tests
crates/downloader/src/repository/tests.rs: real local HTTP server and real transport — list (unknown fields, invalid items ignored, no identifier sent), list errors (500, not JSON, too large, unreachable, redirect to another host), valid file with progress, wrong hash, file too short or too long, 404, redirect to another host, cancellation, existing file never overwritten, base address rules.apps/desktop/src/app/hosted.rs(safe file names on Windows, registry) andprotocol/tests.rs(contract).apps/web/src/lib/catalog.test.ts(states, versions, language, permissions) andtests/catalog-tabs.spec.ts(tabs, confirmation, installation, cancellation, integrity failure, unavailable, language; classic, cabinet, slant).
What is not verified natively
The real server (prism.am or the test address), the installation of a real .pvmod or skin from the catalog in the running
game, and rendering in the real WebView are not exercised: the Rust tests use a local server, and the interface
is viewed with the mock.