Add logout, passkey management, and a standalone login
CI / test (push) Canceled after 0s
CI / Build on the declared MSRV (push) Canceled after 0s
CI / Latest stable (advisory) (push) Canceled after 0s

The "auth routes" open question in GOALS.md conflated four unrelated
things once the upstream routes and controllers were checked: logout and
the passkey settings routes sit behind the same token_auth middleware as
everything else this crate wraps, login needs a credential exchange since
there's no token yet, refresh/refresh_react turned out to be the bulk
sync-dump endpoint already parked as a separate open question, and
connected_account is Gmail OAuth linking that belongs with Integrations.

Adds logout, list_passkeys, delete_passkey (passkey registration is left
out — it needs a WebAuthn ceremony a browser has to drive), and
InvoiceNinjaClient::login as an associated function returning a lean
LoginResponse that types only is_owner/is_admin/token and leaves the
server's much larger nested payload in extra.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 14:38:48 -04:00
co-authored by Claude Sonnet 5
parent ab696a910b
commit 1a8e4aad26
12 changed files with 495 additions and 49 deletions
+24 -7
View File
@@ -228,6 +228,24 @@ last item, so **the roadmap is closed**: every route in `routes/api.php` that
an API-token client can reach is now wrapped. It did land in pieces, as the
item predicted — import/export first, then the integrations.
**Done:** the auth routes, once split apart — the single "auth routes"
label had quietly merged four unrelated things. `logout` and the passkey
list/delete routes sit behind the same `token_auth` middleware as every
other wrapped endpoint, so they needed no new client type, just methods
(and a documented warning that `logout` rotates every token on the whole
company, not just the caller's). `login` *does* need a different entry
point — there is no token yet to build an `InvoiceNinjaClient` with — but
that turned out to be a standalone associated function, not a second
client type: `InvoiceNinjaClient::login` trades an email and password for
a `LoginResponse` carrying a token, which then goes to `new` or `hosted`
as normal. Passkey *registration* and passkey *login* stay unwrapped
because they're WebAuthn ceremonies a browser has to drive; `/refresh` and
`/refresh_react` turned out to be the bulk `updated_at` sync dump already
parked below, not an auth concept, so they stay out for that reason
instead; and `/connected_account` (Gmail OAuth linking) was filed under
the wrong heading — it belongs with the Integrations done item above. See
`COVERAGE.md`'s "Logout, passkeys, and login" section for the detail.
As the item also predicted, it is the least SDK-shaped part of the API, and
the typing reflects that: most of these return `serde_json::Value`, because
their replies are pass-throughs from Storecove, Stripe, Yodlee or GoCardless,
@@ -244,18 +262,17 @@ a registered route with no controller method behind it, so it 500s — there is
nothing to wrap until upstream adds the handler.
What remains unwrapped is now only what each entry in `COVERAGE.md`'s gap
list explains: the auth routes (an open question below), the client-portal
family, the provider webhooks, and a few company and user extras. Keep that
file current — it names exact counts, so it goes stale quietly.
list explains: the passkey and SSO ceremonies and `/login/precheck`
(each needs something — a browser, a redirect — this client doesn't have),
`/refresh`/`/refresh_react` (the sync-engine shape parked below), the
client-portal family, the provider webhooks, and a few company and user
extras. Keep that file current — it names exact counts, so it goes stale
quietly.
## Open questions
These are genuinely undecided. Deciding one is a change to this file.
- **Auth routes** (`/login`, `/logout`, `/refresh`, passkeys) are in the gap
list, but they are a different authentication model from the API token
this client is built around. Including them may mean a second client type
rather than more methods on this one. Not obviously in scope.
- **`chart` is still generic** over `T: DeserializeOwned`, because the
eleven chart endpoints share no response shape. Per-chart structs would
be eleven small models of unstable payloads; unclear whether that earns