Repository navigation
feature(Auth): Add aperiodic login, logout and whoami with a saved per-machine key - #30
Conversation
…r-machine key - `aperiodic login` signs in through aperiodic.io/device: it opens the browser, or prints the link over SSH and without a display, polls until the person allows it, and saves the machine's own named key. - The key is saved in ~/.config/aperiodic/credentials (0600; %APPDATA% on Windows, or $APERIODIC_CONFIG_DIR), which the Python client reads too. - Every command finds its key in APERIODIC_API_KEY first, then the saved key, then (with --preview) the demo key. - `aperiodic logout` revokes this machine's key and removes it; `aperiodic whoami` shows the key and what the plan covers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur
Windows has no permission bits: Go reports a writable file as 0666, and the file is kept private by the ACL of the user's profile instead. The 0600 check now runs on Linux and macOS only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur
Critical review: please address before mergeI checked this against the server routes in dream-faster/unravel-router#1174 and ran 1. Logging in again leaks a key each time (affects the Python client too)
Fix (same in both clients): if a saved key exists, check it with 2.
|
…DEMO-KEY, clearer revoked-key errors - login keeps a saved key that still works (checked with /cli/whoami) and says so; --force, or a revoked saved key, signs in again and then revokes the old key, best effort, so keys no longer pile up to the 25-key limit. - Key order is APERIODIC_API_KEY, then DEMO-KEY for --preview, then the saved key: a revoked saved key can no longer break the free sample. - A 401 on the saved key says which file it came from and to log in again, for data commands, raw, stream and whoami. - The device link goes to the OS opener only when it is https (or http on loopback) on the API's own host; otherwise it is printed. - created_at is kept as text, so no other writer's format can make the key unreadable. - Credentials are written through a fresh 0600 temp file; logout passes on the server's reason; whoami names the demo key; login -h exits 0; the test seams on CLI are unexported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur
|
Thanks for the review. Every point is addressed in 486bcfa:
Nits:
Generated by Claude Code |
… own (#43) **Needs the server side first:** [dream-faster/unravel-router#1174](dream-faster/unravel-router#1174) (the `/api/v1/cli/*` routes and `/device`) is merged; release this after it's live in production. It's the same flow as [aperiodic-io/cli#30](aperiodic-io/cli#30), and both write the same credentials file. Until the server is deployed, `login()` gets a 404 when starting, and everything else works as before. ## What changes - **`aperiodic.login()`**, or `python -m aperiodic login` on a machine without the CLI: - If this machine already has a saved key that still works, it says `Already signed in as "<name>" on <plan>. Call aperiodic.login(force=True) to replace it.` and returns that login. On the command line the flag is `--force`. Logging in twice doesn't pile up keys. - Otherwise it starts a login, prints the `aperiodic.io/device?code=…` link and the code, and polls until the person allows it. - It backs off on `slow_down` and stops on `access_denied` / `expired_token`. - It keeps polling through 5xx, 429 and dropped connections. - It saves the machine's own named key to `~/.config/aperiodic/credentials`. The file is written through a temp file at mode 0600, with the same path rules as the CLI (`$APERIODIC_CONFIG_DIR`, `$XDG_CONFIG_HOME`, `%APPDATA%`). When it replaced a key, it revokes the old one (best effort). - It opens a browser only on a desktop: not over SSH, in a notebook (the kernel may be remote) or on Linux without a display. - It returns `LoginResult(key_name, plan_name, path)`. The key itself is never printed or returned, so it can't end up in a notebook's output. - **Key lookup:** a passed `api_key=` first, then `APERIODIC_API_KEY`, then the demo key for `preview=True`, then the saved key. A saved key never answers a preview request, so a revoked one can't break the free sample. This applies to every endpoint, including `stream()`, `get_symbols()` and `get_symbols_async()`, which now take `api_key=None`. The README spells out the order, and the CLI uses the same one. - **A rejected saved key** raises `The key saved in <path> was rejected (revoked?). Run aperiodic.login() again.` instead of a bare 401. - **`aperiodic.logout()`** revokes the key (`DELETE /cli/key`), removes the file and returns `LogoutResult(removed, revoked, message)`. The message says what happened: revoked, already revoked, or the account's default key (rotate it at /home/api). - **`aperiodic.whoami()`** returns the key and what its plan covers. `python -m aperiodic whoami` prints it, with a Markets line for a plan held to some (Free: "BTC, ETH, SOL and 10 RWA perps"). - **Errors:** - With no key, the error says to run `aperiodic.login()` or set `APERIODIC_API_KEY`. - Offline, the command line prints `Error: Could not reach …` instead of a traceback. - **Standard library only** (`urllib`): the core install has no HTTP client (httpx is an extra). In Pyodide, `login()` explains it needs regular Python. ## Tests - `tests/test_auth.py` runs against a local `ThreadingHTTPServer` stand-in of the server and covers: - login saves the key with mode 0600 and never prints or returns it; a stale temp file can't loosen the mode; - re-login: a working key is kept, `force=True` replaces and revokes, and a revoked key is replaced; - a denial saves nothing, and polling backs off on `slow_down` and survives 5xx / 429 / non-JSON pages; - offline `login` / `whoami` fail with a message, not a traceback; - lookup order: passed key, then env, then the saved key, with no key leaking in from the machine running the tests; - preview stays on the demo key; - a rejected saved key names the file and says to log in again; - logout revokes and removes the key and says what happened in one line; - `get_symbols` (sync and async) finds the saved key; - `whoami` and the `python -m aperiodic` entry point, including the Markets line and `--force`; - the new functions are exported. - `tests/test_stream.py`: `stream()` uses the saved key when none is passed, and a rejected saved key says to log in again. - `test_auth`, `test_readme` and `test_stream` pass (89). ruff 0.14.1 (CI's version) is clean. - The full suite fails the same 34 integration tests here as on `main` (they need a real key and network). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur --------- Co-authored-by: Claude <noreply@anthropic.com>
Needs the server side first: dream-faster/unravel-router#1174 (the
/api/v1/cli/*routes and the/devicepage) is merged; release this after it's live in production. Until thenaperiodic logingets a 404 from/cli/login, and every other command works as before.What changes
aperiodic login:Already signed in as "<name>" on <plan>. Run 'aperiodic login --force' to replace it.and stops. Logging in twice doesn't pile up keys.aperiodic.io/device?code=…in the browser. It only opens an https link on the API's own host; anything else is printed. Over SSH (SSH_CONNECTION/SSH_TTY), on Linux without a display, or with--no-browser, it prints the link to open on any device instead.slow_downand stopping onaccess_denied/expired_token. Then it saves the machine's own named key, and when it replaced one, revokes the old one (best effort).--nameoverrides the key's name, which defaults to<hostname> (aperiodic CLI).~/.config/aperiodic/credentials, JSON, mode 0600 in a 0700 directory, written through a temp file and renamed into place. It lives in$XDG_CONFIG_HOME/aperiodic, or%APPDATA%\aperiodicon Windows;$APERIODIC_CONFIG_DIRoverrides both. Only a missingapi_keycounts as no credentials. The Python client reads the same file (aperiodic-io/client#43).metric,symbols,raw,stream):APERIODIC_API_KEYfirst, thenDEMO-KEYwith--preview, then the saved key. A saved key never answers a preview request, so a revoked one can't break the free sample. The order is written down in the README, and the Python client uses the same one.The key saved in <path> was rejected (revoked?). Run 'aperiodic login' again.instead of a bare 401.aperiodic logout: revokes this machine's key on the server (DELETE /cli/key) and deletes the file. It passes on the server's reason when revoking doesn't happen: for an account's default key it says to rotate it at /home/api, and for a key that's already revoked it says so. If the server can't be reached, it still removes the file and says where to revoke the key by hand.aperiodic whoami: shows the plan, which key is in use and where it came from, datasets in full, markets (for a plan held to some, as Free is: "BTC, ETH, SOL and 10 RWA perps"), history and live.Tests
cli_auth_test.goruns against anhttpteststand-in of the server and covers:--forcereplaces and revokes, and a revoked key is replaced;slow_down, and a denial saves nothing;APERIODIC_API_KEYwins over the saved key, and--previewusesDEMO-KEYeven with a saved key;created_at, and saving leaves no temp file behind;whoamioutput, including the markets line and the demo key;login -hexits 0;aperiodic login.gofmtandgo vet(also withGOOS=windows) are clean. Every test that needs no live key passes.requireAPIKey) fail locally withoutAPERIODIC_API_KEY, as they do onmain. CI sets the key.🤖 Generated with Claude Code
https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur