feat(stockbit): one browser sign-in in idx-core for the CLI and MCP

`stockbit::signin` is now the single sign-in flow. `idx stockbit login`
waits on it in the foreground; the `stockbit_login` MCP tool runs it on a
thread and `stockbit_status` reports `sign_in.state`, so an agent knows when
to stop polling. Auto uses a browser unless the shell is over SSH or has no
display, and falls back to the local page only when the browser cannot start.

The browser way opens Stockbit's sign-in page in a profile only idx uses and
reads the session cookie over DevTools. What a real sign-in showed:

- The cookie is host-only on stockbit.com, so deleting by the assumed
  .stockbit.com removed nothing and left live tokens in the profile. It is
  now deleted by its own domain and path, with every other tab closed first,
  and read back to confirm.
- A signed-out page writes the cookie without tokens; it no longer counts.
- A profile belongs to one browser, so it records the executable that made it.
- On macOS the config directory depends on XDG_CONFIG_HOME, so a shell and an
  app started from the Dock disagreed; both default locations are now checked.
- A thread does not outlive main, so idx-mcp closes a waiting browser when
  stdin closes or on SIGTERM or Ctrl-C.

Progress goes to stderr, which keeps `-o json` to one document on stdout.

Google refused its own sign-in in this window on 2026-10-31, most likely over
the debugging flag; password sign-in works, and the docs and roadmap say so.

Claude-Session: https://claude.ai/code/session_01RqPPVaoFxLToGAXXBEoCtH
This commit is contained in:
Rasyidan Akbar F. 2026-10-31 23:58:21 +07:00
commit ca4d3d09c2
21 changed files with 2799 additions and 212 deletions

View file

@ -1550,12 +1550,12 @@ Options:
## `idx stockbit`
```text
Your own Stockbit session, for `stocks orderbook`, live `--provider stockbit` quotes and Stockbit daily history. Opt-in and personal: idx uses your account's tokens, stored in the config directory with owner-only permissions, and never logs them. Stockbit rotates the refresh token on every refresh, so a browser sharing the session is logged out once idx refreshes: log in through a separate browser profile for idx. Stockbit's terms allow personal, non-commercial use.
Your own Stockbit session, for `stocks orderbook`, live `--provider stockbit` quotes and Stockbit daily history. Opt-in and personal: idx uses your account's tokens, stored in the config directory with owner-only permissions, and never logs them. Stockbit rotates the refresh token on every refresh, so one session has one owner: `login` signs in through a browser profile only idx uses. Stockbit's terms allow personal, non-commercial use.
Usage: idx stockbit [OPTIONS] <COMMAND>
Commands:
login Connect a session in your browser, or from stdin
login Sign in through your browser, or from stdin
status Show whether a session is stored and when its tokens expire
logout Delete the stored session
help Print this message or the help of the given subcommand(s)
@ -1590,8 +1590,9 @@ Options:
Print help (see a summary with '-h')
Examples:
idx stockbit login # paste the credentialStorage cookie
pbpaste | idx stockbit login
idx stockbit login # sign in through a browser
idx stockbit login --no-browser # local page: bookmarklet or paste
pbpaste | idx stockbit login # the credentialStorage cookie on stdin
idx stockbit status
idx stockbit logout
```
@ -1599,13 +1600,13 @@ Examples:
### `idx stockbit login`
```text
In a terminal, opens a local page on 127.0.0.1 with two one-click ways to hand idx your Stockbit session: a bookmarklet that reads stockbit.com's cookie in your own browser, or a read-the-clipboard button. Without a browser (or with --no-browser), the page URL is printed instead. With stdin piped, it reads the value of stockbit.com's `credentialStorage` cookie (DevTools, Application, Cookies) or a refresh token from stdin, as before. Either way idx checks the session with one request. Servers can set IDX_STOCKBIT_REFRESH_TOKEN instead.
In a terminal, idx opens Chrome (or Chromium, Edge, Brave, Helium) on Stockbit's sign-in page with a profile only idx uses. Sign in there: idx reads the session cookie over the DevTools protocol, checks it with one request, takes it out of that profile, and closes the window. Your password, one-time codes and any reCAPTCHA stay in the browser. A stored session that still works means nothing to do: idx exits at once. Over SSH, without a display, without such a browser, or with --no-browser, idx serves a local page on 127.0.0.1 instead, with a bookmarklet that reads the cookie in your own browser and a clipboard button. With stdin piped, it reads the value of stockbit.com's `credentialStorage` cookie (DevTools, Application, Cookies) or a refresh token from stdin. Servers can set IDX_STOCKBIT_REFRESH_TOKEN instead; IDX_STOCKBIT_BROWSER picks the browser.
Usage: idx stockbit login [OPTIONS]
Options:
--no-browser
Print the page URL instead of opening a browser
Use the local page instead of a browser sign-in
-o, --output <OUTPUT>
Output format [default: auto = table on a terminal, json when piped]
@ -1616,7 +1617,7 @@ Options:
Disable colored output
--port <PORT>
Fixed port for the local page (for `ssh -L`); default: any free one
Port for the local page (for `ssh -L`); default 7878, or any free one
-q, --quiet
Suppress non-essential output

View file

@ -80,44 +80,114 @@ idx --provider stockbit stocks history BBCA --period 1y # Stockbit's daily
idx stockbit logout
```
**Logging in.** `idx stockbit login` opens a small page on your own machine
(127.0.0.1, a fresh port each run). With stockbit.com open and logged in,
click the page's **Send my Stockbit session** bookmarklet: it reads
Stockbit's `credentialStorage` cookie in your browser and hands it to the
page, which checks it with one request and says "Connected". If bookmarklets
are blocked, click **Paste from clipboard** after copying the cookie, or
paste it into the box. Without a browser, `--no-browser` prints the page URL
instead, and `--port 7878` pins the port for `ssh -L 7878:127.0.0.1:7878`.
Piping the value in still works (`pbpaste | idx stockbit login`), as does a
refresh token on its own. On a server, set `IDX_STOCKBIT_REFRESH_TOKEN`.
**Logging in.** `idx stockbit login` opens Chrome (or Chromium, Edge, Brave,
Helium) on Stockbit's own sign-in page, in a browser profile only idx uses. Sign in
there as usual, including any one-time code: idx reads the
session cookie over the DevTools protocol, checks it with one request, and
closes the window. Your password never reaches idx. The profile sits beside
`config.toml`; idx takes the session cookie out of it once the session is
stored, so the profile keeps only Stockbit's device cookies. The profile
stays with the browser that made it: idx opens the same one next time, unless
`IDX_STOCKBIT_BROWSER` names another. Choose your browser that way on the
first sign-in if you have several.
Use your Stockbit password in that window. Google can refuse to sign in there
with "This browser or app may not be secure", most likely because idx starts
the browser with remote debugging on. If you only sign in with Google, use the
local page below: sign in from a private window, paste the cookie, then close
that window.
Run `login` again while the session is still good and there is nothing to
do: idx checks it with one request and exits without opening a browser. The
window it does open waits 15 minutes, and closes at once if you close it
yourself.
**How long a session lasts.** Stockbit's refresh token is good for about a
week, and idx spends it on every refresh, which extends the session. Use idx
at least once a week and you stay signed in. A longer gap, a `logout`, or
another tool spending the same token means one more sign-in.
Over SSH, on a machine with no display, without such a browser, or with
`--no-browser`, idx serves a small page on your own machine (127.0.0.1, port
7878 when it is free) instead. With
stockbit.com open and logged in, click the page's **Send my Stockbit
session** bookmarklet: it reads Stockbit's `credentialStorage` cookie in your
browser and hands it to the page, which checks it with one request and says
"Connected". If bookmarklets are blocked, click **Paste from clipboard**
after copying the cookie, or paste it into the box. `--port` picks another
port. Piping the value in still works
(`pbpaste | idx stockbit login`), as does a refresh token on its own. On a
server, set `IDX_STOCKBIT_REFRESH_TOKEN`. Set `IDX_STOCKBIT_BROWSER` to use a
browser idx does not find on its own.
The session lands in `~/.config/idx/stockbit-session.json`, readable only by
you. Tokens never appear in output, logs or errors. The page takes only the
you. On macOS without `XDG_CONFIG_HOME` that is
`~/Library/Application Support/idx/` instead; idx finds a session in either
one, so the CLI and an MCP client started from the Dock share it. Tokens never appear in output, logs or errors. The page takes only the
session value, keeps it in memory, and closes when it is done (or after five
minutes). A session Stockbit refuses leaves a previously stored one in place.
The page listens on this machine's loopback interface only, so the browser
must run on the same machine: from another host over SSH, or from a browser
outside a container or WSL distro, `127.0.0.1` is a different machine and the
page will not load. idx notices an SSH session (`SSH_CONNECTION`) and prints
the exact command to run on the machine with the browser, on port 7878:
## Remote and headless
The page listens on this machine's loopback interface only, and the browser
handoff needs a browser on this machine too. On a server, in a container, or
in a WSL distro, `127.0.0.1` is a different machine and neither will load.
Three ways around that, in order of how little they ask of you.
**Pipe the cookie over SSH.** No tunnel, one command. Copy the
`credentialStorage` value on the machine with the browser (DevTools,
Application, Cookies), then:
```bash
ssh -L 7878:127.0.0.1:7878 <this-host> # run where the browser is, keep it open
pbpaste | ssh <host> "idx stockbit login"
```
Elsewhere, pin the port yourself and forward it the same way:
idx reads stdin, verifies the session, and stores it on the server. The value
never reaches a shell history or a file.
**Forward the page.** Run the page on the server and forward its port from the
machine with the browser:
```bash
idx stockbit login --no-browser --port 7878
idx stockbit login # on the server: over SSH, it serves the page
ssh -L 7878:127.0.0.1:7878 <host> # where the browser is, keep it open
```
Then open the printed URL there and click the bookmarklet. Over SSH, idx
notices `SSH_CONNECTION`, skips the browser, and prints the exact forward
itself.
**Seed a token.** For anything unattended, keep the refresh token in a secret
store and pass it at run time:
```bash
op run --env-file=stockbit.env -- idx stocks orderbook BBCA
```
where the file holds `IDX_STOCKBIT_REFRESH_TOKEN=op://Private/stockbit/refresh`.
Only the refresh token is needed; idx mints its own access token.
**Agents.** Over MCP, an agent calls `stockbit_login`, which answers at once
and does the same as `idx stockbit login`. Where idx-mcp runs on your desktop,
a browser window opens on Stockbit's sign-in page (`"method": "browser"`) and
you sign in there. Over SSH or without a display, the answer carries the page
URL and its `ssh -L` forward (`"method": "page"`) for the agent to hand you.
The agent then polls `stockbit_status` until `sign_in.state` is `signed_in`
or `failed`. A session that already works answers `signed_in` without opening
anything, a second call while one is waiting returns it again, and
`{"method": "page"}` or `{"method": "browser"}` picks the way. If the server
stops while the window waits, the window closes with it.
One rule for all three: only one machine may own a session. Stockbit retires
the refresh token on every refresh, so a token copied to two machines signs the
other one out. Two machines need two sign-ins.
**One refresher per session.** Stockbit replaces the refresh token every time
it is used. Once idx refreshes your session, a browser holding the same
session has to log in again, and two tools sharing one session log each
other out. Log in through a separate browser profile (or a private window
you then close) for idx. idx processes on one machine take turns through a
lock, so they never spend the token twice.
other out. `idx stockbit login` keeps its own browser profile, so this only
affects a browser you use yourself. idx processes on one machine take turns
through a lock, so they never spend the token twice.
What the login adds:
@ -139,7 +209,8 @@ candles explicitly; windows it charts only weekly or hourly (`2y`, `5y`,
Stockbit's [terms](https://stockbit.com/terms) allow its content for personal,
non-commercial use. They prohibit data mining, robots and redistribution
without Stockbit's written consent. idx is not affiliated with or endorsed by
without Stockbit's written consent, which includes serving the data to other
people through `idx-mcp`. idx is not affiliated with or endorsed by
Stockbit. Use this for looking at a stock now and then, not for crawling the
market or republishing prices. With your login, automated use is tied to your
account: keep it human-paced.

View file

@ -94,7 +94,7 @@ Every IDX holding (above 1%) of one investor, matched by name.
## `get_orderbook`
Order book of one IDX stock from the user's own Stockbit session: `bids` and `offers` (best price first) of {price, shares, orders}, the session's OHLC, volume (shares), value and frequency, and foreign buy/sell/net in IDR. `market_status` is Stockbit's state, verbatim; outside STATE_NAME_MARKET_OPEN the book is as the market left it. AUTHERROR means there is no session: call stockbit_status, and ask the user to run `idx stockbit login` in a terminal.
Order book of one IDX stock from the user's own Stockbit session: `bids` and `offers` (best price first) of {price, shares, orders}, the session's OHLC, volume (shares), value and frequency, and foreign buy/sell/net in IDR. `market_status` is Stockbit's state, verbatim; outside STATE_NAME_MARKET_OPEN the book is as the market left it. AUTHERROR means there is no session: call stockbit_status, then stockbit_login for a URL the user can open.
| Input | Type | Required | Description |
| --- | --- | --- | --- |
@ -172,8 +172,16 @@ Search investor names in the KSEI ownership database.
| `limit` | integer | no | Maximum results (default 20, at most 100). |
| `query` | string | yes | Full-text query over investor names. |
## `stockbit_login`
Start a Stockbit sign-in and return at once; this call does not wait for it. With method auto (the default), idx-mcp opens a browser window on Stockbit's own sign-in page when its machine has a display and is not reached over SSH (`method: "browser"` in the answer): the user signs in there and the window closes by itself. Otherwise it serves a page on 127.0.0.1 (`method: "page"`, with `url` and an `ssh -L` `forward` for a browser on another machine) that takes stockbit.com's session cookie from the user's own browser. Never ask the user for their password, and never type it. Then poll stockbit_status until `sign_in.state` is signed_in or failed. A session that already works answers state signed_in without opening anything; a second call while one is waiting returns it again (`already_open`).
| Input | Type | Required | Description |
| --- | --- | --- | --- |
| `method` | `auto` / `browser` / `page` | no | "auto" (default): a browser window when idx-mcp's machine has a display and no SSH, else the local page; "browser" or "page" picks one. |
## `stockbit_status`
Whether a Stockbit session is stored (or seeded by IDX_STOCKBIT_REFRESH_TOKEN) and when its tokens expire; never the tokens. Without one, get_orderbook fails and stockbit-provider quotes use Stockbit's delayed public page; the user adds one with `idx stockbit login` in a terminal.
Whether a Stockbit session is stored (or seeded by IDX_STOCKBIT_REFRESH_TOKEN) and when its tokens expire; never the tokens. Without one, get_orderbook fails and stockbit-provider quotes use Stockbit's delayed public page; the user adds one with stockbit_login, or `idx stockbit login` in a terminal. After stockbit_login, `sign_in.state` says whether that sign-in is waiting, signed_in or failed (with `sign_in.error`).
No inputs.