| D-M3-05-1 |
2026-10-20 |
Ticket layout: the payload is a version byte 1, the file id (16 raw UUID bytes), a variant code byte (original 0, small 1, medium 2, large 3, poster 4), the principal id and the auth session id (16 raw UUID bytes each) and the expiry as uint64 big-endian Unix seconds; the ticket is base64url without padding (RFC 4648 section 5) of the payload followed by the full HMAC-SHA-256 tag over it, and any other length, alphabet, version or variant code is invalid. Key ring: files/app keeps the current key of file.ticket_key_ref and the key it saw before the last version change, verifies against both and mints with the current one; a rotation takes effect per process after secrets.cache_ttl, and a ticket minted by a process that already switched may be refused by one that has not for at most secrets.cache_ttl, after which the player asks for a new ticket. The previous key is kept only in the memory of a process that saw it, so a process started after the change, and every process after a second change within file.ticket_ttl, refuses tickets of the older key until they expire. |
A fixed binary layout has one encoding per content and no parser to attack, and two keys at the same time let a key change happen without an outage (12 9.2 point 2, ADR 0048 point 2). |
| D-M3-05-2 |
2026-10-20 |
createFileTicket for a principal without a login session (API token, agent, app) is 403 forbidden with reason step_up_not_possible; the rule sits in the file.read entry of authz (descriptor fact IssueTicket) and runs after visibility, so a file the principal cannot see stays 404. |
12 9.2 binds every ticket to auth_session_id, and 07 4.1 already defines this reason as "token without a session"; a token holder sends its token with every request and needs no ticket. |
| D-M3-05-3 |
2026-10-20 |
On downloadFile and downloadThumbnail an Authorization header wins over the query parameter ticket: an authenticated request ignores the ticket and gets the authenticated Cache-Control (private, max-age=<client.cache_max_age>, immutable). |
The header is the stronger, proof-bound credential, and one request must have exactly one actor. |
| D-M3-05-4 |
2026-10-20 |
Every failure of the ticket path answers the unknown-id 404: bad encoding or MAC, expiry, another file or variant than the route, an error of Auth.SessionActor, and every decision of authorize(file.read) that is not an allow. |
12 9.2 point 3: a ticket holder learns nothing about why a ticket stopped working or whether the file exists. |
| D-M3-05-5 |
2026-10-20 |
A deleted file and a thumbnail variant the file does not have answer like an unknown id (404 not_found). |
12 2 ends the state "deleted" without delivery; clients request only the variants File.thumbnails lists, so a missing variant of a readable file reveals nothing worth a code of its own. |
| D-M3-05-6 |
2026-10-20 |
A missing object of a deliverable file is 500 internal_error with the file id only in the error log, never in the body; every other failure of the object storage is 503 unavailable. |
A deliverable file without its object is a server fault, not a client error, while a storage outage is transient (02 14.4). |
| D-M3-05-7 |
2026-10-20 |
createFileTicket checks readiness like a download: no ticket for a file that is not deliverable (409 file_not_ready with Retry-After or 409 file_blocked). |
A ticket for a file that cannot be delivered would only lead the player into a second 409. |
| D-M3-05-8 |
2026-10-20 |
getTranscript answers every readable deliverable file, also one with status none; text, language and source are present only with done (no JSON null). |
The status itself is the information a client needs, and ADR 0053 forbids null members. |
| D-M3-05-9 |
2026-10-20 |
Delivery headers as in 12 9.3; the ASCII fallback of Content-Disposition replaces every character outside printable ASCII and every ", \, % and ; by _, and an empty name becomes file; filename* carries the UTF-8 bytes percent-encoded outside the RFC 8187 attr-char set. A thumbnail's name is the original name without its last extension, then -<variant>, then .jpg or .png after its type. |
The fallback can never break the header or be read as a parameter separator, and a thumbnail gets a name with the extension of its own type. |
| D-M3-05-10 |
2026-10-20 |
The ETag and Repr-Digest of a thumbnail come from one extra full read of its object through ObjectStore.Get before the second, possibly ranged read that serves it. |
03 gives file_thumbnails no checksum column; hashing a stream keeps memory constant and thumbnails are small. |
| D-M3-05-11 |
2026-10-20 |
Range rules: only the unit bytes; one range first-last, first- or -suffix gives 206; several ranges or an invalid value give 200; a first byte at or after the end or a zero suffix gives 416 with Content-Range: bytes */<size>. If-Range applies the range only with the current strong ETag; there is no 304 and If-None-Match is not evaluated. |
RFC 9110 section 14 within 12 9.3, which supports exactly one range; the server sends no Last-Modified, and content never changes once deliverable (12 4.4). |
| D-M3-05-12 |
2026-10-20 |
New key file.not_ready_retry_after (default the same value as http.unavailable_retry_after) gives the Retry-After of 409 file_not_ready in whole seconds. |
12 9.3 demands Retry-After but named no key, and limits come only from named keys. |
| D-M3-05-13 |
2026-10-20 |
A ticket request is anonymous in the chain, so rl.read is taken from the bucket of the client IP; an invalid ticket is a 404 of the handler, not a failed credential, so it does not count against rl.public_auth and is not audited. A valid ticket whose authorize fails is audited with the ticket's principal. |
Before the ticket verifies no principal exists to audit, and the MAC is not guessable; once it verifies the denial belongs to a real principal (07 4.2). |
| D-M3-05-14 |
2026-10-20 |
Transfer deadline: the route flag Download of M1-17 already marks binary 2xx responses and StageRoute applies http.transfer_timeout to them, so no new flag. The route generator accepts the requirement fileTicket beside a subset of the root security and records such an operation as root security: the chain authenticates the header as on every route, an anonymous request reaches the handler, which checks the ticket, and a console cookie authenticates a download like any route. |
The existing flag already has the meaning the brief asked for, and the ticket is checked by the use case, not by the chain. |
| D-M3-05-15 |
2026-10-20 |
Start check: the roles api and all do not start when file.ticket_key_ref is missing, unreadable or holds less than 32 bytes as hex, the length of file_ticket_key in configuration.md section 5. |
A server that cannot mint or verify tickets would fail every player at run time; failing at start makes the fault visible at once. |
| D-M3-05-16 |
2026-10-20 |
The migration 00056_files_thumbnails.sql creates file_thumbnails exactly as 03 4.7 describes; M3-06 uses it and creates nothing. |
downloadThumbnail reads the table, and M3-06 is not in the dependency closure of this ticket. |
| D-M3-05-17 |
2026-10-20 |
The ticket is declared as the optional query parameter FileTicketQuery (name ticket) on downloadFile and downloadThumbnail, besides the security scheme fileTicket. |
The strict server hands a handler only declared parameters, so without the parameter the handler would never see the ticket. |
| D-M3-05-18 |
2026-10-20 |
file.read per purpose: the visibility of avatar (every authenticated principal), branding (everybody) and gdpr_export (nobody) applies only to file.read; file.delete keeps its rule. downloadFile is not a public operation, so an anonymous caller gets 401 before the purpose rule and reaches a branding file only through getBrandingLogo (M3-10). The requirement is substituted inside Decide (avatar Auth, branding Public), and the requirement table row stays read. |
Scope requires exactly read for list actions, and filter mode decides file.updated events without facts by their channel path; substituting per purpose keeps both intact (05 2.2). |
| D-M3-05-19 |
2026-10-20 |
Auth.SessionActor also treats a device without contact for auth.device_inactive_revoke_after as an inactive key, like the Authenticator, and uses two reads (AuthState and ActiveFamily) instead of one joined query. |
A ticket must not outlive what a request of the same device would be refused for, and the two existing store reads avoid a new query with the same result. |
| D-M3-05-20 |
2026-10-22 |
The ticket was built on an integration branch and lands on main as a port: one branch from main with only the ticket's own commits, adapted to main, one pull request, squash merge. On main the thumbnail migration is 00056_files_thumbnails.sql (the numbers up to 00055 are taken), the rulings and the changelog entry are fragments, and the spec references name the English chapters (12-files.md, 07-permissions.md). The integration fixes of int/m1-wave50 for M3-05 concern only the merge with M3-06, which is not on main, so none applies. |
Integration branches are retired; every ticket lands on main on its own. |