New

Publishing packs

The three-step publish a tool performs to put a game content pack on the site — claiming a version, uploading, and saying whether it worked.

POST/api/app/v1/packsDevice or CLI token

The publishing tool’s endpoint. Three actions on one route.

Three actions, because a publish is not one request

claim takes the version, then the tool uploads, then complete or fail closes it out. They are one transaction spread over however long an upload takes, and the middle step is the slow one — exploding an archive, hashing every file, signing a manifest. The version number has to be ours before that starts.

claim

Takes version, fileCount, byteSize, and either repoName or an assetId to publish under that asset’s name. Returns the row id and the resolved contentId. The row is created pending and holds the version from this moment.

complete

Takes the row id, the final fileCount and byteSize, an optional manifestSha, and an optional contains bill of materials (at most 200 entries). Marks the pack ready.

fail

Takes the row id and an error string. Closes the claim out as failed — and the version stays spent.

A tool that dies mid-upload leaves a pending row

That is the correct outcome, not a leak. The alternative is handing that version number to the next attempt while the first is still uploading to it — two publishes doing the same work, one of them throwing it away, and the loser has already spent the storage.

What a content id is

<owner>/<repo>, and it is a path component in the player’s client: a pack mounts at res://<mount_root>/<content_id>/<version>/, and its manifest is fetched from {base}/{content_id}/manifest.json.

Each segment is at most 64 characters of lowercase alphanumerics, - and _ — and must equal its own slug, which rules out _foo, foo_, a__b and __x__. Those pass a plain charset test and are then refused by every client that tries to mount them.

Why the separator is a slash and never a flattened name

_ and - are both legal inside an owner and inside a name, so alice_bob is ambiguous: it is alice_bob + x and also alice + bob_x. That is not untidy, it is a security bug — dot-cloud scopes a publisher’s signing key with a glob over the id, so a key scoped alice_* would be entitled to sign for the member called alice_bob. A / cannot occur inside either half, so alice/* is exact.

The owner segment tmc is reserved for first-party content, and staff are the only callers who may publish into it.

The gates, in the order they are asked

Shape, then ownership, then collision, then quota. Telling somebody they are out of space for a version number that was already taken sends them to delete something they did not need to.

Denial What it means
BAD_ID The id or the version cannot survive being a path component.
NOT_YOUR_NAMESPACE That <owner>/<repo> belongs to somebody else.
VERSION_TAKEN That exact version has already been published.
RETIRED That id was renamed away from; the denial names the new one.
MOVING A rename of that pack is still running; try again when it ends.
TOO_MANY_FILES Over uploads.maxFilesPerVersion for your role.
TOO_MANY_VERSIONS Over uploads.maxVersions for that content id.
OUT_OF_SPACE Over uploads.maxBytesPerUser for your role.

Two details worth knowing about the quotas. maxVersions counts rows in every state but deleted, including failed ones — a failed row is still holding its version number, and not counting it would let somebody mint versions by failing. maxBytesPerUser counts ready rows only: a pending row has stored nothing yet and a failed one never will, so charging either would bill for bytes nobody is holding.

The same limits are asked again on complete, because the numbers on a claim are a promise and the numbers on a complete are what actually landed.

Managing your packs

Account, then Game uploads lists one card per pack (content id): its total size, and the newest three versions, with “View all” for the rest. The list is paged, newest activity first. The same list appears on an asset’s Releases tab and in a game’s Content panel, scoped to that item.

Delete

One version, or every version of a pack. Only the owner of the namespace and staff may delete: it is their version numbers that are spent, and permission to edit the asset is not permission to delete.

Rename

Changes the pack’s id and rebuilds every version under the new one. Whoever can edit the asset may rename its pack; a pack with no asset (published from the CLI) can be renamed only by its owner. What changes depends on where the name comes from: the asset’s repo name, a per-archive name override for a map pack, or, for a CLI pack, only the id itself.

The delete itself is queued: the request only marks the rows and a worker removes the files afterwards, then repoints the pack’s latest.json (or removes it when no version is left). Delete and rename are refused while a version is still pending or a rename of the same asset’s packs is running, because either would write files the other has just removed.

A rename does not delete the old id’s files, and the old id is retired: the CLI publishing to it is refused with RETIRED naming the new one. Servers keep installing the old id’s newest version until they are pointed at the new name.

Compression

Files in a pack are content-addressed and cached for a year, and the compressible ones (models, JSON, scenes, scripts and resources) are stored gzip-encoded with Content-Encoding: gzip after delivery. You do nothing for this and the client sees the same bytes. Browser game builds are handled the same way with brotli.

Credentials

A CLI credential, minted by the same device grant the desktop app uses: the member approves a named credential in a real browser and revokes it from the same Account page. allowCli is opt-in across the whole app API, and this is the route that opts in.

An ordinary device credential is accepted too — that is how the site’s own upload form reaches the same code. There is deliberately no second publish path: the namespace check and every quota live in one place, and a second entry point that skipped them would make both advisory.

Publishing under an asset

Pass assetId instead of repoName and the pack publishes under that asset’s name — but the asset has to be yours. Without that check an asset id would be enough to publish into somebody else’s namespace. If the asset has no repo name yet, the first publish claims one derived from it and writes it down; claiming it is what stops two of your assets interleaving their versions under one mount path.