Instances & arr proxy
HTTP routes and behavior for instances & arr proxy.
A kids account’s Radarr and Sonarr reads through the proxy are cut to the records the account may see, judged from the record’s own certification and genres (the policy region’s scheme first, then the US scheme; a string neither knows is unrated): library and calendar arrays, wanted, and the records of queue and history by their embedded movie or series: includeMovie=true / includeSeries=true is forced onto those requests so every row carries a parent to judge, and a row without one is dropped. A single blocked movie/{id}, series/{id}, or episode/{id} is 404 {"error":"not found"}; a gated body the proxy cannot judge (not JSON, an unexpected shape) is the same opaque 502 as any unsanitizable response. Chaptarr and Lidarr carry no ratings and are not gated: those modules are granted per user.
GET|POST /api/instances # admin: list/createGET /api/instances/media-roots # admin: deployment-approved Cantinarr roots for the mapping editorPOST /api/instances/test # admin: dry-run connectivity check, dialed from the serverPOST /api/instances/media-server/libraries # admin: libraries a media server reports (same body + credential fallback as /test)POST /api/instances/plex/link/begin # admin: start a plex.tv PIN link -> { pin_id, code, url } (token held server-side)POST /api/instances/plex/link/check # admin: poll the PIN { pin_id } -> { linked, account } (the account is recorded as the server's owner on save)POST /api/instances/plex/servers # admin: the linked account's owned servers ({ plex_link_pin } or { id }) # -> { server_name, version, libraries: [{ id, name, collection_type }] }PUT|DELETE /api/instances/{instanceID} # admin: update/deleteGET|PUT /api/instances/{instanceID}/users # admin: routing preferences for already assigned usersGET|PUT /api/instances/{instanceID}/grant-users # admin: replace all access grants here (legacy clients)GET|PATCH /api/instances/{instanceID}/assignments # admin: directory and selected-user add/remove # (additive; never moves anyone's default)POST /api/instances/{instanceID}/webhook # admin: rotate credentials and upsert a managed arr webhookGET /api/instances/{instanceID}/webhook # admin: live instant-updates state, read from the arr's Connect listGET /api/instances/{instanceID}/hardcover # admin: {supported, configured, oauth_available, method, reconnect_required, connection_id?, revision}; no tokensPUT /api/instances/{instanceID}/hardcover # admin: {token}; verify catalog reads (400 invalid/insufficient scope, 502 unreachable), then atomically select API tokenDELETE /api/instances/{instanceID}/hardcover # admin: disconnect either method from this instance onlyPOST /api/instances/{instanceID}/hardcover/device/begin # admin: begin actor/instance-bound OAuth; {flow_id,status,user_code,verification_uri,expires_at,interval}GET /api/instances/{instanceID}/hardcover/device/{flowID} # initiating admin: poll; pending/connected/expired/denied/cancelled/superseded/failed, safe error and connection_id on successDELETE /api/instances/{instanceID}/hardcover/device/{flowID} # initiating admin: cancel; reports connected if completion already committedPOST /api/instances/{instanceID}/hardcover/apply # admin: {connection_id,instances:[{instance_id,revision}]}; individual {instance_id,applied,error?} resultsANY /api/instances/{instanceID}/* # proxy to the instance's own API; JSON secrets are redacted, # and upstream redirects are remapped onto this route (off-origin ones become 502)Hardcover uses Cantinarr’s registered public client (internal/hardcover/oauth.go) and only read:catalog:data, against https://api.hardcover.app/oauth2/device and /oauth2/token through httpx.External(). No inbound callback or client secret is required. All connection-management responses use Cache-Control: no-store. Device codes stay in memory, bound to the initiating admin and instance, and expire on restart. Provider polling honors intervals, pending/slow-down responses, denial, expiry, cancellation, and transient failures.
Catalog verification precedes an atomic method switch; failed replacements retain the previous connection. Instance revisions protect against concurrent token changes, disconnected/deleted instances, and superseded flows. OAuth apply uses the expected source connection plus explicit target revisions, returning each result independently. Refresh writes use credential revision checks, preserve an omitted refresh token, and retain a newly issued pair in memory if persistence temporarily fails. Confirmed invalid_grant marks the shared connection for reconnection; transient failures preserve credentials. Restarting during an unresolved storage failure can lose a not-yet-persisted rotation, so restore writable storage before restarting. Disconnecting removes only the local instance link and deletes the credential after its last link; account-wide revocation remains in Hardcover’s Authorized Apps page. Chaptarr’s metadata credential is separate.
The proxy allows read-only Radarr/Sonarr browsing (library, queue, history, wanted, calendar) for regular users, the matching Chaptarr reads plus its book/author lookups and MediaCover art, and the matching Lidarr reads plus its album/artist lookups, mediacover art, and the track/track-file reads album downloads need; writes, commands, interactive search, config, and all non-arr services require admin. Requesters are bound to their explicit instance assignments, exactly as /api/config reports them; a sibling instance the admin has hidden cannot be reached by guessing its ID, and instance authorization is classified from stored metadata so an undecryptable secret can never widen access. JSON responses are bounded and recursively scrubbed for credential fields and secret-bearing URL query parameters before they reach any client. An encoded, malformed, streaming, or oversized JSON response fails closed rather than bypassing that scrubber.