Skip to content

feat: add audible request, for what the api command is not - #316

Open
mkb79 wants to merge 5 commits into
fix/api-takes-a-pathfrom
feat/request-command
Open

mkb79 wants to merge 5 commits into
fix/api-takes-a-pathfrom
feat/request-command

Conversation

@mkb79

@mkb79 mkb79 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Based on #311. The diff below is against fix/api-takes-a-path, not
master. Both belong in the same release: #311 narrows api to the Audible
API and to JSON, and this is where everything else goes, so that nothing is
taken away without the replacement being there. Merge #311 first; GitHub
retargets this one.

audible api speaks to the API and expects JSON back. The rest of what
audible-cli reaches had nowhere to go from the command line: the website a
companion file comes from, the delivery host that hands out the audio, Amazon's
CDE service for annotations, and every answer that is not JSON.

audible request https://www.audible.de/companion-file/B07J2M2VC7 -o chapters.json
audible request https://cde-ta-g7g.amazon.com/FionaCDEServiceEngine/sidecar \
    -q "type=AUDI" -q "key=B07J2M2VC7" -i

Which hosts, and why there is a list at all

The request carries the credentials of the profile, so the list of allowed
hosts is the list of who may be handed them: api, www, cds and
api.amazon of any of the 11 marketplaces, plus cde-ta-g7g.amazon.com. That
is 45 hosts, derived in constants.py from the library's marketplace list, so
it grows with it. A url naming anything else is refused before any credentials
are loaded, as are plain http, a port of its own, and a name and password
written into the url.

What passes through untouched

The url is sent as it was written, not parsed and rebuilt. A delivery url is
signed over those exact bytes, and reading its query into pairs turns %FF
into the replacement character, %20 into + and flag into flag=.
--query appends to the raw query instead of merging into a parsed one.

The body goes out as it was given — bytes, not JSON — and the answer is
streamed, so a file of any size can go through --output without being held
whole in memory. The method is whatever the host understands, PATCH and
OPTIONS included.

--include/-i writes the status line and the response headers in front of the
body, wherever the body goes; --dump-header/-D writes them to a file of their
own. A header that came twice is written twice. A redirect is shown, not
followed. An error status ends the command after the answer has been written,
because the body of a refusal is what explains it.

Shared with api

QueryPair and HeaderPair move to _params.py, so both commands split a
query and a header the same way and refuse the same headers. One name differs:
api writes the body itself and always as JSON, so it owns content-type as
well, while here the body is raw and its type is the caller's to declare.

Tests

59, including two that go through the real AsyncClient behind a mock
transport, where a stand-in cannot help: the url that actually leaves, and that
a redirect is not followed — the second needs two hops to say anything.

mkb79 added 5 commits August 31, 2026 11:49
`QueryPair` and `HeaderPair` leave cmd_api.py for a module of their own,
because a second command needs the same two shapes and the details are
easy to get subtly different: where the split happens, what is trimmed,
which names are refused.

The refusal list is the one thing that differs. `api` writes the body
itself and always as JSON, so it owns `content-type` as well; a command
that sends a body as it was given has to leave the type to whoever wrote
it. `HeaderPair` therefore takes the set it refuses.
`audible api` speaks to the Audible API and expects JSON. The rest of
what audible-cli reaches had nowhere to go from the command line: the
website a companion file comes from, the delivery host that hands out
the audio, Amazon's CDE service for annotations, and every answer that
is not JSON.

`request` takes a whole url. Its host has to be one of the 45 this tool
has business with -- `api`, `www`, `cds` and `api.amazon` of a
marketplace, plus `cde-ta-g7g.amazon.com` -- derived from the library's
marketplace list so it grows with it. The request carries the
credentials of the profile, so the list is the list of who may be handed
them, and a url naming anything else is refused before they are loaded,
as are plain http, a port of its own and a name and password written
into the url.

Body and answer pass through untouched. The url is sent as it was
written rather than parsed and rebuilt: a delivery url is signed over
those exact bytes, and reading its query into pairs would turn `%FF`
into the replacement character and `%20` into `+`. `--query` appends to
the raw query instead. The answer is streamed, so a file of any size can
go through `--output` without being held whole in memory first.

`--include` writes the status line and the response headers in front of
the body, wherever the body goes; `--dump-header` writes them to a file
of their own. A redirect is shown, not followed. An error status ends
the command after the answer has been written, because the body of a
refusal is what explains it.
The host allowlist is the security boundary, so it is tested from both
sides: every family of allowed host reaches the client, and a foreign
host, plain http, an ip literal, a punycode name, a trailing dot, a port
and a name in the url are all refused without a client ever being built.

The rest pins what passes through unchanged: a query with `%FF`, `%20`
and a key without a value, a body from an option, a file and a pipe,
repeated request headers, repeated response headers, a compressed answer
and any method the host might understand.

Two cases go through the real `AsyncClient` behind a mock transport,
where a stand-in cannot help: the url that actually leaves, and that a
redirect is not followed -- the second needs two hops to say anything.
The README section says which hosts are allowed and why the list exists,
what passes through unchanged, and how `--include`, `--dump-header` and
an error status behave.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant