Files
t5de-hub/docs/products.md
T
dhkatzandClaude Opus 5.5 f65f36c1ea
CI / test (push) Successful in 1m13s
Decorations over IMVU rooms: the hub's side
T5 products placed in ordinary IMVU rooms, kept per room instance
(room_decoration, migration 0006). GET /api/v1/decorations/:instance (and
?instances= for up to 20) gives what the viewer may see, with a hash;
PUT is for the room owner's T5DE person only, checks the products exist,
and tells every realtime session with a chat in that room ({"type":
"decor"}). Hosted rooms' IDs are refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:58:42 -07:00

119 lines
8.8 KiB
Markdown

# Products, catalogs and the library: the contract
How the hub (registry), catalogs (`t5de-catalog`, file hosting) and T5DE's proxy work together. The design and the
reasons are in `design.md` ("Products and catalogs", "Revisions", "In the client's shop and inventory"); this is
what each side sends.
## Words
- **Product**: a T5 product, owned by its creator (a hub person). ID: 10 characters of `a-z2-7` (random), e.g.
`k3mq7xa2pd`. Valid as a pack ID, so the proxy can store a downloaded product as a pack.
- **Revision**: a product's files, numbered 1, 2, 3... Never changes. Its **file list** is
`[{name, sha256, size}]`, sorted by name; `sha256` is lowercase hex.
- **Catalog**: a server storing files by sha256 and serving them. Registered on the hub; hidden until a hub
moderator approves it.
- **Library**: the products a person "got" (free), per person (all their IMVU accounts), optionally staying on a
revision.
## Tokens
The hub signs tokens with its own Ed25519 key (EdDSA JWTs, compact form), published as a JWKS at
`GET /.well-known/t5de-keys.json` (`kid` in each token's header). Catalogs fetch it (and again when they see an
unknown `kid`). Claims always include `iss` (the hub's API URL), `typ` (below) and `iat`.
| `typ` | Other claims | Lifetime | Use |
| --- | --- | --- | --- |
| `t5de-upload` | `sub` (person ID), `catalog` (catalog ID), `product` (optional: a new revision of this product) | 30 min | the creator's browser uploads to the catalog |
| `t5de-access` | `sub` (person ID), `adult` (bool) | 1 h | the proxy fetches adult files from catalogs |
| `t5de-revision` | `product`, `revision`, `rating`, `files` (the file list), `parent` (IMVU product ID or 0) | none (`exp` absent) | the hub vouches for a revision's files |
## Catalog API (`t5de-catalog`)
Served at the catalog's public URL. CORS: the hub website's origin may call `POST /api/v1/uploads`.
| | |
| --- | --- |
| `GET /health` | `{ok: true}` |
| `POST /api/v1/uploads` | `Authorization: Bearer <t5de-upload token>`; multipart form: `file` (the product as a zip: Create Mode's `.chkn`, files at the root) and `notes` (text, required for a new revision of an existing product, optional for the first). The catalog validates the files (t5de-api `pack` rules: names, limits, `index.xml` with `ProductRating` GA/AP), stores each file by sha256, then registers the revision with the hub (below) and answers the hub's answer: 201 `{product, revision}`. Errors: `{error, message}`, 400 for bad files, 401 for a bad token, 502 if the hub refused or failed. |
| `GET /api/v1/files/<sha256>` | the file's bytes, `Cache-Control: public, max-age=31536000, immutable` and `ETag` = the hash. A file used only by adult revisions needs `Authorization: Bearer <t5de-access token>` with `adult: true` (else 401/403, and `Cache-Control: private`). 404 if the catalog doesn't have it. |
| `GET /api/v1/revisions/<product>/<revision>` | `{product, revision, rating, files}`, for revisions the catalog hosts |
Configuration (environment): `CATALOG_ADDR` (listen address, default `127.0.0.1:4102`), `CATALOG_DATA` (data folder:
SQLite database and files), `CATALOG_PUBLIC_URL`, `HUB_URL` (the hub's API), `CATALOG_ID` and `CATALOG_KEY` (from
registering on the hub), `WEB_ORIGIN` (the hub website, for CORS).
## Hub API
### For catalogs (server to server)
`Authorization: Catalog <catalog ID>:<key>`. Only approved catalogs.
| | |
| --- | --- |
| `POST /api/v1/catalog/revisions` | `{uploadToken, files, rating ("general" or "adult"), parent (IMVU product ID or 0), name (from index.xml, for a new product), notes}` → 201 `{product, revision}`. The upload token must be for this catalog. Without `product` in the token, a new product is made (owner: the token's person, name from `name`). With it, a new revision of that product (the person must own it; `notes` required). A file list identical to the latest revision's is refused (409 `unchanged`). |
### For people (website, signed in; the proxy with its bearer token)
| | |
| --- | --- |
| `POST /api/v1/catalogs` | register a catalog: `{name, url, contact, visibility ("public"/"private"), membership ("open"/"approval"/"invite")}` → 201 `{catalog, key}` (the key is shown once) |
| `GET /api/v1/catalogs` | (anyone) approved public catalogs; the person's own, any status |
| `POST /api/v1/products/upload` | `{catalog, product?}` → `{token, url}`: an upload token and the catalog's upload URL |
| `GET /api/v1/products?q=&creator=&category=&limit=` | (anyone) `{products}`, newest first; adult ones only for people with an adult account. `category`: an IMVU category ID; only products whose latest revision's parent has it anywhere in its category path (e.g. 106 Clothing, 128 Tops). `limit`: 1-200, default 60 |
| `GET /api/v1/products/:id` | (anyone, adult rule as above) `{product, revisions, inLibrary?, mine?}` |
| `PATCH /api/v1/products/:id` | (creator) `{name?, description?, rating?}`; description is HTML, cleaned by the allowlist; rating may go general → adult |
| `POST /api/v1/products/:id/revisions/:n/retract` | (creator) |
| `GET /api/v1/library` | `{library: [{product, revision, stayOn}]}`: the person's products, each with the revision to use |
| `PUT /api/v1/library/:id` | get a product (add it) |
| `DELETE /api/v1/library/:id` | remove it |
| `PATCH /api/v1/library/:id` | `{stayOn: n or null}` |
| `GET /api/v1/products/:id/files` | for the proxy: `{product, revision, rating, parent, name, files, catalogs: [url], token}` for the revision this person uses (their pin, else the latest that isn't retracted); `token` is the `t5de-revision` token |
| `POST /api/v1/access` | for the proxy: `{token}`, a `t5de-access` token (`adult` if any of the person's IMVU accounts counts as adult) |
The realtime connection also tells the proxy `{"type": "library"}` when the person's library changes.
A product (in lists, its page, the library) is `{id, name, description, rating, latest, creator, creatorUsername,
parent, image, categories, createdAt, updatedAt}`. `parent`, `image` and `categories` are the latest revision's:
`parent` is the IMVU product it derives from (0 if none); `image` (an https address on `*.imvu.com`, or null) and
`categories` (`[{id, name}]`, outermost first, e.g. Clothing / Clothing for Female / Tops / Coats & Jackets; `[]` if
unknown) are that IMVU product's, from IMVU's public API (`GET https://api.imvu.com/product/product-<id>`).
### Parent info
The hub looks up a revision's parent when the revision is registered (after it's stored; an upload never waits for or
fails on it) and stores it with the revision (`product_revision.parent_info`: `{found, image, categories, gender,
checkedAt}`; `found` is false when IMVU shows no such product publicly). Info another revision got for the same
parent in the last 7 days is reused. Revisions still without it (IMVU was unavailable) are looked up at startup and
every 10 minutes, 20 at a time.
### Console (moderators)
`GET /api/v1/console/catalogs`, `POST /api/v1/console/catalogs/:id/approve`, `.../reject`.
## The proxy
- Fetches the library at connect and on `library` messages; for each product, `GET /api/v1/products/:id/files`,
downloads missing files from a listed catalog (`GET <catalog>/api/v1/files/<sha256>`, with an access token for
adult revisions), checks each sha256, and writes the product as a pack in its cache
(`%APPDATA%\T5DE\proxy\hub-products\<id>\`: `t5de-product.json` with the hub's name and rating, and `files/`).
From there it's served like any pack.
- Products others wear in a room that it doesn't have are fetched the same way, then given to the client.
- Library products appear in the client's inventory (see the proxy's README).
## Decorations
T5 furniture and pose products placed in an ordinary IMVU room (design.md, "Decorations over IMVU rooms"). IMVU
keeps the room without them; the hub keeps them (`room_decoration`, routes in api.md), and only T5DE users see them.
- **Saving**: when the room's owner saves the room (`test.setRoomState`) or the client broadcasts a furniture change
in it, the proxy takes the hub products out before it goes to IMVU, and `PUT /api/v1/decorations/<instance>` the
room's full list of them (`[furniture instance ID, hub product ID, placement]`).
- **Loading**: when the client loads an IMVU room (`test.getRoomState`), the proxy `GET`s its decorations and adds
them to the room's contents (as local IDs, fetching the products as for outfits); the `hash` tells it whether
what it has is current. In the room, an incoming furniture broadcast from IMVU gets the decorations added back
the same way.
- **Changes**: after a `PUT`, every proxy with a chat in that room gets `{"type": "decor", "room": instance}` on the
realtime connection and `GET`s again.
- Viewers only get products they may see: registered, with a revision that isn't retracted, adult ones only for
adult accounts. Furniture instance IDs are the proxy's to choose; they must not clash with IMVU's in that room.