Every MUST and SHOULD of the specification has an id (§ 3.5). Here is each one, and what sees it: the checks of the conformance suite that cover it, or why the suite cannot see it, a client's rule or one inside a server.
- In all
- 173 requirements
- Suite
- 115 checked by the suite
- Clients
- 46 a client keeps
- Inside
- 12 inside a server
2. Transport
L-2-1MUSTA client's rule: the suite cannot see it
HTTPS. A client MUST verify the certificate against its CA store, or against the key it pinned at pairing (§ 5.3).
L-2-2MUSTA client's rule: the suite cannot see it
Plain HTTP is allowed only on loopback addresses (
127.0.0.0/8,::1), for testing. A client MUST NOT speak plain HTTP to any other address.L-2-3MUSTChecked by
transport.httpsA server MUST NOT serve Lantern in plain HTTP on one.
L-2-4MUSTChecked by
documents.typeJSON bodies MUST be UTF-8, sent with
Content-Type: application/json(errors:application/problem+json, § 12).L-2-5MUSTA client's rule: the suite cannot see it
The client MUST send
User-Agent: <Name>/<version>.L-2-6SHOULDA client's rule: the suite cannot see it
It SHOULD send
Accept-Language(§ 2.2).L-2-7SHOULDA client's rule: the suite cannot see it
It SHOULD send
Lantern-Platform(§ 4.3).L-2-8MUSTA client's rule: the suite cannot see it
A client MUST send its token (§ 5) and its signatures only to the server's origin (scheme, host, port), never to another host a document points at.
2.1 Compression and HTTP versions
L-2.1-1SHOULDChecked by
compression.jsonA server SHOULD offer
Content-Encodingon its JSON responses, as the client'sAccept-Encodingasks:zstd(RFC 8878) orbr(RFC 7932), andgzip.L-2.1-2MUSTChecked by
compression.filesA server MUST NOT apply a
Content-Encodingto a file response (§ 6.6), whateverAccept-Encodingsays: packages are compressed by their own formats already, and ranges andRepr-Digestare about the bytes as stored.L-2.1-3MUSTChecked by
compression.etagA response a server compresses MUST carry
Vary: Accept-Encoding, and anETagother than the uncompressed one's (RFC 9110 § 8.8.3).
2.2 Languages
L-2.2-1SHOULDChecked by
language.problemA server SHOULD answer in the best language it has for
Accept-Language, and in English when it has none of them.L-2.2-2MUSTChecked by
language.problem,language.handshakeA document with such texts MUST say their language in its
language(a title's, a homebrew's, a problem's, the handshake's).L-2.2-3SHOULDChecked by
language.problemIts response SHOULD also carry
Content-Language.
3.1 Protocol version
L-3.1-1SHOULDInside the server: the suite cannot see it (keeping an old major version needs one)
The major version is in the path:
<base>/v1/…. It changes only for a break thatschemacannot express. A server lists the versions it serves (versionsin § 4.2) and SHOULD keep an old major version while clients still call it.
3.2 Document schema
L-3.2-1MUSTChecked by
documents.schemaEvery JSON document a server returns, and every document a client sends, MUST have an integer
schemaat its top level.L-3.2-2MUSTA client's rule: the suite cannot see it
A reader MUST ignore fields it does not know.
L-3.2-3MUSTA client's rule: the suite cannot see it
A reader that gets a
schemahigher than it knows MUST treat the document as absent (the feature is unavailable), not as an error.L-3.2-4MUSTA client's rule: the suite cannot see it
It MUST NOT write it back.
L-3.2-5MUSTChecked by
handshake.well-known,apps.first-pageEvery document has a JSON Schema in
schemas/v1/(2020-12), with an example inexamples/v1/. The schemas describe schema 1 and allow fields they do not list, as a reader must. A server's documents MUST be valid against them.L-3.2-6MUSTChecked by
schema.refused.*A server MUST answer a request document of a
schemait cannot read with422unsupported_schema(§ 12).L-3.2-7SHOULDChecked by
versions.accept-schemafeatures(§ 4.2) gives, per family, theschemathe server writes; a client MAY sendAccept-Schema: <family>=<n>, …to ask for an older one, which a server SHOULD honour while it still can write it.
3.4 Deprecation and sunset
L-3.4-1MUSTChecked by
deprecation.headersDeprecation(RFC 9745): since when it is deprecated, a Structured Fields date (@and Unix seconds). A server MUST send it on every response of a route, or in a schema, that it has deprecated.L-3.4-2SHOULDChecked by
deprecation.headersSunset(RFC 8594): the HTTP date after which it may stop answering. A server SHOULD send it once it knows that date.L-3.4-3MUSTInside the server: the suite cannot see it (a Sunset is in the future of a run)
It MUST NOT stop a deprecated route or schema before its
Sunset.L-3.4-4MUSTInside the server: the suite cannot see it (as is an older schema's last day)
An older schema stays served while clients read it: a server MUST keep writing it to the clients that ask for it with
Accept-Schema(§ 3.2) at least until itsSunset.L-3.4-5SHOULDA client's rule: the suite cannot see it
A client SHOULD record a
Deprecationit gets where its developers will see it (a log, a report), and move to what replaces it before theSunset.
4.1 On the LAN
L-4.1-1MUSTChecked by
discovery.replyThe server's address is the reply's source address;
portis its HTTPS port,baseandversionsas in the handshake (§ 4.2). A server's reply MUST give theuid,baseandversionsof its handshake, and the port it answers HTTPS on.L-4.1-2SHOULDInside the server: the suite cannot see it (the suite asks from one network)
Each datagram is UTF-8 JSON of at most 1200 bytes. A server answers only a request it can read (§ 3.2) and stays silent otherwise. It SHOULD answer only addresses of its own networks.
L-4.1-3SHOULDInside the server: the suite cannot see it (it would take many datagrams)
It SHOULD answer only a few times a second to the same address.
L-4.1-4SHOULDInside the server: the suite cannot see it (DNS-SD is not read by the suite)
A server SHOULD also advertise itself with DNS-SD (RFC 6763) as
_lantern._tcp, with the TXT keysuid=<uid>,path=<base>andv=1(the versions, comma-separated).L-4.1-5MUSTA client's rule: the suite cannot see it
Discovery is a hint: it finds an address and a name, nothing more. A reply proves nothing, and a client MUST trust a server only through TLS and pairing (§ 5).
4.2 Handshake
L-4.2-1MUSTChecked by
handshake.well-known,handshake.v1,handshake.versionsGET /.well-known/lantern(at the origin's root) andGET <base>/v1/MUST return the same document, with or without a token.L-4.2-2MUSTChecked by
handshake.platforms,apps.platformsplatforms: the platforms the server has something for (§ 4.3). It MUST list every platform of an app, a homebrew or a cheat set it serves, and be the same for every caller.L-4.2-3MUSTChecked by
handshake.anonymousaccount.state:anonymous(no token),active, orfrozen(the account exists but may do nothing;motdsays why). Without a token, the handshake MUST answer200withaccount.stateanonymous.L-4.2-4MUSTChecked by
auth.frozen,features.*,features.anonymousfeatures.<family>: theschemathe server writes for that family, or0when it does not offer it to this caller.L-4.2-5MUSTChecked by
handshake.auth,pairing.offeredpairingandsignaturesMUST be the same for every caller, anonymous included: whoever can reach the server can ask to pair.L-4.2-6MUSTChecked by
auth.unknown-handshakeA call without a token to a private server returns the document with
account.stateanonymousand every feature at 0 butpairingandsignatures, so a client can show the server's name and pair. A frozen account's handshake has every feature at 0 but those two. A wrong token MUST give401here as elsewhere (§ 5.1): only its absence gives that document.L-4.2-7MUSTChecked by
features.*,features.anonymousA route of a family at 0 MUST answer
404(the server does not offer it) or401/403(not to this caller), never2xx.L-4.2-8MUSTChecked by
handshake.account,features.*,saves.limitsA route of a family above 0 MUST answer as this text says.
4.3 Platforms
L-4.3-1MUSTChecked by
platforms.filterWith the header, a server MUST answer for that platform only: the apps' listing, what is missing (§ 6.3), changes (§ 6.4), sections, titles, homebrew, saves and cheats list and read only what is of that platform; anything else is
404.L-4.3-2MUSTChecked by
platforms.unknownA platform the server does not serve MUST give empty lists, not an error.
L-4.3-3MUSTChecked by
platforms.bad-nameA name that is not a platform name MUST give
400.L-4.3-4MUSTChecked by
missing.no-platform,homebrew.updates-platform,saves.no-platformWithout the header, a server answers for every platform, but where a route needs one:
POST <base>/v1/apps/missing(§ 6.3),POST <base>/v1/homebrew/updates(§ 8.5) and the routes that write a save (§ 9) MUST give400without a platform.L-4.3-5MUSTChecked by
platforms.query?platform=<name>on a listing (or on a route that needs a platform) MUST override the header.L-4.3-6SHOULDChecked by
platforms.varyResponses that depend on the platform SHOULD carry
Vary: Lantern-Platform(§ 14).L-4.3-7MUSTA client's rule: the suite cannot see it
A platform's profile (Annex A) says what the core leaves open. On a platform with no profile yet (Annex A.7), a server MAY serve apps and homebrew under the core's rules alone; a client MUST NOT install anything of a platform it has no profile for.
5.1 Tokens
L-5.1-1MUSTChecked by
auth.basic-refused,auth.no-basicEvery authenticated call sends
Authorization: Bearer <token>(RFC 6750), signed when the token is bound to a key (§ 5.4). There is no other scheme: a server MUST refuse a password and HTTP Basic, on any route, and never ask for them.L-5.1-2MUSTInside the server: the suite cannot see it (a token's entropy cannot be measured from one)
A token is opaque to the client and has at least 128 bits of entropy.
L-5.1-3MUSTInside the server: the suite cannot see it (what a server keeps is inside it)
The server MUST keep only a hash of it.
L-5.1-4MUSTChecked by
admin.devicesIt gives one per device, with the scopes the user approved (§ 5.2), and MUST let the user list and revoke them.
L-5.1-5MUSTChecked by
auth.unknown-token,auth.basic-refused,auth.missing,errors.www-authenticateA missing token where one is needed, or an unknown, revoked or expired one, MUST give
401withWWW-Authenticate: Bearer(anderror="invalid_token"when a token was sent). So does anAuthorizationof another scheme.L-5.1-6MUSTChecked by
auth.frozenA valid token without the scope MUST give
403forbidden; one of a frozen account,403account_frozen.
5.2 Pairing (feature pairing)
L-5.2-1MUSTChecked by
pairing.offeredHow a device gets its token, modelled on the OAuth device flow (RFC 8628) without OAuth's other parts. A private server MUST offer it.
L-5.2-2MUSTInside the server: the suite cannot see it (the protocol has no route for it, which is what the rule says)
The device shows the code, the address
verify, and the short fingerprint of the server's key (§ 5.3). The user opensverifyon another device, signs in there by the server's own means, enters the code, compares the fingerprint, and approves the device and its scopes. Approval happens only on the server's side: its web page, or an admin's device through § 5.7. It MUST NOT happen through the device that pairs: no password goes through it.L-5.2-3MUSTChecked by
pairing.waiting,pairing.unknown,admin.pairings,admin.refuseThe device polls
GET polleveryintervalseconds. The server MUST answer202while waiting, then200{"schema":1, "token":"et_…", "scopes":[…], "bound":true}once, and410when refused, expired, already collected or unknown.L-5.2-4MUSTChecked by
admin.pairingsThe scopes MUST be those approved, at most those asked for and those of the account.
L-5.2-5MUSTChecked by
pairing.codeThe code MUST use the alphabet
23456789ABCDEFGHJKLMNPQRSTUVWXYZ(no0,O,1orI), have 8 to 32 symbols (at least 40 bits), and be shown in groups of 4 joined by-.L-5.2-6MUSTChecked by
pairing.codeA code MUST expire within 10 minutes (
expires, in seconds, at most 600).L-5.2-7MUSTChecked by
pairing.codeThe poll address MUST be unguessable (at least 128 bits) and give the token once.
L-5.2-8MUSTInside the server: the suite cannot see it (it would take many requests; the reference server's tests check it)
The server MUST limit pairing requests per address and attempts at a code: beyond them it answers
429slow_downwithRetry-After.L-5.2-9SHOULDChecked by
pairing.slow-downIt SHOULD answer
429slow_downto polls more often thaninterval.L-5.2-10MUSTChecked by
pairing.verify-completeA server MUST give
verify_complete, on its origin, the code in it.L-5.2-11MUSTChecked by
pairing.verify-completeOpening it MUST NOT approve anything by itself: the user still signs in, sees the device, its scopes and the fingerprint, and approves.
L-5.2-12SHOULDA client's rule: the suite cannot see it
A client SHOULD show
verify_completeas a QR code, which it draws itself: the server serves no image.L-5.2-13MUSTA client's rule: the suite cannot see it
It MUST keep the code and the server's short fingerprint shown next to the QR code.
5.3 Pinning, at pairing
L-5.3-1MUSTA client's rule: the suite cannot see it
During pairing, the device MUST show the short form of the key it sees, next to the code.
L-5.3-2MUSTChecked by
pairing.verify-pageThe server's approval page MUST show the short form of its own.
L-5.3-3MUSTA client's rule: the suite cannot see it
Once paired, the client MUST pin the whole SHA-256 for that server's
uid.L-5.3-4MUSTA client's rule: the suite cannot see it
It polls and calls only over connections with that key: a changed key MUST fail, whatever certificate carries it, until the user pairs again.
5.4 Device keys and signed requests (feature signatures)
L-5.4-1MUSTChecked by
handshake.auth,pairing.key,pairing.bearer-onlyA server that issues tokens MUST offer this feature: it takes
keyat pairing, binds the token to it, and checks the signatures below; itsfeatures.signaturesis then 1, for every caller.L-5.4-2SHOULDA client's rule: the suite cannot see it
A client SHOULD send a key when it pairs.
L-5.4-3MUSTA client's rule: the suite cannot see it
A client that paired with a key MUST sign every request that carries its token.
L-5.4-4MUSTA client's rule: the suite cannot see it
It MUST NOT send its private key anywhere.
L-5.4-5SHOULDA client's rule: the suite cannot see it
It SHOULD keep it where its system protects keys, when it has such a place.
L-5.4-6MUSTChecked by
signatures.unsigned,signatures.invalid,signatures.stale,signatures.replayed,signatures.digestA server MUST check the signature of every request that carries a bound token, in this order, and answer
401(withWWW-Authenticate: Bearer realm="lantern", error="invalid_token") with the code given:L-5.4-7MUSTChecked by
documents.dateClock.
auth.skewis 300 seconds, unless a server announces another, between 60 and 900. Every response of a server MUST carryDate(RFC 9110 § 6.6.1).L-5.4-8SHOULDA client's rule: the suite cannot see it
A client whose signature was refused as
signature_expiredSHOULD take the offset of its clock to thatDateand sign again with it, once: a console's clock is often wrong.L-5.4-9MUSTChecked by
signatures.replayedReplay. A server MUST keep each nonce it took, per key, for twice
auth.skewat least, and refuse it again within that time.L-5.4-10SHOULDChecked by
signatures.accept-signatureA
401for a signature SHOULD carryAccept-Signature(RFC 9421 § 5.1) with the components and parameters it wants.L-5.4-11MUSTChecked by
pairing.keyPolling with a key. A pairing that sent
keypolls with requests signed by it, withoutAuthorization: their components are"@method"and"@target-uri"alone. The server MUST give the token only to a poll so signed, and answer401signature_requiredorsignature_invalidto any other.L-5.4-12MUSTChecked by
pairing.keyKeys of small order. A server MUST refuse at pairing, with
400, a device key that does not decode canonically or is one of the 8 points of small order.
5.5 Invitations
L-5.5-1MUSTChecked by
admin.invitationsA server MUST NOT pair a device with an invitation that is unknown, expired, revoked or used up: it answers
410gone.L-5.5-2MUSTChecked by
admin.invitationsIt MUST count each device it pairs with an invitation, and stop at
devices, even when devices redeem it at once.L-5.5-3MUSTInside the server: the suite cannot see it (it would take many requests; the reference server's tests check it)
It MUST limit attempts at invitation codes per address as it does pairing codes (§ 5.2).
5.6 Rating caps
L-5.6-1MUSTChecked by
admin.rating-capA server MUST hide from a capped token every title above its cap (or unrated, with
unratedhide), the apps of such a title, and each homebrew above it, everywhere:L-5.6-2MUSTChecked by
admin.rating-capThe handshake's
account.rating_capMUST show the cap to its token.
5.7 Administration (feature admin)
L-5.7-1MUSTChecked by
admin.refusedA server MUST answer every route of this family with
403forbiddento a token without the scopeadmin(401without a token).L-5.7-2MUSTChecked by
admin.users,admin.pairings,admin.devicesA server MUST NOT return a token, a hash of one, a password or its hash in any document of this family.
L-5.7-3MUSTChecked by
admin.users-write,admin.devicesA device revoked or an account frozen MUST lose its access at once, on its next request.
L-5.7-4MUSTChecked by
admin.pairingsAn approval MUST grant at most the scopes the device asked for and its account holds;
useris the admin's own account when absent, andscopesthose asked for.L-5.7-5MUSTA client's rule: the suite cannot see it
An admin app MUST show, next to each waiting pairing, its code and the short fingerprint of the server's key as the app pinned it, so that its user compares them with the console's screen as on the server's page (§ 5.3).
6.1 App
L-6.1-1MUSTChecked by
apps.sha256sha256: of the file as served, lower-case hex. A server MUST give it, having hashed the file before it lists it.L-6.1-2MUSTA client's rule: the suite cannot see it
atlas, optional: the game's id in Atlas, the open data on games of Boréale: its Wikidata item,Qand digits (Q900001), the key of Atlas'slinks.json, which ties a game's ids on every platform together (a Switch title id, a Steam app id…). A client MAY read Atlas's data with it (how well known a game is, its prices, similar games); Atlas's files of one platform are keyed by that platform's own ids, whichlinks.jsongives. It is a hint about the game, never its identity: a client MUST NOT install or match anything byatlasalone.
6.2 Listing
L-6.2-1MUSTChecked by
apps.filtersplatformoverridesLantern-Platform(§ 4.3);typeandtitle_idMUST filter, andqsearches names.L-6.2-2MUSTChecked by
apps.pages.*sort:name(the default),addedorrelease(both newest first); ties MUST be broken byid, so the order is stable.L-6.2-3MUSTChecked by
apps.pages.*cursoris opaque;nextisnullon the last page. Reading with a cursor while the library changes MUST neither skip nor repeat an app.L-6.2-4MUSTChecked by
apps.bad-cursorA cursor MUST give
410when it expired (the client then reads the list from its start), and400when the server never gave it, or gave it for another listing (or410, when the server cannot tell it from an expired one).L-6.2-5MUSTChecked by
apps.page-sizepage_sizeis at mostlimits.page_size; a larger one MUST be lowered to it.
6.3 What the device is missing
L-6.3-1MUSTChecked by
missing.installed,missing.up-to-date,missing.base→ a listing (§ 6.2), for that platform, which MUST hold exactly:
6.4 Changes (feature changes)
L-6.4-1MUSTChecked by
changes.tokenGET <base>/v1/changes?since=<token>→{"schema":1, "added":[App…], "removed":["a_…"], "since":"<next token>"}, for the platform the client named, or every platform. Withoutsince, it MUST return no apps and the current token.L-6.4-2MUSTChecked by
changes.bad-tokenA
sincetoken MUST give410when it is too old for the server (the client then reads the whole library again), and400when the server never gave it (or410, as for a cursor, § 6.2).
6.6 Files
L-6.6-1MUSTChecked by
files.whole,files.ranges,files.unsatisfiableMUST support
Range(206;416withContent-Range: bytes */<size>) and sendContent-Length,Accept-Ranges: bytesand a strongETag.L-6.6-2MUSTChecked by
files.whole,files.ranges,homebrew.packages,cheats.text,saves.readMUST send
Repr-Digest: sha-256=:<base64>:(RFC 9530) on every response, whole (200) or ranged (206), and onHEAD: the digest of the whole file, the same as the app'ssha256.L-6.6-3MUSTChecked by
files.whole,compression.filesThe bytes served MUST be those whose SHA-256 the document gives, of the size it gives, without a
Content-Encoding(§ 2.1).L-6.6-4MUSTChecked by
files.unknownThe id in the path is opaque and not enumerable. It MAY expire: the client then reads the app again. An unknown or expired one MUST give
404or410.L-6.6-5MUSTA client's rule: the suite cannot see it
A client MUST check the SHA-256 of what it downloaded against the app's
sha256before installing it, and refuse it when they differ.
7.1 A title
L-7.1-1MUSTChecked by
titles.known,titles.unknownplatformandtitle_id: as an app's (§ 6.1). A title is per platform: the same game on two platforms has two titles. A title of the library MUST be served for its platform, and an unknown one give404.L-7.1-2SHOULDInside the server: the suite cannot see it (the suite does not fetch images)
icon,banner,screenshots(16 at most): images. An image on the server's origin SHOULD be content-addressed and served with a long cache lifetime.L-7.1-3MUSTChecked by
titles.known,titles.bulkAn image elsewhere MUST be marked
"external": true.L-7.1-4MUSTA client's rule: the suite cannot see it
A client MUST NOT send a token, or any credential, to an
externaladdress.L-7.1-5SHOULDA client's rule: the suite cannot see it
It SHOULD open a
videoonly when the user asks.
7.2 Many titles at once
L-7.2-1MUSTChecked by
titles.bulk,titles.bulk-limitGET <base>/v1/titles?ids=<id>,<id>,…(100 at most, comma-separated) →{"schema":1, "titles":[Title…], "unknown":["…"]}: the titles the server has among those ids, for the platform the client named (or for each platform that has the id), and the ids it does not know. A server MUST put every id asked for in one of the two lists, and answer400to more than 100 ids.
7.3 Changes
L-7.3-1MUSTChecked by
titles.changes,titles.bad-tokenGET <base>/v1/titles/changes?since=<token>→{"schema":1, "titles":[Title…], "removed":[{"platform":"switch","title_id":"…"}], "since":"<next token>", "more":false}: the titles added or changed, and those removed, since the token; a server MUST follow § 6.4's rules for them, its tokens likewise.
8.1 An entry
L-8.1-1MUSTA client's rule: the suite cannot see it
kind: what it is:app,game(a game of its own),port(a game ported to the platform, § 8.6),emulator,tool,overlay(shown over other software),sysmodule(runs in the background with the system) ortheme. A reader MUST read a kind it does not know, or none, asapp.L-8.1-2MUSTChecked by
homebrew.kindsA client builds its shelves from
kind(Games, Ports, Emulators, Tools, Overlays…) and, within them, sub-shelves fromgenres. An entry has no other shelf field: an earlier draft'scategoryis gone, and a server MUST NOT write it.L-8.1-3MUSTChecked by
homebrew.pages,homebrew.entriesAn entry MUST be the same on its own route as in a listing, but for the manifests the listing leaves out.
L-8.1-4MUSTChecked by
homebrew.unknownAn unknown id MUST give
404.
8.2 Packages and manifests
L-8.2-1MUSTChecked by
homebrew.entriesA package is one file, downloaded like an app's (§ 6.6):
format,file,size,sha256andmanifest_sha256are REQUIRED.L-8.2-2MUSTChecked by
homebrew.manifestsan archive (
zip) with a manifest: every file it installs, itspathon the device,sizeandsha256, all REQUIRED.L-8.2-3MUSTChecked by
homebrew.packagesThe archive MUST hold each of them under its path without the leading
/, of that size and SHA-256; anything else in it is ignored.L-8.2-4MUSTChecked by
homebrew.manifestsA
pathis absolute on the device's storage, as the profile maps it: it starts with/; its parts are separated by/, none empty,.or..; it has no\,:,", control character or NUL; a part is 255 bytes at most, the path 1024. No two paths of a manifest are the same when case is ignored. A server MUST NOT serve a manifest that breaks these rules, or whose paths lie outside the profile's roots for homebrew (Annex A).L-8.2-5MUSTChecked by
homebrew.manifestsmanifest_sha256is the SHA-256, lower-case hex, of the manifest's canonical form: the JSON array of its entries sorted bypath(as UTF-8 bytes), each entry{"path":…,"sha256":…,"size":…}with its keys in that order, no space, and characters other than ASCII written as they are, in UTF-8. A server MUST give the digest of the manifest it serves.L-8.2-6MUSTA client's rule: the suite cannot see it
On a platform with several architectures (Annex A), a package MAY carry
arch, the list of those it runs on:aarch64,armhf(32-bit ARM, hard float),x86,x86_64orriscv64(64-bit RISC-V); a later version may add others, of 1 to 32 lower-case letters, digits and_. A name not of that form breaks this text. A reader MUST treat a well-formed name it does not know as not its own architecture: it never installs a package for that name alone.L-8.2-7MUSTChecked by
homebrew.kindsA package without
archruns on every architecture of its platform (a package for a runtime, such as a script or bytecode, has none). Each package ofpackagesMUST have anarch, and asignaturewhen the entry is signed (§ 8.4).L-8.2-8MUSTA client's rule: the suite cannot see it
A client takes a package whose
archlists its own, or else one withoutarch; it MUST skip the others, and install nothing when none fits.L-8.2-9MUSTChecked by
homebrew.kindsOn a platform with one architecture (Annex A says so), a package MUST NOT carry
arch.
8.3 Installing
L-8.3-1MUSTA client's rule: the suite cannot see it
refuse a package whose download does not match its
sha256, or a file of an archive that does not match its manifest entryL-8.3-2MUSTA client's rule: the suite cannot see it
refuse a manifest with a path that breaks § 8.2, or that lies outside the profile's roots for homebrew (Annex A), and install nothing of it
L-8.3-3MUSTA client's rule: the suite cannot see it
write only the manifest's paths, and only files: never follow a link out of them
L-8.3-4MUSTA client's rule: the suite cannot see it
write without asking only under the profile's roots that need no consent; for any path elsewhere, show every such path to the user and install only when they agree
L-8.3-5MUSTA client's rule: the suite cannot see it
refuse an entry whose
system_idis that of software anotheridinstalled, and install nothing of itL-8.3-6MUSTA client's rule: the suite cannot see it
keep each installed manifest, and uninstall by it alone: remove its files, then the folders it made that are left empty, nothing else. An update removes the files of the old manifest the new one no longer has.
L-8.3-7MUSTChecked by
homebrew.kindsA server MUST NOT serve two entries of the same platform with the same
system_id.
8.4 Signature
L-8.4-1SHOULDChecked by
homebrew.signaturesA server MAY serve a signature it is given, as given; it does not sign for an author. It SHOULD check a signature before it serves it.
L-8.4-2MUSTA client's rule: the suite cannot see it
A client MUST check the signature of the package it installs when there is one, and refuse the package when it does not verify.
L-8.4-4MUSTA client's rule: the suite cannot see it
A verifier of an Ed25519 signature (of a homebrew here, of a request in § 5.4) MUST decode the public key and the signature's R first, and refuse the signature when either does not decode, is not encoded canonically (a y of p or more, or x = 0 with its sign bit set), or is one of the 8 points of small order ([8]P is the identity), and when S is L or more; only then does it check the equation.
L-8.4-3MUSTA client's rule: the suite cannot see it
On the first install of an
id, the client pins itspublic_key(or that it had none). A later version signed by another key, or unsigned after a signed one, MUST be refused unless the user explicitly accepts the change, after the client showed both keys' short fingerprints (the SHA-256 of the 32 bytes, in the form of § 5.3).L-8.4-5MUSTChecked by
homebrew.signaturesA server MUST NOT serve a signature that L-8.4-4 refuses.
8.5 Routes
L-8.5-1MUSTChecked by
homebrew.filtersGET <base>/v1/homebrew?platform=&q=&kind=&genre=&page_size=&cursor=→ a page (§ 6.2) of entries, sorted by name:{"schema":1, "items":[Entry…], "total":12, "next":null}.qkeeps the entries whose name holds it, case ignored;kind, those of that kind (as a reader reads it, § 8.1);genre, those whosegenreshold it. These filters MUST keep those entries and no others.L-8.5-2MUSTChecked by
homebrew.filtersA
kindor agenrethat is not a lower-case word (1 to 32 letters, digits and_, starting with a letter) MUST give400; one the server does not know, an empty page.L-8.5-3MUSTChecked by
homebrew.updatesPOST <base>/v1/homebrew/updates, with a platform (§ 4.3):{"schema":1, "installed":[{"id":"org.example.frostline", "build":10300}]}→ a page that MUST hold the entries of those ids whosebuildis above the installed one, and only those, paginated withcursorin the query, like § 6.3.
8.6 Game ports
L-8.6-1MUSTChecked by
homebrew.portsA port (
kindport) is a game ported to the platform, such as a source port or a recreation of a game's engine, that may run on the data of the user's own copy of the original game. A server MUST NOT serve that data: a port's package holds the port and, at most, assets free to share, and no route of this family serves a file the port requires from the user.L-8.6-2MUSTChecked by
homebrew.portsA server MUST serve a port's object of these forms, its
data_rootin the profile's areas.L-8.6-3MUSTA client's rule: the suite cannot see it
refuse a port whose
data_rootor one of whose data paths breaks these rules, and install nothing of itL-8.6-4MUSTA client's rule: the suite cannot see it
before it launches the port, check that each file that is not optional is present, and, when an
acceptgives it, that its SHA-256 is one of them (it MAY checksha1andmd5where nosha256is given); otherwise, launch nothing, and show the user each missing or unknown file with itshintL-8.6-5MUSTA client's rule: the suite cannot see it
never fetch that data from the network: the user copies it from their own copy, by the means the device has.
8.7 Requirements
L-8.7-1MUSTChecked by
homebrew.requirestext, REQUIRED: the requirements, for people.L-8.7-2MUSTA client's rule: the suite cannot see it
minandgpuare conditions. A client MUST check them before it offers a package:L-8.7-3MUSTChecked by
homebrew.requiresA server only serves the conditions, as the entry's author gave them: it MUST keep the keys it does not know, and never judge a device by them nor filter a listing with them.
9.1 Backups
L-9.1-1MUSTChecked by
saves.list,saves.readGET <base>/v1/saves?platform=&title_id=→{"schema":1, "saves":[Save…]}, which MUST hold the account's own backups, newest first per title, where a save is:L-9.1-2SHOULDA client's rule: the suite cannot see it
game_version_code: the game'sversion_code(§ 6.1) when it was saved, so that a client SHOULD warn before restoring a save on an older game.L-9.1-3MUSTChecked by
saves.cleanupGETandDELETE <base>/v1/saves/{title_id}/{save_id}: the archive, as a file (§ 6.6), and its removal, which MUST answer204, the backup then giving404.
9.2 In one request
L-9.2-1MUSTChecked by
saves.singleContent-Digest: sha-256=:<base64>:(RFC 9530): the server MUST answer400without it, and422digest_mismatchwhen the body does not match it.
9.3 Resumable upload
L-9.3-1MUSTChecked by
saves.upload-too-largePOST <base>/v1/saves/{title_id}/uploads{"schema":1, "size":5242880, "sha256":"…", "kind":"account", "game_version_code":196608, "profile":"Lea", "note":"Chapter 4"}→201{"schema":1, "upload":"<base>/v1/uploads/u_…", "chunk_size":1048576, "offset":0, "expires":"2026-10-31T09:12:00Z"}. Asizeabovelimits.save_sizeMUST give413.L-9.3-2MUSTChecked by
saves.uploadPATCH <upload>withUpload-Offset: <n>and a chunk of at mostchunk_sizebytes as the body (Content-Type: application/offset+octet-stream) →204with the newUpload-Offset. An offset other than the server's MUST give409conflictwith the server'sUpload-Offset; a larger chunk,413.L-9.3-3MUSTChecked by
saves.uploadHEAD <upload>MUST answer204withUpload-OffsetandUpload-Length: where to resume after an interruption.L-9.3-4MUSTChecked by
saves.upload-digestThe chunk that reaches
sizeends the upload: the server MUST check the whole archive's SHA-256, then answer201with the save entry, or422digest_mismatchand discard it.
10. Cheats (feature cheats)
L-10-1MUSTChecked by
cheats.setsGET <base>/v1/cheats?platform=&title_id=&build_id=→{"schema":1, "cheats":[Set…]}, where a set is{"id":"cs_…", "platform":"switch", "title_id":"0100ABCD00010000", "build_id":"4A1B2C3D4E5F6071", "source":"Community list", "note":"Endless lamp oil", "updated":"2026-10-29T21:00:00Z", "size":512, "sha256":"…"}. A build may have several sets, from several sources. The filters MUST keep those sets and no others.L-10-2MUSTChecked by
cheats.unknownGET <base>/v1/cheats/{id}→ the set's text, as a file (§ 6.6), in the platform's own format and at most 1 MiB, which the client writes where the profile says (Annex A). An unknown id MUST give404.L-10-3MUSTChecked by
cheats.write-cycleWith scope
cheats.write, a server MAY takePOST <base>/v1/cheatsandPUTandDELETE <base>/v1/cheats/{id}; the body ofPOSTandPUTis{"schema":1, "platform", "title_id", "build_id", "source", "note", "text"}.POSTanswers201with the new set ({"schema":1, "id":…}, as listed),PUTandDELETE204. APOSTof a set that exists (the same platform, title, build and source) answers409conflict:PUTreplaces it. A server without them answers405. A server that takes them MUST answer so.L-10-4MUSTChecked by
cheats.writeWithout the scope, writing MUST be refused (
401,403or405).
11. Requests (feature requests)
L-11-1MUSTChecked by
requests.list,requests.createPOST <base>/v1/requests{"schema":1, "platform":"switch", "title_id":"…"}→201with the new request ({"schema":1, "id":…}, as listed), or200with the open request for the same title.platformdefaults to the client's (§ 4.3). A server MUST answer so.L-11-2MUSTChecked by
requests.deleteDELETE <base>/v1/requests/{id}: the account withdraws its own request, whatever its status. A server MUST answer204, and404for another account's request or an unknown one.
12. Errors
L-12-1MUSTChecked by
missing.malformed,errors.problem,errors.never-200Every error MUST be
application/problem+json(RFC 9457) with a real status code, never2xx.L-12-2MUSTChecked by
missing.malformed,errors.problemIts
statusMUST be the response's, and itscodeone the table gives for that status.L-12-3MUSTChecked by
errors.unknown-routeAn unknown route MUST give
404not_found.L-12-4MUSTA client's rule: the suite cannot see it
New codes MAY come with later versions of this text: a client that gets a
codeit does not know MUST act on the status alone.
13. Limits
L-13-1MUSTA client's rule: the suite cannot see it
A client MUST bound what it reads.
L-13-2MUSTChecked by
documents.limitsA server MUST stay within:
L-13-3MUSTChecked by
limits.depthA server MUST refuse a request document beyond these limits, or beyond its own lower ones, with
400or413, never fail on it.
14. Caching
L-14-1SHOULDChecked by
apps.etagEvery
GETreturning JSON SHOULD carry anETagand honourIf-None-Matchwith304.L-14-2SHOULDChecked by
caching.privateResponses to authenticated calls SHOULD carry
Cache-Control: privateandVary: Authorization; responses that depend on the platform or the language also vary onLantern-PlatformandAccept-Language, and compressed ones onAccept-Encoding(§ 2.1).