CI / test (push) Successful in 1m4s
A creator deletes a product: it's no longer listed and nobody new can get it, while people who have it keep it (its page, files and library entries stay, and rooms keep showing it), as old revisions stay available. Moderation removes one everywhere, with a reason: its page and files answer 410, and it leaves libraries and rooms. Both are on the product's page and in the audit log. Co-Authored-By: Claude Opus 5.5 <[email protected]>
121 lines
9.2 KiB
Markdown
121 lines
9.2 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) |
|
|
| `DELETE /api/v1/products/:id` | (creator) deletes it: no longer listed, and nobody new can get it; people who have it keep it (its page, files and library entries stay, and rooms keep showing it). Its creator can't edit it or add revisions after. 204 |
|
|
| `POST /api/v1/products/:id/remove` | (moderator) `{reason}`: removes it everywhere: its page and files answer 410 `removed`, and it leaves libraries and rooms |
|
|
| `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.
|